From 1cf4021d4f7576eedd7f03fd7f0fe010fb982e35 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Steffen=20Schro=CC=88der?= Date: Thu, 7 Nov 2013 20:13:13 +0100 Subject: [PATCH] Initial Commit --- .../configs/authentication.iControl.xml | 36 + application/configs/chat.iControl.xml | 12 + application/configs/chatlog.plugin.xml | 14 + application/configs/commands.iControl.xml | 44 + application/configs/core.iControl.xml | 15 + application/configs/database.iControl.xml | 16 + application/configs/karma.plugin.xml | 15 + application/configs/obstacle.plugin.xml | 11 + application/configs/plugins.iControl.xml | 13 + application/configs/records.plugin.xml | 67 + application/configs/server.iControl.xml | 17 + application/configs/stats.iControl.xml | 43 + application/configs/united.plugin.xml | 119 ++ application/core/PhpRemote/GbxRemote.bem.php | 892 ++++++++++++ application/core/PhpRemote/GbxRemote.inc.php | 855 ++++++++++++ application/core/authentication.iControl.php | 103 ++ application/core/callbacks.iControl.php | 191 +++ application/core/chat.iControl.php | 85 ++ application/core/commands.iControl.php | 684 ++++++++++ application/core/core.iControl.php | 348 +++++ application/core/database.iControl.php | 401 ++++++ application/core/server.iControl.php | 381 ++++++ application/core/stats.iControl.php | 297 ++++ application/core/tools.iControl.php | 240 ++++ application/iControl.bat | 2 + application/iControl.php | 26 + application/iControl.sh | 3 + application/plugins/chatlog.plugin.php | 85 ++ application/plugins/karma.plugin.php | 305 +++++ application/plugins/obstacle.plugin.php | 63 + application/plugins/plugin.iControl.php | 37 + application/plugins/records.plugin.php | 1216 +++++++++++++++++ application/plugins/united.plugin.php | 677 +++++++++ application/readme.txt | 44 + 34 files changed, 7357 insertions(+) create mode 100644 application/configs/authentication.iControl.xml create mode 100644 application/configs/chat.iControl.xml create mode 100644 application/configs/chatlog.plugin.xml create mode 100644 application/configs/commands.iControl.xml create mode 100644 application/configs/core.iControl.xml create mode 100644 application/configs/database.iControl.xml create mode 100644 application/configs/karma.plugin.xml create mode 100644 application/configs/obstacle.plugin.xml create mode 100644 application/configs/plugins.iControl.xml create mode 100644 application/configs/records.plugin.xml create mode 100644 application/configs/server.iControl.xml create mode 100644 application/configs/stats.iControl.xml create mode 100644 application/configs/united.plugin.xml create mode 100644 application/core/PhpRemote/GbxRemote.bem.php create mode 100644 application/core/PhpRemote/GbxRemote.inc.php create mode 100644 application/core/authentication.iControl.php create mode 100644 application/core/callbacks.iControl.php create mode 100644 application/core/chat.iControl.php create mode 100644 application/core/commands.iControl.php create mode 100644 application/core/core.iControl.php create mode 100644 application/core/database.iControl.php create mode 100644 application/core/server.iControl.php create mode 100644 application/core/stats.iControl.php create mode 100644 application/core/tools.iControl.php create mode 100644 application/iControl.bat create mode 100644 application/iControl.php create mode 100644 application/iControl.sh create mode 100644 application/plugins/chatlog.plugin.php create mode 100644 application/plugins/karma.plugin.php create mode 100644 application/plugins/obstacle.plugin.php create mode 100644 application/plugins/plugin.iControl.php create mode 100644 application/plugins/records.plugin.php create mode 100644 application/plugins/united.plugin.php create mode 100644 application/readme.txt diff --git a/application/configs/authentication.iControl.xml b/application/configs/authentication.iControl.xml new file mode 100644 index 00000000..38659bfb --- /dev/null +++ b/application/configs/authentication.iControl.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.iControl.xml b/application/configs/chat.iControl.xml new file mode 100644 index 00000000..4afb1ee3 --- /dev/null +++ b/application/configs/chat.iControl.xml @@ -0,0 +1,12 @@ + + + + + + + $fff + $0f0 + $f00 + + + diff --git a/application/configs/chatlog.plugin.xml b/application/configs/chatlog.plugin.xml new file mode 100644 index 00000000..ee659092 --- /dev/null +++ b/application/configs/chatlog.plugin.xml @@ -0,0 +1,14 @@ + + + + + + true + + + chat.log + + + true + + diff --git a/application/configs/commands.iControl.xml b/application/configs/commands.iControl.xml new file mode 100644 index 00000000..5eb30730 --- /dev/null +++ b/application/configs/commands.iControl.xml @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/application/configs/core.iControl.xml b/application/configs/core.iControl.xml new file mode 100644 index 00000000..50386359 --- /dev/null +++ b/application/configs/core.iControl.xml @@ -0,0 +1,15 @@ + + + + + + 20 + + + + tracklist.txt + + + mx + + diff --git a/application/configs/database.iControl.xml b/application/configs/database.iControl.xml new file mode 100644 index 00000000..153fc767 --- /dev/null +++ b/application/configs/database.iControl.xml @@ -0,0 +1,16 @@ + + + + + + localhost + 3306 + + + steff + kjhgvhbjnfih2394ugnjk + + + steff_united + + diff --git a/application/configs/karma.plugin.xml b/application/configs/karma.plugin.xml new file mode 100644 index 00000000..7198d770 --- /dev/null +++ b/application/configs/karma.plugin.xml @@ -0,0 +1,15 @@ + + + + + + true + + + Map Voting + + + 92 + 83 + + diff --git a/application/configs/obstacle.plugin.xml b/application/configs/obstacle.plugin.xml new file mode 100644 index 00000000..7d6378e5 --- /dev/null +++ b/application/configs/obstacle.plugin.xml @@ -0,0 +1,11 @@ + + + + + + true + + + operator + + diff --git a/application/configs/plugins.iControl.xml b/application/configs/plugins.iControl.xml new file mode 100644 index 00000000..7f546f47 --- /dev/null +++ b/application/configs/plugins.iControl.xml @@ -0,0 +1,13 @@ + + + + + + chatlog.plugin.php + karma.plugin.php + records.plugin.php + united.plugin.php + + + + diff --git a/application/configs/records.plugin.xml b/application/configs/records.plugin.xml new file mode 100644 index 00000000..bba44fdb --- /dev/null +++ b/application/configs/records.plugin.xml @@ -0,0 +1,67 @@ + + + + + + true + + + + true + + + 200 + + + + true + -139 + 65 + Local Records + 40 + 25 + 4 + + + + + + + true + + + + steff_test + 468a0a185c + + + nsa_dev + 3c20cbc737 + + + united_canyon + 703d080ddd + + + united_stadium + 067b0d0017 + + + united_valley + 37ed993fe1 + + + + + true + 139 + 60 + Dedimania + 40 + 20 + 4 + + + + + diff --git a/application/configs/server.iControl.xml b/application/configs/server.iControl.xml new file mode 100644 index 00000000..179b9eb5 --- /dev/null +++ b/application/configs/server.iControl.xml @@ -0,0 +1,17 @@ + + + + + + true + + + 144.76.158.111 + localhost + 21003 + + + SuperAdmin + dtcfvgubhnjomkjnbhv + + diff --git a/application/configs/stats.iControl.xml b/application/configs/stats.iControl.xml new file mode 100644 index 00000000..6bd8da92 --- /dev/null +++ b/application/configs/stats.iControl.xml @@ -0,0 +1,43 @@ + + + + + + + + + + + true + + + true + + + true + + + true + + + + + + + + true + + + true + + + true + + + true + + + + + + diff --git a/application/configs/united.plugin.xml b/application/configs/united.plugin.xml new file mode 100644 index 00000000..acb0ebeb --- /dev/null +++ b/application/configs/united.plugin.xml @@ -0,0 +1,119 @@ + + + + + + + false + + + false + + + + 2 + + + false + + + true + + + + + + a + + + + + + + + + + + + 144.76.158.111 + 21013 + SuperAdmin + dtcfvgubhnjomkjnbhv + + + + + 144.76.158.111 + 21033 + SuperAdmin + dtcfvgubhnjomkjnbhv + + + + + 144.76.158.111 + 21053 + SuperAdmin + dtcfvgubhnjomkjnbhv + + + + + + + + + a + + + + + + + + + + + + 144.76.158.111 + 21023 + SuperAdmin + dtcfvgubhnjomkjnbhv + + + + + 144.76.158.111 + 21043 + SuperAdmin + dtcfvgubhnjomkjnbhv + + + + + 144.76.158.111 + 21063 + SuperAdmin + dtcfvgubhnjomkjnbhv + + + + + + + true + + + + true + 76 + 85.5 + 10 + 10 + + true + + + + + diff --git a/application/core/PhpRemote/GbxRemote.bem.php b/application/core/PhpRemote/GbxRemote.bem.php new file mode 100644 index 00000000..20803cb0 --- /dev/null +++ b/application/core/PhpRemote/GbxRemote.bem.php @@ -0,0 +1,892 @@ + htmlspecialchars) + Site: http://scripts.incutio.com/xmlrpc/ + Manual: http://scripts.incutio.com/xmlrpc/manual.php + Errors: http://xmlrpc-epi.sourceforge.net/specs/rfc.fault_codes.php + Made available under the Artistic License: http://www.opensource.org/licenses/artistic-license.php + + Modified to support protocol 'GbxRemote 2' ('GbxRemote 1') + This version is for BigEndian machines. For LittleEndian (e.g. Intel PC) + machines use the original GbxRemote.inc.php instead. + + Release 2007-09-22 - Slig: + Modified to support >256KB received data (and now >2MB data produce a specific error message) + Modified readCB() to wait the initial timeout only before first read packet + Modified readCB() to return true if there is data to get with getCBResponses() + Modified to support amd64 (for $recvhandle) + Modified IXR_ClientMulticall_Gbx::addCall() to fit Aseco 0.6.1 + Added IXR_Client_Gbx::bytes_sent & bytes_received counters + Fix for a changed feature since php5.1.1 about reference parameter assignment (was used in stream_select) + Workaround for stream_select return value bug with amd64 + + Release 2008-01-20 - Slig / Xymph / Assembler Maniac: + Workaround for fread delay bug in some cases + Added IXR_Client_Gbx::resetError() method (by Xymph) + Some comments and strings code cleanup (by Xymph) + Fix stream_set_timeout($this->socket,...) (thx to CavalierDeVache) + Added a default timeout value to IXR_Client_Gbx::readCB($timeout) + Changed calls with timeout on a stream to use microseconds instead of seconds (by AM) + Removed IXR_Client_Gbx::bytes_sent & bytes_received counters - not used (by AM) + + Release 2008-02-05 - Slig: + Changed some socket read/write timeouts back to seconds to avoid 'transport error' + Changed max data received from 2MB to 4MB + + Release 2008-05-20 - Xymph: + Prevented unpack() warnings in IXR_Client_Gbx::query() when the connection dies + Changed IXR_Client_Gbx::resetError() to assign 'false' for correct isError() + Tweaked some 'transport error' messages + + Release 2009-04-08 - Gou1: + Added method IXR_Client_Gbx::queryIgnoreResult() + Added methods IXR_Client_Gbx::sendRequest() & IXR_Client_Gbx::getResult() + IXR_Client_Gbx::queryIgnoreResult checks if the request is larger than 512KB to avoid errors + If larger than 512KB and method is system.multicall, try to divide the request into + two separate requests with two separate IXR_Client_Gbx::queryIgnoreResult() calls + + Release 2009-06-03 - Xymph: + Suppress possible repetitive CRT warning at stream_select + + Release 2011-04-09 - Xymph / La beuze: + Added optional timeout mechanism to IXR_Client_Gbx::InitWithIp() + + Release 2011-05-22 - Xymph: + Added non-error (true) return status to IXR_Client_Gbx::queryIgnoreResult() + Updated status codes and messages for transport/endian errors + Prevented possible PHP warning in IXR_Client_Gbx::getErrorCode() and getErrorMessage() +*/ + +if (!defined('LF')) { + define('LF', "\n"); +} + +class IXR_Value { + public $data; + public $type; + + function IXR_Value ($data, $type = false) { + $this->data = $data; + if (!$type) { + $type = $this->calculateType(); + } + $this->type = $type; + if ($type == 'struct') { + // Turn all the values in the array into new IXR_Value objects + foreach ($this->data as $key => $value) { + $this->data[$key] = new IXR_Value($value); + } + } + if ($type == 'array') { + for ($i = 0, $j = count($this->data); $i < $j; $i++) { + $this->data[$i] = new IXR_Value($this->data[$i]); + } + } + } + + function calculateType() { + if ($this->data === true || $this->data === false) { + return 'boolean'; + } + if (is_integer($this->data)) { + return 'int'; + } + if (is_double($this->data)) { + return 'double'; + } + // Deal with IXR object types base64 and date + if (is_object($this->data) && is_a($this->data, 'IXR_Date')) { + return 'date'; + } + if (is_object($this->data) && is_a($this->data, 'IXR_Base64')) { + return 'base64'; + } + // If it is a normal PHP object convert it into a struct + if (is_object($this->data)) { + $this->data = get_object_vars($this->data); + return 'struct'; + } + if (!is_array($this->data)) { + return 'string'; + } + // We have an array - is it an array or a struct? + if ($this->isStruct($this->data)) { + return 'struct'; + } else { + return 'array'; + } + } + + function getXml() { + // Return XML for this value + switch ($this->type) { + case 'boolean': + return '' . ($this->data ? '1' : '0') . ''; + break; + case 'int': + return '' . $this->data . ''; + break; + case 'double': + return '' . $this->data . ''; + break; + case 'string': + return '' . htmlspecialchars($this->data) . ''; + break; + case 'array': + $return = '' . LF; + foreach ($this->data as $item) { + $return .= ' ' . $item->getXml() . '' . LF; + } + $return .= ''; + return $return; + break; + case 'struct': + $return = '' . LF; + foreach ($this->data as $name => $value) { + $return .= ' ' . $name . ''; + $return .= $value->getXml() . '' . LF; + } + $return .= ''; + return $return; + break; + case 'date': + case 'base64': + return $this->data->getXml(); + break; + } + return false; + } + + function isStruct($array) { + // Nasty function to check if an array is a struct or not + $expected = 0; + foreach ($array as $key => $value) { + if ((string)$key != (string)$expected) { + return true; + } + $expected++; + } + return false; + } +} + + +class IXR_Message { + public $message; + public $messageType; // methodCall / methodResponse / fault + public $faultCode; + public $faultString; + public $methodName; + public $params; + // Current variable stacks + protected $_arraystructs = array(); // Stack to keep track of the current array/struct + protected $_arraystructstypes = array(); // Stack to keep track of whether things are structs or array + protected $_currentStructName = array(); // A stack as well + protected $_param; + protected $_value; + protected $_currentTag; + protected $_currentTagContents; + // The XML parser + protected $_parser; + + function IXR_Message ($message) { + $this->message = $message; + } + + function parse() { + // first remove the XML declaration + $this->message = preg_replace('/<\?xml(.*)?\?'.'>/', '', $this->message); + if (trim($this->message) == '') { + return false; + } + $this->_parser = xml_parser_create(); + // Set XML parser to take the case of tags into account + xml_parser_set_option($this->_parser, XML_OPTION_CASE_FOLDING, false); + // Set XML parser callback functions + xml_set_object($this->_parser, $this); + xml_set_element_handler($this->_parser, 'tag_open', 'tag_close'); + xml_set_character_data_handler($this->_parser, 'cdata'); + if (!xml_parse($this->_parser, $this->message)) { + /* die(sprintf('GbxRemote XML error: %s at line %d', + xml_error_string(xml_get_error_code($this->_parser)), + xml_get_current_line_number($this->_parser))); */ + return false; + } + xml_parser_free($this->_parser); + // Grab the error messages, if any + if ($this->messageType == 'fault') { + $this->faultCode = $this->params[0]['faultCode']; + $this->faultString = $this->params[0]['faultString']; + } + return true; + } + + function tag_open($parser, $tag, $attr) { + $this->currentTag = $tag; + switch ($tag) { + case 'methodCall': + case 'methodResponse': + case 'fault': + $this->messageType = $tag; + break; + // Deal with stacks of arrays and structs + case 'data': // data is to all intents and purposes more interesting than array + $this->_arraystructstypes[] = 'array'; + $this->_arraystructs[] = array(); + break; + case 'struct': + $this->_arraystructstypes[] = 'struct'; + $this->_arraystructs[] = array(); + break; + } + } + + function cdata($parser, $cdata) { + $this->_currentTagContents .= $cdata; + } + + function tag_close($parser, $tag) { + $valueFlag = false; + switch ($tag) { + case 'int': + case 'i4': + $value = (int)trim($this->_currentTagContents); + $this->_currentTagContents = ''; + $valueFlag = true; + break; + case 'double': + $value = (double)trim($this->_currentTagContents); + $this->_currentTagContents = ''; + $valueFlag = true; + break; + case 'string': + $value = (string)trim($this->_currentTagContents); + $this->_currentTagContents = ''; + $valueFlag = true; + break; + case 'dateTime.iso8601': + $value = new IXR_Date(trim($this->_currentTagContents)); + // $value = $iso->getTimestamp(); + $this->_currentTagContents = ''; + $valueFlag = true; + break; + case 'value': + // If no type is indicated, the type is string + if (trim($this->_currentTagContents) != '') { + $value = (string)$this->_currentTagContents; + $this->_currentTagContents = ''; + $valueFlag = true; + } + break; + case 'boolean': + $value = (boolean)trim($this->_currentTagContents); + $this->_currentTagContents = ''; + $valueFlag = true; + break; + case 'base64': + $value = base64_decode($this->_currentTagContents); + $this->_currentTagContents = ''; + $valueFlag = true; + break; + // Deal with stacks of arrays and structs + case 'data': + case 'struct': + $value = array_pop($this->_arraystructs); + array_pop($this->_arraystructstypes); + $valueFlag = true; + break; + case 'member': + array_pop($this->_currentStructName); + break; + case 'name': + $this->_currentStructName[] = trim($this->_currentTagContents); + $this->_currentTagContents = ''; + break; + case 'methodName': + $this->methodName = trim($this->_currentTagContents); + $this->_currentTagContents = ''; + break; + } + + if ($valueFlag) { + /* + if (!is_array($value) && !is_object($value)) { + $value = trim($value); + } + */ + if (count($this->_arraystructs) > 0) { + // Add value to struct or array + if ($this->_arraystructstypes[count($this->_arraystructstypes)-1] == 'struct') { + // Add to struct + $this->_arraystructs[count($this->_arraystructs)-1][$this->_currentStructName[count($this->_currentStructName)-1]] = $value; + } else { + // Add to array + $this->_arraystructs[count($this->_arraystructs)-1][] = $value; + } + } else { + // Just add as a paramater + $this->params[] = $value; + } + } + } +} + + +class IXR_Request { + public $method; + public $args; + public $xml; + + function IXR_Request($method, $args) { + $this->method = $method; + $this->args = $args; + $this->xml = '' . $this->method . ''; + foreach ($this->args as $arg) { + $this->xml .= ''; + $v = new IXR_Value($arg); + $this->xml .= $v->getXml(); + $this->xml .= '' . LF; + } + $this->xml .= ''; + } + + function getLength() { + return strlen($this->xml); + } + + function getXml() { + return $this->xml; + } +} + + +class IXR_Error { + public $code; + public $message; + + function IXR_Error($code, $message) { + $this->code = $code; + $this->message = $message; + } + + function getXml() { + $xml = << + + + + + faultCode + {$this->code} + + + faultString + {$this->message} + + + + + +EOD; + return $xml; + } +} + + +class IXR_Date { + public $year; + public $month; + public $day; + public $hour; + public $minute; + public $second; + + function IXR_Date($time) { + // $time can be a PHP timestamp or an ISO one + if (is_numeric($time)) { + $this->parseTimestamp($time); + } else { + $this->parseIso($time); + } + } + + function parseTimestamp($timestamp) { + $this->year = date('Y', $timestamp); + $this->month = date('Y', $timestamp); + $this->day = date('Y', $timestamp); + $this->hour = date('H', $timestamp); + $this->minute = date('i', $timestamp); + $this->second = date('s', $timestamp); + } + + function parseIso($iso) { + $this->year = substr($iso, 0, 4); + $this->month = substr($iso, 4, 2); + $this->day = substr($iso, 6, 2); + $this->hour = substr($iso, 9, 2); + $this->minute = substr($iso, 12, 2); + $this->second = substr($iso, 15, 2); + } + + function getIso() { + return $this->year.$this->month.$this->day.'T'.$this->hour.':'.$this->minute.':'.$this->second; + } + + function getXml() { + return ''.$this->getIso().''; + } + + function getTimestamp() { + return mktime($this->hour, $this->minute, $this->second, $this->month, $this->day, $this->year); + } +} + + +class IXR_Base64 { + public $data; + + function IXR_Base64($data) { + $this->data = $data; + } + + function getXml() { + return ''.base64_encode($this->data).''; + } +} + + +////////////////////////////////////////////////////////// +// Nadeo modifications // +// (many thanks to slig for adding callback support) // +////////////////////////////////////////////////////////// +class IXR_Client_Gbx { + public $socket; + public $message = false; + public $cb_message = array(); + public $reqhandle; + public $protocol = 0; + // Storage place for an error message + public $error = false; + + function bigEndianTest() { + list($endiantest) = array_values(unpack('L1L', pack('V', 1))); + if ($endiantest == 1) { + echo "Machine reports itself as LittleEndian, float handling will work correctly with unpack\r\n"; + echo "Use original GbxRemote.inc.php instead of GbxRemote.bem.php\r\n"; + die('App Terminated'); + return false; + } + return true; + } // bigEndianTest + + function IXR_Client_Gbx() { + $this->socket = false; + $this->reqhandle = 0x80000000; + } + + function InitWithIp($ip, $port, $timeout = null) { + + if (!$this->bigEndianTest()) { + $this->error = new IXR_Error(-31999, 'endian error - script doesn\'t match machine type'); + return false; + } + + // open connection, with timeout if specified + if (!isset($timeout)) { + $this->socket = @fsockopen($ip, $port, $errno, $errstr); + } else { + $init_time = microtime(true); + $init_timeout = 5; // retry every 5s + while (true) { + $this->socket = @fsockopen($ip, $port, $errno, $errstr, $init_timeout); + if ($this->socket || (microtime(true) - $init_time >= $timeout)) + break; + } + } + if (!$this->socket) { + $this->error = new IXR_Error(-32300, "transport error - could not open socket (error: $errno, $errstr)"); + return false; + } + // handshake + $array_result = big_endian_unpack('Vsize', fread($this->socket, 4)); + $size = $array_result['size']; + if ($size > 64) { + $this->error = new IXR_Error(-32300, 'transport error - wrong lowlevel protocol header'); + return false; + } + $handshake = fread($this->socket, $size); + if ($handshake == 'GBXRemote 1') { + $this->protocol = 1; + } else if ($handshake == 'GBXRemote 2') { + $this->protocol = 2; + } else { + $this->error = new IXR_Error(-32300, 'transport error - wrong lowlevel protocol version'); + return false; + } + return true; + } + + function Init($port) { + return $this->InitWithIp('localhost', $port); + } + + function Terminate() { + if ($this->socket) { + fclose($this->socket); + $this->socket = false; + } + } + + protected function sendRequest(IXR_Request $request) { + $xml = $request->getXml(); + + @stream_set_timeout($this->socket, 20); // timeout 20s (to write the request) + // send request + $this->reqhandle++; + if ($this->protocol == 1) { + $bytes = pack('Va*', strlen($xml), $xml); + } else { + $bytes = pack('VVa*', strlen($xml), $this->reqhandle, $xml); + } + + $bytes_to_write = strlen($bytes); + while ($bytes_to_write > 0) { + $r = @fwrite($this->socket, $bytes); + if ($r === false || $r == 0) { + // connection interrupted + return false; // or die? + } + + $bytes_to_write -= $r; + if ($bytes_to_write == 0) + break; + + $bytes = substr($bytes, $r); + } + + return true; + } + + protected function getResult() { + $contents = ''; + $contents_length = 0; + do { + $size = 0; + $recvhandle = 0; + @stream_set_timeout($this->socket, 20); // timeout 20s (to read the reply header) + // Get result + if ($this->protocol == 1) { + $contents = fread($this->socket, 4); + if (strlen($contents) == 0) { + $this->error = new IXR_Error(-32300, 'transport error - cannot read size'); + return false; + } + $array_result = big_endian_unpack('Vsize', $contents); + $size = $array_result['size']; + $recvhandle = $this->reqhandle; + } else { + $contents = fread($this->socket, 8); + if (strlen($contents) == 0) { + $this->error = new IXR_Error(-32300, 'transport error - cannot read size/handle'); + return false; + } + $array_result = big_endian_unpack('Vsize/Vhandle', $contents); + $size = $array_result['size']; + $recvhandle = $array_result['handle']; + // -- amd64 support -- + $bits = sprintf('%b', $recvhandle); + if (strlen($bits) == 64) { + $recvhandle = bindec(substr($bits, 32)); + } + } + + if ($recvhandle == 0 || $size == 0) { + $this->error = new IXR_Error(-32300, 'transport error - connection interrupted!'); + return false; + } + if ($size > 4096*1024) { + $this->error = new IXR_Error(-32300, "transport error - response too large ($size)"); + return false; + } + + $contents = ''; + $contents_length = 0; + @stream_set_timeout($this->socket, 0, 10000); // timeout 10 ms (for successive reads until end) + while ($contents_length < $size) { + $contents .= fread($this->socket, $size-$contents_length); + $contents_length = strlen($contents); + } + + if (($recvhandle & 0x80000000) == 0) { + // this is a callback, not our answer! handle= $recvhandle, xml-rpc= $contents + // just add it to the message list for the user to read + $new_cb_message = new IXR_Message($contents); + if ($new_cb_message->parse() && $new_cb_message->messageType != 'fault') { + array_push($this->cb_message, array($new_cb_message->methodName, $new_cb_message->params)); + } + } + } while ((int)$recvhandle != (int)$this->reqhandle); + + $this->message = new IXR_Message($contents); + if (!$this->message->parse()) { + // XML error + $this->error = new IXR_Error(-32700, 'parse error. not well formed'); + return false; + } + // Is the message a fault? + if ($this->message->messageType == 'fault') { + $this->error = new IXR_Error($this->message->faultCode, $this->message->faultString); + return false; + } + // Message must be OK + return true; + } + + + function query() { + $args = func_get_args(); + $method = array_shift($args); + + if (!$this->socket || $this->protocol == 0) { + $this->error = new IXR_Error(-32300, 'transport error - client not initialized'); + return false; + } + + $request = new IXR_Request($method, $args); + + // Check if request is larger than 512 Kbytes + if (($size = $request->getLength()) > 512*1024-8) { + $this->error = new IXR_Error(-32300, "transport error - request too large ($size)"); + return false; + } + + // Send request + $ok = $this->sendRequest($request); + if (!$ok) { + $this->error = new IXR_Error(-32300, 'transport error - connection interrupted!'); + return false; + } + + // Get result + return $this->getResult(); + } + + // Non-blocking query method: doesn't read the response + function queryIgnoreResult() { + $args = func_get_args(); + $method = array_shift($args); + + if (!$this->socket || $this->protocol == 0) { + $this->error = new IXR_Error(-32300, 'transport error - client not initialized'); + return false; + } + + $request = new IXR_Request($method, $args); + + // Check if the request is greater than 512 Kbytes to avoid errors + // If the method is system.multicall, make two calls (possibly recursively) + if (($size = $request->getLength()) > 512*1024-8) { + if ($method = 'system.multicall' && isset($args[0])) { + $count = count($args[0]); + // If count is 1, query cannot be reduced + if ($count < 2) { + $this->error = new IXR_Error(-32300, "transport error - request too large ($size)"); + return false; + } + $length = floor($count/2); + $args1 = array_slice($args[0], 0, $length); + $args2 = array_slice($args[0], $length, ($count-$length)); + + $res1 = $this->queryIgnoreResult('system.multicall', $args1); + $res2 = $this->queryIgnoreResult('system.multicall', $args2); + return ($res1 && $res2); + } + // If the method is not a multicall, just stop + else { + $this->error = new IXR_Error(-32300, "transport error - request too large ($size)"); + return false; + } + } + + // Send request + $ok = $this->sendRequest($request); + if (!$ok) { + $this->error = new IXR_Error(-32300, 'transport error - connection interrupted!'); + return false; + } + return true; + } + + function readCB($timeout = 2000) { // timeout 2 ms + if (!$this->socket || $this->protocol == 0) { + $this->error = new IXR_Error(-32300, 'transport error - client not initialized'); + return false; + } + if ($this->protocol == 1) + return false; + + $something_received = count($this->cb_message)>0; + $contents = ''; + $contents_length = 0; + + @stream_set_timeout($this->socket, 0, 10000); // timeout 10 ms (to read available data) + // (assignment in arguments is forbidden since php 5.1.1) + $read = array($this->socket); + $write = NULL; + $except = NULL; + $nb = @stream_select($read, $write, $except, 0, $timeout); + // workaround for stream_select bug with amd64 + if ($nb !== false) + $nb = count($read); + + while ($nb !== false && $nb > 0) { + $timeout = 0; // we don't want to wait for the full time again, just flush the available data + + $size = 0; + $recvhandle = 0; + // Get result + $contents = fread($this->socket, 8); + if (strlen($contents) == 0) { + $this->error = new IXR_Error(-32300, 'transport error - cannot read size/handle'); + return false; + } + $array_result = big_endian_unpack('Vsize/Vhandle', $contents); + $size = $array_result['size']; + $recvhandle = $array_result['handle']; + + if ($recvhandle == 0 || $size == 0) { + $this->error = new IXR_Error(-32300, 'transport error - connection interrupted!'); + return false; + } + if ($size > 4096*1024) { + $this->error = new IXR_Error(-32300, "transport error - response too large ($size)"); + return false; + } + + $contents = ''; + $contents_length = 0; + while ($contents_length < $size) { + $contents .= fread($this->socket, $size-$contents_length); + $contents_length = strlen($contents); + } + + if (($recvhandle & 0x80000000) == 0) { + // this is a callback. handle= $recvhandle, xml-rpc= $contents + //echo 'CALLBACK('.$contents_length.')[ '.$contents.' ]' . LF; + $new_cb_message = new IXR_Message($contents); + if ($new_cb_message->parse() && $new_cb_message->messageType != 'fault') { + array_push($this->cb_message, array($new_cb_message->methodName, $new_cb_message->params)); + } + $something_received = true; + } + + // (assignment in arguments is forbidden since php 5.1.1) + $read = array($this->socket); + $write = NULL; + $except = NULL; + $nb = @stream_select($read, $write, $except, 0, $timeout); + // workaround for stream_select bug with amd64 + if ($nb !== false) + $nb = count($read); + } + return $something_received; + } + + function getResponse() { + // methodResponses can only have one param - return that + return $this->message->params[0]; + } + + function getCBResponses() { + // (look at the end of basic.php for an example) + $messages = $this->cb_message; + $this->cb_message = array(); + return $messages; + } + + function isError() { + return is_object($this->error); + } + + function resetError() { + $this->error = false; + } + + function getErrorCode() { + if ($this->isError()) + return $this->error->code; + else + return 0; + } + + function getErrorMessage() { + if ($this->isError()) + return $this->error->message; + else + return ''; + } +} + + +class IXR_ClientMulticall_Gbx extends IXR_Client_Gbx { + public $calls = array(); + + function addCall($methodName, $args) { + $struct = array('methodName' => $methodName, 'params' => $args); + $this->calls[] = $struct; + + return (count($this->calls) - 1); + } + + function multiquery($ignoreResult = false) { + // Prepare multicall, then call the parent::query() (or queryIgnoreResult) method + if ($ignoreResult) { + $result = parent::queryIgnoreResult('system.multicall', $this->calls); + } else { + $result = parent::query('system.multicall', $this->calls); + } + $this->calls = array(); // reset for next calls + return $result; + } +} + +/** + * The following code is a workaround for php's unpack function which + * does not have the capability of unpacking double precision floats + * that were packed in the opposite byte order of the current machine. + */ +function big_endian_unpack($format, $data) { + $ar = unpack($format, $data); + $vals = array_values($ar); + $f = explode('/', $format); + $i = 0; + foreach ($f as $f_k => $f_v) { + $repeater = intval(substr($f_v, 1)); + if ($repeater == 0) + $repeater = 1; + if ($f_v{1} == '*') { + $repeater = count($ar) - $i; + } + if ($f_v{0} != 'd') { + $i += $repeater; + continue; + } + $j = $i + $repeater; + for ($a = $i; $a < $j; ++$a) { + $p = pack('d', $vals[$i]); + $p = strrev($p); + list($vals[$i]) = array_values(unpack('d1d', $p)); + ++$i; + } + } + $a = 0; + foreach ($ar as $ar_k => $ar_v) { + $ar[$ar_k] = $vals[$a]; + ++$a; + } + return $ar; +} +?> diff --git a/application/core/PhpRemote/GbxRemote.inc.php b/application/core/PhpRemote/GbxRemote.inc.php new file mode 100644 index 00000000..a4e17724 --- /dev/null +++ b/application/core/PhpRemote/GbxRemote.inc.php @@ -0,0 +1,855 @@ + htmlspecialchars) + Site: http://scripts.incutio.com/xmlrpc/ + Manual: http://scripts.incutio.com/xmlrpc/manual.php + Errors: http://xmlrpc-epi.sourceforge.net/specs/rfc.fault_codes.php + Made available under the Artistic License: http://www.opensource.org/licenses/artistic-license.php + + Modified to support protocol 'GbxRemote 2' ('GbxRemote 1') + This version is for LittleEndian (e.g. Intel PC) machines. For BegEndian + machines use GbxRemote.bem.php as GbxRemote.inc.php instead. + + Release 2007-09-22 - Slig: + Modified to support >256KB received data (and now >2MB data produce a specific error message) + Modified readCB() to wait the initial timeout only before first read packet + Modified readCB() to return true if there is data to get with getCBResponses() + Modified to support amd64 (for $recvhandle) + Modified IXR_ClientMulticall_Gbx::addCall() to fit Aseco 0.6.1 + Added IXR_Client_Gbx::bytes_sent & bytes_received counters + Fix for a changed feature since php5.1.1 about reference parameter assignment (was used in stream_select) + Workaround for stream_select return value bug with amd64 + + Release 2008-01-20 - Slig / Xymph / Assembler Maniac: + Workaround for fread delay bug in some cases + Added IXR_Client_Gbx::resetError() method (by Xymph) + Some comments and strings code cleanup (by Xymph) + Fix stream_set_timeout($this->socket,...) (thx to CavalierDeVache) + Added a default timeout value to IXR_Client_Gbx::readCB($timeout) + Changed calls with timeout on a stream to use microseconds instead of seconds (by AM) + Removed IXR_Client_Gbx::bytes_sent & bytes_received counters - not used (by AM) + + Release 2008-02-05 - Slig: + Changed some socket read/write timeouts back to seconds to avoid 'transport error' + Changed max data received from 2MB to 4MB + + Release 2008-05-20 - Xymph: + Prevented unpack() warnings in IXR_Client_Gbx::query() when the connection dies + Changed IXR_Client_Gbx::resetError() to assign 'false' for correct isError() + Tweaked some 'transport error' messages + + Release 2009-04-08 - Gou1: + Added method IXR_Client_Gbx::queryIgnoreResult() + Added methods IXR_Client_Gbx::sendRequest() & IXR_Client_Gbx::getResult() + IXR_Client_Gbx::queryIgnoreResult checks if the request is larger than 512KB to avoid errors + If larger than 512KB and method is system.multicall, try to divide the request into + two separate requests with two separate IXR_Client_Gbx::queryIgnoreResult() calls + + Release 2009-06-03 - Xymph: + Suppress possible repetitive CRT warning at stream_select + + Release 2011-04-09 - Xymph / La beuze: + Added optional timeout mechanism to IXR_Client_Gbx::InitWithIp() + + Release 2011-05-22 - Xymph: + Added non-error (true) return status to IXR_Client_Gbx::queryIgnoreResult() + Updated status codes and messages for transport/endian errors + Prevented possible PHP warning in IXR_Client_Gbx::getErrorCode() and getErrorMessage() +*/ + +if (!defined('LF')) { + define('LF', "\n"); +} + +class IXR_Value { + public $data; + public $type; + + function IXR_Value ($data, $type = false) { + $this->data = $data; + if (!$type) { + $type = $this->calculateType(); + } + $this->type = $type; + if ($type == 'struct') { + // Turn all the values in the array into new IXR_Value objects + foreach ($this->data as $key => $value) { + $this->data[$key] = new IXR_Value($value); + } + } + if ($type == 'array') { + for ($i = 0, $j = count($this->data); $i < $j; $i++) { + $this->data[$i] = new IXR_Value($this->data[$i]); + } + } + } + + function calculateType() { + if ($this->data === true || $this->data === false) { + return 'boolean'; + } + if (is_integer($this->data)) { + return 'int'; + } + if (is_double($this->data)) { + return 'double'; + } + // Deal with IXR object types base64 and date + if (is_object($this->data) && is_a($this->data, 'IXR_Date')) { + return 'date'; + } + if (is_object($this->data) && is_a($this->data, 'IXR_Base64')) { + return 'base64'; + } + // If it is a normal PHP object convert it into a struct + if (is_object($this->data)) { + $this->data = get_object_vars($this->data); + return 'struct'; + } + if (!is_array($this->data)) { + return 'string'; + } + // We have an array - is it an array or a struct? + if ($this->isStruct($this->data)) { + return 'struct'; + } else { + return 'array'; + } + } + + function getXml() { + // Return XML for this value + switch ($this->type) { + case 'boolean': + return '' . ($this->data ? '1' : '0') . ''; + break; + case 'int': + return '' . $this->data . ''; + break; + case 'double': + return '' . $this->data . ''; + break; + case 'string': + return '' . htmlspecialchars($this->data) . ''; + break; + case 'array': + $return = '' . LF; + foreach ($this->data as $item) { + $return .= ' ' . $item->getXml() . '' . LF; + } + $return .= ''; + return $return; + break; + case 'struct': + $return = '' . LF; + foreach ($this->data as $name => $value) { + $return .= ' ' . $name . ''; + $return .= $value->getXml() . '' . LF; + } + $return .= ''; + return $return; + break; + case 'date': + case 'base64': + return $this->data->getXml(); + break; + } + return false; + } + + function isStruct($array) { + // Nasty function to check if an array is a struct or not + $expected = 0; + foreach ($array as $key => $value) { + if ((string)$key != (string)$expected) { + return true; + } + $expected++; + } + return false; + } +} + + +class IXR_Message { + public $message; + public $messageType; // methodCall / methodResponse / fault + public $faultCode; + public $faultString; + public $methodName; + public $params; + // Current variable stacks + protected $_arraystructs = array(); // Stack to keep track of the current array/struct + protected $_arraystructstypes = array(); // Stack to keep track of whether things are structs or array + protected $_currentStructName = array(); // A stack as well + protected $_param; + protected $_value; + protected $_currentTag; + protected $_currentTagContents; + // The XML parser + protected $_parser; + + function IXR_Message ($message) { + $this->message = $message; + } + + function parse() { + // first remove the XML declaration + $this->message = preg_replace('/<\?xml(.*)?\?'.'>/', '', $this->message); + if (trim($this->message) == '') { + return false; + } + $this->_parser = xml_parser_create(); + // Set XML parser to take the case of tags into account + xml_parser_set_option($this->_parser, XML_OPTION_CASE_FOLDING, false); + // Set XML parser callback functions + xml_set_object($this->_parser, $this); + xml_set_element_handler($this->_parser, 'tag_open', 'tag_close'); + xml_set_character_data_handler($this->_parser, 'cdata'); + if (!xml_parse($this->_parser, $this->message)) { + /* die(sprintf('GbxRemote XML error: %s at line %d', + xml_error_string(xml_get_error_code($this->_parser)), + xml_get_current_line_number($this->_parser))); */ + return false; + } + xml_parser_free($this->_parser); + // Grab the error messages, if any + if ($this->messageType == 'fault') { + $this->faultCode = $this->params[0]['faultCode']; + $this->faultString = $this->params[0]['faultString']; + } + return true; + } + + function tag_open($parser, $tag, $attr) { + $this->currentTag = $tag; + switch ($tag) { + case 'methodCall': + case 'methodResponse': + case 'fault': + $this->messageType = $tag; + break; + // Deal with stacks of arrays and structs + case 'data': // data is to all intents and purposes more interesting than array + $this->_arraystructstypes[] = 'array'; + $this->_arraystructs[] = array(); + break; + case 'struct': + $this->_arraystructstypes[] = 'struct'; + $this->_arraystructs[] = array(); + break; + } + } + + function cdata($parser, $cdata) { + $this->_currentTagContents .= $cdata; + } + + function tag_close($parser, $tag) { + $valueFlag = false; + switch ($tag) { + case 'int': + case 'i4': + $value = (int)trim($this->_currentTagContents); + $this->_currentTagContents = ''; + $valueFlag = true; + break; + case 'double': + $value = (double)trim($this->_currentTagContents); + $this->_currentTagContents = ''; + $valueFlag = true; + break; + case 'string': + $value = (string)trim($this->_currentTagContents); + $this->_currentTagContents = ''; + $valueFlag = true; + break; + case 'dateTime.iso8601': + $value = new IXR_Date(trim($this->_currentTagContents)); + // $value = $iso->getTimestamp(); + $this->_currentTagContents = ''; + $valueFlag = true; + break; + case 'value': + // If no type is indicated, the type is string + if (trim($this->_currentTagContents) != '') { + $value = (string)$this->_currentTagContents; + $this->_currentTagContents = ''; + $valueFlag = true; + } + break; + case 'boolean': + $value = (boolean)trim($this->_currentTagContents); + $this->_currentTagContents = ''; + $valueFlag = true; + break; + case 'base64': + $value = base64_decode($this->_currentTagContents); + $this->_currentTagContents = ''; + $valueFlag = true; + break; + // Deal with stacks of arrays and structs + case 'data': + case 'struct': + $value = array_pop($this->_arraystructs); + array_pop($this->_arraystructstypes); + $valueFlag = true; + break; + case 'member': + array_pop($this->_currentStructName); + break; + case 'name': + $this->_currentStructName[] = trim($this->_currentTagContents); + $this->_currentTagContents = ''; + break; + case 'methodName': + $this->methodName = trim($this->_currentTagContents); + $this->_currentTagContents = ''; + break; + } + + if ($valueFlag) { + /* + if (!is_array($value) && !is_object($value)) { + $value = trim($value); + } + */ + if (count($this->_arraystructs) > 0) { + // Add value to struct or array + if ($this->_arraystructstypes[count($this->_arraystructstypes)-1] == 'struct') { + // Add to struct + $this->_arraystructs[count($this->_arraystructs)-1][$this->_currentStructName[count($this->_currentStructName)-1]] = $value; + } else { + // Add to array + $this->_arraystructs[count($this->_arraystructs)-1][] = $value; + } + } else { + // Just add as a paramater + $this->params[] = $value; + } + } + } +} + + +class IXR_Request { + public $method; + public $args; + public $xml; + + function IXR_Request($method, $args) { + $this->method = $method; + $this->args = $args; + $this->xml = '' . $this->method . ''; + foreach ($this->args as $arg) { + $this->xml .= ''; + $v = new IXR_Value($arg); + $this->xml .= $v->getXml(); + $this->xml .= '' . LF; + } + $this->xml .= ''; + } + + function getLength() { + return strlen($this->xml); + } + + function getXml() { + return $this->xml; + } +} + + +class IXR_Error { + public $code; + public $message; + + function IXR_Error($code, $message) { + $this->code = $code; + $this->message = $message; + } + + function getXml() { + $xml = << + + + + + faultCode + {$this->code} + + + faultString + {$this->message} + + + + + +EOD; + return $xml; + } +} + + +class IXR_Date { + public $year; + public $month; + public $day; + public $hour; + public $minute; + public $second; + + function IXR_Date($time) { + // $time can be a PHP timestamp or an ISO one + if (is_numeric($time)) { + $this->parseTimestamp($time); + } else { + $this->parseIso($time); + } + } + + function parseTimestamp($timestamp) { + $this->year = date('Y', $timestamp); + $this->month = date('Y', $timestamp); + $this->day = date('Y', $timestamp); + $this->hour = date('H', $timestamp); + $this->minute = date('i', $timestamp); + $this->second = date('s', $timestamp); + } + + function parseIso($iso) { + $this->year = substr($iso, 0, 4); + $this->month = substr($iso, 4, 2); + $this->day = substr($iso, 6, 2); + $this->hour = substr($iso, 9, 2); + $this->minute = substr($iso, 12, 2); + $this->second = substr($iso, 15, 2); + } + + function getIso() { + return $this->year.$this->month.$this->day.'T'.$this->hour.':'.$this->minute.':'.$this->second; + } + + function getXml() { + return ''.$this->getIso().''; + } + + function getTimestamp() { + return mktime($this->hour, $this->minute, $this->second, $this->month, $this->day, $this->year); + } +} + + +class IXR_Base64 { + public $data; + + function IXR_Base64($data) { + $this->data = $data; + } + + function getXml() { + return ''.base64_encode($this->data).''; + } +} + + +////////////////////////////////////////////////////////// +// Nadeo modifications // +// (many thanks to slig for adding callback support) // +////////////////////////////////////////////////////////// +class IXR_Client_Gbx { + public $socket; + public $message = false; + public $cb_message = array(); + public $reqhandle; + public $protocol = 0; + // Storage place for an error message + public $error = false; + + function bigEndianTest() { + list($endiantest) = array_values(unpack('L1L', pack('V', 1))); + if ($endiantest != 1) { + echo "Machine reports itself as BigEndian, float handling must be altered\r\n"; + echo "Overwrite GbxRemote.inc.php with GbxRemote.bem.php\r\n"; + die('App Terminated'); + return false; + } + return true; + } // bigEndianTest + + function IXR_Client_Gbx() { + $this->socket = false; + $this->reqhandle = 0x80000000; + } + + function InitWithIp($ip, $port, $timeout = null) { + + if (!$this->bigEndianTest()) { + $this->error = new IXR_Error(-31999, 'endian error - script doesn\'t match machine type'); + return false; + } + + // open connection, with timeout if specified + if (!isset($timeout)) { + $this->socket = @fsockopen($ip, $port, $errno, $errstr); + } else { + $init_time = microtime(true); + $init_timeout = 5; // retry every 5s + while (true) { + $this->socket = @fsockopen($ip, $port, $errno, $errstr, $init_timeout); + if ($this->socket || (microtime(true) - $init_time >= $timeout)) + break; + } + } + if (!$this->socket) { + $this->error = new IXR_Error(-32300, "transport error - could not open socket (error: $errno, $errstr)"); + return false; + } + // handshake + $array_result = unpack('Vsize', fread($this->socket, 4)); + $size = $array_result['size']; + if ($size > 64) { + $this->error = new IXR_Error(-32300, 'transport error - wrong lowlevel protocol header'); + return false; + } + $handshake = fread($this->socket, $size); + if ($handshake == 'GBXRemote 1') { + $this->protocol = 1; + } else if ($handshake == 'GBXRemote 2') { + $this->protocol = 2; + } else { + $this->error = new IXR_Error(-32300, 'transport error - wrong lowlevel protocol version'); + return false; + } + return true; + } + + function Init($port) { + return $this->InitWithIp('localhost', $port); + } + + function Terminate() { + if ($this->socket) { + fclose($this->socket); + $this->socket = false; + } + } + + protected function sendRequest(IXR_Request $request) { + $xml = $request->getXml(); + + @stream_set_timeout($this->socket, 20); // timeout 20s (to write the request) + // send request + $this->reqhandle++; + if ($this->protocol == 1) { + $bytes = pack('Va*', strlen($xml), $xml); + } else { + $bytes = pack('VVa*', strlen($xml), $this->reqhandle, $xml); + } + + $bytes_to_write = strlen($bytes); + while ($bytes_to_write > 0) { + $r = @fwrite($this->socket, $bytes); + if ($r === false || $r == 0) { + // connection interrupted + return false; // or die? + } + + $bytes_to_write -= $r; + if ($bytes_to_write == 0) + break; + + $bytes = substr($bytes, $r); + } + + return true; + } + + protected function getResult() { + $contents = ''; + $contents_length = 0; + do { + $size = 0; + $recvhandle = 0; + @stream_set_timeout($this->socket, 20); // timeout 20s (to read the reply header) + // Get result + if ($this->protocol == 1) { + $contents = fread($this->socket, 4); + if (strlen($contents) == 0) { + $this->error = new IXR_Error(-32300, 'transport error - cannot read size'); + return false; + } + $array_result = unpack('Vsize', $contents); + $size = $array_result['size']; + $recvhandle = $this->reqhandle; + } else { + $contents = fread($this->socket, 8); + if (strlen($contents) == 0) { + $this->error = new IXR_Error(-32300, 'transport error - cannot read size/handle'); + return false; + } + $array_result = unpack('Vsize/Vhandle', $contents); + $size = $array_result['size']; + $recvhandle = $array_result['handle']; + // -- amd64 support -- + $bits = sprintf('%b', $recvhandle); + if (strlen($bits) == 64) { + $recvhandle = bindec(substr($bits, 32)); + } + } + + if ($recvhandle == 0 || $size == 0) { + $this->error = new IXR_Error(-32300, 'transport error - connection interrupted!'); + return false; + } + if ($size > 4096*1024) { + $this->error = new IXR_Error(-32300, "transport error - response too large ($size)"); + return false; + } + + $contents = ''; + $contents_length = 0; + @stream_set_timeout($this->socket, 0, 10000); // timeout 10 ms (for successive reads until end) + while ($contents_length < $size) { + $contents .= fread($this->socket, $size-$contents_length); + $contents_length = strlen($contents); + } + + if (($recvhandle & 0x80000000) == 0) { + // this is a callback, not our answer! handle= $recvhandle, xml-rpc= $contents + // just add it to the message list for the user to read + $new_cb_message = new IXR_Message($contents); + if ($new_cb_message->parse() && $new_cb_message->messageType != 'fault') { + array_push($this->cb_message, array($new_cb_message->methodName, $new_cb_message->params)); + } + } + } while ((int)$recvhandle != (int)$this->reqhandle); + + $this->message = new IXR_Message($contents); + if (!$this->message->parse()) { + // XML error + $this->error = new IXR_Error(-32700, 'parse error. not well formed'); + return false; + } + // Is the message a fault? + if ($this->message->messageType == 'fault') { + $this->error = new IXR_Error($this->message->faultCode, $this->message->faultString); + return false; + } + // Message must be OK + return true; + } + + + function query() { + $args = func_get_args(); + $method = array_shift($args); + + if (!$this->socket || $this->protocol == 0) { + $this->error = new IXR_Error(-32300, 'transport error - client not initialized'); + return false; + } + + $request = new IXR_Request($method, $args); + + // Check if request is larger than 512 Kbytes + if (($size = $request->getLength()) > 512*1024-8) { + $this->error = new IXR_Error(-32300, "transport error - request too large ($size)"); + return false; + } + + // Send request + $ok = $this->sendRequest($request); + if (!$ok) { + $this->error = new IXR_Error(-32300, 'transport error - connection interrupted!'); + return false; + } + + // Get result + return $this->getResult(); + } + + // Non-blocking query method: doesn't read the response + function queryIgnoreResult() { + $args = func_get_args(); + $method = array_shift($args); + + if (!$this->socket || $this->protocol == 0) { + $this->error = new IXR_Error(-32300, 'transport error - client not initialized'); + return false; + } + + $request = new IXR_Request($method, $args); + + // Check if the request is greater than 512 Kbytes to avoid errors + // If the method is system.multicall, make two calls (possibly recursively) + if (($size = $request->getLength()) > 512*1024-8) { + if ($method = 'system.multicall' && isset($args[0])) { + $count = count($args[0]); + // If count is 1, query cannot be reduced + if ($count < 2) { + $this->error = new IXR_Error(-32300, "transport error - request too large ($size)"); + return false; + } + $length = floor($count/2); + $args1 = array_slice($args[0], 0, $length); + $args2 = array_slice($args[0], $length, ($count-$length)); + + $res1 = $this->queryIgnoreResult('system.multicall', $args1); + $res2 = $this->queryIgnoreResult('system.multicall', $args2); + return ($res1 && $res2); + } + // If the method is not a multicall, just stop + else { + $this->error = new IXR_Error(-32300, "transport error - request too large ($size)"); + return false; + } + } + + // Send request + $ok = $this->sendRequest($request); + if (!$ok) { + $this->error = new IXR_Error(-32300, 'transport error - connection interrupted!'); + return false; + } + return true; + } + + function readCB($timeout = 2000) { // timeout 2 ms + if (!$this->socket || $this->protocol == 0) { + $this->error = new IXR_Error(-32300, 'transport error - client not initialized'); + return false; + } + if ($this->protocol == 1) + return false; + + $something_received = count($this->cb_message)>0; + $contents = ''; + $contents_length = 0; + + @stream_set_timeout($this->socket, 0, 10000); // timeout 10 ms (to read available data) + // (assignment in arguments is forbidden since php 5.1.1) + $read = array($this->socket); + $write = NULL; + $except = NULL; + $nb = @stream_select($read, $write, $except, 0, $timeout); + // workaround for stream_select bug with amd64 + if ($nb !== false) + $nb = count($read); + + while ($nb !== false && $nb > 0) { + $timeout = 0; // we don't want to wait for the full time again, just flush the available data + + $size = 0; + $recvhandle = 0; + // Get result + $contents = fread($this->socket, 8); + if (strlen($contents) == 0) { + $this->error = new IXR_Error(-32300, 'transport error - cannot read size/handle'); + return false; + } + $array_result = unpack('Vsize/Vhandle', $contents); + $size = $array_result['size']; + $recvhandle = $array_result['handle']; + + if ($recvhandle == 0 || $size == 0) { + $this->error = new IXR_Error(-32300, 'transport error - connection interrupted!'); + return false; + } + if ($size > 4096*1024) { + $this->error = new IXR_Error(-32300, "transport error - response too large ($size)"); + return false; + } + + $contents = ''; + $contents_length = 0; + while ($contents_length < $size) { + $contents .= fread($this->socket, $size-$contents_length); + $contents_length = strlen($contents); + } + + if (($recvhandle & 0x80000000) == 0) { + // this is a callback. handle= $recvhandle, xml-rpc= $contents + //echo 'CALLBACK('.$contents_length.')[ '.$contents.' ]' . LF; + $new_cb_message = new IXR_Message($contents); + if ($new_cb_message->parse() && $new_cb_message->messageType != 'fault') { + array_push($this->cb_message, array($new_cb_message->methodName, $new_cb_message->params)); + } + $something_received = true; + } + + // (assignment in arguments is forbidden since php 5.1.1) + $read = array($this->socket); + $write = NULL; + $except = NULL; + $nb = @stream_select($read, $write, $except, 0, $timeout); + // workaround for stream_select bug with amd64 + if ($nb !== false) + $nb = count($read); + } + return $something_received; + } + + function getResponse() { + // methodResponses can only have one param - return that + return $this->message->params[0]; + } + + function getCBResponses() { + // (look at the end of basic.php for an example) + $messages = $this->cb_message; + $this->cb_message = array(); + return $messages; + } + + function isError() { + return is_object($this->error); + } + + function resetError() { + $this->error = false; + } + + function getErrorCode() { + if ($this->isError()) + return $this->error->code; + else + return 0; + } + + function getErrorMessage() { + if ($this->isError()) + return $this->error->message; + else + return ''; + } +} + + +class IXR_ClientMulticall_Gbx extends IXR_Client_Gbx { + public $calls = array(); + + function addCall($methodName, $args) { + $struct = array('methodName' => $methodName, 'params' => $args); + $this->calls[] = $struct; + + return (count($this->calls) - 1); + } + + function multiquery($ignoreResult = false) { + // Prepare multicall, then call the parent::query() (or queryIgnoreResult) method + if ($ignoreResult) { + $result = parent::queryIgnoreResult('system.multicall', $this->calls); + } else { + $result = parent::query('system.multicall', $this->calls); + } + $this->calls = array(); // reset for next calls + return $result; + } +} +?> diff --git a/application/core/authentication.iControl.php b/application/core/authentication.iControl.php new file mode 100644 index 00000000..f8dfffc7 --- /dev/null +++ b/application/core/authentication.iControl.php @@ -0,0 +1,103 @@ + 'none', 0 => 'superadmin', 1 => 'admin', 2 => 'operator', 3 => 'all'); + + /** + * Private properties + */ + private $iControl = null; + + private $config = null; + + /** + * Construct authentication manager + */ + public function __construct($iControl) { + $this->iControl = $iControl; + + // Load config + $this->config = Tools::loadConfig('authentication.iControl.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.iControl.php b/application/core/callbacks.iControl.php new file mode 100644 index 00000000..87c4c9b0 --- /dev/null +++ b/application/core/callbacks.iControl.php @@ -0,0 +1,191 @@ +iControl = $iControl; + + // 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 iControl 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.iControl.php b/application/core/chat.iControl.php new file mode 100644 index 00000000..e5df2af3 --- /dev/null +++ b/application/core/chat.iControl.php @@ -0,0 +1,85 @@ +'; + + /** + * Construct iControl chat + */ + public function __construct($iControl) { + $this->iControl = $iControl; + + // Load config + $this->config = Tools::loadConfig('chat.iControl.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.iControl.php b/application/core/commands.iControl.php new file mode 100644 index 00000000..ccb4fe61 --- /dev/null +++ b/application/core/commands.iControl.php @@ -0,0 +1,684 @@ +iControl = $iControl; + + // Load config + $this->config = Tools::loadConfig('commands.iControl.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 iControl 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 iControl v' . iControl::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("iControl 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 iControl 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("iControl couldn't retrieve the maps directory.", $login); + return; + } + else { + $mapDir = $this->iControl->client->getResponse(); + if (!is_dir($mapDir)) { + trigger_error("iControl doesn't have have access to the maps directory in '" . $mapDir . "'."); + $this->iControl->chat->sendError("iControl 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("iControl doesn't have to rights to save maps in'" . $mapDir . $dlDir, "'."); + $this->iControl->chat->sendError("iControl 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.iControl.php b/application/core/core.iControl.php new file mode 100644 index 00000000..7c35b0f4 --- /dev/null +++ b/application/core/core.iControl.php @@ -0,0 +1,348 @@ +config = Tools::loadConfig('core.iControl.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 iControl and log the given message + */ + public function quit($message = false) { + if ($this->shutdownRequested) return; + + if ($this->client) { + // Announce quit + $this->chat->sendInformation('iControl 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 iControl!"); + exit(); + } + + /** + * Run iControl + */ + public function run($debug = false) { + error_log('Starting iControl v' . self::VERSION . '!'); + $this->debug = (bool) $debug; + + // Load plugins + $this->loadPlugins(); + + // Connect to server + $this->connect(); + + // Loading finished + error_log("Loading completed!"); + + // Announce iControl + if (!$this->chat->sendInformation('iControl v' . self::VERSION . ' successfully started!')) { + trigger_error("Couldn't announce iControl. " . $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 iControl plugins + */ + private function loadPlugins() { + $pluginsConfig = Tools::loadConfig('plugins.iControl.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 = ICONTROL . '/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.iControl.php b/application/core/database.iControl.php new file mode 100644 index 00000000..4e231c6b --- /dev/null +++ b/application/core/database.iControl.php @@ -0,0 +1,401 @@ +iControl = $iControl; + + // Load config + $this->config = Tools::loadConfig('database.iControl.xml'); + $this->iControl->checkConfig($this->config, array("host", "user"), 'database.iControl.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.iControl.php b/application/core/server.iControl.php new file mode 100644 index 00000000..bbb20ed9 --- /dev/null +++ b/application/core/server.iControl.php @@ -0,0 +1,381 @@ +iControl = $iControl; + + // Load config + $this->config = Tools::loadConfig('server.iControl.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 iControl 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.iControl.php b/application/core/stats.iControl.php new file mode 100644 index 00000000..97805195 --- /dev/null +++ b/application/core/stats.iControl.php @@ -0,0 +1,297 @@ +iControl = $iControl; + + // Load config + $this->config = Tools::loadConfig('stats.iControl.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.iControl.php b/application/core/tools.iControl.php new file mode 100644 index 00000000..af130eb4 --- /dev/null +++ b/application/core/tools.iControl.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: iControl v' . iControl::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 = ICONTROL . '/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/iControl.bat b/application/iControl.bat new file mode 100644 index 00000000..c52e833d --- /dev/null +++ b/application/iControl.bat @@ -0,0 +1,2 @@ +REM set the path to your php.exe here +START "" /B "D:\Programme\xampp\php\php.exe" -f "iControl.php" 2>&1 diff --git a/application/iControl.php b/application/iControl.php new file mode 100644 index 00000000..a06dcc28 --- /dev/null +++ b/application/iControl.php @@ -0,0 +1,26 @@ +run(true); + +?> diff --git a/application/iControl.sh b/application/iControl.sh new file mode 100644 index 00000000..352ae1e6 --- /dev/null +++ b/application/iControl.sh @@ -0,0 +1,3 @@ +#!/bin/sh +php iControl.php 2>&1 & +echo $! > iControl.pid diff --git a/application/plugins/chatlog.plugin.php b/application/plugins/chatlog.plugin.php new file mode 100644 index 00000000..86caced0 --- /dev/null +++ b/application/plugins/chatlog.plugin.php @@ -0,0 +1,85 @@ +iControl = $iControl; + + // 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 callbacks + $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 = ICONTROL . '/' . $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(iControl::DATE) . '>> ' . ($login ? $login . ': ' : '') . $text . PHP_EOL; + file_put_contents($this->settings->fileName, $message, FILE_APPEND); + } +} + +?> diff --git a/application/plugins/karma.plugin.php b/application/plugins/karma.plugin.php new file mode 100644 index 00000000..ff11aeb3 --- /dev/null +++ b/application/plugins/karma.plugin.php @@ -0,0 +1,305 @@ +iControl = $iControl; + + // 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 iControl callback + * + * @param array $callback + */ + public function handleOnInitCallback($callback) { + // Send manialink to all players once + $this->sendManialinkRequested = time() + 3; + } + + /** + * Handle iControl 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/obstacle.plugin.php b/application/plugins/obstacle.plugin.php new file mode 100644 index 00000000..85ba29ee --- /dev/null +++ b/application/plugins/obstacle.plugin.php @@ -0,0 +1,63 @@ +iControl = $iControl; + + // 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/plugin.iControl.php b/application/plugins/plugin.iControl.php new file mode 100644 index 00000000..81cb6767 --- /dev/null +++ b/application/plugins/plugin.iControl.php @@ -0,0 +1,37 @@ +iControl = $iControl; + + error_log('Pugin v' . self::VERSION . ' ready!'); + } + + /** + * Perform actions during each loop + */ + public function loop() { + } +} + +?> diff --git a/application/plugins/records.plugin.php b/application/plugins/records.plugin.php new file mode 100644 index 00000000..206e9cfb --- /dev/null +++ b/application/plugins/records.plugin.php @@ -0,0 +1,1216 @@ +iControl = $iControl; + + // 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 iControl 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'] = 'iControl'; + $serverData['Version'] = iControl::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: iControl v' . iControl::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/united.plugin.php b/application/plugins/united.plugin.php new file mode 100644 index 00000000..543bab36 --- /dev/null +++ b/application/plugins/united.plugin.php @@ -0,0 +1,677 @@ +iControl = $iControl; + + // 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 iControl 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', iControl::API_VERSION)) { + trigger_error( + "Couldn't set API version '" . iControl::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 diff --git a/application/readme.txt b/application/readme.txt new file mode 100644 index 00000000..ba576bae --- /dev/null +++ b/application/readme.txt @@ -0,0 +1,44 @@ +********************************************** +* * +* iControl ManiaPlanet Server Control * +* Written by steeffeen * +* Contact: mail@steeffeen.com * +* * +********************************************** + +SETUP: + +1. Copy all files into your desired directory. + +2. Configure the needed settings: + + 2.1 Open the file 'configs/server.iControl.xml'. + Enter your maniaplanet server information. + + 2.2 Open the file 'configs/database.iControl.xml' + Enter your mysql server information or disable database usage if you don't have a mysql server available. + + 2.3 Open the file 'configs/authentication.iControl.xml'. + Add the player logins who should have access to the commands of iControl. + +3. (Optional) Enable or disable the available plugins in the file 'configs/plugins.iControl.xml'. + +4. (Optional) Edit the other config files in 'configs/' in order to customize your iControl to fit your needs. + +5. Run the tool via the shell script 'iControl.sh' (UNIX) or the batch file 'iControl.bat' (Windows) + +6. Enjoy! + + +INFORMATION: + +- iControl is only tested on UNIX machines + - even though it might run properly on Windows I can't promise it will work all the time + - furthermore I can't promise that there won't be a feature in the future that makes it impossible to run iControl under Windows + - in order to run iControl under Windows you have to alter the file iControl.bat and enter the path to your php.exe + +- Tests were performed using PHP Version 5.4 +- If you notice problems with other version please let me know + +- Please report bugs by writing a mail to mail@steeffeen.com +