<?php
/* vim: set noexpandtab tabstop=2 softtabstop=2 shiftwidth=2: */

/**
 * Dedimania plugin.
 * Handles interaction with the Dedimania world database.
 * Created by Xymph, based on FAST
 *
 * Dependencies: requires chat.dedimania.php, plugin.checkpoints.php
 *               used by chat.dedimania.php
 *               requires plugin.panels.php on TMF
 */

require_once('includes/GbxRemote.response.php');
require_once('includes/web_access.inc.php');
require_once('includes/xmlrpc_db.inc.php');

define('DEDICONFIG', 'dedimania.xml');

global $dedi_db, $dedi_db_defaults, $dedi_debug, $dedi_lastsent, $dedi_timeout,
       $dedi_refresh, $dedi_minauth, $dedi_mintime, $dedi_webaccess;

// how many seconds before retrying connection
$dedi_timeout = 1800;  // 30 mins
// how many seconds before reannouncing server
$dedi_refresh = 240;   // 4 mins
// minimum author & finish times that are still accepted
$dedi_minauth = 8000;  // 8 secs
$dedi_mintime = 6000;  // 6 secs
$dedi_debug = 0;  /* max debug level = 5:
1 +internal warnings
2 +main data structure, initial connection response, progress messages, dedicated callback data
3 +config defaults, XML config, full record lists, data in XML responses
4 +full XML responses
5 +record checkpoints
*/

// overrule these in dedimania.xml, don't change them here
$dedi_db_defaults = array(
	'Name' => 'Dedimania',
	'LogNews' => false,
	'ShowWelcome' => true,
	'ShowMinRecs' => 8,
	'ShowRecsBefore' => 1,
	'ShowRecsAfter' => 1,
	'ShowRecsRange' => true,
	'DisplayRecs' => true,
	'RecsInWindow' => false,
	'ShowRecLogins' => true,
	'LimitRecs' => 10,
);

Aseco::registerEvent('onSync', 'dedimania_init');
Aseco::registerEvent('onEverySecond', 'dedimania_update');
Aseco::registerEvent('onPlayerConnect', 'dedimania_playerconnect');
Aseco::registerEvent('onPlayerDisconnect', 'dedimania_playerdisconnect');
Aseco::registerEvent('onNewChallenge', 'dedimania_newchallenge');
Aseco::registerEvent('onEndRace', 'dedimania_endrace');
Aseco::registerEvent('onPlayerFinish', 'dedimania_playerfinish');

