diff --git a/application/plugins/oldstructure/chatlog.plugin.php b/application/plugins/oldstructure/chatlog.plugin.php new file mode 100644 index 00000000..8b77449b --- /dev/null +++ b/application/plugins/oldstructure/chatlog.plugin.php @@ -0,0 +1,85 @@ +mControl = $mControl; + + // Load config + $this->config = Tools::loadConfig('chatlog.plugin.xml'); + + // Check for enabled setting + if (!Tools::toBool($this->config->enabled)) return; + + // Load settings + $this->loadSettings(); + + // Register for callbacksc + $this->iControl->callbacks->registerCallbackHandler(Callbacks::CB_MP_PLAYERCHAT, $this, 'handlePlayerChatCallback'); + + error_log('Chatlog Pugin v' . self::VERSION . ' ready!'); + } + + /** + * Load settings from config + */ + private function loadSettings() { + $this->settings = new \stdClass(); + + // File name + $fileName = (string) $this->config->filename; + $this->settings->fileName = ManiaControlDir . '/' . $fileName; + + // log_server_messages + $log_server_messages = $this->config->xpath('log_server_messages'); + $this->settings->log_server_messages = ($log_server_messages ? (Tools::toBool($log_server_messages[0])) : false); + } + + /** + * Handle PlayerChat callback + */ + public function handlePlayerChatCallback($callback) { + $data = $callback[1]; + if ($data[0] <= 0 && !$this->settings->log_server_messages) { + // Skip server message + return; + } + $this->logText($data[2], $data[1]); + } + + /** + * Log the given message + * + * @param string $message + * @param string $login + */ + private function logText($text, $login = null) { + $message = date(ManiaControl::DATE) . '>> ' . ($login ? $login . ': ' : '') . $text . PHP_EOL; + file_put_contents($this->settings->fileName, $message, FILE_APPEND); + } +} + +?> diff --git a/application/plugins/oldstructure/karma.plugin.php b/application/plugins/oldstructure/karma.plugin.php new file mode 100644 index 00000000..26b5cf91 --- /dev/null +++ b/application/plugins/oldstructure/karma.plugin.php @@ -0,0 +1,305 @@ +mControl = $mControl; + + // Load config + $this->config = Tools::loadConfig('karma.plugin.xml'); + if (!Tools::toBool($this->config->enabled)) return; + + // Init database + $this->initDatabase(); + + // Register for callbacks + $this->iControl->callbacks->registerCallbackHandler(Callbacks::CB_IC_ONINIT, $this, 'handleOnInitCallback'); + $this->iControl->callbacks->registerCallbackHandler(Callbacks::CB_IC_BEGINMAP, $this, 'handleBeginMapCallback'); + $this->iControl->callbacks->registerCallbackHandler(Callbacks::CB_MP_PLAYERCONNECT, $this, 'handlePlayerConnectCallback'); + $this->iControl->callbacks->registerCallbackHandler(Callbacks::CB_MP_PLAYERMANIALINKPAGEANSWER, $this, + 'handleManialinkPageAnswerCallback'); + + error_log('Karma Pugin v' . self::VERSION . ' ready!'); + } + + /** + * Repetitive actions + */ + public function loop() { + if ($this->sendManialinkRequested > 0 && $this->sendManialinkRequested <= time()) { + $this->sendManialinkRequested = -1; + + // Send manialink to all players + $players = $this->iControl->server->getPlayers(); + foreach ($players as $player) { + $login = $player['Login']; + $manialink = $this->buildManialink($login); + if (!$manialink) { + // Skip and retry + $this->sendManialinkRequested = time() + 5; + continue; + } + Tools::sendManialinkPage($this->iControl->client, $manialink->asXml(), $login); + } + } + } + + /** + * Handle OnInit ManiaControl callback + * + * @param array $callback + */ + public function handleOnInitCallback($callback) { + // Send manialink to all players once + $this->sendManialinkRequested = time() + 3; + } + + /** + * Handle ManiaControl BeginMap callback + * + * @param array $callback + */ + public function handleBeginMapCallback($callback) { + // Send manialink to all players once + $this->sendManialinkRequested = time() + 2; + } + + /** + * Handle PlayerConnect callback + * + * @param array $callback + */ + public function handlePlayerConnectCallback($callback) { + $login = $callback[1][0]; + $manialink = $this->buildManialink($login); + if (!$manialink) return; + Tools::sendManialinkPage($this->iControl->client, $manialink->asXml(), $login); + } + + /** + * Create necessary tables + */ + private function initDatabase() { + $query = "CREATE TABLE IF NOT EXISTS `" . self::TABLE_KARMA . "` ( + `index` int(11) NOT NULL AUTO_INCREMENT, + `mapIndex` int(11) NOT NULL, + `playerIndex` int(11) NOT NULL, + `vote` tinyint(1) NOT NULL, + `changed` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (`index`), + UNIQUE KEY `player_map_vote` (`mapIndex`, `playerIndex`) + ) ENGINE=MyISAM DEFAULT CHARSET=utf8 COMMENT='Save players map votes' AUTO_INCREMENT=1;"; + $result = $this->iControl->database->query($query); + if ($this->iControl->database->mysqli->error) { + trigger_error('MySQL Error on creating karma table. ' . $this->iControl->database->mysqli->error, E_USER_ERROR); + } + } + + /** + * Handle ManialinkPageAnswer callback + * + * @param array $callback + */ + public function handleManialinkPageAnswerCallback($callback) { + $action = $callback[1][2]; + if (substr($action, 0, strlen(self::MLID_KARMA)) !== self::MLID_KARMA) return; + + // Get vote + $action = substr($action, -4); + $vote = null; + switch ($action) { + case '.pos': + { + $vote = 1; + break; + } + case '.neu': + { + $vote = 0; + break; + } + case '.neg': + { + $vote = -1; + break; + } + default: + { + return; + } + } + + // Save vote + $login = $callback[1][1]; + $playerIndex = $this->iControl->database->getPlayerIndex($login); + $map = $this->iControl->server->getMap(); + $mapIndex = $this->iControl->database->getMapIndex($map['UId']); + $query = "INSERT INTO `" . self::TABLE_KARMA . "` ( + `mapIndex`, + `playerIndex`, + `vote` + ) VALUES ( + " . $mapIndex . ", + " . $playerIndex . ", + " . $vote . " + ) ON DUPLICATE KEY UPDATE + `vote` = VALUES(`vote`);"; + $result = $this->iControl->database->query($query); + if (!$result) return; + + // Send success message + $this->iControl->chat->sendSuccess('Vote successfully updated!', $login); + + // Send updated manialink + $this->sendManialinkRequested = time() + 1; + } + + /** + * Build karma voting manialink xml for the given login + */ + private function buildManialink($login) { + // Get config + $title = (string) $this->config->title; + $pos_x = (float) $this->config->pos_x; + $pos_y = (float) $this->config->pos_y; + + $mysqli = $this->iControl->database->mysqli; + + // Get indezes + $playerIndex = $this->iControl->database->getPlayerIndex($login); + if ($playerIndex === null) return null; + $map = $this->iControl->server->getMap(); + if (!$map) return null; + $mapIndex = $this->iControl->database->getMapIndex($map['UId']); + if ($mapIndex === null) return null; + + // Get votings + $query = "SELECT + (SELECT `vote` FROM `" . + self::TABLE_KARMA . "` WHERE `mapIndex` = " . $mapIndex . " AND `playerIndex` = " . $playerIndex . ") as `playerVote`, + (SELECT COUNT(`vote`) FROM `" . + self::TABLE_KARMA . "` WHERE `mapIndex` = " . $mapIndex . " AND `vote` = 1) AS `positiveVotes`, + (SELECT COUNT(`vote`) FROM `" . + self::TABLE_KARMA . "` WHERE `mapIndex` = " . $mapIndex . " AND `vote` = 0) AS `neutralVotes`, + (SELECT COUNT(`vote`) FROM `" . + self::TABLE_KARMA . "` WHERE `mapIndex` = " . $mapIndex . " AND `vote` = -1) AS `negativeVotes` + FROM `" . self::TABLE_KARMA . "`;"; + $result = $mysqli->query($query); + if ($mysqli->error) { + trigger_error('MySQL ERROR: ' . $mysqli->error); + } + $votes = $result->fetch_assoc(); + if (!$votes) { + $votes = array('playerVote' => null, 'positiveVotes' => 0, 'neutralVotes' => 0, 'negativeVotes' => 0); + } + + // Build manialink + $xml = Tools::newManialinkXml(self::MLID_KARMA); + + $frameXml = $xml->addChild('frame'); + $frameXml->addAttribute('posn', $pos_x . ' ' . $pos_y); + + // Title + $labelXml = $frameXml->addChild('label'); + Tools::addAlignment($labelXml); + $labelXml->addAttribute('posn', '0 4.5 -1'); + $labelXml->addAttribute('sizen', '22 0'); + $labelXml->addAttribute('style', 'TextTitle1'); + $labelXml->addAttribute('textsize', '1'); + $labelXml->addAttribute('text', $title); + + // Background + $quadXml = $frameXml->addChild('quad'); + Tools::addAlignment($quadXml); + $quadXml->addAttribute('sizen', '23 15 -2'); + $quadXml->addAttribute('style', 'Bgs1InRace'); + $quadXml->addAttribute('substyle', 'BgTitleShadow'); + + // Votes + for ($i = 1; $i >= -1; $i--) { + $x = $i * 7.; + + // Vote button + $quadXml = $frameXml->addChild('quad'); + Tools::addAlignment($quadXml); + $quadXml->addAttribute('posn', $x . ' 0 0'); + $quadXml->addAttribute('sizen', '6 6'); + $quadXml->addAttribute('style', 'Icons64x64_1'); + + // Vote count + $labelXml = $frameXml->addChild('label'); + Tools::addAlignment($labelXml); + $labelXml->addAttribute('posn', $x . ' -4.5 0'); + $labelXml->addAttribute('style', 'TextTitle1'); + $labelXml->addAttribute('textsize', '2'); + + if ((string) $i === $votes['playerVote']) { + // Player vote X + $voteQuadXml = $frameXml->addChild('quad'); + Tools::addAlignment($voteQuadXml); + $voteQuadXml->addAttribute('posn', $x . ' 0 1'); + $voteQuadXml->addAttribute('sizen', '6 6'); + $voteQuadXml->addAttribute('style', 'Icons64x64_1'); + $voteQuadXml->addAttribute('substyle', 'Close'); + } + + switch ($i) { + case 1: + { + // Positive + $quadXml->addAttribute('substyle', 'LvlGreen'); + $quadXml->addAttribute('action', self::MLID_KARMA . '.pos'); + $labelXml->addAttribute('text', $votes['positiveVotes']); + break; + } + case 0: + { + // Neutral + $quadXml->addAttribute('substyle', 'LvlYellow'); + $quadXml->addAttribute('action', self::MLID_KARMA . '.neu'); + $labelXml->addAttribute('text', $votes['neutralVotes']); + break; + } + case -1: + { + // Negative + $quadXml->addAttribute('substyle', 'LvlRed'); + $quadXml->addAttribute('action', self::MLID_KARMA . '.neg'); + $labelXml->addAttribute('text', $votes['negativeVotes']); + break; + } + } + } + + return $xml; + } +} + +?> diff --git a/application/plugins/oldstructure/obstacle.plugin.php b/application/plugins/oldstructure/obstacle.plugin.php new file mode 100644 index 00000000..0084b486 --- /dev/null +++ b/application/plugins/oldstructure/obstacle.plugin.php @@ -0,0 +1,63 @@ +mControl = $mControl; + + // Load config + $this->config = Tools::loadConfig('obstacle.plugin.xml'); + + // Check for enabled setting + if (!Tools::toBool($this->config->enabled)) return; + + // Register for jump command + $this->iControl->commands->registerCommandHandler('jumpto', $this, 'command_jumpto'); + + error_log('Obstacle Pugin v' . self::VERSION . ' ready!'); + } + + /** + * Handle jumpto command + */ + public function command_jumpto($chat) { + $login = $chat[1][1]; + $rightLevel = (string) $this->config->jumps_rightlevel; + if (!$this->iControl->authentication->checkRight($login, $rightLevel)) { + // Not allowed + $this->iControl->authentication->sendNotAllowed($login); + } + else { + // Send jump callback + $params = explode(' ', $chat[1][2], 2); + $param = $login . ";" . $params[1] . ";"; + if (!$this->iControl->client->query('TriggerModeScriptEvent', self::CB_JUMPTO, $param)) { + trigger_error("Couldn't send jump callback for '" . $login . "'. " . $this->iControl->getClientErrorText()); + } + } + } +} + +?> diff --git a/application/plugins/oldstructure/plugin.ManiaControl.php b/application/plugins/oldstructure/plugin.ManiaControl.php new file mode 100644 index 00000000..a51d9066 --- /dev/null +++ b/application/plugins/oldstructure/plugin.ManiaControl.php @@ -0,0 +1,37 @@ +mControl = $mControl; + + error_log('Pugin v' . self::VERSION . ' ready!'); + } + + /** + * Perform actions during each loop + */ + public function loop() { + } +} + +?> diff --git a/application/plugins/oldstructure/records.plugin.php b/application/plugins/oldstructure/records.plugin.php new file mode 100644 index 00000000..71dbbefd --- /dev/null +++ b/application/plugins/oldstructure/records.plugin.php @@ -0,0 +1,1216 @@ +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'] . ')'); + } +} + +?> diff --git a/application/plugins/oldstructure/united.plugin.php b/application/plugins/oldstructure/united.plugin.php new file mode 100644 index 00000000..f4cdb774 --- /dev/null +++ b/application/plugins/oldstructure/united.plugin.php @@ -0,0 +1,677 @@ +mControl = $mControl; + + // Load config + $this->config = Tools::loadConfig('united.plugin.xml'); + $this->loadSettings(); + + // Check for enabled setting + if (!$this->settings->enabled) return; + + // Load clients + $this->loadClients(); + + // Register for callbacks + $this->iControl->callbacks->registerCallbackHandler(Callbacks::CB_IC_ONINIT, $this, 'handleOnInitCallback'); + $this->iControl->callbacks->registerCallbackHandler(Callbacks::CB_IC_5_SECOND, $this, 'handle5Seconds'); + $this->iControl->callbacks->registerCallbackHandler(Callbacks::CB_MP_PLAYERCONNECT, $this, 'handlePlayerConnect'); + $this->iControl->callbacks->registerCallbackHandler(Callbacks::CB_MP_PLAYERMANIALINKPAGEANSWER, $this, + 'handleManialinkPageAnswer'); + + // Register for commands + $this->iControl->commands->registerCommandHandler('nextserver', $this, 'handleNextServerCommand'); + + if ($this->settings->widgets_enabled) { + // Build addfavorite manialink + $this->buildFavoriteManialink(); + } + + error_log('United Pugin v' . self::VERSION . ' ready!'); + } + + /** + * Handle ManiaControl OnInit callback + * + * @param array $callback + */ + public function handleOnInitCallback($callback) { + if ($this->settings->widgets_enabled) { + // Send widgets to all players + + if (Tools::toBool($this->config->widgets->addfavorite->enabled)) { + // Send favorite widget + if (!$this->iControl->client->query('SendDisplayManialinkPage', $this->manialinks[self::ML_ADDFAVORITE]->asXml(), 0, + false)) { + trigger_error("Couldn't send favorite widget! " . $this->iControl->getClientErrorText()); + } + } + } + } + + /** + * Load settings from config + */ + private function loadSettings() { + $this->settings = new \stdClass(); + + // Enabled + $this->settings->enabled = Tools::toBool($this->config->enabled); + + // Timeout + $timeout = $this->iControl->server->config->xpath('timeout'); + if ($timeout) { + $this->settings->timeout = (int) $timeout[0]; + } + else { + $this->settings->timeout = 30; + } + + // Game mode + $mode = $this->config->xpath('mode'); + if ($mode) { + $mode = (int) $mode[0]; + if ($mode < 1 || $mode > 6) { + $this->settings->gamemode = 2; + } + else { + $this->settings->gamemode = $mode; + } + } + + // Server status + $hide_game_server = $this->config->xpath('hide_game_server'); + if ($hide_game_server) { + $this->settings->hide_game_server = Tools::toBool($hide_game_server[0]); + } + else { + $this->settings->hide_game_server = true; + } + + // Passwords + $lobbyPassword = $this->config->xpath('lobbies/password'); + if ($lobbyPassword) { + $this->settings->lobbyPassword = (string) $lobbyPassword[0]; + } + else { + $this->settings->lobbyPassword = ''; + } + $gamePassword = $this->config->xpath('gameserver/password'); + if ($gamePassword) { + $this->settings->gamePassword = (string) $gamePassword[0]; + } + else { + $this->settings->gamePassword = ''; + } + + // Widgets + $this->settings->widgets_enabled = Tools::toBool($this->config->widgets->enabled); + } + + /** + * Loop events on clients + */ + public function loop() { + if (!$this->settings->enabled) return; + + // Check callbacks all clients + $clients = array_merge($this->gameServer, $this->lobbies); + $currentServer = $this->gameServer[$this->currentClientIndex]; + foreach ($clients as $index => $client) { + $client->resetError(); + $client->readCB(); + $callbacks = $client->getCBResponses(); + if (!is_array($callbacks) || $client->isError()) { + trigger_error("Error reading server callbacks! " . $this->iControl->getClientErrorText($client)); + } + else { + if ($client == $currentServer) { + // Currently active game server + foreach ($callbacks as $index => $callback) { + $callbackName = $callback[0]; + switch ($callbackName) { + case Callbacks::CB_MP_ENDMAP: + { + $this->switchToNextServer(false); + break; + } + } + } + + if ($this->lastStatusCheck + 2 > time()) continue; + $this->lastStatusCheck = time(); + + if (!$client->query('CheckEndMatchCondition')) { + trigger_error("Couldn't get game server status. " . $this->iControl->getClientErrorText($client)); + } + else { + $response = $client->getResponse(); + switch ($response) { + case 'Finished': + { + if ($this->finishedBegin < 0) { + $this->finishedBegin = time(); + } + else if ($this->finishedBegin + 13 <= time()) { + $this->switchToNextServer(true); + } + break; + } + default: + { + $this->finishedBegin = -1; + break; + } + } + } + } + else { + // Lobby or inactive game server -> Redirect players + foreach ($callbacks as $callback) { + switch ($callback[0]) { + case Callbacks::CB_MP_PLAYERCONNECT: + { + $this->playerJoinedLobby($client, $callback); + break; + } + } + } + } + } + } + + // Check for switch server request + if ($this->switchServerRequested > 0 && $this->switchServerRequested <= time()) { + $this->switchServerRequested = -1; + + // Switch server + $this->switchToNextServer(true); + } + } + + /** + * Handle 5 seconds callback + */ + public function handle5Seconds($callback = null) { + // Update lobby infos + $players = $this->iControl->server->getPlayers(); + if (is_array($players)) { + $playerCount = count($players); + $playerLevel = 0.; + if ($playerCount > 0) { + foreach ($players as $player) { + $playerLevel += $player['LadderRanking']; + } + $playerLevel /= $playerCount; + } + foreach ($this->lobbies as $lobby) { + if (!$lobby->query('SetLobbyInfo', true, $playerCount, 255, $playerLevel)) { + trigger_error("Couldn't update lobby info. " . $this->iControl->getClientErrorText($lobby)); + } + } + } + + // Check for not-redirected players + $clients = array_merge($this->gameServer, $this->lobbies); + $joinLink = $this->getJoinLink(); + foreach ($clients as $client) { + if ($client == $this->gameServer[$this->currentClientIndex]) continue; + $players = $this->iControl->server->getPlayers($client); + if (!is_array($players)) continue; + foreach ($players as $player) { + $login = $player['Login']; + if (!$client->query('SendOpenLinkToLogin', $login, $joinLink, 1)) { + trigger_error( + "Couldn't redirect player '" . $login . "' to active game server. " . + $this->iControl->getClientErrorText($client)); + } + } + } + } + + /** + * Handle player manialink page answer callback + */ + public function handleManialinkPageAnswer($callback) { + $login = $callback[1][1]; + $action = $callback[1][2]; + switch ($action) { + case self::ML_ADDFAVORITE: + { + // Open manialink to add server logins to favorite + $serverLogins = array(); + $add_all = Tools::toBool($this->config->widgets->addfavorite->add_all); + if ($add_all) { + // Add all server + foreach ($this->gameServer as $serverClient) { + array_push($serverLogins, $this->iControl->server->getLogin($serverClient)); + } + foreach ($this->lobbies as $serverClient) { + array_push($serverLogins, $this->iControl->server->getLogin($serverClient)); + } + } + else { + // Add only current server + array_push($serverLogins, $this->iControl->server->getLogin()); + } + + // Build manialink url + $manialink = 'iControl?favorite'; + foreach ($serverLogins as $serverLogin) { + $manialink .= '&' . $serverLogin; + } + + // Send url to player + if (!$this->iControl->client->query('SendOpenLinkToLogin', $login, $manialink, 1)) { + trigger_error( + "Couldn't open manialink to add server to favorite for '" . $login . "'! " . + $this->iControl->getClientErrorText()); + } + break; + } + } + } + + /** + * Switch to the next server + * + * @param bool $simulateMapEnd + * Simulate end of the map by sending callbacks + */ + private function switchToNextServer($simulateMapEnd) { + $this->finishedBegin = -1; + $oldClient = $this->gameServer[$this->currentClientIndex]; + + $random_order = Tools::toBool($this->config->random_order); + if ($random_order) { + // Random next server + $this->currentClientIndex = rand(0, count($this->gameServer) - 1); + } + else { + // Next server in list + $this->currentClientIndex++; + } + if ($this->currentClientIndex >= count($this->gameServer)) $this->currentClientIndex = 0; + + $newClient = $this->gameServer[$this->currentClientIndex]; + if ($newClient == $oldClient) return; + + // Restart map on next game server + if (!$newClient->query('RestartMap')) { + trigger_error("Couldn't restart map on next game server. " . $this->iControl->getClientErrorText($newClient)); + } + + if ($simulateMapEnd) { + // Simulate EndMap on old client + $this->iControl->callbacks->triggerCallback(Callbacks::CB_IC_ENDMAP, array(Callbacks::CB_IC_ENDMAP)); + } + + // Transfer players to next server + $joinLink = $this->getJoinLink($newClient); + if (!$oldClient->query('GetPlayerList', 255, 0)) { + trigger_error("Couldn't get player list. " . $this->iControl->getClientErrorText($oldClient)); + } + else { + $playerList = $oldClient->getResponse(); + foreach ($playerList as $player) { + $login = $player['Login']; + if (!$oldClient->query('SendOpenLinkToLogin', $login, $joinLink, 1)) { + trigger_error("Couldn't redirect player to next game server. " . $this->iControl->getClientErrorText($oldClient)); + } + } + + $this->iControl->client = $newClient; + } + + // Trigger client updated callback + $this->iControl->callbacks->triggerCallback(Callbacks::CB_IC_CLIENTUPDATED, "Plugin_United.SwitchedServer"); + + if ($simulateMapEnd) { + // Simulate BeginMap on new client + $map = $this->iControl->server->getMap(); + if ($map) { + $this->iControl->callbacks->triggerCallback(Callbacks::CB_IC_BEGINMAP, array(Callbacks::CB_IC_BEGINMAP, array($map))); + } + } + } + + /** + * Handle nextserver command + * + * @param mixed $command + */ + public function handleNextServerCommand($command) { + if (!$command) return; + $login = $command[1][1]; + + if (!$this->iControl->authentication->checkRight($login, 'operator')) { + // Not allowed + $this->iControl->authentication->sendNotAllowed($login); + return; + } + + // Request skip to next server + $this->switchServerRequested = time() + 3; + + // Send chat message + $this->iControl->chat->sendInformation("Switching to next server in 3 seconds..."); + } + + /** + * Handle PlayerConnect callback + */ + public function playerJoinedLobby($client, $callback) { + if (!$client) return; + + $data = $callback[1]; + $login = $data[0]; + + // Redirect player to current game server + $gameserver = $this->gameServer[$this->currentClientIndex]; + $joinLink = $this->getJoinLink($gameserver, !$data[1]); + if (!$client->query('SendOpenLinkToLogin', $login, $joinLink, 1)) { + trigger_error( + "United Plugin: Couldn't redirect player to current game server. " . $this->iControl->getClientErrorText($client)); + } + } + + /** + * Connect to the game server defined in the config + */ + private function loadClients() { + $gameserver = $this->config->xpath('gameserver/server'); + $lobbies = $this->config->xpath('lobbies/server'); + + $clientsConfig = array_merge($gameserver, $lobbies); + foreach ($clientsConfig as $index => $serv) { + $isGameServer = (in_array($serv, $gameserver)); + + $host = $serv->xpath('host'); + $port = $serv->xpath('port'); + if (!$host || !$port) { + trigger_error("Invalid configuration!", E_USER_ERROR); + } + $host = (string) $host[0]; + $port = (string) $port[0]; + + error_log("Connecting to united " . ($isGameServer ? 'game' : 'lobby') . " server at " . $host . ":" . $port . "..."); + $client = new \IXR_ClientMulticall_Gbx(); + + // Connect + if (!$client->InitWithIp($host, $port, $this->settings->timeout)) { + trigger_error( + "Couldn't connect to united " . ($isGameServer ? 'game' : lobby) . " server! " . $client->getErrorMessage() . + "(" . $client->getErrorCode() . ")", E_USER_ERROR); + } + + $login = $serv->xpath('login'); + $pass = $serv->xpath('pass'); + if (!$login || !$pass) { + trigger_error("Invalid configuration!", E_USER_ERROR); + } + $login = (string) $login[0]; + $pass = (string) $pass[0]; + + // Authenticate + if (!$client->query('Authenticate', $login, $pass)) { + trigger_error( + "Couldn't authenticate on united " . ($isGameServer ? 'game' : 'lobby') . " server with user '" . $login . "'! " . + $client->getErrorMessage() . "(" . $client->getErrorCode() . ")", E_USER_ERROR); + } + + // Enable callback system + if (!$client->query('EnableCallbacks', true)) { + trigger_error("Couldn't enable callbacks! " . $client->getErrorMessage() . "(" . $client->getErrorCode() . ")", + E_USER_ERROR); + } + + // Wait for server to be ready + if (!$this->iControl->server->waitForStatus($client, 4)) { + trigger_error("Server couldn't get ready!", E_USER_ERROR); + } + + // Set api version + if (!$client->query('SetApiVersion', ManiaControl::API_VERSION)) { + trigger_error( + "Couldn't set API version '" . ManiaControl::API_VERSION . "'! This might cause problems. " . + $this->iControl->getClientErrorText($client)); + } + + // Set server settings + $password = ($isGameServer ? $this->settings->gamePassword : $this->settings->lobbyPassword); + $hideServer = ($isGameServer && $this->settings->hide_game_server ? 1 : 0); + // Passwords + if (!$client->query('SetServerPassword', $password)) { + trigger_error("Couldn't set server join password. " . $this->iControl->getClientErrorText($client)); + } + if (!$client->query('SetServerPasswordForSpectator', $password)) { + trigger_error("Couldn't set server spec password. " . $this->iControl->getClientErrorText($client)); + } + // Show/Hide server + if (!$client->query('SetHideServer', $hideServer)) { + trigger_error( + "Couldn't set server '" . ($hideServer == 0 ? 'shown' : 'hidden') . "'. " . + $this->iControl->getClientErrorText($client)); + } + + // Enable service announces + if (!$client->query("DisableServiceAnnounces", false)) { + trigger_error("Couldn't enable service announces. " . $this->iControl->getClientErrorText($client)); + } + + // Set game mode + if (!$client->query('SetGameMode', $this->settings->gamemode)) { + trigger_error( + "Couldn't set game mode (" . $this->settings->gamemode . "). " . $this->iControl->getClientErrorText($client)); + } + else if (!$client->query('RestartMap')) { + trigger_error("Couldn't restart map to change game mode. " . $this->iControl->getClientErrorText($client)); + } + + // Save client + $client->index = $index; + if ($isGameServer) { + array_push($this->gameServer, $client); + if (count($this->gameServer) === 1) { + $this->iControl->client = $client; + } + } + else { + array_push($this->lobbies, $client); + } + } + + error_log("United Plugin: Connected to all game and lobby server!"); + } + + /** + * Handle PlayerConnect callback + * + * @param array $callback + */ + public function handlePlayerConnect($callback) { + if ($this->settings->widgets_enabled) { + // Send manialinks to the client + $login = $callback[1][0]; + + if (Tools::toBool($this->config->widgets->addfavorite->enabled)) { + // Send favorite widget + if (!$this->iControl->client->query('SendDisplayManialinkPageToLogin', $login, + $this->manialinks[self::ML_ADDFAVORITE]->asXml(), 0, false)) { + trigger_error("Couldn't send favorite widget to player '" . $login . "'! " . $this->iControl->getClientErrorText()); + } + } + } + } + + /** + * Build join link for the given client + */ + private function getJoinLink(&$client = null, $play = true) { + if (!$client) { + $client = $this->gameServer[$this->currentClientIndex]; + } + if (!$client->query('GetSystemInfo')) { + trigger_error("Couldn't fetch server system info. " . $this->iControl->getClientErrorText($client)); + return null; + } + else { + $systemInfo = $client->getResponse(); + $password = ''; + if (!$client->query('GetServerPassword')) { + trigger_error("Couldn't get server password. " . $this->iControl->getClientErrorText($client)); + } + else { + $password = $client->getResponse(); + } + return '#q' . ($play ? 'join' : 'spectate') . '=' . $systemInfo['ServerLogin'] . + (strlen($password) > 0 ? ':' . $password : '') . '@' . $systemInfo['TitleId']; + } + } + + /** + * Build manialink for addfavorite button + */ + private function buildFavoriteManialink() { + // Load configs + $config = $this->config->widgets->addfavorite; + if (!Tools::toBool($config->enabled)) return; + + $pos_x = (float) $config->pos_x; + $pos_y = (float) $config->pos_y; + $height = (float) $config->height; + $width = (float) $config->width; + $add_all = Tools::toBool($config->add_all); + + // Build manialink + $xml = Tools::newManialinkXml(self::ML_ADDFAVORITE); + + $frameXml = $xml->addChild('frame'); + $frameXml->addAttribute('posn', $pos_x . ' ' . $pos_y); + + // Background + $quadXml = $frameXml->addChild('quad'); + Tools::addAlignment($quadXml); + $quadXml->addAttribute('posn', '0 0 0'); + $quadXml->addAttribute('sizen', $width . ' ' . $height); + $quadXml->addAttribute('style', 'Bgs1InRace'); + $quadXml->addAttribute('substyle', 'BgTitleShadow'); + $quadXml->addAttribute('action', self::ML_ADDFAVORITE); + + // Heart + $quadXml = $frameXml->addChild('quad'); + Tools::addAlignment($quadXml); + $quadXml->addAttribute('id', 'Quad_AddFavorite'); + $quadXml->addAttribute('posn', '0 0 1'); + $quadXml->addAttribute('sizen', ($width - 1.) . ' ' . ($height - 0.8)); + $quadXml->addAttribute('style', 'Icons64x64_1'); + $quadXml->addAttribute('substyle', 'StateFavourite'); + $quadXml->addAttribute('scriptevents', '1'); + + // Tooltip + $tooltipFrameXml = $frameXml->addChild('frame'); + $tooltipFrameXml->addAttribute('id', 'Frame_FavoriteTooltip'); + $tooltipFrameXml->addAttribute('posn', '0 ' . ($pos_y >= 0 ? '-' : '') . '13'); + $tooltipFrameXml->addAttribute('hidden', '1'); + + $quadXml = $tooltipFrameXml->addChild('quad'); + Tools::addAlignment($quadXml); + $quadXml->addAttribute('posn', '0 0 2'); + $quadXml->addAttribute('sizen', '28 16'); + $quadXml->addAttribute('style', 'Bgs1InRace'); + $quadXml->addAttribute('substyle', 'BgTitleShadow'); + + $labelXml = $tooltipFrameXml->addChild('label'); + Tools::addAlignment($labelXml); + Tools::addTranslate($labelXml); + $labelXml->addAttribute('posn', '0 0 3'); + $labelXml->addAttribute('sizen', '26 0'); + $labelXml->addAttribute('style', 'TextTitle1'); + $labelXml->addAttribute('textsize', '2'); + $labelXml->addAttribute('autonewline', '1'); + $countText = ''; + if ($add_all) { + $count = count($this->gameServer) + count($this->lobbies); + $countText = 'all ' . $count . ' '; + } + $labelXml->addAttribute('text', 'Add ' . $countText . 'server to Favorite!'); + + // Script for tooltip + $script = ' +declare Frame_FavoriteTooltip <=> (Page.GetFirstChild("Frame_FavoriteTooltip") as CMlFrame); +while (True) { + yield; + foreach (Event in PendingEvents) { + switch (Event.Type) { + case CMlEvent::Type::MouseOver: { + switch (Event.ControlId) { + case "Quad_AddFavorite": { + Frame_FavoriteTooltip.Visible = True; + } + } + } + case CMlEvent::Type::MouseOut: { + switch (Event.ControlId) { + case "Quad_AddFavorite": { + Frame_FavoriteTooltip.Visible = False; + } + } + } + } + } +}'; + $xml->addChild('script', $script); + + $this->manialinks[self::ML_ADDFAVORITE] = $xml; + } +} + +?> + \ No newline at end of file