TrackManiaControl/application/plugins/records.plugin.php
2013-11-09 11:20:11 +01:00

1217 lines
38 KiB
PHP

<?php
namespace ManiaControl;
// TODO: show mix of best and next records (depending on own one)
// TODO: enable keep-alive
// TODO: enable gzip compression
// TODO: log errors from WarningsAndTTR2
// TODO: check map requirements (cp count + length)
// TODO: check checkpoints on finish
// TODO: threaded requests
/**
* ManiaControl Records Plugin
*
* @author steeffeen
*/
class Plugin_Records {
/**
* Constants
*/
const VERSION = '1.0';
const MLID_LOCAL = 'ml_local_records';
const MLID_DEDI = 'ml_dedimania_records';
const TABLE_RECORDS = 'ic_records';
const XMLRPC_MULTICALL = 'system.multicall';
const DEDIMANIA_URL = 'http://dedimania.net:8081/Dedimania';
const DEDIMANIA_OPENSESSION = 'dedimania.OpenSession';
const DEDIMANIA_CHECKSESSION = 'dedimania.CheckSession';
const DEDIMANIA_GETRECORDS = 'dedimania.GetChallengeRecords';
const DEDIMANIA_PLAYERCONNECT = 'dedimania.PlayerConnect';
const DEDIMANIA_PLAYERDISCONNECT = 'dedimania.PlayerDisconnect';
const DEDIMANIA_UPDATESERVERPLAYERS = 'dedimania.UpdateServerPlayers';
const DEDIMANIA_SETCHALLENGETIMES = 'dedimania.SetChallengeTimes';
const DEDIMANIA_WARNINGSANDTTR2 = 'dedimania.WarningsAndTTR2';
/**
* Private properties
*/
private $mControl = null;
private $settings = null;
private $config = null;
private $mapInfo = null;
private $manialinks = array();
private $lastSendManialinks = array();
private $updateManialinks = array();
private $dedimaniaData = array();
private $checkpoints = array();
/**
* Constuct plugin
*/
public function __construct($mControl) {
$this->mControl = $mControl;
// Load config
$this->config = Tools::loadConfig('records.plugin.xml');
// Check for enabled setting
if (!Tools::toBool($this->config->enabled)) return;
// Load settings
$this->loadSettings();
// Init tables
$this->initTables();
// Register for callbacks
$this->iControl->callbacks->registerCallbackHandler(Callbacks::CB_IC_ONINIT, $this, 'handleOnInit');
$this->iControl->callbacks->registerCallbackHandler(Callbacks::CB_IC_1_SECOND, $this, 'handle1Second');
$this->iControl->callbacks->registerCallbackHandler(Callbacks::CB_IC_1_MINUTE, $this, 'handle1Minute');
$this->iControl->callbacks->registerCallbackHandler(Callbacks::CB_IC_3_MINUTE, $this, 'handle3Minute');
$this->iControl->callbacks->registerCallbackHandler(Callbacks::CB_IC_BEGINMAP, $this, 'handleMapBegin');
$this->iControl->callbacks->registerCallbackHandler(Callbacks::CB_IC_CLIENTUPDATED, $this, 'handleClientUpdated');
$this->iControl->callbacks->registerCallbackHandler(Callbacks::CB_IC_ENDMAP, $this, 'handleMapEnd');
$this->iControl->callbacks->registerCallbackHandler(Callbacks::CB_MP_PLAYERCONNECT, $this, 'handlePlayerConnect');
$this->iControl->callbacks->registerCallbackHandler(Callbacks::CB_MP_PLAYERDISCONNECT, $this, 'handlePlayerDisconnect');
$this->iControl->callbacks->registerCallbackHandler(Callbacks::CB_TM_PLAYERFINISH, $this, 'handlePlayerFinish');
$this->iControl->callbacks->registerCallbackHandler(Callbacks::CB_TM_PLAYERCHECKPOINT, $this, 'handlePlayerCheckpoint');
error_log('Records Pugin v' . self::VERSION . ' ready!');
}
/**
* Init needed database tables
*/
private function initTables() {
$database = $this->iControl->database;
// Records table
$query = "CREATE TABLE IF NOT EXISTS `" . self::TABLE_RECORDS . "` (
`index` int(11) NOT NULL AUTO_INCREMENT,
`mapUId` varchar(100) NOT NULL,
`Login` varchar(100) NOT NULL,
`time` int(11) NOT NULL,
`changed` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`index`),
UNIQUE KEY `player_map_record` (`mapUId`,`Login`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8 AUTO_INCREMENT=1;";
if (!$database->query($query)) {
trigger_error("Couldn't create records table. " . $database->mysqli->error);
}
}
/**
* Load settings from config
*/
private function loadSettings() {
$this->settings = new \stdClass();
$this->settings->enabled = Tools::toBool($this->config->enabled);
$this->settings->local_records_enabled = $this->settings->enabled && Tools::toBool($this->config->local_records->enabled);
$this->settings->dedimania_enabled = $this->settings->enabled && Tools::toBool($this->config->dedimania_records->enabled);
}
/**
* Handle ManiaControl init
*/
public function handleOnInit($callback = null) {
// Let manialinks update
if ($this->settings->local_records_enabled) {
$this->updateManialinks[self::MLID_LOCAL] = true;
}
// Update mapinfo
$this->mapInfo = $this->getMapInfo();
if ($this->settings->dedimania_enabled) {
// Open dedimania session
$accounts = $this->config->xpath('dedimania_records/account');
if (!$accounts) {
trigger_error('Invalid dedimania_code in config.');
$this->settings->dedimania_enabled = false;
}
else {
$this->openDedimaniaSession();
}
$this->fetchDedimaniaRecords();
$this->updateManialinks[self::MLID_DEDI] = true;
}
}
/**
* Fetch dedimania records of the current map
*/
private function fetchDedimaniaRecords($reset = true) {
if (!isset($this->dedimaniaData['context'])) return false;
if ($reset || !isset($this->dedimaniaData['records']) && !is_array($this->dedimaniaData['records'])) {
// Reset records
$this->dedimaniaData['records'] = array();
}
// Fetch records
$servInfo = $this->getSrvInfo();
$playerInfo = $this->getPlayerList();
$gameMode = $this->getGameModeString();
$data = array($this->dedimaniaData['sessionId'], $this->mapInfo, $gameMode, $servInfo, $playerInfo);
$request = $this->encode_request(self::DEDIMANIA_GETRECORDS, $data);
stream_context_set_option($this->dedimaniaData['context'], 'http', 'content', $request);
$file = file_get_contents(self::DEDIMANIA_URL, false, $this->dedimaniaData['context']);
// Handle response
$response = $this->decode($file);
if (is_array($response)) {
foreach ($response as $index => $methodResponse) {
if (xmlrpc_is_fault($methodResponse)) {
$this->handleXmlRpcFault($methodResponse);
return false;
}
else if ($index <= 0) {
$responseData = $methodResponse[0];
$this->dedimaniaData['records'] = $responseData;
}
}
}
return true;
}
/**
* Checks dedimania session
*/
private function checkDedimaniaSession() {
if (!$this->settings->dedimania_enabled) return false;
if (!isset($this->dedimaniaData['context'])) return false;
if (!isset($this->dedimaniaData['sessionId']) || !is_string($this->dedimaniaData['sessionId'])) return false;
// Check session
$request = $this->encode_request(self::DEDIMANIA_CHECKSESSION, array($this->dedimaniaData['sessionId']));
stream_context_set_option($this->dedimaniaData['context'], 'http', 'content', $request);
$file = file_get_contents(self::DEDIMANIA_URL, false, $this->dedimaniaData['context']);
// Handle response
$response = $this->decode($file);
$result = false;
if (is_array($response)) {
foreach ($response as $methodResponse) {
if (xmlrpc_is_fault($methodResponse)) {
$this->handleXmlRpcFault($methodResponse);
}
else {
$responseData = $methodResponse[0];
if (is_bool($responseData)) {
$result = $responseData;
}
}
}
}
return $result;
}
/**
* Renews dedimania session
*/
private function openDedimaniaSession($init = false) {
if (!$this->settings->dedimania_enabled) return false;
// Get server data
if ($init || !array_key_exists('serverData', $this->dedimaniaData) || !is_array($this->dedimaniaData['serverData'])) {
$serverData = array();
$serverData['Game'] = 'TM2';
$serverInfo = $this->iControl->server->getInfo(true);
// Get dedimania account data
$accounts = $this->config->xpath('dedimania_records/account');
foreach ($accounts as $account) {
$login = (string) $account->login;
if ($login != $serverInfo['Login']) continue;
$code = (string) $account->code;
$serverData['Login'] = $login;
$serverData['Code'] = $code;
break;
}
if (!isset($serverData['Login']) || !isset($serverData['Code'])) {
// Wrong configuration for current server
trigger_error("Records Plugin: Invalid dedimania configuration for login '" . $serverInfo['Login'] . "'.");
if (isset($this->dedimaniaData['context'])) unset($this->dedimaniaData['context']);
if (isset($this->dedimaniaData['sessionId'])) unset($this->dedimaniaData['sessionId']);
return false;
}
// Complete seesion data
$serverData['Path'] = $serverInfo['Path'];
$systemInfo = $this->iControl->server->getSystemInfo();
$serverData['Packmask'] = substr($systemInfo['TitleId'], 2);
$serverVersion = $this->iControl->server->getVersion();
$serverData['ServerVersion'] = $serverVersion['Version'];
$serverData['ServerBuild'] = $serverVersion['Build'];
$serverData['Tool'] = 'ManiaControl';
$serverData['Version'] = ManiaControl::VERSION;
$this->dedimaniaData['serverData'] = $serverData;
}
// Init header
if ($init || !array_key_exists('header', $this->dedimaniaData) || !is_array($this->dedimaniaData['header'])) {
$header = '';
$header .= 'Accept-Charset: utf-8;' . PHP_EOL;
$header .= 'Accept-Encoding: gzip;' . PHP_EOL;
$header .= 'Content-Type: text/xml; charset=utf-8;' . PHP_EOL;
$header .= 'Keep-Alive: 300;' . PHP_EOL;
$header .= 'User-Agent: ManiaControl v' . ManiaControl::VERSION . ';' . PHP_EOL;
$this->dedimaniaData['header'] = $header;
}
// Open session
$request = $this->encode_request(self::DEDIMANIA_OPENSESSION, array($this->dedimaniaData['serverData']));
$context = stream_context_create(array('http' => array('method' => 'POST', 'header' => $this->dedimaniaData['header'])));
stream_context_set_option($context, 'http', 'content', $request);
$file = file_get_contents(self::DEDIMANIA_URL, false, $context);
// Handle response
$response = $this->decode($file);
$result = false;
if (is_array($response)) {
foreach ($response as $index => $methodResponse) {
if (xmlrpc_is_fault($methodResponse)) {
$this->handleXmlRpcFault($methodResponse);
}
else if ($index <= 0) {
$responseData = $methodResponse[0];
$this->dedimaniaData['context'] = $context;
$this->dedimaniaData['sessionId'] = $responseData['SessionId'];
$result = true;
}
}
}
if ($result) error_log("Dedimania connection successfully established.");
return $result;
}
/**
* Handle 1Second callback
*/
public function handle1Second() {
if (!$this->settings->enabled) return;
// Send records manialinks if needed
foreach ($this->updateManialinks as $id => $update) {
if (!$update) continue;
if (array_key_exists($id, $this->lastSendManialinks) && $this->lastSendManialinks[$id] + 2 > time()) continue;
switch ($id) {
case self::MLID_LOCAL:
{
$this->manialinks[$id] = $this->buildLocalManialink();
break;
}
case self::MLID_DEDI:
{
$this->manialinks[$id] = $this->buildDedimaniaManialink();
break;
}
default:
{
continue 2;
break;
}
}
$this->updateManialinks[$id] = false;
$this->lastSendManialinks[$id] = time();
$this->sendManialink($this->manialinks[$id]);
}
}
/**
* Handle 1Minute callback
*/
public function handle1Minute($callback = null) {
if ($this->settings->dedimania_enabled) {
// Keep dedimania session alive
if (!$this->checkDedimaniaSession()) {
// Renew session
$this->openDedimaniaSession();
}
}
}
/**
* Handle PlayerConnect callback
*/
public function handlePlayerConnect($callback) {
$login = $callback[1][0];
if ($this->settings->local_records_enabled) $this->sendManialink($this->manialinks[self::MLID_LOCAL], $login);
if ($this->settings->dedimania_enabled && $this->dedimaniaData['context']) {
$player = $this->iControl->server->getPlayer($login, true);
if ($player) {
// Send dedimania request
$data = array($this->dedimaniaData['sessionId'], $player['Login'], $player['NickName'], $player['Path'],
$player['IsSpectator']);
$request = $this->encode_request(self::DEDIMANIA_PLAYERCONNECT, $data);
stream_context_set_option($this->dedimaniaData['context'], 'http', 'content', $request);
$file = file_get_contents(self::DEDIMANIA_URL, false, $this->dedimaniaData['context']);
// Handle response
$response = $this->decode($file);
if (is_array($response)) {
foreach ($response as $methodResponse) {
if (xmlrpc_is_fault($methodResponse)) {
$this->handleXmlRpcFault($methodResponse);
}
}
}
else {
if (!$response) {
trigger_error('XmlRpc Error.');
var_dump($response);
}
}
}
$this->sendManialink($this->manialinks[self::MLID_DEDI], $login);
}
}
/**
* Handle PlayerDisconnect callback
*/
public function handlePlayerDisconnect($callback) {
$login = $callback[1][0];
if ($this->settings->dedimania_enabled && $this->dedimaniaData['context']) {
// Send dedimania request
$data = array($this->dedimaniaData['sessionId'], $login, '');
$request = $this->encode_request(self::DEDIMANIA_PLAYERDISCONNECT, $data);
stream_context_set_option($this->dedimaniaData['context'], 'http', 'content', $request);
$file = file_get_contents(self::DEDIMANIA_URL, false, $this->dedimaniaData['context']);
// Handle response
$response = $this->decode($file);
if (is_array($response)) {
foreach ($response as $methodResponse) {
if (xmlrpc_is_fault($methodResponse)) {
$this->handleXmlRpcFault($methodResponse);
}
}
}
else {
if (!$response) {
trigger_error('XmlRpc Error.');
var_dump($response);
}
}
}
}
/**
* Handle BeginMap callback
*/
public function handleMapBegin($callback) {
// Update map
$this->mapInfo = $this->getMapInfo();
if ($this->settings->local_records_enabled) {
// Update local records
$this->updateManialinks[self::MLID_LOCAL] = true;
}
if ($this->settings->dedimania_enabled) {
// Update dedimania records
$this->fetchDedimaniaRecords(true);
$this->updateManialinks[self::MLID_DEDI] = true;
}
}
/**
* Build map info struct for dedimania requests
*/
private function getMapInfo() {
$map = $this->iControl->server->getMap();
if (!$map) return null;
$mapInfo = array();
$mapInfo['UId'] = $map['UId'];
$mapInfo['Name'] = $map['Name'];
$mapInfo['Author'] = $map['Author'];
$mapInfo['Environment'] = $map['Environnement'];
$mapInfo['NbCheckpoints'] = $map['NbCheckpoints'];
$mapInfo['NbLaps'] = $map['NbLaps'];
return $mapInfo;
}
/**
* Handle EndMap callback
*/
public function handleMapEnd($callback) {
if ($this->settings->dedimania_enabled) {
// Send dedimania records
$gameMode = $this->getGameModeString();
$times = array();
$replays = array();
foreach ($this->dedimaniaData['records']['Records'] as $record) {
if (!isset($record['New']) || !$record['New']) continue;
array_push($times, array('Login' => $record['Login'], 'Best' => $record['Best'], 'Checks' => $record['Checks']));
if (!isset($replays['VReplay'])) {
$replays['VReplay'] = $record['VReplay'];
}
if (!isset($replays['Top1GReplay']) && isset($record['Top1GReplay'])) {
$replays['Top1GReplay'] = $record['Top1GReplay'];
}
// TODO: VReplayChecks
}
if (!isset($replays['VReplay'])) $replays['VReplay'] = '';
if (!isset($replays['VReplayChecks'])) $replays['VReplayChecks'] = '';
if (!isset($replays['Top1GReplay'])) $replays['Top1GReplay'] = '';
xmlrpc_set_type($replays['VReplay'], 'base64');
xmlrpc_set_type($replays['Top1GReplay'], 'base64');
$data = array($this->dedimaniaData['sessionId'], $this->mapInfo, $gameMode, $times, $replays);
$request = $this->encode_request(self::DEDIMANIA_SETCHALLENGETIMES, $data);
stream_context_set_option($this->dedimaniaData['context'], 'http', 'content', $request);
$file = file_get_contents(self::DEDIMANIA_URL, false, $this->dedimaniaData['context']);
// Handle response
$response = $this->decode($file);
if (is_array($response)) {
foreach ($response as $index => $methodResponse) {
if (xmlrpc_is_fault($methodResponse)) {
$this->handleXmlRpcFault($methodResponse);
}
else {
if ($index <= 0) {
// Called method response
$responseData = $methodResponse[0];
if (!$responseData) {
trigger_error("Records Plugin: Submitting dedimania records failed.");
}
continue;
}
// Warnings and TTR
$errors = $methodResponse[0]['methods'][0]['errors'];
if ($errors) {
trigger_error($errors);
}
}
}
}
}
}
/**
* Get current checkpoint string for dedimania record
*
* @param string $login
* @return string
*/
private function getChecks($login) {
if (!$login || !isset($this->checkpoints[$login])) return null;
$string = '';
$count = count($this->checkpoints[$login]);
foreach ($this->checkpoints[$login] as $index => $check) {
$string .= $check;
if ($index < $count - 1) $string .= ',';
}
return $string;
}
/**
* Build server info struct for callbacks
*/
private function getSrvInfo() {
$server = $this->iControl->server->getOptions();
if (!$server) return null;
$client = null;
$players = null;
$spectators = null;
$this->iControl->server->getPlayers($client, $players, $spectators);
if (!is_array($players) || !is_array($spectators)) return null;
return array('SrvName' => $server['Name'], 'Comment' => $server['Comment'], 'Private' => (strlen($server['Password']) > 0),
'NumPlayers' => count($players), 'MaxPlayers' => $server['CurrentMaxPlayers'], 'NumSpecs' => count($spectators),
'MaxSpecs' => $server['CurrentMaxSpectators']);
}
/**
* Build simple player list for callbacks
*/
private function getPlayerList($votes = false) {
$client = null;
$players;
$spectators;
$allPlayers = $this->iControl->server->getPlayers($client, $players, $spectators);
if (!is_array($players) || !is_array($spectators)) return null;
$playerInfo = array();
foreach ($allPlayers as $player) {
array_push($playerInfo, array('Login' => $player['Login'], 'IsSpec' => in_array($player, $spectators)));
}
return $playerInfo;
}
/**
* Get dedi string representation of the current game mode
*/
private function getGameModeString() {
$gameMode = $this->iControl->server->getGameMode();
if ($gameMode === null) {
trigger_error("Couldn't retrieve game mode. " . $this->iControl->getClientErrorText());
return null;
}
switch ($gameMode) {
case 1:
case 3:
case 5:
{
return 'Rounds';
}
case 2:
case 4:
{
return 'TA';
}
}
return null;
}
/**
* Build votes info struct for callbacks
*/
private function getVotesInfo() {
$map = $this->iControl->server->getMap();
if (!$map) return null;
$gameMode = $this->getGameModeString();
if (!$gameMode) return null;
return array('UId' => $map['UId'], 'GameMode' => $gameMode);
}
/**
* Handle 3Minute callback
*/
public function handle3Minute($callback = null) {
if ($this->settings->dedimania_enabled) {
// Update dedimania players
$servInfo = $this->getSrvInfo();
$votesInfo = $this->getVotesInfo();
$playerList = $this->getPlayerList(true);
if ($servInfo && $votesInfo && $playerList) {
$data = array($this->dedimaniaData['sessionId'], $servInfo, $votesInfo, $playerList);
$request = $this->encode_request(self::DEDIMANIA_UPDATESERVERPLAYERS, $data);
stream_context_set_option($this->dedimaniaData['context'], 'http', 'content', $request);
$file = file_get_contents(self::DEDIMANIA_URL, false, $this->dedimaniaData['context']);
// Handle response
$response = $this->decode($file);
if (is_array($response)) {
foreach ($response as $methodResponse) {
if (xmlrpc_is_fault($methodResponse)) {
$this->handleXmlRpcFault($methodResponse);
}
}
}
else if (!$response) {
trigger_error('XmlRpc Error.');
var_dump($response);
}
}
}
}
/**
* Handle PlayerCheckpoint callback
*/
public function handlePlayerCheckpoint($callback) {
$data = $callback[1];
$login = $data[1];
$time = $data[2];
$lap = $data[3];
$cpIndex = $data[4];
if (!isset($this->checkpoints[$login]) || $cpIndex <= 0) $this->checkpoints[$login] = array();
$this->checkpoints[$login][$cpIndex] = $time;
}
/**
* Handle PlayerFinish callback
*/
public function handlePlayerFinish($callback) {
$data = $callback[1];
if ($data[0] <= 0 || $data[2] <= 0) return;
$login = $data[1];
$time = $data[2];
$newMap = $this->iControl->server->getMap();
if (!$newMap) return;
if (!$this->mapInfo || $this->mapInfo['UId'] !== $newMap['UId']) {
$this->mapInfo = $this->getMapInfo();
}
$map = $newMap;
$player = $this->iControl->server->getPlayer($login);
if ($this->settings->local_records_enabled) {
// Get old record of the player
$oldRecord = $this->getLocalRecord($map['UId'], $login);
$save = true;
if ($oldRecord) {
if ($oldRecord['time'] < $time) {
// Not improved
$save = false;
}
else if ($oldRecord['time'] == $time) {
// Same time
$message = '$<' . $player['NickName'] . '$> equalized her/his $<$o' . $oldRecord['rank'] . '.$> Local Record: ' .
Tools::formatTime($oldRecord['time']);
$this->iControl->chat->sendInformation($message);
$save = false;
}
}
if ($save) {
// Save time
$database = $this->iControl->database;
$query = "INSERT INTO `" . self::TABLE_RECORDS . "` (
`mapUId`,
`Login`,
`time`
) VALUES (
'" . $database->escape($map['UId']) . "',
'" . $database->escape($login) . "',
" . $time . "
) ON DUPLICATE KEY UPDATE
`time` = VALUES(`time`);";
if (!$database->query($query)) {
trigger_error("Couldn't save player record. " . $database->mysqli->error);
}
else {
// Announce record
$newRecord = $this->getLocalRecord($map['UId'], $login);
if ($oldRecord == null || $newRecord['rank'] < $oldRecord['rank']) {
// Gained rank
$improvement = 'gained the';
}
else {
// Only improved time
$improvement = 'improved her/his';
}
$message = '$<' . $player['NickName'] . '$> ' . $improvement . ' $<$o' . $newRecord['rank'] . '.$> Local Record: ' .
Tools::formatTime($newRecord['time']);
$this->iControl->chat->sendInformation($message);
$this->updateManialinks[self::MLID_LOCAL] = true;
}
}
}
if ($this->settings->dedimania_enabled) {
// Get old record of the player
$oldRecord = $this->getDediRecord($login);
$save = true;
if ($oldRecord) {
if ($oldRecord['Best'] < $time) {
// Not improved
$save = false;
}
else if ($oldRecord['Best'] == $time) {
// Same time
$save = false;
}
}
if ($save) {
// Save time
$newRecord = array('Login' => $login, 'NickName' => $player['NickName'], 'Best' => $data[2],
'Checks' => $this->getChecks($login), 'New' => true);
$inserted = $this->insertDediRecord($newRecord, $oldRecord);
if ($inserted) {
// Get newly saved record
foreach ($this->dedimaniaData['records']['Records'] as $key => &$record) {
if ($record['Login'] !== $newRecord['Login']) continue;
$newRecord = $record;
break;
}
// Announce record
if (!$oldRecord || $newRecord['Rank'] < $oldRecord['Rank']) {
// Gained rank
$improvement = 'gained the';
}
else {
// Only improved time
$improvement = 'improved her/his';
}
$message = '$<' . $player['NickName'] . '$> ' . $improvement . ' $<$o' . $newRecord['Rank'] .
'.$> Dedimania Record: ' . Tools::formatTime($newRecord['Best']);
$this->iControl->chat->sendInformation($message);
$this->updateManialinks[self::MLID_DEDI] = true;
}
}
}
}
/**
* Get max rank for given login
*/
private function getMaxRank($login) {
if (!isset($this->dedimaniaData['records'])) return null;
$records = $this->dedimaniaData['records'];
$maxRank = $records['ServerMaxRank'];
foreach ($records['Players'] as $player) {
if ($player['Login'] === $login) {
if ($player['MaxRank'] > $maxRank) $maxRank = $player['MaxRank'];
break;
}
}
return $maxRank;
}
/**
* Inserts the given new dedimania record at the proper position
*
* @param struct $newRecord
* @return bool
*/
private function insertDediRecord(&$newRecord, $oldRecord) {
if (!$newRecord || !isset($this->dedimaniaData['records']) || !isset($this->dedimaniaData['records']['Records'])) return false;
$insert = false;
$newRecords = array();
// Get max possible rank
$maxRank = $this->getMaxRank($newRecord['Login']);
if (!$maxRank) $maxRank = 30;
// Loop through existing records
foreach ($this->dedimaniaData['records']['Records'] as $key => &$record) {
if ($record['Rank'] > $maxRank) {
// Max rank reached
return false;
}
if ($record['Login'] === $newRecord['Login']) {
// Old record of the same player
if ($record['Best'] <= $newRecord['Best']) {
// It's better - Do nothing
return false;
}
// Replace old record
unset($this->dedimaniaData['records']['Records'][$key]);
$insert = true;
break;
}
// Other player's record
if ($record['Best'] <= $newRecord['Best']) {
// It's better - Skip
continue;
}
// New record is better - Insert it
$insert = true;
if ($oldRecord) {
// Remove old record
foreach ($this->dedimaniaData['records']['Records'] as $key2 => $record2) {
if ($record2['Login'] !== $oldRecord['Login']) continue;
unset($this->dedimaniaData['records']['Records'][$key2]);
break;
}
}
break;
}
if (!$insert && count($this->dedimaniaData['records']['Records']) < $maxRank) {
// Records list not full - Append new record
$insert = true;
}
if ($insert) {
// Insert new record
array_push($this->dedimaniaData['records']['Records'], $newRecord);
// Update ranks
$this->updateDediRecordRanks();
// Save replays
foreach ($this->dedimaniaData['records']['Records'] as $key => &$record) {
if ($record['Login'] !== $newRecord['Login']) continue;
$this->setRecordReplays($record);
break;
}
// Record inserted
return true;
}
// No new record
return false;
}
/**
* Updates the replay values for the given record
*
* @param struct $record
*/
private function setRecordReplays(&$record) {
if (!$record || !$this->settings->dedimania_enabled) return;
// Set validation replay
$validationReplay = $this->iControl->server->getValidationReplay($record['Login']);
if ($validationReplay) $record['VReplay'] = $validationReplay;
// Set ghost replay
if ($record['Rank'] <= 1) {
$dataDirectory = $this->iControl->server->getDataDirectory();
if (!isset($this->dedimaniaData['directoryAccessChecked'])) {
$access = $this->iControl->server->checkAccess($dataDirectory);
if (!$access) {
trigger_error("No access to the servers data directory. Can't retrieve ghost replays.");
}
$this->dedimaniaData['directoryAccessChecked'] = $access;
}
if ($this->dedimaniaData['directoryAccessChecked']) {
$ghostReplay = $this->iControl->server->getGhostReplay($record['Login']);
if ($ghostReplay) $record['Top1GReplay'] = $ghostReplay;
}
}
}
/**
* Update the sorting and the ranks of all dedimania records
*/
private function updateDediRecordRanks() {
if (!isset($this->dedimaniaData['records']) || !isset($this->dedimaniaData['records']['Records'])) return;
// Sort records
usort($this->dedimaniaData['records']['Records'], array($this, 'compareRecords'));
// Update ranks
$rank = 1;
foreach ($this->dedimaniaData['records']['Records'] as &$record) {
$record['Rank'] = $rank;
$rank++;
}
}
/**
* Compare function for sorting dedimania records
*
* @param struct $first
* @param struct $second
* @return int
*/
private function compareRecords($first, $second) {
if ($first['Best'] < $second['Best']) {
return -1;
}
else if ($first['Best'] > $second['Best']) {
return 1;
}
else {
if ($first['Rank'] < $second['Rank']) {
return -1;
}
else {
return 1;
}
}
}
/**
* Get the dedimania record of the given login
*
* @param string $login
* @return struct
*/
private function getDediRecord($login) {
if (!isset($this->dedimaniaData['records'])) return null;
$records = $this->dedimaniaData['records']['Records'];
foreach ($records as $index => $record) {
if ($record['Login'] === $login) return $record;
}
return null;
}
/**
* Send manialink to clients
*/
private function sendManialink($manialink, $login = null) {
if (!$manialink || !$this->iControl->client) return;
if (!$login) {
if (!$this->iControl->client->query('SendDisplayManialinkPage', $manialink->asXML(), 0, false)) {
trigger_error("Couldn't send manialink to players. " . $this->iControl->getClientErrorText());
}
}
else {
if (!$this->iControl->client->query('SendDisplayManialinkPageToLogin', $login, $manialink->asXML(), 0, false)) {
trigger_error("Couldn't send manialink to player '" . $login . "'. " . $this->iControl->getClientErrorText());
}
}
}
/**
* Handle ClientUpdated callback
*
* @param mixed $data
*/
public function handleClientUpdated($data) {
$this->openDedimaniaSession(true);
if (isset($this->updateManialinks[self::MLID_LOCAL])) $this->updateManialinks[self::MLID_LOCAL] = true;
if (isset($this->updateManialinks[self::MLID_DEDI])) $this->updateManialinks[self::MLID_DEDI] = true;
}
/**
* Update local records manialink
*/
private function buildLocalManialink() {
$map = $this->iControl->server->getMap();
if (!$map) {
return null;
}
$pos_x = (float) $this->config->local_records->widget->pos_x;
$pos_y = (float) $this->config->local_records->widget->pos_y;
$title = (string) $this->config->local_records->widget->title;
$width = (float) $this->config->local_records->widget->width;
$lines = (int) $this->config->local_records->widget->lines;
$line_height = (float) $this->config->local_records->widget->line_height;
$recordResult = $this->getLocalRecords($map['UId']);
if (!$recordResult) {
trigger_error("Couldn't fetch player records.");
return null;
}
$xml = Tools::newManialinkXml(self::MLID_LOCAL);
$frame = $xml->addChild('frame');
$frame->addAttribute('posn', $pos_x . ' ' . $pos_y);
// Background
$quad = $frame->addChild('quad');
Tools::addAlignment($quad, 'center', 'top');
$quad->addAttribute('sizen', ($width * 1.05) . ' ' . (7. + $lines * $line_height));
$quad->addAttribute('style', 'Bgs1InRace');
$quad->addAttribute('substyle', 'BgTitleShadow');
// Title
$label = $frame->addChild('label');
Tools::addAlignment($label);
Tools::addTranslate($xml);
$label->addAttribute('posn', '0 ' . ($line_height * -0.9));
$label->addAttribute('sizen', $width . ' 0');
$label->addAttribute('style', 'TextTitle1');
$label->addAttribute('textsize', '2');
$label->addAttribute('text', $title);
// Times
$index = 0;
while ($record = $recordResult->fetch_assoc()) {
$y = -8. - $index * $line_height;
$recordFrame = $frame->addChild('frame');
$recordFrame->addAttribute('posn', '0 ' . $y);
// Background
$quad = $recordFrame->addChild('quad');
Tools::addAlignment($quad);
$quad->addAttribute('sizen', $width . ' ' . $line_height);
$quad->addAttribute('style', 'Bgs1InRace');
$quad->addAttribute('substyle', 'BgTitleGlow');
// Rank
$label = $recordFrame->addChild('label');
Tools::addAlignment($label, 'left');
$label->addAttribute('posn', ($width * -0.47) . ' 0');
$label->addAttribute('sizen', ($width * 0.06) . ' ' . $line_height);
$label->addAttribute('textsize', '1');
$label->addAttribute('textprefix', '$o');
$label->addAttribute('text', $record['rank']);
// Name
$label = $recordFrame->addChild('label');
Tools::addAlignment($label, 'left');
$label->addAttribute('posn', ($width * -0.4) . ' 0');
$label->addAttribute('sizen', ($width * 0.6) . ' ' . $line_height);
$label->addAttribute('textsize', '1');
$label->addAttribute('text', $record['NickName']);
// Time
$label = $recordFrame->addChild('label');
Tools::addAlignment($label, 'right');
$label->addAttribute('posn', ($width * 0.47) . ' 0');
$label->addAttribute('sizen', ($width * 0.25) . ' ' . $line_height);
$label->addAttribute('textsize', '1');
$label->addAttribute('text', Tools::formatTime($record['time']));
$index++;
}
return $xml;
}
/**
* Update dedimania records manialink
*/
private function buildDedimaniaManialink() {
if (!isset($this->dedimaniaData['records'])) {
return;
}
$records = $this->dedimaniaData['records']['Records'];
$pos_x = (float) $this->config->dedimania_records->widget->pos_x;
$pos_y = (float) $this->config->dedimania_records->widget->pos_y;
$title = (string) $this->config->dedimania_records->widget->title;
$width = (float) $this->config->dedimania_records->widget->width;
$lines = (int) $this->config->dedimania_records->widget->lines;
$line_height = (float) $this->config->dedimania_records->widget->line_height;
$xml = Tools::newManialinkXml(self::MLID_DEDI);
$frame = $xml->addChild('frame');
$frame->addAttribute('posn', $pos_x . ' ' . $pos_y);
// Background
$quad = $frame->addChild('quad');
Tools::addAlignment($quad, 'center', 'top');
$quad->addAttribute('sizen', ($width * 1.05) . ' ' . (7. + $lines * $line_height));
$quad->addAttribute('style', 'Bgs1InRace');
$quad->addAttribute('substyle', 'BgTitleShadow');
// Title
$label = $frame->addChild('label');
Tools::addAlignment($label);
Tools::addTranslate($xml);
$label->addAttribute('posn', '0 ' . ($line_height * -0.9));
$label->addAttribute('sizen', $width . ' 0');
$label->addAttribute('style', 'TextTitle1');
$label->addAttribute('textsize', '2');
$label->addAttribute('text', $title);
// Times
foreach ($records as $index => $record) {
$y = -8. - $index * $line_height;
$recordFrame = $frame->addChild('frame');
$recordFrame->addAttribute('posn', '0 ' . $y);
// Background
$quad = $recordFrame->addChild('quad');
Tools::addAlignment($quad);
$quad->addAttribute('sizen', $width . ' ' . $line_height);
$quad->addAttribute('style', 'Bgs1InRace');
$quad->addAttribute('substyle', 'BgTitleGlow');
// Rank
$label = $recordFrame->addChild('label');
Tools::addAlignment($label, 'left');
$label->addAttribute('posn', ($width * -0.47) . ' 0');
$label->addAttribute('sizen', ($width * 0.06) . ' ' . $line_height);
$label->addAttribute('textsize', '1');
$label->addAttribute('textprefix', '$o');
$label->addAttribute('text', $record['Rank']);
// Name
$label = $recordFrame->addChild('label');
Tools::addAlignment($label, 'left');
$label->addAttribute('posn', ($width * -0.4) . ' 0');
$label->addAttribute('sizen', ($width * 0.6) . ' ' . $line_height);
$label->addAttribute('textsize', '1');
$label->addAttribute('text', $record['NickName']);
// Time
$label = $recordFrame->addChild('label');
Tools::addAlignment($label, 'right');
$label->addAttribute('posn', ($width * 0.47) . ' 0');
$label->addAttribute('sizen', ($width * 0.25) . ' ' . $line_height);
$label->addAttribute('textsize', '1');
$label->addAttribute('text', Tools::formatTime($record['Best']));
if ($index >= $lines - 1) break;
}
return $xml;
}
/**
* Fetch local records for the given map
*
* @param string $mapUId
* @param int $limit
* @return array
*/
private function getLocalRecords($mapUId, $limit = -1) {
$query = "SELECT * FROM (
SELECT recs.*, @rank := @rank + 1 as `rank` FROM `" . self::TABLE_RECORDS . "` recs, (SELECT @rank := 0) ra
WHERE recs.`mapUId` = '" . $this->iControl->database->escape($mapUId) . "'
ORDER BY recs.`time` ASC
" . ($limit > 0 ? "LIMIT " . $limit : "") . ") records
LEFT JOIN `" . Database::TABLE_PLAYERS . "` players
ON records.`Login` = players.`Login`;";
return $this->iControl->database->query($query);
}
/**
* Retrieve the local record for the given map and login
*
* @param string $mapUId
* @param string $login
* @return array
*/
private function getLocalRecord($mapUId, $login) {
if (!$mapUId || !$login) return null;
$database = $this->iControl->database;
$query = "SELECT records.* FROM (
SELECT recs.*, @rank := @rank + 1 as `rank` FROM `" . self::TABLE_RECORDS . "` `recs`, (SELECT @rank := 0) r
WHERE recs.`mapUid` = '" . $database->escape($mapUId) . "'
ORDER BY recs.`time` ASC) `records`
WHERE records.`Login` = '" . $database->escape($login) . "';";
$result = $database->query($query);
if (!$result || !is_object($result)) {
trigger_error("Couldn't retrieve player record for '" . $login . "'." . $database->mysqli->error);
return null;
}
while ($record = $result->fetch_assoc()) {
return $record;
}
return null;
}
/**
* Encodes the given xml rpc method and params
*
* @param string $method
* @param array $params
* @return string
*/
private function encode_request($method, $params) {
$paramArray = array(array('methodName' => $method, 'params' => $params),
array('methodName' => self::DEDIMANIA_WARNINGSANDTTR2, 'params' => array()));
return xmlrpc_encode_request(self::XMLRPC_MULTICALL, array($paramArray), array('encoding' => 'UTF-8', 'escaping' => 'markup'));
}
/**
* Decodes xml rpc response
*
* @param string $response
* @return mixed
*/
private function decode($response) {
return xmlrpc_decode($response, 'utf-8');
}
/**
* Handles xml rpc fault
*
* @param struct $fault
*/
private function handleXmlRpcFault($fault) {
trigger_error('XmlRpc Fault: ' . $fault['faultString'] . ' (' . $fault['faultCode'] . ')');
}
}
?>