// Initialize Dedimania subsystem
// called @ onSync
function dedimania_init($aseco) {
	global $dedi_db, $dedi_db_defaults, $dedi_debug, $dedi_webaccess, $dedi_lastsent,
	       $checkpoints;  // from plugin.checkpoints.php

	// check for checkpoints plugin
	if (!isset($checkpoints) || !is_array($checkpoints))
		trigger_error('Dedimania system cannot find $checkpoints - include plugin.checkpoints.php in plugins.xml!', E_USER_ERROR);

	// create web access
	$dedi_webaccess = new Webaccess();

	if ($dedi_debug > 2)
		print_r($dedi_db_defaults);

	// read & parse config file
	$dedi_db = array();
	if ($config = $aseco->xml_parser->parseXml(DEDICONFIG)) {
		if ($dedi_debug > 2)
			print_r($config);

		// read the XML structure into array
		if (isset($config['DEDIMANIA']['DATABASE']) && is_array($config['DEDIMANIA']['DATABASE']) &&
		    isset($config['DEDIMANIA']['MASTERSERVER_ACCOUNT']) && is_array($config['DEDIMANIA']['MASTERSERVER_ACCOUNT'])) {
			$dbdata = &$config['DEDIMANIA']['DATABASE'][0];

			if ($dedi_debug > 2)
				print_r($dbdata);

			if (isset($dbdata['URL'][0])) {
				if (!is_array($dbdata['URL'][0]))
					$dedi_db['Url'] = $dbdata['URL'][0];
				else
					trigger_error('Multiple URLs specified in your Dedimania config file!', E_USER_ERROR);

				if (isset($dbdata['WELCOME'][0]))
					$dedi_db['Welcome'] = $dbdata['WELCOME'][0];
				else
					$dedi_db['Welcome'] = '';

				if (isset($dbdata['TIMEOUT'][0]))
					$dedi_db['Timeout'] = $dbdata['TIMEOUT'][0];
				else
					$dedi_db['Timeout'] = '';

				if (isset($dbdata['NAME'][0]))
					$dedi_db['Name'] = $dbdata['NAME'][0];
				else
					$dedi_db['Name'] = $dedi_db_defaults['Name'];

				if (isset($dbdata['LOG_NEWS'][0]))
					$dedi_db['LogNews'] = (strtolower($dbdata['LOG_NEWS'][0]) == 'true');
				else
					$dedi_db['LogNews'] = $dedi_db_defaults['LogNews'];

				if (isset($dbdata['SHOW_WELCOME'][0]))
					$dedi_db['ShowWelcome'] = (strtolower($dbdata['SHOW_WELCOME'][0]) == 'true');
				else
					$dedi_db['ShowWelcome'] = $dedi_db_defaults['ShowWelcome'];

				if (isset($dbdata['SHOW_MIN_RECS'][0]))
					$dedi_db['ShowMinRecs'] = intval($dbdata['SHOW_MIN_RECS'][0]);
				else
					$dedi_db['ShowMinRecs'] = $dedi_db_defaults['ShowMinRecs'];

				if (isset($dbdata['SHOW_RECS_BEFORE'][0]))
					$dedi_db['ShowRecsBefore'] = intval($dbdata['SHOW_RECS_BEFORE'][0]);
				else
					$dedi_db['ShowRecsBefore'] = $dedi_db_defaults['ShowRecsBefore'];

				if (isset($dbdata['SHOW_RECS_AFTER'][0]))
					$dedi_db['ShowRecsAfter'] = intval($dbdata['SHOW_RECS_AFTER'][0]);
				else
					$dedi_db['ShowRecsAfter'] = $dedi_db_defaults['ShowRecsAfter'];

				if (isset($dbdata['SHOW_RECS_RANGE'][0]))
					$dedi_db['ShowRecsRange'] = (strtolower($dbdata['SHOW_RECS_RANGE'][0]) == 'true');
				else
					$dedi_db['ShowRecsRange'] = $dedi_db_defaults['ShowRecsRange'];

				if (isset($dbdata['DISPLAY_RECS'][0]))
					$dedi_db['DisplayRecs'] = (strtolower($dbdata['DISPLAY_RECS'][0]) == 'true');
				else
					$dedi_db['DisplayRecs'] = $dedi_db_defaults['DisplayRecs'];

				if (isset($dbdata['RECS_IN_WINDOW'][0]))
					$dedi_db['RecsInWindow'] = (strtolower($dbdata['RECS_IN_WINDOW'][0]) == 'true');
				else
					$dedi_db['RecsInWindow'] = $dedi_db_defaults['RecsInWindow'];

				if (isset($dbdata['SHOW_REC_LOGINS'][0]))
					$dedi_db['ShowRecLogins'] = (strtolower($dbdata['SHOW_REC_LOGINS'][0]) == 'true');
				else
					$dedi_db['ShowRecLogins'] = $dedi_db_defaults['ShowRecLogins'];

				if (isset($dbdata['LIMIT_RECS'][0]))
					$dedi_db['LimitRecs'] = intval($dbdata['LIMIT_RECS'][0]);
				else
					$dedi_db['LimitRecs'] = $dedi_db_defaults['LimitRecs'];

				// set default MaxRank
				$dedi_db['MaxRank'] = 30;

				// check/initialise server configuration
				$dbdata = &$config['DEDIMANIA']['MASTERSERVER_ACCOUNT'][0];
				$dedi_db['Login'] = $dbdata['LOGIN'][0];
				$dedi_db['Password'] = $dbdata['PASSWORD'][0];
				$dedi_db['Nation'] = $dbdata['NATION'][0];
				if ($dedi_db['Login'] == '' || $dedi_db['Login'] == 'YOUR_SERVER_LOGIN' ||
				    $dedi_db['Password'] == '' || $dedi_db['Password'] == 'YOUR_SERVER_PASSWORD' ||
				    $dedi_db['Nation'] == '' || $dedi_db['Nation'] == 'YOUR_SERVER_NATION')
					trigger_error('Dedimania not configured! <masterserver_account> contains default or empty value(s)', E_USER_ERROR);

				if ($aseco->server->getGame() == 'TMF' && strtolower($dedi_db['Login']) != $aseco->server->serverlogin)
					trigger_error('Dedimania misconfigured! <masterserver_account><login> (' . $dedi_db['Login'] . ') is not the actual server login (' . $aseco->server->serverlogin . ')', E_USER_ERROR);

				$dedi_db['Messages'] = &$config['DEDIMANIA']['MESSAGES'][0];
				$dedi_db['RecsValid'] = false;
				$dedi_db['BannedLogins'] = array();

				$dedi_db['ModeList'] = array();
				$dedi_db['ModeList'][Gameinfo::RNDS] = 'Rounds';
				$dedi_db['ModeList'][Gameinfo::TA]   = 'TA';
				$dedi_db['ModeList'][Gameinfo::TEAM] = 'Rounds';
				$dedi_db['ModeList'][Gameinfo::LAPS] = 'TA';
				$dedi_db['ModeList'][Gameinfo::STNT] = '';
				$dedi_db['ModeList'][Gameinfo::CUP]  = 'Rounds';
			} else {
				trigger_error('No URL specified in your Dedimania config file!', E_USER_ERROR);
			}
		} else {
			trigger_error('Structure error in your Dedimania config file!', E_USER_ERROR);
		}
	} else {
		trigger_error('Could not read/parse Dedimania config file ' . DEDICONFIG . ' !', E_USER_ERROR);
	}

	if ($dedi_debug > 1)
		print_r($dedi_db);

	// connect to Dedimania server
	$aseco->console('************* (Dedimania) *************');
	dedimania_connect($aseco);
	$aseco->console('------------- (Dedimania) -------------');

	$dedi_lastsent = time();
}  // dedimania_init

function dedimania_connect($aseco) {
	global $dedi_db, $dedi_debug, $dedi_timeout, $dedi_webaccess;

	$time = time();

	// check for no or timed-out connection
	if (!isset($dedi_db['XmlrpcDB']) &&
	    (!isset($dedi_db['XmlrpcDBbadTime']) || ($time - $dedi_db['XmlrpcDBbadTime']) > $dedi_timeout)) {

		$aseco->console('* Dataserver connection on ' . $dedi_db['Name'] . ' ...');
		$aseco->console('* Try connection on ' . $dedi_db['Url'] . ' ...');

		// establish Dedimania connection and login
		$xmlrpcdb = new XmlrpcDB($dedi_webaccess, $dedi_db['Url'],
		                         $aseco->server->getGame(),
		                         $dedi_db['Login'],
		                         $dedi_db['Password'],
		                         'XASECO', XASECO_VERSION,
		                         $dedi_db['Nation'],
		                         $aseco->server->packmask);
		$response = $xmlrpcdb->RequestWait('dedimania.ValidateAccount');
		if ($dedi_debug > 3)
			$aseco->console_text('dedimania_connect - response' . CRLF . print_r($response, true));
		elseif ($dedi_debug > 2)
			$aseco->console_text('dedimania_connect - response[Data]' . CRLF . print_r($response['Data'], true));

		// Reply a struct {'Status': boolean,
		//                 'Messages': array of struct {'Date': string, 'Text': string} }

		// check response
		if ($response === false) {
			$aseco->console_text('  !!!' . CRLF . '  !!! Error bad database response !' . CRLF . '  !!!');
		}
		elseif (isset($response['Data']['params']['Status']) && $response['Data']['params']['Status']) {
			// establish Dedimania connection and login
			$xmlrpcdb = new XmlrpcDB($dedi_webaccess, $dedi_db['Url'],
			                         $aseco->server->getGame(),
			                         $dedi_db['Login'],
			                         $dedi_db['Password'],
			                         'XASECO', XASECO_VERSION,
			                         $dedi_db['Nation'],
			                         $aseco->server->packmask);
			$dedi_db['XmlrpcDB'] = $xmlrpcdb;
			$dedi_db['News'] = $response['Data']['params']['Messages'];
			$aseco->console('* Connection and status ok! (' . $response['Headers']['server'][0] . ')');
			if (($errors = dedi_iserror($response)) !== false)
				$aseco->console_text('  !!!' . CRLF . '  !!! ...with authentication warning(s): ' . $errors);
		}
		elseif (($errors = dedi_iserror($response)) !== false) {
			$aseco->console_text('  !!!' . CRLF . '  !!! Connection Error !!! (' . $response['Headers']['server'][0] . ')' . CRLF . $errors . CRLF . '  !!!');
		}
		elseif (!isset($response['Code'])) {
			$aseco->console_text('  !!!' . CRLF . '  !!! Error no database response (' . $dedi_db['Url'] . ')' . CRLF . '  !!!');
		}
		else {
			$aseco->console_text('  !!!' . CRLF . '  !!! Error bad database response or contents (' . $response['Headers']['server'][0] . ') ['
			                     . $response['Code'] . ', ' . $response['Reason'] . ']' . CRLF . '  !!!');
			if ($dedi_debug > 1) {
				if ($response['Code'] == 200)
					$aseco->console_text('dedimania_connect - response[Message]' . CRLF . $response['Message']);
				elseif ($response['Code'] != 404)
					$aseco->console_text('dedimania_connect - response' . CRLF . print_r($response, true));
			}
		}

		// check for valid connection
		if (isset($dedi_db['XmlrpcDB'])) {
			// log Dedimania news
			if ($dedi_db['LogNews'])
				foreach ($dedi_db['News'] as $news)
					$aseco->console('* NEWS (' . $dedi_db['Name'] . ', ' . $news['Date'] . '): ' . $news['Text']);
			return;
		}

		// prepare for next connection attempt
		$dedi_db['XmlrpcDBbadTime'] = $time;
	}
}  // dedimania_connect


