diff --git a/application/configs/authentication.mControl.xml b/application/configs/authentication.mControl.xml
new file mode 100644
index 00000000..38659bfb
--- /dev/null
+++ b/application/configs/authentication.mControl.xml
@@ -0,0 +1,36 @@
+
+
+
+
+
+
+
+ steeffeen
+
+
+
+
+
+ gorby
+ canyondrive
+
+
+
+
+
+ eyebo
+ jojo95183
+ xanashea
+ ardid
+ gugli
+ phil13hebert
+ xcaliber
+ eole
+ fix
+ kremsy
+ papychampy
+ titishu
+ wurstigewurst
+
+
+
diff --git a/application/configs/chat.mControl.xml b/application/configs/chat.mControl.xml
new file mode 100644
index 00000000..2f1fc9f2
--- /dev/null
+++ b/application/configs/chat.mControl.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+ $fff
+ $0f0
+ $f00
+
+
+
diff --git a/application/configs/commands.mControl.xml b/application/configs/commands.mControl.xml
new file mode 100644
index 00000000..5eb30730
--- /dev/null
+++ b/application/configs/commands.mControl.xml
@@ -0,0 +1,44 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/application/configs/core.mControl.xml b/application/configs/core.mControl.xml
new file mode 100644
index 00000000..ddf27ebb
--- /dev/null
+++ b/application/configs/core.mControl.xml
@@ -0,0 +1,15 @@
+
+
+
+
+
+ 20
+
+
+
+ tracklist.txt
+
+
+ mx
+
+
diff --git a/application/configs/database.mControl.xml b/application/configs/database.mControl.xml
new file mode 100644
index 00000000..39097388
--- /dev/null
+++ b/application/configs/database.mControl.xml
@@ -0,0 +1,16 @@
+
+
+
+
+
+ localhost
+ 3306
+
+
+ steff
+ kjhgvhbjnfih2394ugnjk
+
+
+ steff_united
+
+
diff --git a/application/configs/plugins.mControl.xml b/application/configs/plugins.mControl.xml
new file mode 100644
index 00000000..7f546f47
--- /dev/null
+++ b/application/configs/plugins.mControl.xml
@@ -0,0 +1,13 @@
+
+
+
+
+
+ chatlog.plugin.php
+ karma.plugin.php
+ records.plugin.php
+ united.plugin.php
+
+
+
+
diff --git a/application/configs/server.mControl.xml b/application/configs/server.mControl.xml
new file mode 100644
index 00000000..179b9eb5
--- /dev/null
+++ b/application/configs/server.mControl.xml
@@ -0,0 +1,17 @@
+
+
+
+
+
+ true
+
+
+ 144.76.158.111
+ localhost
+ 21003
+
+
+ SuperAdmin
+ dtcfvgubhnjomkjnbhv
+
+
diff --git a/application/configs/stats.mControl.xml b/application/configs/stats.mControl.xml
new file mode 100644
index 00000000..6bd8da92
--- /dev/null
+++ b/application/configs/stats.mControl.xml
@@ -0,0 +1,43 @@
+
+
+
+
+
+
+
+
+
+
+ true
+
+
+ true
+
+
+ true
+
+
+ true
+
+
+
+
+
+
+
+ true
+
+
+ true
+
+
+ true
+
+
+ true
+
+
+
+
+
+
diff --git a/application/core/authentication.mControl.php b/application/core/authentication.mControl.php
new file mode 100644
index 00000000..7d9823be
--- /dev/null
+++ b/application/core/authentication.mControl.php
@@ -0,0 +1,103 @@
+ 'none', 0 => 'superadmin', 1 => 'admin', 2 => 'operator', 3 => 'all');
+
+ /**
+ * Private properties
+ */
+ private $mControl = null;
+
+ private $config = null;
+
+ /**
+ * Construct authentication manager
+ */
+ public function __construct($mControl) {
+ $this->mControl = $mControl;
+
+ // Load config
+ $this->config = Tools::loadConfig('authentication.mControl.xml');
+ }
+
+ /**
+ * Check if the player has enough rights
+ *
+ * @param string $login
+ * @param string $defaultRight
+ * @param string $neededRight
+ * @return bool
+ */
+ public function checkRight($login, $neededRight) {
+ $right = $this->getRights($login);
+ return $this->compareRights($right, $neededRight);
+ }
+
+ /**
+ * Compare if the rights are enough
+ *
+ * @param string $hasRight
+ * @param string $neededRight
+ * @return bool
+ */
+ public function compareRights($hasRight, $neededRight) {
+ if (!in_array($hasRight, $this->RIGHTS_LEVELS) || !in_array($neededRight, $this->RIGHTS_LEVELS)) {
+ return false;
+ }
+ $hasLevel = array_search($hasRight, $this->RIGHTS_LEVELS);
+ $neededLevel = array_search($neededRight, $this->RIGHTS_LEVELS);
+ if ($hasLevel > $neededLevel) {
+ return false;
+ }
+ else {
+ return true;
+ }
+ }
+
+ /**
+ * Get rights of the given login
+ *
+ * @param string $login
+ * @param string $defaultRights
+ * @return string
+ */
+ public function getRights($login, $defaultRight = 'all') {
+ $groups = $this->config->xpath('//login[text()="' . $login . '"]/..');
+ if (empty($groups)) return $defaultRight;
+ $right = $defaultRight;
+ $rightLevel = array_search($right, $this->RIGHTS_LEVELS);
+ foreach ($groups as $group) {
+ $level = array_search($group->getName(), $this->RIGHTS_LEVELS);
+ if ($level === false) continue;
+ if ($level < $rightLevel || $rightLevel === false) {
+ $right = $group->getName();
+ $rightLevel = $level;
+ }
+ }
+ return $right;
+ }
+
+ /**
+ * Sends an error message to the login
+ *
+ * @param string $login
+ */
+ public function sendNotAllowed($login) {
+ if (!$this->iControl->chat->sendError('You do not have the required rights to perform this command!', $login)) {
+ trigger_error("Couldn't send forbidden message to login '" . $login . "'. " . $this->iControl->getClientErrorText());
+ }
+ }
+}
+
+?>
diff --git a/application/core/callbacks.mControl.php b/application/core/callbacks.mControl.php
new file mode 100644
index 00000000..18e7a59a
--- /dev/null
+++ b/application/core/callbacks.mControl.php
@@ -0,0 +1,191 @@
+mControl = $mControl;
+
+ // Init values
+ $this->last1Second = time();
+ $this->last5Second = time();
+ $this->last1Minute = time();
+ $this->last3Minute = time();
+ }
+
+ /**
+ * Perform OnInit callback
+ */
+ public function onInit() {
+ // On init callback
+ $this->triggerCallback(self::CB_IC_ONINIT, array(self::CB_IC_ONINIT));
+
+ // Simulate begin map
+ $map = $this->iControl->server->getMap();
+ if ($map) {
+ $this->triggerCallback(self::CB_IC_BEGINMAP, array(self::CB_IC_BEGINMAP, array($map)));
+ }
+ }
+
+ /**
+ * Handles the given array of callbacks
+ */
+ public function handleCallbacks() {
+ // Perform mControl callbacks
+ if ($this->last1Second <= time() - 1) {
+ $this->last1Second = time();
+
+ // 1 second
+ $this->triggerCallback(self::CB_IC_1_SECOND, array(self::CB_IC_1_SECOND));
+
+ if ($this->last5Second <= time() - 5) {
+ $this->last5Second = time();
+
+ // 5 second
+ $this->triggerCallback(self::CB_IC_5_SECOND, array(self::CB_IC_5_SECOND));
+
+ if ($this->last1Minute <= time() - 60) {
+ $this->last1Minute = time();
+
+ // 1 minute
+ $this->triggerCallback(self::CB_IC_1_MINUTE, array(self::CB_IC_1_MINUTE));
+
+ if ($this->last3Minute <= time() - 180) {
+ $this->last3Minute = time();
+
+ // 3 minute
+ $this->triggerCallback(self::CB_IC_3_MINUTE, array(self::CB_IC_3_MINUTE));
+ }
+ }
+ }
+ }
+
+ // Get server callbacks
+ if (!$this->iControl->client) return;
+ $this->iControl->client->resetError();
+ $this->iControl->client->readCB();
+ $callbacks = $this->iControl->client->getCBResponses();
+ if (!is_array($callbacks) || $this->iControl->client->isError()) {
+ trigger_error("Error reading server callbacks. " . $this->iControl->getClientErrorText());
+ return;
+ }
+
+ // Handle callbacks
+ foreach ($callbacks as $index => $callback) {
+ $callbackName = $callback[0];
+ switch ($callbackName) {
+ case self::CB_MP_BEGINMAP:
+ {
+ // Map begin
+ $this->triggerCallback($callbackName, $callback);
+ $this->triggerCallback(self::CB_IC_BEGINMAP, $callback);
+ break;
+ }
+ case self::CB_MP_ENDMAP:
+ {
+ // Map end
+ $this->triggerCallback($callbackName, $callback);
+ $this->triggerCallback(self::CB_IC_ENDMAP, $callback);
+ break;
+ }
+ default:
+ {
+ $this->triggerCallback($callbackName, $callback);
+ break;
+ }
+ }
+ }
+ }
+
+ /**
+ * Trigger a specific callback
+ *
+ * @param string $callbackName
+ * @param mixed $data
+ */
+ public function triggerCallback($callbackName, $data) {
+ if (!array_key_exists($callbackName, $this->callbackHandlers) || !is_array($this->callbackHandlers[$callbackName])) return;
+ foreach ($this->callbackHandlers[$callbackName] as $handler) {
+ call_user_func(array($handler[0], $handler[1]), $data);
+ }
+ }
+
+ /**
+ * Add a new callback handler
+ */
+ public function registerCallbackHandler($callback, $handler, $method) {
+ if (!is_object($handler) || !method_exists($handler, $method)) {
+ trigger_error("Given handler can't handle callback '" . $callback . "' (no method '" . $method . "')!");
+ return;
+ }
+ if (!array_key_exists($callback, $this->callbackHandlers) || !is_array($this->callbackHandlers[$callback])) {
+ // Init callback handler array
+ $this->callbackHandlers[$callback] = array();
+ }
+ // Register callback handler
+ array_push($this->callbackHandlers[$callback], array($handler, $method));
+ }
+}
+
+?>
diff --git a/application/core/chat.mControl.php b/application/core/chat.mControl.php
new file mode 100644
index 00000000..8b9aced2
--- /dev/null
+++ b/application/core/chat.mControl.php
@@ -0,0 +1,85 @@
+';
+
+ /**
+ * Construct mControl chat
+ */
+ public function __construct($mControl) {
+ $this->mControl = $mControl;
+
+ // Load config
+ $this->config = Tools::loadConfig('chat.mControl.xml');
+ }
+
+ /**
+ * Send a chat message to the given login
+ *
+ * @param string $login
+ * @param string $message
+ * @param bool $prefix
+ */
+ public function sendChat($message, $login = null, $prefix = false) {
+ if (!$this->iControl->client) return false;
+ if ($login === null) {
+ return $this->iControl->client->query('ChatSendServerMessage', ($prefix ? $this->prefix : '') . $message);
+ }
+ else {
+ return $this->iControl->client->query('ChatSendServerMessageToLogin', ($prefix ? $this->prefix : '') . $message, $login);
+ }
+ }
+
+ /**
+ * Send an information message to the given login
+ *
+ * @param string $login
+ * @param string $message
+ * @param bool $prefix
+ */
+ public function sendInformation($message, $login = null, $prefix = false) {
+ $format = (string) $this->config->messages->information;
+ return $this->sendChat($format . $message, $login);
+ }
+
+ /**
+ * Send a success message to the given login
+ *
+ * @param string $message
+ * @param string $login
+ * @param bool $prefix
+ */
+ public function sendSuccess($message, $login = null, $prefix = false) {
+ $format = (string) $this->config->messages->success;
+ return $this->sendChat($format . $message, $login);
+ }
+
+ /**
+ * Send an error message to the given login
+ *
+ * @param string $login
+ * @param string $message
+ * @param bool $prefix
+ */
+ public function sendError($message, $login = null, $prefix = false) {
+ $format = (string) $this->config->messages->error;
+ return $this->sendChat($format . $message, $login);
+ }
+}
+
+?>
diff --git a/application/core/commands.mControl.php b/application/core/commands.mControl.php
new file mode 100644
index 00000000..983dd45f
--- /dev/null
+++ b/application/core/commands.mControl.php
@@ -0,0 +1,684 @@
+mControl = $mControl;
+
+ // Load config
+ $this->config = Tools::loadConfig('commands.mControl.xml');
+
+ // Register for callbacks
+ $this->iControl->callbacks->registerCallbackHandler(Callbacks::CB_IC_5_SECOND, $this, 'each5Seconds');
+ $this->iControl->callbacks->registerCallbackHandler(Callbacks::CB_MP_BILLUPDATED, $this, 'handleBillUpdated');
+ $this->iControl->callbacks->registerCallbackHandler(Callbacks::CB_MP_PLAYERCHAT, $this, 'handleChatCallback');
+
+ // Register basic commands
+ $commands = array('help', 'version', 'shutdown', 'shutdownserver', 'networkstats', 'systeminfo', 'getservername',
+ 'setservername', 'getplanets', 'donate', 'pay', 'kick', 'nextmap', 'restartmap', 'addmap', 'removemap', 'startwarmup',
+ 'stopwarmup');
+ foreach ($commands as $command) {
+ $this->registerCommandHandler($command, $this, 'command_' . $command);
+ }
+ }
+
+ /**
+ * Register a command handler
+ *
+ * @param string $commandName
+ * @param object $handler
+ * @param string $method
+ */
+ public function registerCommandHandler($commandName, $handler, $method) {
+ $command = strtolower($commandName);
+ if (!is_object($handler) || !method_exists($handler, $method)) {
+ trigger_error("Given handler can't handle command '" . $command . "' (no method '" . $method . "')!");
+ return;
+ }
+ if (!array_key_exists($command, $this->commandHandlers) || !is_array($this->commandHandlers[$command])) {
+ // Init handlers array
+ $this->commandHandlers[$command] = array();
+ }
+ // Register command handler
+ array_push($this->commandHandlers[$command], array($handler, $method));
+ }
+
+ /**
+ * Handle chat callback
+ */
+ public function handleChatCallback($callback) {
+ $chat = $callback[1];
+ // Check for command
+ if (!$chat[3]) return;
+ // Check for valid player
+ if ($chat[0] <= 0 || strlen($chat[1]) <= 0) return;
+ // Handle command
+ $command = explode(" ", substr($chat[2], 1));
+ $command = strtolower($command[0]);
+ if (!array_key_exists($command, $this->commandHandlers) || !is_array($this->commandHandlers[$command])) {
+ // No command handler registered
+ return;
+ }
+ // Inform command handlers
+ foreach ($this->commandHandlers[$command] as $handler) {
+ call_user_func(array($handler[0], $handler[1]), $callback);
+ }
+ }
+
+ /**
+ * Handle bill updated callback
+ */
+ public function handleBillUpdated($callback) {
+ $bill = $callback[1];
+ if (!array_key_exists($bill[0], $this->openBills)) return;
+ $login = $this->openBills[$bill[0]];
+ switch ($bill[1]) {
+ case 4:
+ {
+ // Payed
+ $message = 'Success! Thanks.';
+ $this->iControl->chat->sendSuccess($message, $login);
+ unset($this->openBills[$bill[0]]);
+ break;
+ }
+ case 5:
+ {
+ // Refused
+ $message = 'Transaction cancelled.';
+ $this->iControl->chat->sendError($message, $login);
+ unset($this->openBills[$bill[0]]);
+ break;
+ }
+ case 6:
+ {
+ // Error
+ $this->iControl->chat->sendError($bill[2], $login);
+ unset($this->openBills[$bill[0]]);
+ break;
+ }
+ }
+ }
+
+ /**
+ * Retrieve the needed rights level to perform the given command
+ *
+ * @param string $commandName
+ * @param string $defaultLevel
+ * @return string
+ */
+ private function getRightsLevel($commandName, $defaultLevel) {
+ $command_rights = $this->config->xpath('//' . strtolower($commandName) . '/..');
+ if (empty($command_rights)) return $defaultLevel;
+ $rights = $this->iControl->authentication->RIGHTS_LEVELS;
+ $highest_level = null;
+ foreach ($command_rights as $right) {
+ $levelName = $right->getName();
+ $levelInt = array_search($levelName, $rights);
+ if ($levelInt !== false && ($highest_level === null || $highest_level < $levelInt)) {
+ $highest_level = $levelInt;
+ }
+ }
+ if ($highest_level === null || !array_key_exists($highest_level, $rights)) return $defaultLevel;
+ return $rights[$highest_level];
+ }
+
+ /**
+ * Send mControl version
+ */
+ private function command_version($chat) {
+ $login = $chat[1][1];
+ if (!$this->iControl->authentication->checkRight($login, $this->getRightsLevel('version', 'all'))) {
+ // Not allowed!
+ $this->iControl->authentication->sendNotAllowed($login);
+ return;
+ }
+ if (!$this->iControl->chat->sendInformation('This server is using mControl v' . mControl::VERSION . '!', $login)) {
+ trigger_error("Couldn't send version to '" . $login . "'. " . $this->iControl->getClientErrorText());
+ }
+ }
+
+ /**
+ * Send help list
+ */
+ private function command_help($chat) {
+ $login = $chat[1][1];
+ if (!$this->iControl->authentication->checkRight($login, $this->getRightsLevel('help', 'all'))) {
+ // Not allowed!
+ $this->iControl->authentication->sendNotAllowed($login);
+ return;
+ }
+ // TODO: improve help command
+ // TODO: enable help for specific commands
+ $list = 'Available commands: ';
+ $commands = array_keys($this->commandHandlers);
+ $count = count($commands);
+ for ($index = 0; $index < $count; $index++) {
+ if (!$this->iControl->authentication->checkRight($login, $this->getRightsLevel($commands[$index], 'superadmin'))) {
+ unset($commands[$index]);
+ }
+ }
+ $count = count($commands);
+ $index = 0;
+ foreach ($commands as $command) {
+ $list .= $command;
+ if ($index < $count - 1) {
+ $list .= ', ';
+ }
+ $index++;
+ }
+ if (!$this->iControl->chat->sendInformation($list, $login)) {
+ trigger_error("Couldn't send help list to '" . $login . "'. " . $this->iControl->getClientErrorText());
+ }
+ }
+
+ /**
+ * Handle getplanets command
+ */
+ private function command_getplanets($chat) {
+ $login = $chat[1][1];
+ if (!$this->iControl->authentication->checkRight($login, $this->getRightsLevel('getplanets', 'admin'))) {
+ // Not allowed!
+ $this->iControl->authentication->sendNotAllowed($login);
+ return;
+ }
+ if (!$this->iControl->client->query('GetServerPlanets')) {
+ trigger_error("Couldn't retrieve server planets. " . $this->iControl->getClientErrorText());
+ }
+ else {
+ $planets = $this->iControl->client->getResponse();
+ if (!$this->iControl->chat->sendInformation('This Server has ' . $planets . ' Planets!', $login)) {
+ trigger_error("Couldn't send server planets to '" . $login . "'. " . $this->iControl->getClientErrorText());
+ }
+ }
+ }
+
+ /**
+ * Handle donate command
+ */
+ private function command_donate($chat) {
+ $login = $chat[1][1];
+ if (!$this->iControl->authentication->checkRight($login, $this->getRightsLevel('donate', 'all'))) {
+ // Not allowed!
+ $this->iControl->authentication->sendNotAllowed($login);
+ return;
+ }
+ $params = explode(' ', $chat[1][2]);
+ if (count($params) < 2) {
+ // TODO: send usage information
+ return;
+ }
+ $amount = (int) $params[1];
+ if (!$amount || $amount <= 0) {
+ // TODO: send usage information
+ return;
+ }
+ if (count($params) >= 3) {
+ $receiver = $params[2];
+ $receiverPlayer = $this->iControl->database->getPlayer($receiver);
+ $receiverName = ($receiverPlayer ? $receiverPlayer['NickName'] : $receiver);
+ }
+ else {
+ $receiver = '';
+ $receiverName = $this->iControl->server->getName();
+ }
+ $message = 'Donate ' . $amount . ' Planets to $<' . $receiverName . '$>?';
+ if (!$this->iControl->client->query('SendBill', $login, $amount, $message, $receiver)) {
+ trigger_error(
+ "Couldn't create donation of " . $amount . " planets from '" . $login . "' for '" . $receiver . "'. " .
+ $this->iControl->getClientErrorText());
+ $this->iControl->chat->sendError("Creating donation failed.", $login);
+ }
+ else {
+ $bill = $this->iControl->client->getResponse();
+ $this->openBills[$bill] = $login;
+ }
+ }
+
+ /**
+ * Handle pay command
+ */
+ private function command_pay($chat) {
+ $login = $chat[1][1];
+ if (!$this->iControl->authentication->checkRight($login, $this->getRightsLevel('pay', 'superadmin'))) {
+ // Not allowed!
+ $this->iControl->authentication->sendNotAllowed($login);
+ return;
+ }
+ $params = explode(' ', $chat[1][2]);
+ if (count($params) < 2) {
+ // TODO: send usage information
+ return;
+ }
+ $amount = (int) $params[1];
+ if (!$amount || $amount <= 0) {
+ // TODO: send usage information
+ return;
+ }
+ if (count($params) >= 3) {
+ $receiver = $params[2];
+ }
+ else {
+ $receiver = $login;
+ }
+ $message = 'Payout from $<' . $this->iControl->server->getName() . '$>.';
+ if (!$this->iControl->client->query('Pay', $receiver, $amount, $message)) {
+ trigger_error(
+ "Couldn't create payout of" . $amount . " planets by '" . $login . "' for '" . $receiver . "'. " .
+ $this->iControl->getClientErrorText());
+ $this->iControl->chat->sendError("Creating payout failed.", $login);
+ }
+ else {
+ $bill = $this->iControl->client->getResponse();
+ $this->openBills[$bill] = $login;
+ }
+ }
+
+ /**
+ * Handle networkstats command
+ */
+ private function command_networkstats($chat) {
+ $login = $chat[1][1];
+ if (!$this->iControl->authentication->checkRight($login, $this->getRightsLevel('networkstats', 'superadmin'))) {
+ // Not allowed!
+ $this->iControl->authentication->sendNotAllowed($login);
+ return;
+ }
+ $networkStats = $this->iControl->server->getNetworkStats();
+ $message = 'NetworkStats: ' . 'uptime=' . $networkStats['Uptime'] . ', ' . 'nbConn=' . $networkStats['NbrConnection'] . ', ' .
+ 'recvRate=' . $networkStats['RecvNetRate'] . ', ' . 'sendRate=' . $networkStats['SendNetRate'] . ', ' . 'recvTotal=' .
+ $networkStats['SendNetRate'] . ', ' . 'sentTotal=' . $networkStats['SendNetRate'];
+ if (!$this->iControl->chat->sendInformation($message, $login)) {
+ trigger_error("Couldn't send network stats to '" . $login . "'. " . $this->iControl->getClientErrorText());
+ }
+ }
+
+ /**
+ * Handle systeminfo command
+ */
+ private function command_systeminfo($chat) {
+ $login = $chat[1][1];
+ if (!$this->iControl->authentication->checkRight($login, $this->getRightsLevel('systeminfo', 'superadmin'))) {
+ // Not allowed!
+ $this->iControl->authentication->sendNotAllowed($login);
+ return;
+ }
+ $systemInfo = $this->iControl->server->getSystemInfo();
+ $message = 'SystemInfo: ' . 'ip=' . $systemInfo['PublishedIp'] . ', ' . 'port=' . $systemInfo['Port'] . ', ' . 'p2pPort=' .
+ $systemInfo['P2PPort'] . ', ' . 'title=' . $systemInfo['TitleId'] . ', ' . 'login=' . $systemInfo['ServerLogin'] . ', ';
+ if (!$this->iControl->chat->sendInformation($message, $login)) {
+ trigger_error("Couldn't send system info to '" . $login . "'. " . $this->iControl->getClientErrorText());
+ }
+ }
+
+ /**
+ * Handle shutdown command
+ */
+ private function command_shutdown($chat) {
+ $login = $chat[1][1];
+ if (!$this->iControl->authentication->checkRight($login, $this->getRightsLevel('shutdown', 'superadmin'))) {
+ // Not allowed!
+ $this->iControl->authentication->sendNotAllowed($login);
+ return;
+ }
+ $this->iControl->quit("mControl shutdown requested by '" . $login . "'");
+ }
+
+ /**
+ * Handle startwarmup command
+ */
+ private function command_startwarmup($chat) {
+ $login = $chat[1][1];
+ if (!$this->iControl->authentication->checkRight($login, $this->getRightsLevel('startwarmup', 'operator'))) {
+ // Not allowed!
+ $this->iControl->authentication->sendNotAllowed($login);
+ return;
+ }
+ if (!$this->iControl->client->query("SetWarmUp", true)) {
+ trigger_error("Couldn't start warmup. " . $this->iControl->getClientErrorText());
+ $player = $this->iControl->database->getPlayer($login);
+ $this->iControl->chat->sendInformation('$<' . ($player ? $player['NickName'] : $login) . '$> started WarmUp!');
+ }
+ }
+
+ /**
+ * Handle stopwarmup command
+ */
+ private function command_stopwarmup($chat) {
+ $login = $chat[1][1];
+ if (!$this->iControl->authentication->checkRight($login, $this->getRightsLevel('stopwarmup', 'operator'))) {
+ // Not allowed!
+ $this->iControl->authentication->sendNotAllowed($login);
+ return;
+ }
+ if (!$this->iControl->client->query("SetWarmUp", false)) {
+ trigger_error("Couldn't stop warmup. " . $this->iControl->getClientErrorText());
+ }
+ else {
+ $player = $this->iControl->database->getPlayer($login);
+ $this->iControl->chat->sendInformation('$<' . ($player ? $player['NickName'] : $login) . '$> stopped WarmUp!');
+ }
+ }
+
+ /**
+ * Handle server shutdown command
+ */
+ private function command_shutdownserver($chat) {
+ $login = $chat[1][1];
+ if (!$this->iControl->authentication->checkRight($login, $this->getRightsLevel('shutdownserver', 'superadmin'))) {
+ // Not allowed!
+ $this->iControl->authentication->sendNotAllowed($login);
+ return;
+ }
+ // Check for delayed shutdown
+ $params = explode(' ', $chat[1][2]);
+ if (count($params) >= 2) {
+ $param = $params[1];
+ if ($param == 'empty') {
+ $this->serverShutdownEmpty = !$this->serverShutdownEmpty;
+ if ($this->serverShutdownEmpty) {
+ $this->iControl->chat->sendInformation("The server will shutdown as soon as it's empty!", $login);
+ }
+ else {
+ $this->iControl->chat->sendInformation("Empty-shutdown cancelled!", $login);
+ }
+ }
+ else {
+ $delay = (int) $param;
+ if ($delay <= 0) {
+ // Cancel shutdown
+ $this->serverShutdownTime = -1;
+ $this->iControl->chat->sendInformation("Delayed shutdown cancelled!", $login);
+ }
+ else {
+ // Trigger delayed shutdown
+ $this->serverShutdownTime = time() + $delay * 60.;
+ $this->iControl->chat->sendInformation("The server will shut down in " . $delay . " minutes!", $login);
+ }
+ }
+ }
+ else {
+ $this->shutdownServer($login);
+ }
+ }
+
+ /**
+ * Handle kick command
+ */
+ private function command_kick($chat) {
+ $login = $chat[1][1];
+ if (!$this->iControl->authentication->checkRight($login, $this->getRightsLevel('kick', 'operator'))) {
+ // Not allowed!
+ $this->iControl->authentication->sendNotAllowed($login);
+ return;
+ }
+ $params = explode(' ', $chat[1][2], 3);
+ if (count($params) < 2) {
+ // TODO: show usage
+ return;
+ }
+ $target = $params[1];
+ $players = $this->iControl->server->getPlayers();
+ foreach ($players as $player) {
+ if ($player['Login'] != $target) continue;
+ // Kick player
+ if (isset($params[2])) {
+ $message = $params[2];
+ }
+ else {
+ $message = "";
+ }
+ if (!$this->iControl->client->query('Kick', $target, $message)) {
+ trigger_error("Couldn't kick player '" . $target . "'! " . $this->iControl->getClientErrorText());
+ }
+ return;
+ }
+ $this->iControl->chat->sendError("Invalid player login.", $login);
+ }
+
+ /**
+ * Handle removemap command
+ */
+ private function command_removemap($chat) {
+ $login = $chat[1][1];
+ if (!$this->iControl->authentication->checkRight($login, $this->getRightsLevel('kick', 'operator'))) {
+ // Not allowed!
+ $this->iControl->authentication->sendNotAllowed($login);
+ return;
+ }
+ // TODO: allow params
+ // Get map name
+ $map = $this->iControl->server->getMap();
+ if (!$map) {
+ $this->iControl->chat->sendError("Couldn't remove map.", $login);
+ }
+ else {
+ $mapName = $map['FileName'];
+
+ // Remove map
+ if (!$this->iControl->client->query('RemoveMap', $mapName)) {
+ trigger_error("Couldn't remove current map. " . $this->iControl->getClientErrorText());
+ }
+ else {
+ $this->iControl->chat->sendSuccess('Map removed.', $login);
+ }
+ }
+ }
+
+ /**
+ * Handle addmap command
+ */
+ private function command_addmap($chat) {
+ $login = $chat[1][1];
+ if (!$this->iControl->authentication->checkRight($login, $this->getRightsLevel('addmap', 'operator'))) {
+ // Not allowed!
+ $this->iControl->authentication->sendNotAllowed($login);
+ return;
+ }
+ $params = explode(' ', $chat[1][2], 2);
+ if (count($params) < 2) {
+ // TODO: show usage
+ return;
+ }
+ // Check if mControl can even write to the maps dir
+ if (!$this->iControl->client->query('GetMapsDirectory')) {
+ trigger_error("Couldn't get map directory. " . $this->iControl->getClientErrorText());
+ $this->iControl->chat->sendError("mControl couldn't retrieve the maps directory.", $login);
+ return;
+ }
+ else {
+ $mapDir = $this->iControl->client->getResponse();
+ if (!is_dir($mapDir)) {
+ trigger_error("mControl doesn't have have access to the maps directory in '" . $mapDir . "'.");
+ $this->iControl->chat->sendError("mControl doesn't have access to the maps directory.", $login);
+ return;
+ }
+ $dlDir = (string) $this->iControl->config->maps_dir;
+ // Create mx directory if necessary
+ if (!is_dir($mapDir . $dlDir) && !mkdir($mapDir . $dlDir)) {
+ trigger_error("mControl doesn't have to rights to save maps in'" . $mapDir . $dlDir, "'.");
+ $this->iControl->chat->sendError("mControl doesn't have to rights to save maps.", $login);
+ return;
+ }
+ $mapDir .= $dlDir . '/';
+ // Download the map
+ if (is_numeric($params[1])) {
+ $serverInfo = $this->iControl->server->getSystemInfo();
+ $title = strtolower(substr($serverInfo['TitleId'], 0, 2));
+ // Check if map exists
+ $url = 'http://' . $title . '.mania-exchange.com/api/tracks/get_track_info/id/' . $params[1] . '?format=json';
+ $mapInfo = Tools::loadFile($url);
+ if (!$mapInfo || strlen($mapInfo) <= 0) {
+ // Invalid id
+ $this->iControl->chat->sendError('Invalid MX-Id!', $login);
+ return;
+ }
+ $mapInfo = json_decode($mapInfo, true);
+ $url = 'http://' . $title . '.mania-exchange.com/tracks/download/' . $params[1];
+ $file = Tools::loadFile($url);
+ if (!$file) {
+ // Download error
+ $this->iControl->chat->sendError('Download failed!', $login);
+ return;
+ }
+ // Save map
+ $fileName = $mapDir . $mapInfo['TrackID'] . '_' . $mapInfo['Name'] . '.Map.Gbx';
+ if (!file_put_contents($fileName, $file)) {
+ // Save error
+ $this->iControl->chat->sendError('Saving map failed!', $login);
+ return;
+ }
+ // Check for valid map
+ if (!$this->iControl->client->query('CheckMapForCurrentServerParams', $fileName)) {
+ trigger_error("Couldn't check if map is valid. " . $this->iControl->getClientErrorText());
+ }
+ else {
+ $response = $this->iControl->client->getResponse();
+ if (!$response) {
+ // Inalid map type
+ $this->iControl->chat->sendError("Invalid map type.", $login);
+ return;
+ }
+ }
+ // Add map to map list
+ if (!$this->iControl->client->query('InsertMap', $fileName)) {
+ $this->iControl->chat->sendError("Couldn't add map to match settings!", $login);
+ return;
+ }
+ $this->iControl->chat->sendSuccess('Map $<' . $mapInfo['Name'] . '$> successfully added!');
+ }
+ else {
+ // TODO: check if map exists locally
+ // TODO: load map from direct url
+ }
+ }
+ }
+
+ /**
+ * Handle nextmap command
+ */
+ private function command_nextmap($chat) {
+ $login = $chat[1][1];
+ if (!$this->iControl->authentication->checkRight($login, $this->getRightsLevel('nextmap', 'operator'))) {
+ // Not allowed!
+ $this->iControl->authentication->sendNotAllowed($login);
+ return;
+ }
+ if (!$this->iControl->client->query('NextMap')) {
+ trigger_error("Couldn't skip map. " . $this->iControl->getClientErrorText());
+ }
+ }
+
+ /**
+ * Handle retartmap command
+ */
+ private function command_restartmap($chat) {
+ $login = $chat[1][1];
+ if (!$this->iControl->authentication->checkRight($login, $this->getRightsLevel('restartmap', 'operator'))) {
+ // Not allowed!
+ $this->iControl->authentication->sendNotAllowed($login);
+ return;
+ }
+ if (!$this->iControl->client->query('RestartMap')) {
+ trigger_error("Couldn't restart map. " . $this->iControl->getClientErrorText());
+ }
+ }
+
+ /**
+ * Handle getservername command
+ */
+ private function command_getservername($chat) {
+ $login = $chat[1][1];
+ if (!$this->iControl->authentication->checkRight($login, $this->getRightsLevel('getservername', 'operator'))) {
+ // Not allowed!
+ $this->iControl->authentication->sendNotAllowed($login);
+ return;
+ }
+ $serverName = $this->iControl->server->getName();
+ $this->iControl->chat->sendInformation("Server Name: " . $serverName, $login);
+ }
+
+ /**
+ * Handle setservername command
+ */
+ private function command_setservername($chat) {
+ $login = $chat[1][1];
+ if (!$this->iControl->authentication->checkRight($login, $this->getRightsLevel('setservername', 'admin'))) {
+ // Not allowed!
+ $this->iControl->authentication->sendNotAllowed($login);
+ return;
+ }
+ $params = explode(' ', $chat[1][2], 2);
+ if (count($params) < 2) {
+ // TODO: show usage
+ return;
+ }
+ $serverName = $params[1];
+ if (!$this->iControl->client->query('SetServerName', $serverName)) {
+ trigger_error("Couldn't set server name. " . $this->iControl->getClientErrorText());
+ $this->iControl->chat->sendError("Error!");
+ }
+ else {
+ $serverName = $this->iControl->server->getName();
+ $this->iControl->chat->sendInformation("New Name: " . $serverName);
+ }
+ }
+
+ /**
+ * Check stuff each 5 seconds
+ */
+ public function each5Seconds() {
+ // Empty shutdown
+ if ($this->serverShutdownEmpty) {
+ $players = $this->iControl->server->getPlayers();
+ if (count($players) <= 0) {
+ $this->shutdownServer('empty');
+ }
+ }
+
+ // Delayed shutdown
+ if ($this->serverShutdownTime > 0) {
+ if (time() >= $this->serverShutdownTime) {
+ $this->shutdownServer('delayed');
+ }
+ }
+ }
+
+ /**
+ * Perform server shutdown
+ */
+ private function shutdownServer($login = '#') {
+ $this->iControl->client->resetError();
+ if (!$this->iControl->client->query('StopServer') || $this->iControl->client->isError()) {
+ trigger_error("Server shutdown command from '" . $login . "' failed. " . $this->iControl->getClientErrorText());
+ return;
+ }
+ $this->iControl->quit("Server shutdown requested by '" . $login . "'");
+ }
+}
+
+?>
diff --git a/application/core/core.mControl.php b/application/core/core.mControl.php
new file mode 100644
index 00000000..23c57aa3
--- /dev/null
+++ b/application/core/core.mControl.php
@@ -0,0 +1,348 @@
+config = Tools::loadConfig('core.mControl.xml');
+ $this->startTime = time();
+
+ // Load chat tool
+ $this->chat = new Chat($this);
+
+ // Load callbacks handler
+ $this->callbacks = new Callbacks($this);
+
+ // Load database
+ $this->database = new Database($this);
+
+ // Load server
+ $this->server = new Server($this);
+
+ // Load authentication
+ $this->authentication = new Authentication($this);
+
+ // Load commands handler
+ $this->commands = new Commands($this);
+
+ // Load stats manager
+ $this->stats = new Stats($this);
+
+ // Register for core callbacks
+ $this->callbacks->registerCallbackHandler(Callbacks::CB_MP_ENDMAP, $this, 'handleEndMap');
+ }
+
+ /**
+ * Return message composed of client error message and error code
+ *
+ * @param object $client
+ * @return string
+ */
+ public function getClientErrorText($client = null) {
+ if (is_object($client)) {
+ return $client->getErrorMessage() . ' (' . $client->getErrorCode() . ')';
+ }
+ return $this->client->getErrorMessage() . ' (' . $this->client->getErrorCode() . ')';
+ }
+
+ /**
+ * Quit mControl and log the given message
+ */
+ public function quit($message = false) {
+ if ($this->shutdownRequested) return;
+
+ if ($this->client) {
+ // Announce quit
+ $this->chat->sendInformation('mControl shutting down.');
+
+ // Hide manialinks
+ $this->client->query('SendHideManialinkPage');
+ }
+
+ // Log quit reason
+ if ($message) {
+ error_log($message);
+ }
+
+ // Shutdown
+ if ($this->client) $this->client->Terminate();
+
+ error_log("Quitting mControl!");
+ exit();
+ }
+
+ /**
+ * Run mControl
+ */
+ public function run($debug = false) {
+ error_log('Starting mControl v' . self::VERSION . '!');
+ $this->debug = (bool) $debug;
+
+ // Load plugins
+ $this->loadPlugins();
+
+ // Connect to server
+ $this->connect();
+
+ // Loading finished
+ error_log("Loading completed!");
+
+ // Announce mControl
+ if (!$this->chat->sendInformation('mControl v' . self::VERSION . ' successfully started!')) {
+ trigger_error("Couldn't announce mControl. " . $this->iControl->getClientErrorText());
+ }
+
+ // OnInit
+ $this->callbacks->onInit();
+
+ // Main loop
+ while (!$this->shutdownRequested) {
+ $loopStart = microtime(true);
+
+ // Disable script timeout
+ set_time_limit(30);
+
+ // Handle server callbacks
+ $this->callbacks->handleCallbacks();
+
+ // Loop plugins
+ foreach ($this->plugins as $plugin) {
+ if (!method_exists($plugin, 'loop')) {
+ continue;
+ }
+ $plugin->loop();
+ }
+
+ // Yield for next tick
+ $loopEnd = microtime(true);
+ $sleepTime = 300000 - $loopEnd + $loopStart;
+ if ($sleepTime > 0) {
+ usleep($sleepTime);
+ }
+ }
+
+ // Shutdown
+ $this->client->Terminate();
+ }
+
+ /**
+ * Connect to ManiaPlanet server
+ */
+ private function connect() {
+ $enable = $this->server->config->xpath('enable');
+ $enable = Tools::toBool($enable[0]);
+ if (!$enable) return;
+
+ // Load remote client
+ $this->client = new \IXR_ClientMulticall_Gbx();
+
+ $host = $this->server->config->xpath('host');
+ if (!$host) trigger_error("Invalid server configuration (host).", E_USER_ERROR);
+ $host = (string) $host[0];
+ $port = $this->server->config->xpath('port');
+ if (!$host) trigger_error("Invalid server configuration (port).", E_USER_ERROR);
+ $port = (string) $port[0];
+ $timeout = $this->config->xpath('timeout');
+ if (!$timeout) trigger_error("Invalid core configuration (timeout).", E_USER_ERROR);
+ $timeout = (int) $timeout[0];
+
+ error_log("Connecting to server at " . $host . ":" . $port . "...");
+
+ // Connect
+ if (!$this->client->InitWithIp($host, $port, $timeout)) {
+ trigger_error(
+ "Couldn't connect to server! " . $this->client->getErrorMessage() . "(" . $this->client->getErrorCode() . ")",
+ E_USER_ERROR);
+ }
+
+ $login = $this->server->config->xpath('login');
+ if (!$login) trigger_error("Invalid server configuration (login).", E_USER_ERROR);
+ $login = (string) $login[0];
+ $pass = $this->server->config->xpath('pass');
+ if (!$pass) trigger_error("Invalid server configuration (password).", E_USER_ERROR);
+ $pass = (string) $pass[0];
+
+ // Authenticate
+ if (!$this->client->query('Authenticate', $login, $pass)) {
+ trigger_error(
+ "Couldn't authenticate on server with user '" . $login . "'! " . $this->client->getErrorMessage() . "(" .
+ $this->client->getErrorCode() . ")", E_USER_ERROR);
+ }
+
+ // Enable callback system
+ if (!$this->client->query('EnableCallbacks', true)) {
+ trigger_error("Couldn't enable callbacks! " . $this->client->getErrorMessage() . "(" . $this->client->getErrorCode() . ")",
+ E_USER_ERROR);
+ }
+
+ // Wait for server to be ready
+ if (!$this->server->waitForStatus($this->client, 4)) {
+ trigger_error("Server couldn't get ready!", E_USER_ERROR);
+ }
+
+ // Set api version
+ if (!$this->client->query('SetApiVersion', self::API_VERSION)) {
+ trigger_error(
+ "Couldn't set API version '" . self::API_VERSION . "'! This might cause problems. " .
+ $this->iControl->getClientErrorText());
+ }
+
+ // Connect finished
+ error_log("Server connection succesfully established!");
+
+ // Enable service announces
+ if (!$this->client->query("DisableServiceAnnounces", false)) {
+ trigger_error("Couldn't enable service announces. " . $this->iControl->getClientErrorText());
+ }
+
+ // Enable script callbacks if needed
+ if ($this->server->getGameMode() === 0) {
+ if (!$this->client->query('GetModeScriptSettings')) {
+ trigger_error("Couldn't get mode script settings. " . $this->iControl->getClientErrorText());
+ }
+ else {
+ $scriptSettings = $this->client->getResponse();
+ if (array_key_exists('S_UseScriptCallbacks', $scriptSettings)) {
+ $scriptSettings['S_UseScriptCallbacks'] = true;
+ if (!$this->client->query('SetModeScriptSettings', $scriptSettings)) {
+ trigger_error(
+ "Couldn't set mode script settings to enable script callbacks. " . $this->iControl->getClientErrorText());
+ }
+ else {
+ error_log("Script callbacks successfully enabled.");
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Load mControl plugins
+ */
+ private function loadPlugins() {
+ $pluginsConfig = Tools::loadConfig('plugins.mControl.xml');
+ if (!$pluginsConfig || !isset($pluginsConfig->plugin)) {
+ trigger_error('Invalid plugins config.');
+ return;
+ }
+
+ // Load plugin classes
+ $classes = get_declared_classes();
+ foreach ($pluginsConfig->xpath('plugin') as $plugin) {
+ $fileName = mControl . '/plugins/' . $plugin;
+ if (!file_exists($fileName)) {
+ trigger_error("Couldn't load plugin '" . $plugin . "'! File doesn't exist. (/plugins/" . $plugin . ")");
+ }
+ else {
+ require_once $fileName;
+ error_log("Loading plugin: " . $plugin);
+ }
+ }
+ $plugins = array_diff(get_declared_classes(), $classes);
+
+ // Create plugins
+ foreach ($plugins as $plugin) {
+ $nameIndex = stripos($plugin, 'plugin');
+ if ($nameIndex === false) continue;
+ array_push($this->plugins, new $plugin($this));
+ }
+ }
+
+ /**
+ * Handle EndMap callback
+ */
+ public function handleEndMap($callback) {
+ // Autosave match settings
+ $autosaveMatchsettings = $this->config->xpath('autosave_matchsettings');
+ if ($autosaveMatchsettings) {
+ $autosaveMatchsettings = (string) $autosaveMatchsettings[0];
+ if ($autosaveMatchsettings) {
+ if (!$this->client->query('SaveMatchSettings', 'MatchSettings/' . $autosaveMatchsettings)) {
+ trigger_error("Couldn't autosave match settings. " . $this->iControl->getClientErrorText());
+ }
+ }
+ }
+ }
+
+ /**
+ * Check config settings
+ */
+ public function checkConfig($config, $settings, $name = 'Config XML') {
+ if (!is_array($settings)) $settings = array($settings);
+ foreach ($settings as $setting) {
+ $settingTags = $config->xpath('//' . $setting);
+ if (empty($settingTags)) {
+ trigger_error("Missing property '" . $setting . "' in config '" . $name . "'!", E_USER_ERROR);
+ }
+ }
+ }
+}
+
+?>
diff --git a/application/core/database.mControl.php b/application/core/database.mControl.php
new file mode 100644
index 00000000..40d2ee78
--- /dev/null
+++ b/application/core/database.mControl.php
@@ -0,0 +1,401 @@
+mControl = $mControl;
+
+ // Load config
+ $this->config = Tools::loadConfig('database.mControl.xml');
+ $this->iControl->checkConfig($this->config, array("host", "user"), 'database.mControl.xml');
+
+ // Get mysql server information
+ $host = $this->config->xpath('host');
+ if (!$host) trigger_error("Invalid database configuration (host).", E_USER_ERROR);
+ $host = (string) $host[0];
+
+ $port = $this->config->xpath('port');
+ if (!$port) trigger_error("Invalid database configuration (port).", E_USER_ERROR);
+ $port = (int) $port[0];
+
+ $user = $this->config->xpath('user');
+ if (!$user) trigger_error("Invalid database configuration (user).", E_USER_ERROR);
+ $user = (string) $user[0];
+
+ $pass = $this->config->xpath('pass');
+ if (!$pass) trigger_error("Invalid database configuration (pass).", E_USER_ERROR);
+ $pass = (string) $pass[0];
+
+ // Open database connection
+ $this->mysqli = new \mysqli($host, $user, $pass, null, $port);
+ if ($this->mysqli->connect_error) {
+ // Connection error
+ throw new \Exception(
+ "Error on connecting to mysql server. " . $this->mysqli->connect_error . " (" . $this->mysqli->connect_errno . ")");
+ }
+
+ // Set charset
+ $this->mysqli->set_charset("utf8");
+
+ // Create/Connect database
+ $this->initDatabase();
+
+ // Init tables
+ $this->initTables();
+
+ // Register for callbacks
+ $this->iControl->callbacks->registerCallbackHandler(Callbacks::CB_IC_5_SECOND, $this, 'handle5Second');
+ $this->iControl->callbacks->registerCallbackHandler(Callbacks::CB_IC_BEGINMAP, $this, 'handleBeginMap');
+ }
+
+ /**
+ * Destruct database connection
+ */
+ public function __destruct() {
+ $this->mysqli->close();
+ }
+
+ /**
+ * Connect to the defined database (create it if needed)
+ */
+ private function initDatabase() {
+ $dbname = $this->config->xpath('database');
+ if (!$dbname) trigger_error("Invalid database configuration (database).", E_USER_ERROR);
+ $dbname = (string) $dbname[0];
+
+ // Try to connect
+ $result = $this->mysqli->select_db($dbname);
+ if (!$result) {
+ // Create database
+ $query = "CREATE DATABASE `" . $this->escape($dbname) . "`;";
+ $result = $this->mysqli->query($query);
+ if (!$result) {
+ trigger_error(
+ "Couldn't create database '" . $dbname . "'. " . $this->mysqli->error . ' (' . $this->mysqli->errno . ')',
+ E_USER_ERROR);
+ }
+ else {
+ // Connect to database
+ $result = $this->mysqli->select_db($dbname);
+ if (!$result) {
+ trigger_error(
+ "Couldn't select database '" . $dbname . "'. " . $this->mysqli->error . ' (' . $this->mysqli->errno . ')',
+ E_USER_ERROR);
+ }
+ }
+ }
+ }
+
+ /**
+ * Create the needed tables
+ */
+ private function initTables() {
+ $query = "";
+
+ // Players table
+ $query .= "CREATE TABLE IF NOT EXISTS `" . self::TABLE_PLAYERS . "` (
+ `index` int(11) NOT NULL AUTO_INCREMENT,
+ `Login` varchar(100) NOT NULL,
+ `NickName` varchar(250) NOT NULL,
+ `PlayerId` int(11) NOT NULL,
+ `LadderRanking` int(11) NOT NULL,
+ `Flags` varchar(50) NOT NULL,
+ `changed` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+ PRIMARY KEY (`index`),
+ UNIQUE KEY `Login` (`Login`)
+ ) ENGINE=MyISAM DEFAULT CHARSET=utf8 COMMENT='Store player metadata' AUTO_INCREMENT=1;";
+
+ // Maps table
+ $query .= "CREATE TABLE IF NOT EXISTS `ic_maps` (
+ `index` int(11) NOT NULL AUTO_INCREMENT,
+ `UId` varchar(100) NOT NULL,
+ `Name` varchar(100) NOT NULL,
+ `FileName` varchar(200) NOT NULL,
+ `Author` varchar(150) NOT NULL,
+ `Environnement` varchar(50) NOT NULL,
+ `Mood` varchar(50) NOT NULL,
+ `BronzeTime` int(11) NOT NULL DEFAULT '-1',
+ `SilverTime` int(11) NOT NULL DEFAULT '-1',
+ `GoldTime` int(11) NOT NULL DEFAULT '-1',
+ `AuthorTime` int(11) NOT NULL DEFAULT '-1',
+ `CopperPrice` int(11) NOT NULL DEFAULT '-1',
+ `LapRace` tinyint(1) NOT NULL,
+ `NbLaps` int(11) NOT NULL DEFAULT '-1',
+ `NbCheckpoints` int(11) NOT NULL DEFAULT '-1',
+ `MapType` varchar(100) NOT NULL,
+ `MapStyle` varchar(100) NOT NULL,
+ `changed` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+ PRIMARY KEY (`index`),
+ UNIQUE KEY `UId` (`UId`)
+ ) ENGINE=MyISAM DEFAULT CHARSET=utf8 COMMENT='Store map metadata' AUTO_INCREMENT=1;";
+
+ // Perform queries
+ if (!$this->multiQuery($query)) {
+ trigger_error("Creating basic tables failed. " . $this->mysqli->error . ' (' . $this->mysqli->errno . ')', E_USER_ERROR);
+ }
+
+ // Optimize all existing tables
+ $query = "SHOW TABLES;";
+ $result = $this->query($query);
+ if (!$result || !is_object($result)) {
+ trigger_error("Couldn't select tables. " . $this->mysqli->error . ' (' . $this->mysqli->errno . ')');
+ }
+ else {
+ $query = "OPTIMIZE TABLE ";
+ $count = $result->num_rows;
+ $index = 0;
+ while ($row = $result->fetch_row()) {
+ $query .= "`" . $row[0] . "`";
+ if ($index < $count - 1) $query .= ", ";
+ $index++;
+ }
+ $query .= ";";
+ if (!$this->query($query)) {
+ trigger_error("Couldn't optimize tables. " . $this->mysqli->error . ' (' . $this->mysqli->errno . ')');
+ }
+ }
+ }
+
+ /**
+ * Wrapper for performing a simple query
+ *
+ * @param string $query
+ * @return mixed query result
+ */
+ public function query($query) {
+ if (!is_string($query)) return false;
+ if (strlen($query) <= 0) return true;
+ return $this->mysqli->query($query);
+ }
+
+ /**
+ * Perform multi query
+ *
+ * @param
+ * string multi_query
+ * @return bool whether no error occured during executing the multi query
+ */
+ public function multiQuery($query) {
+ if (!is_string($query)) return false;
+ if (strlen($query) <= 0) return true;
+ $noError = true;
+ $this->mysqli->multi_query($query);
+ if ($this->mysqli->error) {
+ trigger_error("Executing multi query failed. " . $this->mysqli->error . ' (' . $this->mysqli->errno . ')');
+ $noError = false;
+ }
+ while ($this->mysqli->more_results() && $this->mysqli->next_result()) {
+ if ($this->mysqli->error) {
+ trigger_error("Executing multi query failed. " . $this->mysqli->error . ' (' . $this->mysqli->errno . ')');
+ $noError = false;
+ }
+ }
+ return $noError;
+ }
+
+ /**
+ * Handle 5Second callback
+ */
+ public function handle5Second($callback = null) {
+ // Save current players in database
+ $players = $this->iControl->server->getPlayers();
+ if ($players) {
+ $query = "";
+ foreach ($players as $player) {
+ if (!Tools::isPlayer($player)) continue;
+ $query .= $this->composeInsertPlayer($player);
+ }
+ $this->multiQuery($query);
+ }
+ }
+
+ /**
+ * Handle BeginMap callback
+ */
+ public function handleBeginMap($callback) {
+ $map = $callback[1][0];
+ $query = $this->composeInsertMap($map);
+ $result = $this->query($query);
+ if ($this->mysqli->error) {
+ trigger_error("Couldn't save map. " . $this->mysqli->error . ' (' . $this->mysqli->errno . ')');
+ }
+ }
+
+ /**
+ * Get the player index for the given login
+ *
+ * @param string $login
+ * @return int null
+ */
+ public function getPlayerIndex($login) {
+ $query = "SELECT `index` FROM `" . self::TABLE_PLAYERS . "` WHERE `Login` = '" . $this->escape($login) . "';";
+ $result = $this->query($query);
+ $result = $result->fetch_assoc();
+ if ($result) {
+ return $result['index'];
+ }
+ return null;
+ }
+
+ /**
+ * Get the map index for the given UId
+ *
+ * @param string $uid
+ * @return int null
+ */
+ public function getMapIndex($uid) {
+ $query = "SELECT `index` FROM `" . self::TABLE_MAPS . "` WHERE `UId` = '" . $this->escape($uid) . "';";
+ $result = $this->query($query);
+ $result = $result->fetch_assoc();
+ if ($result) {
+ return $result['index'];
+ }
+ return null;
+ }
+
+ /**
+ * Compose a query string for inserting the given player
+ *
+ * @param array $player
+ */
+ private function composeInsertPlayer($player) {
+ if (!Tools::isPlayer($player)) return "";
+ return "INSERT INTO `" . self::TABLE_PLAYERS . "` (
+ `Login`,
+ `NickName`,
+ `PlayerId`,
+ `LadderRanking`,
+ `Flags`
+ ) VALUES (
+ '" . $this->escape($player['Login']) . "',
+ '" . $this->escape($player['NickName']) . "',
+ " . $player['PlayerId'] . ",
+ " . $player['LadderRanking'] . ",
+ '" . $this->escape($player['Flags']) . "'
+ ) ON DUPLICATE KEY UPDATE
+ `NickName` = VALUES(`NickName`),
+ `PlayerId` = VALUES(`PlayerId`),
+ `LadderRanking` = VALUES(`LadderRanking`),
+ `Flags` = VALUES(`Flags`);";
+ }
+
+ /**
+ * Compose a query string for inserting the given map
+ *
+ * @param array $map
+ */
+ private function composeInsertMap($map) {
+ if (!$map) return "";
+ return "INSERT INTO `" . self::TABLE_MAPS . "` (
+ `UId`,
+ `Name`,
+ `FileName`,
+ `Author`,
+ `Environnement`,
+ `Mood`,
+ `BronzeTime`,
+ `SilverTime`,
+ `GoldTime`,
+ `AuthorTime`,
+ `CopperPrice`,
+ `LapRace`,
+ `NbLaps`,
+ `NbCheckpoints`,
+ `MapType`,
+ `MapStyle`
+ ) VALUES (
+ '" . $this->escape($map['UId']) . "',
+ '" . $this->escape($map['Name']) . "',
+ '" . $this->escape($map['FileName']) . "',
+ '" . $this->escape($map['Author']) . "',
+ '" . $this->escape($map['Environnement']) . "',
+ '" . $this->escape($map['Mood']) . "',
+ " . $map['BronzeTime'] . ",
+ " . $map['SilverTime'] . ",
+ " . $map['GoldTime'] . ",
+ " . $map['AuthorTime'] . ",
+ " . $map['CopperPrice'] . ",
+ " . Tools::boolToInt($map['LapRace']) . ",
+ " . $map['NbLaps'] . ",
+ " . $map['NbCheckpoints'] . ",
+ '" . $this->escape($map['MapType']) . "',
+ '" . $this->escape($map['MapStyle']) . "'
+ ) ON DUPLICATE KEY UPDATE
+ `Name` = VALUES(`Name`),
+ `FileName` = VALUES(`FileName`),
+ `Author` = VALUES(`Author`),
+ `Environnement` = VALUES(`Environnement`),
+ `Mood` = VALUES(`Mood`),
+ `BronzeTime` = VALUES(`BronzeTime`),
+ `SilverTime` = VALUES(`SilverTime`),
+ `GoldTime` = VALUES(`GoldTime`),
+ `AuthorTime` = VALUES(`AuthorTime`),
+ `CopperPrice` = VALUES(`CopperPrice`),
+ `LapRace` = VALUES(`LapRace`),
+ `NbLaps` = VALUES(`NbLaps`),
+ `NbCheckpoints` = VALUES(`NbCheckpoints`),
+ `MapType` = VALUES(`MapType`),
+ `MapStyle` = VALUES(`MapStyle`);";
+ }
+
+ /**
+ * Retrieve all information about the player with the given login
+ */
+ public function getPlayer($login) {
+ if (!$login) return null;
+ $query = "SELECT * FROM `" . self::TABLE_PLAYERS . "` WHERE `Login` = '" . $this->escape($login) . "';";
+ $result = $this->mysqli->query($query);
+ if ($this->mysqli->error || !$result) {
+ trigger_error(
+ "Couldn't select player with login '" . $login . "'. " . $this->mysqli->error . ' (' . $this->mysqli->errno . ')');
+ return null;
+ }
+ else {
+ while ($player = $result->fetch_assoc()) {
+ return $player;
+ }
+ return null;
+ }
+ }
+
+ /**
+ * Escapes the given string for a mysql query
+ *
+ * @param string $string
+ * @return string
+ */
+ public function escape($string) {
+ return $this->mysqli->escape_string($string);
+ }
+}
+
+?>
diff --git a/application/core/server.mControl.php b/application/core/server.mControl.php
new file mode 100644
index 00000000..5e87f1da
--- /dev/null
+++ b/application/core/server.mControl.php
@@ -0,0 +1,381 @@
+mControl = $mControl;
+
+ // Load config
+ $this->config = Tools::loadConfig('server.mControl.xml');
+ $this->iControl->checkConfig($this->config, array('host', 'port', 'login', 'pass'), 'server');
+
+ // Register for callbacks
+ $this->iControl->callbacks->registerCallbackHandler(Callbacks::CB_IC_1_SECOND, $this, 'eachSecond');
+ }
+
+ /**
+ * Perform actions every second
+ */
+ public function eachSecond() {
+ // Delete cached information
+ $this->players = null;
+ }
+
+ /**
+ * Fetch game directory of the server
+ *
+ * @return string
+ */
+ public function getDataDirectory() {
+ if (!$this->iControl->client->query('GameDataDirectory')) {
+ trigger_error("Couldn't get data directory. " . $this->iControl->getClientErrorText());
+ return null;
+ }
+ return $this->iControl->client->getResponse();
+ }
+
+ /**
+ * Checks if mControl has access to the given directory (server data directory if no param)
+ *
+ * @param string $directory
+ * @return bool
+ */
+ public function checkAccess($directory = null) {
+ if (!$directory) {
+ $directory = $this->getDataDirectory();
+ }
+ return is_dir($directory) && is_writable($directory);
+ }
+
+ /**
+ * Fetch server login
+ */
+ public function getLogin($client = null) {
+ $systemInfo = $this->getSystemInfo(false, $client);
+ if (!$systemInfo) return null;
+ return $systemInfo['ServerLogin'];
+ }
+
+ /**
+ * Get detailed server info
+ */
+ public function getInfo($detailed = false) {
+ if ($detailed) {
+ $login = $this->getLogin();
+ if (!$this->iControl->client->query('GetDetailedPlayerInfo', $login)) {
+ trigger_error("Couldn't fetch detailed server player info. " . $this->iControl->getClientErrorText());
+ return null;
+ }
+ }
+ else {
+ if (!$this->iControl->client->query('GetMainServerPlayerInfo')) {
+ trigger_error("Couldn't fetch server player info. " . $this->iControl->getClientErrorText());
+ return null;
+ }
+ }
+ return $this->iControl->client->getResponse();
+ }
+
+ /**
+ * Get server options
+ */
+ public function getOptions() {
+ if (!$this->iControl->client->query('GetServerOptions')) {
+ trigger_error("Couldn't fetch server options. " . $this->iControl->getClientErrorText());
+ return null;
+ }
+ return $this->iControl->client->getResponse();
+ }
+
+ /**
+ * Fetch server name
+ */
+ public function getName() {
+ if (!$this->iControl->client->query('GetServerName')) {
+ trigger_error("Couldn't fetch server name. " . $this->iControl->getClientErrorText());
+ return null;
+ }
+ return $this->iControl->client->getResponse();
+ }
+
+ /**
+ * Fetch server version
+ */
+ public function getVersion($forceRefresh = false) {
+ if (isset($this->iControl->client->version) && !$forceRefresh) return $this->iControl->client->version;
+ if (!$this->iControl->client->query('GetVersion')) {
+ trigger_error("Couldn't fetch server version. " . $this->iControl->getClientErrorText());
+ return null;
+ }
+ else {
+ $this->iControl->client->version = $this->iControl->client->getResponse();
+ return $this->iControl->client->version;
+ }
+ }
+
+ /**
+ * Fetch server system info
+ */
+ public function getSystemInfo($forceRefresh = false, &$client = null) {
+ if (!$this->iControl->client && !$client) return null;
+ if (!$client) $client = $this->iControl->client;
+ if (isset($client->systemInfo) && !$forceRefresh) return $client->systemInfo;
+ if (!$client->query('GetSystemInfo')) {
+ trigger_error("Couldn't fetch server system info. " . $this->iControl->getClientErrorText($client));
+ return null;
+ }
+ else {
+ $client->systemInfo = $client->getResponse();
+ return $client->systemInfo;
+ }
+ }
+
+ /**
+ * Fetch network status
+ */
+ public function getNetworkStats($forceRefresh = false) {
+ if (isset($this->iControl->client->networkStats) && !$forceRefresh) return $this->iControl->client->networkStats;
+ if (!$this->iControl->client->query('GetNetworkStats')) {
+ trigger_error("Couldn't fetch network stats. " . $this->iControl->getClientErrorText());
+ return null;
+ }
+ else {
+ $this->iControl->client->networkStats = $this->iControl->client->getResponse();
+ return $this->iControl->client->networkStats;
+ }
+ }
+
+ /**
+ * Fetch current game mode
+ *
+ * @param bool $stringValue
+ * @param int $parseValue
+ * @return int | string
+ */
+ public function getGameMode($stringValue = false, $parseValue = null) {
+ if (is_int($parseValue)) {
+ $gameMode = $parseValue;
+ }
+ else {
+ if (!$this->iControl->client->query('GetGameMode')) {
+ trigger_error("Couldn't fetch current game mode. " . $this->iControl->getClientErrorText());
+ return null;
+ }
+ $gameMode = $this->iControl->client->getResponse();
+ }
+ if ($stringValue) {
+ switch ($gameMode) {
+ case 0:
+ {
+ return 'Script';
+ }
+ case 1:
+ {
+ return 'Rounds';
+ }
+ case 2:
+ {
+ return 'TimeAttack';
+ }
+ case 3:
+ {
+ return 'Team';
+ }
+ case 4:
+ {
+ return 'Laps';
+ }
+ case 5:
+ {
+ return 'Cup';
+ }
+ case 6:
+ {
+ return 'Stunts';
+ }
+ default:
+ {
+ return 'Unknown';
+ }
+ }
+ }
+ return $gameMode;
+ }
+
+ /**
+ * Fetch player info
+ *
+ * @param string $login
+ * @return struct
+ */
+ public function getPlayer($login, $detailed = false) {
+ if (!$login) return null;
+ $command = ($detailed ? 'GetDetailedPlayerInfo' : 'GetPlayerInfo');
+ if (!$this->iControl->client->query($command, $login)) {
+ trigger_error("Couldn't player info for '" . $login . "'. " . $this->iControl->getClientErrorText());
+ return null;
+ }
+ return $this->iControl->client->getResponse();
+ }
+
+ /**
+ * Fetch all players
+ */
+ public function getPlayers(&$client = null, &$purePlayers = null, &$pureSpectators = null) {
+ if (!$this->iControl->client && !$client) return null;
+ if (!$client) $client = $this->iControl->client;
+ $fetchLength = 30;
+ $offset = 0;
+ $players = array();
+ if (!is_array($purePlayers)) $purePlayers = array();
+ if (!is_array($pureSpectators)) $pureSpectators = array();
+ $tries = 0;
+ while ($tries < 10) {
+ if (!$client->query('GetPlayerList', $fetchLength, $offset)) {
+ trigger_error("Couldn't get player list. " . $this->iControl->getClientErrorText($client));
+ $tries++;
+ }
+ else {
+ $chunk = $client->getResponse();
+ $count = count($chunk);
+ $serverLogin = $this->getLogin($client);
+ for ($index = 0; $index < $count; $index++) {
+ $login = $chunk[$index]['Login'];
+ if ($login === $serverLogin) {
+ // Ignore server
+ unset($chunk[$index]);
+ }
+ else {
+ if ($chunk[$index]['SpectatorStatus'] > 0) {
+ // Pure spectator
+ array_push($pureSpectators, $chunk[$index]);
+ }
+ else {
+ // Pure player
+ array_push($purePlayers, $chunk[$index]);
+ }
+ }
+ }
+ $players = array_merge($players, $chunk);
+ $offset += $count;
+ if ($count < $fetchLength) break;
+ }
+ }
+ return $players;
+ }
+
+ /**
+ * Retrieve validation replay for given login
+ *
+ * @param string $login
+ * @return string
+ */
+ public function getValidationReplay($login) {
+ if (!$login) return null;
+ if (!$this->iControl->client->query('GetValidationReplay', $login)) {
+ trigger_error("Couldn't get validation replay of '" . $login . "'. " . $this->iControl->getClientErrorText());
+ return null;
+ }
+ return $this->iControl->client->getResponse();
+ }
+
+ public function getGhostReplay($login) {
+ $dataDir = $this->getDataDirectory();
+ if (!$this->checkAccess($dataDir)) {
+ return null;
+ }
+
+ // Build file name
+ $map = $this->getMap();
+ $gameMode = $this->getGameMode();
+ $time = time();
+ $fileName = 'Ghost.' . $login . '.' . $gameMode . '.' . $time . '.' . $map['UId'] . '.Replay.Gbx';
+
+ // Save ghost replay
+ if (!$this->iControl->client->query('SaveBestGhostsReplay', $login, self::GHOSTREPLAYDIR . $fileName)) {
+ trigger_error("Couldn't save ghost replay. " . $this->iControl->getClientErrorText());
+ return null;
+ }
+
+ // Load replay file
+ $ghostReplay = file_get_contents($dataDir . 'Replays/' . self::GHOSTREPLAYDIR . $fileName);
+ if (!$ghostReplay) {
+ trigger_error("Couldn't retrieve saved ghost replay.");
+ return null;
+ }
+ return $ghostReplay;
+ }
+
+ /**
+ * Fetch current map
+ */
+ public function getMap() {
+ if (!$this->iControl->client) return null;
+ if (!$this->iControl->client->query('GetCurrentMapInfo')) {
+ trigger_error("Couldn't fetch map info. " . $this->iControl->getClientErrorText());
+ return null;
+ }
+ return $this->iControl->client->getResponse();
+ }
+
+ /**
+ * Waits for the server to have the given status
+ */
+ public function waitForStatus($client, $statusCode = 4) {
+ $client->query('GetStatus');
+ $response = $client->getResponse();
+ // Check if server reached given status
+ if ($response['Code'] === 4) return true;
+ // Server not yet in given status -> Wait for it...
+ $waitBegin = time();
+ $timeoutTags = $this->iControl->config->xpath('timeout');
+ $maxWaitTime = (!empty($timeoutTags) ? (int) $timeoutTags[0] : 20);
+ $lastStatus = $response['Name'];
+ error_log("Waiting for server to reach status " . $statusCode . "...");
+ error_log("Current Status: " . $lastStatus);
+ while ($response['Code'] !== 4) {
+ sleep(1);
+ $client->query('GetStatus');
+ $response = $client->getResponse();
+ if ($lastStatus !== $response['Name']) {
+ error_log("New Status: " . $response['Name']);
+ $lastStatus = $response['Name'];
+ }
+ if (time() - $maxWaitTime > $waitBegin) {
+ // It took too long to reach the status
+ trigger_error(
+ "Server couldn't reach status " . $statusCode . " after " . $maxWaitTime . " seconds! " .
+ $this->iControl->getClientErrorText());
+ return false;
+ }
+ }
+ return true;
+ }
+}
+
+?>
diff --git a/application/core/stats.mControl.php b/application/core/stats.mControl.php
new file mode 100644
index 00000000..f7d4faae
--- /dev/null
+++ b/application/core/stats.mControl.php
@@ -0,0 +1,297 @@
+mControl = $mControl;
+
+ // Load config
+ $this->config = Tools::loadConfig('stats.mControl.xml');
+ $this->loadSettings();
+
+ // Init database tables
+ $this->initTables();
+
+ // Register for needed callbacks
+ $this->iControl->callbacks->registerCallbackHandler(Callbacks::CB_MP_ENDMAP, $this, 'handleEndMap');
+ $this->iControl->callbacks->registerCallbackHandler(Callbacks::CB_MP_PLAYERCHAT, $this, 'handlePlayerChat');
+ $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');
+ }
+
+ /**
+ * Create the database tables
+ */
+ private function initTables() {
+ $query = "";
+
+ // Server stats
+ $query .= "CREATE TABLE IF NOT EXISTS `" . self::TABLE_STATS_SERVER . "` (
+ `index` int(11) NOT NULL AUTO_INCREMENT,
+ `day` date NOT NULL,
+ `connectCount` int(11) NOT NULL DEFAULT '0',
+ `maxPlayerCount` int(11) NOT NULL DEFAULT '0',
+ `playedMaps` int(11) NOT NULL DEFAULT '0',
+ `finishCount` int(11) NOT NULL DEFAULT '0',
+ `changed` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+ PRIMARY KEY (`index`),
+ UNIQUE KEY `day` (`day`)
+ ) ENGINE=MyISAM DEFAULT CHARSET=utf8 COMMENT='Stores server stats' AUTO_INCREMENT=1;";
+
+ // Player stats
+ $query .= "CREATE TABLE IF NOT EXISTS `" . self::TABLE_STATS_PLAYERS . "` (
+ `index` int(11) NOT NULL AUTO_INCREMENT,
+ `Login` varchar(100) NOT NULL,
+ `playTime` int(11) NOT NULL DEFAULT '0',
+ `connectCount` int(11) NOT NULL DEFAULT '0',
+ `chatCount` int(11) NOT NULL DEFAULT '0',
+ `finishCount` int(11) NOT NULL DEFAULT '0',
+ `hitCount` int(11) NOT NULL DEFAULT '0',
+ `eliminationCount` int(11) NOT NULL DEFAULT '0',
+ `lastJoin` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00',
+ `changed` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+ PRIMARY KEY (`index`),
+ UNIQUE KEY `Login` (`Login`)
+ ) ENGINE=MyISAM DEFAULT CHARSET=utf8 COMMENT='Tracks player stats' AUTO_INCREMENT=1;";
+
+ // Perform queries
+ if (!$this->iControl->database->multiQuery($query)) {
+ trigger_error("Creating stats tables failed.");
+ }
+ }
+
+ /**
+ * Load settings from config
+ */
+ private function loadSettings() {
+ $this->settings = new \stdClass();
+
+ $this->settings->track_server_connects = Tools::checkSetting($this->config, 'track_server_connects');
+ $this->settings->track_server_max_players = Tools::checkSetting($this->config, 'track_server_max_players');
+ $this->settings->track_server_played_maps = Tools::checkSetting($this->config, 'track_server_played_maps');
+ $this->settings->track_server_finishes = Tools::checkSetting($this->config, 'track_server_finishes');
+
+ $this->settings->track_player_connects = Tools::checkSetting($this->config, 'track_player_connects');
+ $this->settings->track_player_playtime = Tools::checkSetting($this->config, 'track_player_playtime');
+ $this->settings->track_player_chats = Tools::checkSetting($this->config, 'track_player_chats');
+ $this->settings->track_player_finishes = Tools::checkSetting($this->config, 'track_player_finishes');
+ }
+
+ /**
+ * Handle EndMap callback
+ */
+ public function handleEndMap($callback) {
+ $multiquery = "";
+
+ // Track played server maps
+ if ($this->settings->track_server_played_maps) {
+ $multiquery .= "INSERT INTO `" . self::TABLE_STATS_SERVER . "` (
+ `day`,
+ `playedMaps`
+ ) VALUES (
+ CURDATE(),
+ 1
+ ) ON DUPLICATE KEY UPDATE
+ `playedMaps` = `playedMaps` + VALUES(`playedMaps`)
+ ;";
+ }
+
+ // Perform query
+ if (!$this->iControl->database->multiQuery($multiquery)) {
+ trigger_error("Perform queries on end map failed.");
+ }
+ }
+
+ /**
+ * Handle PlayerChat callback
+ */
+ public function handlePlayerChat($callback) {
+ if ($callback[1][0] <= 0) return;
+ $multiquery = "";
+ $login = $callback[1][1];
+
+ // Track chats
+ if ($this->settings->track_player_chats) {
+ $multiquery .= "INSERT INTO `" . self::TABLE_STATS_PLAYERS . "` (
+ `Login`,
+ `chatCount`
+ ) VALUES (
+ '" . $this->iControl->database->escape($login) . "',
+ 1
+ ) ON DUPLICATE KEY UPDATE
+ `chatCount` = `chatCount` + VALUES(`chatCount`)
+ ;";
+ }
+
+ // Perform query
+ if (!$this->iControl->database->multiQuery($multiquery)) {
+ trigger_error("Perform queries on player chat failed.");
+ }
+ }
+
+ /**
+ * Handle PlayerConnect callback
+ */
+ public function handlePlayerConnect($callback) {
+ $multiquery = "";
+ $login = $callback[1][0];
+
+ // Track server connect
+ if ($this->settings->track_server_connects) {
+ $multiquery .= "INSERT INTO `" . self::TABLE_STATS_SERVER . "` (
+ `day`,
+ `connectCount`
+ ) VALUES (
+ CURDATE(),
+ 1
+ ) ON DUPLICATE KEY UPDATE
+ `connectCount` = `connectCount` + VALUES(`connectCount`)
+ ;";
+ }
+
+ // Track server max players
+ if ($this->settings->track_server_max_players) {
+ $players = $this->iControl->server->getPlayers();
+ $multiquery .= "INSERT INTO `" . self::TABLE_STATS_SERVER . "` (
+ `day`,
+ `maxPlayerCount`
+ ) VALUES (
+ CURDATE(),
+ " . count($players) . "
+ ) ON DUPLICATE KEY UPDATE
+ `maxPlayerCount` = GREATEST(`maxPlayerCount`, VALUES(`maxPlayerCount`))
+ ;";
+ }
+
+ // Track player connect
+ if ($this->settings->track_player_connects) {
+ $multiquery .= "INSERT INTO `" . self::TABLE_STATS_PLAYERS . "` (
+ `Login`,
+ `lastJoin`,
+ `connectCount`
+ ) VALUES (
+ '" . $this->iControl->database->escape($login) . "',
+ NOW(),
+ 1
+ ) ON DUPLICATE KEY UPDATE
+ `lastJoin` = VALUES(`lastJoin`),
+ `connectCount` = `connectCount` + VALUES(`connectCount`)
+ ;";
+ }
+
+ // Perform query
+ if (!$this->iControl->database->multiQuery($multiquery)) {
+ trigger_error("Perform queries on player connect failed.");
+ }
+ }
+
+ /**
+ * Handle PlayerDisconnect callback
+ */
+ public function handlePlayerDisconnect($callback) {
+ $multiquery = "";
+ $login = $callback[1][0];
+
+ // Track player playtime
+ if ($this->settings->track_player_playtime) {
+ $query = "SELECT `lastJoin` FROM `" . self::TABLE_STATS_PLAYERS . "`
+ WHERE `Login` = '" . $this->iControl->database->escape($login) . "'
+ ;";
+ $result = $this->iControl->database->query($query);
+ if (!$result) {
+ // Error
+ trigger_error("Error selecting player join time from '" . $login . "'.");
+ }
+ else {
+ // Add play time
+ while ($row = $result->fetch_object()) {
+ if (!property_exists($row, 'lastJoin')) continue;
+ $lastJoin = strtotime($row->lastJoin);
+ $lastJoin = ($lastJoin > $this->iControl->startTime ? $lastJoin : $this->iControl->startTime);
+ $multiquery .= "INSERT INTO `" . self::TABLE_STATS_PLAYERS . "` (
+ `Login`,
+ `playTime`
+ ) VALUES (
+ '" . $this->iControl->database->escape($login) . "',
+ TIMESTAMPDIFF(SECOND, '" . Tools::timeToTimestamp($lastJoin) . "', NOW())
+ ) ON DUPLICATE KEY UPDATE
+ `playTime` = `playTime` + VALUES(`playTime`)
+ ;";
+ break;
+ }
+ }
+ }
+
+ // Perform query
+ if (!$this->iControl->database->multiQuery($multiquery)) {
+ trigger_error("Perform queries on player connect failed.");
+ }
+ }
+
+ /**
+ * Handle the PlayerFinish callback
+ */
+ public function handlePlayerFinish($callback) {
+ if ($callback[1][0] <= 0) return;
+ if ($callback[1][2] <= 0) return;
+
+ $multiquery = "";
+ $login = $callback[1][1];
+
+ // Track server finishes
+ if ($this->settings->track_server_finishes) {
+ $multiquery .= "INSERT INTO `" . self::TABLE_STATS_SERVER . "` (
+ `day`,
+ `finishCount`
+ ) VALUES (
+ CURDATE(),
+ 1
+ ) ON DUPLICATE KEY UPDATE
+ `finishCount` = `finishCount` + VALUES(`finishCount`)
+ ;";
+ }
+
+ // Track player finishes
+ if ($this->settings->track_player_finishes) {
+ $multiquery .= "INSERT INTO `" . self::TABLE_STATS_PLAYERS . "` (
+ `Login`,
+ `finishCount`
+ ) VALUES (
+ '" . $this->iControl->database->escape($login) . "',
+ 1
+ ) ON DUPLICATE KEY UPDATE
+ `finishCount` = `finishCount` + VALUES(`finishCount`)
+ ;";
+ }
+
+ // Perform query
+ if (!$this->iControl->database->multiQuery($multiquery)) {
+ trigger_error("Perform queries on player finish failed.");
+ }
+ }
+}
+
+?>
diff --git a/application/core/tools.mControl.php b/application/core/tools.mControl.php
new file mode 100644
index 00000000..9490a1d0
--- /dev/null
+++ b/application/core/tools.mControl.php
@@ -0,0 +1,240 @@
+xpath('//' . $setting);
+ if (empty($settings)) {
+ return false;
+ }
+ else {
+ foreach ($settings as $setting) {
+ return self::toBool((string) $setting[0]);
+ }
+ }
+ }
+
+ /**
+ * Check if the given data describes a player
+ *
+ * @param array $player
+ * @return bool
+ */
+ public static function isPlayer($player) {
+ if (!$player || !is_array($player)) return false;
+ if (!array_key_exists('PlayerId', $player) || !is_int($player['PlayerId']) || $player['PlayerId'] <= 0) return false;
+ return true;
+ }
+
+ /**
+ * Convert the given time int to mysql timestamp
+ *
+ * @param int $time
+ * @return string
+ */
+ public static function timeToTimestamp($time) {
+ return date("Y-m-d H:i:s", $time);
+ }
+
+ /**
+ * Add alignment attributes to an xml element
+ *
+ * @param simple_xml_element $xml
+ * @param string $halign
+ * @param string $valign
+ */
+ public static function addAlignment($xml, $halign = 'center', $valign = 'center2') {
+ if (!is_object($xml) || !method_exists($xml, 'addAttribute')) return;
+ if (!property_exists($xml, 'halign')) $xml->addAttribute('halign', $halign);
+ if (!property_exists($xml, 'valign')) $xml->addAttribute('valign', $valign);
+ }
+
+ /**
+ * Add translate attribute to an xml element
+ *
+ * @param simple_xml_element $xml
+ * @param bool $translate
+ */
+ public static function addTranslate($xml, $translate = true) {
+ if (!is_object($xml) || !method_exists($xml, 'addAttribute')) return;
+ if (!property_exists($xml, 'translate')) $xml->addAttribute('translate', ($translate ? 1 : 0));
+ }
+
+ /**
+ * Load a remote file
+ *
+ * @param string $url
+ * @return string || null
+ */
+ public static function loadFile($url) {
+ if (!$url) return false;
+ $urlData = parse_url($url);
+ $port = (isset($urlData['port']) ? $urlData['port'] : 80);
+
+ $fsock = fsockopen($urlData['host'], $port);
+ stream_set_timeout($fsock, 3);
+
+ $query = 'GET ' . $urlData['path'] . ' HTTP/1.0' . PHP_EOL;
+ $query .= 'Host: ' . $urlData['host'] . PHP_EOL;
+ $query .= 'Content-Type: UTF-8' . PHP_EOL;
+ $query .= 'User-Agent: mControl v' . mControl::VERSION . PHP_EOL;
+ $query .= PHP_EOL;
+
+ fwrite($fsock, $query);
+
+ $buffer = '';
+ $info = array('timed_out' => false);
+ while (!feof($fsock) && !$info['timed_out']) {
+ $buffer .= fread($fsock, 1024);
+ $info = stream_get_meta_data($fsock);
+ }
+ fclose($fsock);
+
+ if ($info['timed_out'] || !$buffer) {
+ return null;
+ }
+ if (substr($buffer, 9, 3) != "200") {
+ return null;
+ }
+
+ $result = explode("\r\n\r\n", $buffer, 2);
+
+ if (count($result) < 2) {
+ return null;
+ }
+
+ return $result[1];
+ }
+
+ /**
+ * Formats the given time (milliseconds)
+ *
+ * @param int $time
+ * @return string
+ */
+ public static function formatTime($time) {
+ if (!is_int($time)) $time = (int) $time;
+ $milliseconds = $time % 1000;
+ $seconds = floor($time / 1000);
+ $minutes = floor($seconds / 60);
+ $hours = floor($minutes / 60);
+ $minutes -= $hours * 60;
+ $seconds -= $hours * 60 + $minutes * 60;
+ $format = ($hours > 0 ? $hours . ':' : '');
+ $format .= ($hours > 0 && $minutes < 10 ? '0' : '') . $minutes . ':';
+ $format .= ($seconds < 10 ? '0' : '') . $seconds . ':';
+ $format .= ($milliseconds < 100 ? '0' : '') . ($milliseconds < 10 ? '0' : '') . $milliseconds;
+ return $format;
+ }
+
+ /**
+ * Convert given data to real boolean
+ *
+ * @param
+ * mixed data
+ */
+ public static function toBool($var) {
+ if ($var === true) return true;
+ if ($var === false) return false;
+ if ($var === null) return false;
+ if (is_object($var)) {
+ $var = (string) $var;
+ }
+ if (is_int($var)) {
+ return ($var > 0);
+ }
+ else if (is_string($var)) {
+ $text = strtolower($var);
+ if ($text === 'true' || $text === 'yes') {
+ return true;
+ }
+ else if ($text === 'false' || $text === 'no') {
+ return false;
+ }
+ else {
+ return ((int) $text > 0);
+ }
+ }
+ else {
+ return (bool) $var;
+ }
+ }
+
+ /**
+ * Converts the given boolean to an int representation
+ *
+ * @param bool $bool
+ * @return int
+ */
+ public static function boolToInt($bool) {
+ return ($bool ? 1 : 0);
+ }
+
+ /**
+ * Build new simple xml element
+ *
+ * @param string $name
+ * @param string $id
+ * @return \SimpleXMLElement
+ */
+ public static function newManialinkXml($id = null) {
+ $xml = new \SimpleXMLElement('');
+ $xml->addAttribute('version', '1');
+ if ($id) $xml->addAttribute('id', $id);
+ return $xml;
+ }
+
+ /**
+ * Load config xml-file
+ *
+ * @param string $fileName
+ * @return \SimpleXMLElement
+ */
+ public static function loadConfig($fileName) {
+ // Load config file from configs folder
+ $fileLocation = mControl . '/configs/' . $fileName;
+ if (!file_exists($fileLocation)) {
+ trigger_error("Config file doesn't exist! (" . $fileName . ")", E_USER_ERROR);
+ }
+ return simplexml_load_file($fileLocation);
+ }
+
+ /**
+ * Send the given manialink to players
+ *
+ * @param string $manialink
+ * @param array $logins
+ */
+ public static function sendManialinkPage($client, $manialink, $logins = null, $timeout = 0, $hideOnClick = false) {
+ if (!$client || !$manialink) return;
+ if (!$logins) {
+ // Send manialink to all players
+ $client->query('SendDisplayManialinkPage', $manialink, $timeout, $hideOnClick);
+ }
+ else if (is_array($logins)) {
+ // Send manialink to players
+ foreach ($logins as $login) {
+ $client->query('SendDisplayManialinkPageToLogin', $login, $manialink, $timeout, $hideOnClick);
+ }
+ }
+ else if (is_string($logins)) {
+ // Send manialink to player
+ $client->query('SendDisplayManialinkPageToLogin', $logins, $manialink, $timeout, $hideOnClick);
+ }
+ }
+}
+
+?>
diff --git a/application/mControl.bat b/application/mControl.bat
new file mode 100644
index 00000000..e6c8fdb6
--- /dev/null
+++ b/application/mControl.bat
@@ -0,0 +1,2 @@
+REM set the path to your php.exe here
+START "" /B "D:\Programme\xampp\php\php.exe" -f "mControl.php" 2>&1
diff --git a/application/mControl.php b/application/mControl.php
new file mode 100644
index 00000000..983ac3cf
--- /dev/null
+++ b/application/mControl.php
@@ -0,0 +1,26 @@
+run(true);
+
+?>
diff --git a/application/mControl.sh b/application/mControl.sh
new file mode 100644
index 00000000..90a61ecb
--- /dev/null
+++ b/application/mControl.sh
@@ -0,0 +1,3 @@
+#!/bin/sh
+php mControl.php 2>&1 &
+echo $! > mControl.pid
diff --git a/application/plugins/plugin.mControl.php b/application/plugins/plugin.mControl.php
new file mode 100644
index 00000000..d46297da
--- /dev/null
+++ b/application/plugins/plugin.mControl.php
@@ -0,0 +1,37 @@
+mControl = $mControl;
+
+ error_log('Pugin v' . self::VERSION . ' ready!');
+ }
+
+ /**
+ * Perform actions during each loop
+ */
+ public function loop() {
+ }
+}
+
+?>