function dedimania_announce() {
	global $aseco, $dedi_db, $dedi_debug, $dedi_lastsent;

	// check for valid track
	if (isset($aseco->server->challenge->uid)) {
		// check for valid connection
		if (isset($dedi_db['XmlrpcDB']) && !$dedi_db['XmlrpcDB']->isBad()) {
			if ($dedi_debug > 1)
				$aseco->console('** Update server Dedimania info...');

			// collect server & players info
			$serverinfo = dedimania_serverinfo($aseco);
			$players = dedimania_players($aseco);

			$dedi_lastsent = time();
			$callback = array('dedimania_announce_cb');
			$dedi_db['XmlrpcDB']->addRequest($callback,
			                                 'dedimania.UpdateServerPlayers',
			                                 $aseco->server->getGame(),
			                                 $aseco->server->gameinfo->mode,
			                                 $serverinfo,
			                                 $players);
			// UpdateServerPlayers(Game, Mode, SrvInfo, Players)
		}
	}
}  // dedimania_announce

function dedimania_announce_cb($response) {
	global $aseco, $dedi_debug;

	// Reply true

	if (($errors = dedi_iserror($response)) !== false) {
		if ($dedi_debug > 3)
			$aseco->console_text('dedimania_announce_cb - response' . CRLF . print_r($response, true));
		elseif ($dedi_debug > 2)
			$aseco->console_text('dedimania_announce_cb - response[Data]' . CRLF . print_r($response['Data'], true));
		else
			$aseco->console_text('dedimania_announce_cb - error(s): ' . $errors);
	}
}  // dedimania_announce_cb

// called @ onEverySecond
function dedimania_update($aseco) {
	global $dedi_db, $dedi_lastsent, $dedi_timeout, $dedi_refresh, $dedi_webaccess;

	// check for valid connection
	if (isset($dedi_db['XmlrpcDB'])) {
		// refresh DB every 4 mins after last DB update
		if ($dedi_lastsent + $dedi_refresh < time())
			dedimania_announce();

		if ($dedi_db['XmlrpcDB']->isBad()) {
			// retry after 30 mins of bad state
			if ($dedi_db['XmlrpcDB']->badTime() > $dedi_timeout) {
				$aseco->console('Dedimania retry to send after ' . round($dedi_timeout/60) . ' minutes...');
				$dedi_db['XmlrpcDB']->retry();
			}
		} else {
			$response = $dedi_db['XmlrpcDB']->sendRequests();
			if (!$response) {
				$message = '{#server}>> ' . formatText($dedi_db['Timeout'], round($dedi_timeout/60));
				$aseco->client->query('ChatSendServerMessage', $aseco->formatColors($message));
				trigger_error('Dedimania has consecutive connection errors!', E_USER_WARNING);
			}
		}
	} else {
		// reconnect to Dedimania server
		dedimania_connect($aseco);
	}

	// trigger pending callbacks
	$read = array();
	$write = null;
	$except = null;
	$dedi_webaccess->select($read, $write, $except, 0);
}  // dedimania_update


// called @ onPlayerConnect
function dedimania_playerconnect($aseco, $player) {
	global $dedi_db, $dedi_debug;

	if ($dedi_debug > 1)
		$aseco->console_text('dedimania_playerconnect - ' . $player->login . ' : ' . stripColors($player->nickname, false));

	// get player info & check for non-LAN login
	if ($pinfo = dedimania_playerinfo($aseco, $player)) {
		if ($dedi_debug > 1)
			$aseco->console_text('dedimania_playerconnect - pinfo' . CRLF . print_r($pinfo, true));

		// check for valid connection
		if (isset($dedi_db['XmlrpcDB']) && !$dedi_db['XmlrpcDB']->isBad()) {
			$callback = array('dedimania_playerconnect_cb', $player->login);
			$dedi_db['XmlrpcDB']->addRequest($callback,
			                                 'dedimania.PlayerArrive',
			                                 $aseco->server->getGame(),
			                                 $player->login,
			                                 $player->nickname,
			                                 $pinfo['Nation'],
			                                 $pinfo['TeamName'],
			                                 $pinfo['Ranking'],
			                                 $pinfo['IsSpec'],
			                                 $pinfo['IsOff']);
			// PlayerArrive(Game, Login, Nickname, Nation, TeamName, LadderRanking, IsSpectator, IsOfficial)
		}
	}
}  // dedimania_playerconnect

function dedimania_playerconnect_cb($response, $login) {
	global $aseco, $dedi_db, $dedi_debug;

	// Reply a struct {'Login': string, 'TeamName': string, 'Nation': string,
	//                 'Options': array of struct {'Option': string, 'Value': string, 'Tool': string},
	//                 'Aliases': array of struct {'Alias': string, 'Text': string, 'Tool': string} }

	if ($dedi_debug > 3)
		$aseco->console_text('dedimania_playerconnect_cb - response' . CRLF . print_r($response, true));
	elseif ($dedi_debug > 2)
		$aseco->console_text('dedimania_playerconnect_cb - response[Data]' . CRLF . print_r($response['Data'], true));
	elseif (($errors = dedi_iserror($response)) !== false)
		$aseco->console_text('dedimania_playerconnect_cb - error(s): ' . $errors);

	// check response
	if (!$player = $aseco->server->players->getPlayer($login)) {
		if ($dedi_debug > 0)
			$aseco->console('dedimania_playerconnect_cb - ' . $login . ' does not exist!');
	}
	elseif (isset($response['Data']['params'])) {
		// update nickname in record
		if ($dedi_db['RecsValid'] && !empty($dedi_db['Challenge']['Records']) && isset($player->nickname)) {
			foreach ($dedi_db['Challenge']['Records'] as &$rec) {
				if ($rec['Login'] == $login && $rec['Game'] ==
				    ($aseco->server->getGame() == 'TMF' ? 'TMU' : $aseco->server->getGame())) {
					$rec['NickName'] = $player->nickname;
					break;
				}
			}
		}

		// show welcome message
		if ($dedi_db['ShowWelcome']) {
			$message = '{#server}> ' . $dedi_db['Welcome'];
			$message = str_replace('{br}', LF, $message);  // split long message
			// hyperlink Dedimania site on TMF
			if ($aseco->server->getGame() == 'TMF')
				$message = str_replace('www.dedimania.com', '$l[http://www.dedimania.com/]www.dedimania.com$l', $message);
			$aseco->client->query('ChatSendServerMessageToLogin', $aseco->formatColors($message), $login);
		}

		// get player rank
		$player->dedirank = $dedi_db['MaxRank'];
		if (isset($response['Data']['params']['MaxRank']))
			$player->dedirank = $response['Data']['params']['MaxRank']+0;

		// check for banned player
		if (!isset($response['Data']['params']['Status']))
			trigger_error('Incomplete XASECO update - includes/xmlrpc_db.inc.php is out of date!', E_USER_ERROR);
		if ($response['Data']['params']['Status'] % 2 == 1) {
			// remember banned login
			$dedi_db['BannedLogins'][] = $login;
			// show chat message to all
			$message = formatText($dedi_db['Messages']['BANNED_LOGIN'][0],
			                      stripColors($player->nickname), $login);
			$aseco->client->query('ChatSendServerMessage', $aseco->formatColors($message));
			// log banned player
			$aseco->console('[Dedimania] player {1} is banned - finishes ignored!', $login);
		}
	}
	else {
		if ($dedi_debug > 2)
			$aseco->console('dedimania_playerconnect_cb - bad response!');
	}
}  // dedimania_playerconnect_cb


// called @ onPlayerDisconnect
function dedimania_playerdisconnect($aseco, $player) {
	global $dedi_db, $dedi_debug;

	if ($dedi_debug > 1)
		$aseco->console_text('dedimania_playerdisconnect - ' . $player->login . ' : ' . stripColors($player->nickname, false));

	// check for non-LAN login
	if (!isLANLogin($player->login)) {
		// check for valid connection
		if (isset($dedi_db['XmlrpcDB']) && !$dedi_db['XmlrpcDB']->isBad()) {
			$dedi_db['XmlrpcDB']->addRequest(null,
			                                 'dedimania.PlayerLeave',
			                                 $aseco->server->getGame(),
			                                 $player->login);
			// PlayerLeave(Game, Login)
			// ignore: Reply a struct {'Login': string}
		}
	}

	// clear possible banned login
	if (($i = array_search($player->login, $dedi_db['BannedLogins'])) !== false)
		unset($dedi_db['BannedLogins'][$i]);
}  // dedimania_playerdisconnect


// called @ onNewChallenge
function dedimania_newchallenge($aseco, $challenge) {
	global $dedi_db, $dedi_debug, $dedi_minauth;

	if ($dedi_debug > 1)
		$aseco->console_text('dedimania_newchallenge - challenge' . CRLF . print_r($challenge, true));

	// check for valid connection
	$dedi_db['Challenge'] = array();
	if (isset($dedi_db['XmlrpcDB']) && !$dedi_db['XmlrpcDB']->isBad()) {
		// collect server & players info
		$serverinfo = dedimania_serverinfo($aseco);
		$players = dedimania_players($aseco);

		$callback = array('dedimania_newchallenge_cb', $challenge);
		$dedi_db['XmlrpcDB']->addRequest($callback,
		                                 'dedimania.CurrentChallenge',
		                                 $challenge->uid,
		                                 $challenge->name,
		                                 $challenge->environment,
		                                 $challenge->author,
		                                 $aseco->server->getGame(),
		                                 $aseco->server->gameinfo->mode,
		                                 $serverinfo,
		                                 $dedi_db['MaxRank'],
		                                 $players);
		// CurrentChallenge(Uid, Name, Environment, Author, Game, Mode, SrvInfos, MaxGetTimes, Players)
	}

	$dedi_db['RecsValid'] = false;
	$dedi_db['TrackValid'] = false;
	$dedi_db['ServerMaxRank'] = $dedi_db['MaxRank'];
	// check for Stunts mode
	if ($aseco->server->gameinfo->mode == Gameinfo::STNT)
		$aseco->console('[Dedimania] Stunts mode unsupported: records ignored');
	// check for multilap track in TMF Rounds/Team/Cup modes
	elseif ($aseco->server->getGame() == 'TMF' &&
	        $challenge->laprace && $challenge->forcedlaps != 0 &&
	        ($aseco->server->gameinfo->mode == Gameinfo::RNDS ||
	         $aseco->server->gameinfo->mode == Gameinfo::TEAM ||
	         $aseco->server->gameinfo->mode == Gameinfo::CUP))
		$aseco->console('[Dedimania] RoundForcedLaps != 0: records ignored');
	// check for minimum author time
	elseif ($challenge->authortime < $dedi_minauth)
		$aseco->console('[Dedimania] Map\'s Author time < ' . ($dedi_minauth / 1000) . 's: records ignored');
	else
		$dedi_db['TrackValid'] = true;
}  // dedimania_newchallenge

function dedimania_newchallenge_cb($response, $challenge) {
	global $aseco, $dedi_db, $dedi_debug,
	       $checkpoints;  // from plugin.checkpoints.php

	// Reply a struct {'Uid': string, 'TotalRaces': int, 'TotalPlayers': int,
	//                 'TimeAttackRaces': int, 'TimeAttackPlayers': int,
	//                 'NumberOfChecks': int, 'ServerMaxRecords': int,
	//                 'Records': array of struct {'Login': string, 'NickName': string,
	//                                             'Best': int, 'Rank': int,
	//                                             'Checks': array of int, 'Vote': int} }

	// if Stunts mode, bail out
	if ($aseco->server->gameinfo->mode == Gameinfo::STNT) return;

	if ($dedi_debug > 3)
		$aseco->console_text('dedimania_newchallenge_cb - response' . CRLF . print_r($response, true));
	elseif ($dedi_debug > 2)
		$aseco->console_text('dedimania_newchallenge_cb - response[Data]' . CRLF . print_r($response['Data'], true));
	elseif (($errors = dedi_iserror($response)) !== false)
		$aseco->console_text('dedimania_newchallenge_cb - error(s): ' . $errors);

	// check response
	if (isset($response['Data']['params']) && $dedi_db['TrackValid']) {
		$dedi_db['Challenge'] = $response['Data']['params'];
		$dedi_db['RecsValid'] = true;
		if (isset($response['Data']['params']['ServerMaxRecords']))
			$dedi_db['ServerMaxRank'] = $response['Data']['params']['ServerMaxRecords']+0;

		if ($dedi_debug > 1)
			$aseco->console_text('dedimania_newchallenge_cb - records' . CRLF . print_r($dedi_db['Challenge']['Records'], true));

		// check for records
		if (!empty($dedi_db['Challenge']['Records'])) {
			// strip line breaks in nicknames
			foreach ($dedi_db['Challenge']['Records'] as &$rec) {
				$rec['NickName'] = str_replace("\n", '', $rec['NickName']);
			}

			// set Dedimania record/checkpoints references
			if ($aseco->settings['display_checkpoints']) {
				foreach ($checkpoints as $login => $cp) {
					$drec = $checkpoints[$login]->dedirec - 1;

					// check for specific record
					if ($drec+1 > 0) {
						// if specific record unavailable, use last one
						if ($drec > count($dedi_db['Challenge']['Records']) - 1)
							$drec = count($dedi_db['Challenge']['Records']) - 1;
						// store record/checkpoints reference
						$checkpoints[$login]->best_fin = $dedi_db['Challenge']['Records'][$drec]['Best'];
						$checkpoints[$login]->best_cps = $dedi_db['Challenge']['Records'][$drec]['Checks'];
					}
					elseif ($drec+1 == 0) {
						// search for own/last record
						$drec = 0;
						while ($drec < count($dedi_db['Challenge']['Records'])) {
							if ($dedi_db['Challenge']['Records'][$drec++]['Login'] == $login)
								break;
						}
						$drec--;
						// store record/checkpoints reference
						$checkpoints[$login]->best_fin = $dedi_db['Challenge']['Records'][$drec]['Best'];
						$checkpoints[$login]->best_cps = $dedi_db['Challenge']['Records'][$drec]['Checks'];
					}  // else -1
				}
			}
			if ($dedi_debug > 4)
				$aseco->console_text('dedimania_newchallenge_cb - checkpoints' . CRLF . print_r($checkpoints, true));

			// notify records panel & update all panels
			if ($aseco->server->getGame() == 'TMF') {
				setRecordsPanel('dedi', ($aseco->server->gameinfo->mode == Gameinfo::STNT ?
				                         str_pad($dedi_db['Challenge']['Records'][0]['Best'], 5, ' ', STR_PAD_LEFT) :
				                         formatTime($dedi_db['Challenge']['Records'][0]['Best'])));
				if (function_exists('update_allrecpanels'))
					update_allrecpanels($aseco, null);  // from plugin.panels.php
			}
		}

		if ($dedi_db['ShowRecsBefore'] > 0)
			show_dedirecs($aseco, $challenge->name, $challenge->uid,
			              $dedi_db['Challenge']['Records'], false, 1,
			              $dedi_db['ShowRecsBefore']);  // from chat.dedimania.php
	} else {
		if ($dedi_debug > 2)
			$aseco->console('dedimania_newchallenge_cb - bad response or track invalid!');
	}

	// throw 'Dedimania records loaded' event
	$aseco->releaseEvent('onDediRecsLoaded', $dedi_db['RecsValid']);
}  // dedimania_newchallenge_cb


// called @ onEndRace
function dedimania_endrace($aseco, $data) {
	global $dedi_db, $dedi_debug, $dedi_lastsent, $dedi_mintime;

	// notify records panel
	if ($aseco->server->getGame() == 'TMF') {
		setRecordsPanel('dedi', ($aseco->server->gameinfo->mode == Gameinfo::STNT ?
		                         '  ---' : '   --.--'));
	}

	// if Stunts mode, bail out
	if ($aseco->server->gameinfo->mode == Gameinfo::STNT) return;

	if ($dedi_debug > 1)
		$aseco->console_text('dedimania_endrace - data' . CRLF . print_r($data, true));

	// check for valid track
	if (isset($data[1]['UId']) && isset($dedi_db['TrackValid']) && $dedi_db['TrackValid']) {
		// check for valid connection
		if (isset($dedi_db['XmlrpcDB']) && !$dedi_db['XmlrpcDB']->isBad()) {
			// collect/sort new finish times & checkpoints
			if ($dedi_db['RecsValid'] && !empty($dedi_db['Challenge']['Records'])) {
				$times = array();
				foreach ($dedi_db['Challenge']['Records'] as $rec) {
					// check for valid, minimum finish time
					if (isset($rec['NewBest']) && $rec['NewBest'] &&
					    $rec['Best'] >= $dedi_mintime)
						$times[] = array('Login' => $rec['Login'], 'Best' => $rec['Best'],
						                 'Checks' => implode(',', $rec['Checks']));
				}
				if (!empty($times))
					usort($times, 'dedi_timecompare');

				// compute number of checkpoints from best time
				$numchecks = 0;
				if (isset($times[0]['Checks']))
					$numchecks = count(explode(',', $times[0]['Checks']));

				if ($dedi_debug > 1) {
					$aseco->console_text('dedimania_endrace - numchecks: ' . $numchecks);
					$aseco->console_text('dedimania_endrace - times' . CRLF . print_r($times, true));
				}

				$dedi_lastsent = time();
				$callback = array('dedimania_endrace_cb', $data[1]);
				$dedi_db['XmlrpcDB']->addRequest($callback,
				                                 'dedimania.ChallengeRaceTimes',
				                                 $data[1]['UId'],
				                                 $data[1]['Name'],
				                                 $data[1]['Environnement'],
				                                 $data[1]['Author'],
				                                 $aseco->server->getGame(),
				                                 $aseco->server->gameinfo->mode,
				                                 $numchecks,
				                                 $dedi_db['MaxRank'],
				                                 $times);
				// ChallengeRaceTimes(Uid, Name, Environment, Author, Game, Mode, MaxGetTimes, Times)
				// Times is an array of struct {'Login': string, 'Best': int, 'Checks': array of int or comma-separated string of int}
			}
		}
	}
}  // dedimania_endrace

function dedimania_endrace_cb($response, $challenge) {
	global $aseco, $dedi_db, $dedi_debug;

	//Reply a struct {'Uid': string, 'TotalRaces': int, 'TotalPlayers': int,
	//                'TimeAttackRaces': int, 'TimeAttackPlayers': int,
	//                'NumberOfChecks': int, 'ServerMaxRecords': int,
	//                'Records': array of struct {'Login': string, 'NickName': string,
	//                                            'Best': int, 'Rank': int,
	//                                            'Checks': array of int, 'NewBest': boolean} }

	if ($dedi_debug > 3)
		$aseco->console_text('dedimania_endrace_cb - response' . CRLF . print_r($response, true));
	elseif ($dedi_debug > 2)
		$aseco->console_text('dedimania_endrace_cb - response[Data]' . CRLF . print_r($response['Data'], true));
	elseif (($errors = dedi_iserror($response)) !== false)
		$aseco->console_text('dedimania_endrace_cb - error(s): ' . $errors);

	// check response
	if (isset($response['Data']['params'])) {
		$dedi_db['Results'] = $response['Data']['params'];

		// check for records
		if (!empty($dedi_db['Results']['Records'])) {
			// strip line breaks in nicknames
			foreach ($dedi_db['Results']['Records'] as &$rec) {
				$rec['NickName'] = str_replace("\n", '', $rec['NickName']);
			}
			if ($dedi_debug > 1)
				$aseco->console_text('dedimania_endrace_cb - results' . CRLF . print_r($dedi_db['Results'], true));

			if ($dedi_db['ShowRecsAfter'] > 0)
				show_dedirecs($aseco, $challenge['Name'], $challenge['UId'],
				              $dedi_db['Results']['Records'], false, 3,
				              $dedi_db['ShowRecsAfter']);  // from chat.dedimania.php
		}

		// check for banned players
		if (isset($response['Data']['errors']) &&
		    preg_match('/Warning.+Player TM.+is banned on Dedimania/', $response['Data']['errors'])) {
			// log banned players
			$errors = explode("\n", $response['Data']['errors']);
			foreach ($errors as $error) {
				if (preg_match('/Warning.+Player TM[A-Z]+:(.+) is banned on Dedimania/', $error, $login))
					$aseco->console('[Dedimania] player {1} is banned - record ignored!', $login[1]);
			}
		}
	} else {
		if ($dedi_debug > 2)
			$aseco->console('dedimania_endrace_cb - bad response!');
	}
}  // dedimania_endrace_cb


// called @ onPlayerFinish
function dedimania_playerfinish($aseco, $finish_item) {
	global $dedi_db, $dedi_debug,
	       $checkpoints;  // from plugin.checkpoints.php

	// if no Dedimania records, bail out - Stunts mode temporarily too
	if (!$dedi_db['RecsValid'] || $aseco->server->gameinfo->mode == Gameinfo::STNT) return;

	// if no actual finish, bail out immediately
	if ($finish_item->score == 0) return;

	// in Laps mode on real PlayerFinish event, bail out too
	if ($aseco->server->gameinfo->mode == Gameinfo::LAPS && !$finish_item->new) return;

	$login = $finish_item->player->login;
	$nickname = stripColors($finish_item->player->nickname);

	// if LAN login, bail out immediately
	if (isLANLogin($login)) return;

	// if banned login, notify player and bail out
	if (in_array($login, $dedi_db['BannedLogins'])) {
		$message = formatText($dedi_db['Messages']['BANNED_FINISH'][0]);
		$aseco->client->query('ChatSendServerMessageToLogin', $aseco->formatColors($message), $login);
		return;
	}

	if ($dedi_debug > 4)
		$aseco->console_text('dedimania_playerfinish - checkpoints ' . $login . CRLF . print_r($checkpoints[$login], true));

	// check finish/checkpoints consistency, unless Stunts mode
	if ($aseco->server->gameinfo->mode != Gameinfo::STNT) {
		if (($aseco->server->gameinfo->mode != Gameinfo::LAPS && $finish_item->score != $checkpoints[$login]->curr_fin) ||
		    $finish_item->score != end($checkpoints[$login]->curr_cps)) {
			$aseco->console('[Dedimania] player ' . $login . ' inconsistent finish/checks, ignored: ' . $finish_item->score . CRLF . print_r($checkpoints[$login], true));
			return;
		}
	}

	// point to master records list
	$dedi_recs = &$dedi_db['Challenge']['Records'];
	$maxrank = max($dedi_db['ServerMaxRank'], $finish_item->player->dedirank);

	// go through all records
	for ($i = 0; $i < $maxrank; $i++) {
		// check if no record, or player's time/score is better
		if (!isset($dedi_recs[$i]) || ($aseco->server->gameinfo->mode == Gameinfo::STNT ?
		                               $finish_item->score > $dedi_recs[$i]['Best'] :
		                               $finish_item->score < $dedi_recs[$i]['Best'])) {
			// does player have a record already?
			$cur_rank = -1;
			$cur_score = 0;
			for ($rank = 0; $rank < count($dedi_recs); $rank++) {
				$rec = $dedi_recs[$rank];

				if ($login == $rec['Login'] && $rec['Game'] ==
				    ($aseco->server->getGame() == 'TMF' ? 'TMU' : $aseco->server->getGame())) {
					// new record worse than old one
					if ($aseco->server->gameinfo->mode == Gameinfo::STNT ?
					    $finish_item->score < $rec['Best'] :
					    $finish_item->score > $rec['Best']) {
						return;

					// new record is better than or equal to old one
					} else {
						$cur_rank = $rank;
						$cur_score = $rec['Best'];
						break;
					}
				}
			}

			$finish_time = $finish_item->score;
			if ($aseco->server->gameinfo->mode != Gameinfo::STNT)
				$finish_time = formatTime($finish_time);

			if ($cur_rank != -1) {  // player has a record in topXX already

				// compute difference to old record
				if ($aseco->server->gameinfo->mode != Gameinfo::STNT) {
					$diff = $cur_score - $finish_item->score;
					$sec = floor($diff/1000);
					$hun = ($diff - ($sec * 1000)) / 10;
				} else {  // Stunts
					$diff = $finish_item->score - $cur_score;
				}

				// update the record if improved
				if ($diff > 0) {
					// ignore 'Rank' field - not used in /dedi* commands
					$dedi_recs[$cur_rank]['Best'] = $finish_item->score;
					$dedi_recs[$cur_rank]['Checks'] = $checkpoints[$login]->curr_cps;
					$dedi_recs[$cur_rank]['NewBest'] = true;
				}

				// player moved up in Dedimania list
				if ($cur_rank > $i) {

					// move record to the new position
					moveArrayElement($dedi_recs, $cur_rank, $i);

					// do a player improved his/her Dedimania rank message
					$message = formatText($dedi_db['Messages']['RECORD_NEW_RANK'][0],
					                      $nickname,
					                      $i+1,
					                      ($aseco->server->gameinfo->mode == Gameinfo::STNT ? 'Score' : 'Time'),
					                      $finish_time,
					                      $cur_rank+1,
					                      ($aseco->server->gameinfo->mode == Gameinfo::STNT ?
					                       '+' . $diff : sprintf('-%d.%02d', $sec, $hun)));

					// show chat message to all or player
					if ($dedi_db['DisplayRecs']) {
						if ($i < $dedi_db['LimitRecs']) {
							if ($dedi_db['RecsInWindow'] && function_exists('send_window_message'))
								send_window_message($aseco, $message, false);
							else
								$aseco->client->query('ChatSendServerMessage', $aseco->formatColors($message));
						} else {
							$message = str_replace('{#server}>> ', '{#server}> ', $message);
							$aseco->client->query('ChatSendServerMessageToLogin', $aseco->formatColors($message), $login);
						}
					}

				} else {

					if ($diff == 0) {
						// do a player equaled his/her record message
						$message = formatText($dedi_db['Messages']['RECORD_EQUAL'][0],
						                      $nickname,
						                      $cur_rank+1,
						                      ($aseco->server->gameinfo->mode == Gameinfo::STNT ? 'Score' : 'Time'),
						                      $finish_time);
					} else {
						// do a player secured his/her record message
						$message = formatText($dedi_db['Messages']['RECORD_NEW'][0],
						                      $nickname,
						                      $i+1,
						                      ($aseco->server->gameinfo->mode == Gameinfo::STNT ? 'Score' : 'Time'),
						                      $finish_time,
						                      $cur_rank+1,
						                      ($aseco->server->gameinfo->mode == Gameinfo::STNT ?
						                       '+' . $diff : sprintf('-%d.%02d', $sec, $hun)));
					}

					// show chat message to all or player
					if ($dedi_db['DisplayRecs']) {
						if ($i < $dedi_db['LimitRecs']) {
							if ($dedi_db['RecsInWindow'] && function_exists('send_window_message'))
								send_window_message($aseco, $message, false);
							else
								$aseco->client->query('ChatSendServerMessage', $aseco->formatColors($message));
						} else {
							$message = str_replace('{#server}>> ', '{#server}> ', $message);
							$aseco->client->query('ChatSendServerMessageToLogin', $aseco->formatColors($message), $login);
						}
					}
				}

			} else {  // player hasn't got a record yet

				// if previously tracking own/last Dedi record, now track new one
				if ($checkpoints[$login]->dedirec == 0) {
					$checkpoints[$login]->best_fin = $checkpoints[$login]->curr_fin;
					$checkpoints[$login]->best_cps = $checkpoints[$login]->curr_cps;
					// store timestamp for sorting in case of equal bests
					$checkpoints[$login]->best_time = microtime(true);
				}

				// insert new record at the specified position
				// ignore 'Rank' field - not used in /dedi* commands
				$record = array('Game' => ($aseco->server->getGame() == 'TMF' ? 'TMU' : $aseco->server->getGame()),
				                'Login' => $login,
				                'NickName' => $finish_item->player->nickname,
				                'Best' => $finish_item->score,
				                'Checks' => $checkpoints[$login]->curr_cps,
				                'NewBest' => true);
				insertArrayElement($dedi_recs, $record, $i);

				// do a player drove first record message
				$message = formatText($dedi_db['Messages']['RECORD_FIRST'][0],
				                      $nickname,
				                      $i+1,
				                      ($aseco->server->gameinfo->mode == Gameinfo::STNT ? 'Score' : 'Time'),
				                      $finish_time);

				// show chat message to all or player
				if ($dedi_db['DisplayRecs']) {
					if ($i < $dedi_db['LimitRecs']) {
						if ($dedi_db['RecsInWindow'] && function_exists('send_window_message'))
							send_window_message($aseco, $message, false);
						else
							$aseco->client->query('ChatSendServerMessage', $aseco->formatColors($message));
					} else {
						$message = str_replace('{#server}>> ', '{#server}> ', $message);
						$aseco->client->query('ChatSendServerMessageToLogin', $aseco->formatColors($message), $login);
					}
				}
			}

			// log a new Dedimania record (not an equalled one)
			if (isset($dedi_recs[$i]['NewBest']) && $dedi_recs[$i]['NewBest']) {
				// update all panels if new #1 record
				if ($aseco->server->getGame() == 'TMF' && $i == 0) {
					setRecordsPanel('dedi', ($aseco->server->gameinfo->mode == Gameinfo::STNT ?
					                         str_pad($finish_item->score, 5, ' ', STR_PAD_LEFT) :
					                         formatTime($finish_item->score)));
					if (function_exists('update_allrecpanels'))
						update_allrecpanels($aseco, null);  // from plugin.panels.php
				}

				// log record message in console
				$aseco->console('[Dedimania] player {1} finished with {2} and took the {3}. WR place!',
				                $login, $finish_item->score, $i+1);

				// throw 'Dedimania record' event
				$dedi_recs[$i]['Pos'] = $i+1;
				$aseco->releaseEvent('onDedimaniaRecord', $dedi_recs[$i]);
			}
			if ($dedi_debug > 1)
				$aseco->console_text('dedimania_playerfinish - dedi_recs' . CRLF . print_r($dedi_recs, true));

			// got the record, now stop!
			return;
		}
	}
}  // dedimania_playerfinish


/*
 * Support functions
 */
function dedimania_players($aseco) {
	global $dedi_debug;

	// collect all players
	$players = array();
	foreach ($aseco->server->players->player_list as $pl) {
		$pinfo = dedimania_playerinfo($aseco, $pl);
		if ($pinfo !== false)
			$players[] = $pinfo;
	}
	if ($dedi_debug > 2 || ($dedi_debug > 1 && count($players) > 0))
		$aseco->console_text('dedimania_players - players' . CRLF . print_r($players, true));
	return $players;
}  // dedimania_players

function dedimania_playerinfo($aseco, $player) {

	// check for non-LAN login
	if (!isLANLogin($player->login)) {
		$aseco->client->resetError();
		// get current player info
		if ($aseco->server->getGame() == 'TMF') {
			$aseco->client->query('GetDetailedPlayerInfo', $player->login);
			$info = $aseco->client->getResponse();

			if ($aseco->client->isError()) {
				return false;
			} else {
				$nation = explode('|', $info['Path']);
				if (isset($nation[1]))
					$nation = mapCountry($nation[1]);
				else
					$nation = mapCountry('');

				return array('Login' => $info['Login'],
				             'Nation' => $nation,
				             'TeamName' => $info['LadderStats']['TeamName'],
				             'TeamId' => -1,
				             'IsSpec' => $info['IsSpectator'],
				             'Ranking' => $info['LadderStats']['PlayerRankings'][0]['Ranking'],
				             'IsOff' => $info['IsInOfficialMode']
				            );
			}
		} else {  // TMN/TMS/TMO
			$aseco->client->query('GetPlayerInfo', $player->login);
			$info = $aseco->client->getResponse();

			if ($aseco->client->isError()) {
				return false;
			} else {
				return array('Login' => $info['Login'],
				             'Nation' => $info['Nation'],
				             'TeamName' => $info['LadderStats']['TeamName'],
				             'TeamId' => -1,
				             'IsSpec' => $info['IsSpectator'],
				             'Ranking' => $info['LadderStats']['Ranking'],
				             'IsOff' => $info['IsInOfficialMode']
				            );
			}
		}
	}
	return false;
}  // dedimania_playerinfo

function dedimania_serverinfo($aseco) {
	global $dedi_debug;

	// compute number of players and spectators
	$numplayers = 0;
	$numspecs = 0;
	foreach ($aseco->server->players->player_list as $pl) {
		if ($aseco->isSpectator($pl))
			$numspecs++;
		else
			$numplayers++;
	}

	// get current server options
	$aseco->client->query('GetServerOptions');
	$options = $aseco->client->getResponse();

	$serverinfo = array('SrvName' => $options['Name'],
	                    'Comment' => $options['Comment'],
	                    'Private' => ($options['Password'] != ''),
	                    'SrvIP' => '',
	                    'SrvPort' => 0,
	                    'XmlrpcPort' => 0,
	                    'NumPlayers' => $numplayers,
	                    'MaxPlayers' => $options['CurrentMaxPlayers'],
	                    'NumSpecs' => $numspecs,
	                    'MaxSpecs' => $options['CurrentMaxSpectators'],
	                    'LadderMode' => $options['CurrentLadderMode'],
	                    'NextFiveUID' => dedi_getnextuid($aseco)
	                   );
	if ($dedi_debug > 1)
		$aseco->console_text('dedimania_serverinfo - serverinfo' . CRLF . print_r($serverinfo, true));
	return $serverinfo;
}  // dedimania_serverinfo

function dedi_getnextuid($aseco) {
	global $jukebox;  // from plugin.rasp_jukebox.php

	// check for jukeboxed track
	if (isset($jukebox) && !empty($jukebox)) {
		$jbtemp = $jukebox;
		$track = array_shift($jbtemp);
		$next = $track['uid'];
	} else {
		// check server for next track
		if ($aseco->server->getGame() != 'TMF') {
			$aseco->client->query('GetCurrentChallengeIndex');
			$current = $aseco->client->getResponse();
			$aseco->client->query('GetChallengeList', 1, ++$current);
			$track = $aseco->client->getResponse();
			if ($aseco->client->isError()) {
				$aseco->client->query('GetChallengeList', 1, 0);
				$track = $aseco->client->getResponse();
			}
		} else {  // TMF
			$aseco->client->query('GetNextChallengeIndex');
			$next = $aseco->client->getResponse();
			$aseco->client->query('GetChallengeList', 1, $next);
			$track = $aseco->client->getResponse();
		}
		$next = $track[0]['UId'];
	}
	return $next;
}  // dedi_getnextuid

function dedi_iserror(&$response) {

	if (!isset($response))
		return 'No response!';
	if (isset($response['Error'])) {
		if (is_string($response['Error']) && strlen($response['Error']) > 0)
			return $response['Error'];
	}
	if (isset($response['Data']['errors'])) {
		if (is_string($response['Data']['errors']) && strlen($response['Data']['errors']) > 0)
			return $response['Data']['errors'];
		if (is_array($response['Data']['errors']) && count($response['Data']['errors']) > 0)
			return print_r($response['Data']['errors'], true);
	}
	return false;
}  // dedi_iserror

// usort comparison function: return -1 if $a should be before $b, 1 if vice-versa
function dedi_timecompare($a, $b) {
	global $checkpoints;  // from plugin.checkpoints.php

	// best a better than best b
	if ($a['Best'] < $b['Best'])
		return -1;
	// best b better than best a
	elseif ($a['Best'] > $b['Best'])
		return 1;
	// same best, use timestamp
	else
		return ($checkpoints[$a['Login']]->best_time < $checkpoints[$b['Login']]->best_time) ? -1 : 1;
}  // dedi_timecompare
?>
