diff --git a/third_party/gameq_v3.1/GameQ/Autoloader.php b/third_party/gameq_v3.1/GameQ/Autoloader.php new file mode 100644 index 00000000..7c8eb415 --- /dev/null +++ b/third_party/gameq_v3.1/GameQ/Autoloader.php @@ -0,0 +1,60 @@ +. + * + * + */ + +/** + * A simple PSR-4 spec auto loader to allow GameQ to function the same as if it were loaded via Composer + * + * To use this just include this file in your script and the GameQ namespace will be made available + * + * i.e. require_once('/path/to/src/GameQ/Autoloader.php'); + * + * See: https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-4-autoloader-examples.md + * + * @codeCoverageIgnore + */ +spl_autoload_register(function ($class) { + + // project-specific namespace prefix + $prefix = 'GameQ\\'; + + // base directory for the namespace prefix + $base_dir = __DIR__ . DIRECTORY_SEPARATOR; + + // does the class use the namespace prefix? + $len = strlen($prefix); + + if (strncmp($prefix, $class, $len) !== 0) { + // no, move to the next registered autoloader + return; + } + + // get the relative class name + $relative_class = substr($class, $len); + + // replace the namespace prefix with the base directory, replace namespace + // separators with directory separators in the relative class name, append + // with .php + $file = $base_dir . str_replace('\\', '/', $relative_class) . '.php'; + + // if the file exists, require it + if (file_exists($file)) { + require $file; + } +}); diff --git a/third_party/gameq_v3.1/GameQ/Buffer.php b/third_party/gameq_v3.1/GameQ/Buffer.php new file mode 100644 index 00000000..a080e427 --- /dev/null +++ b/third_party/gameq_v3.1/GameQ/Buffer.php @@ -0,0 +1,526 @@ +. + * + * + */ + +namespace GameQ; + +use GameQ\Exception\Protocol as Exception; + +/** + * Class Buffer + * + * Read specific byte sequences from a provided string or Buffer + * + * @package GameQ + * + * @author Austin Bischoff + * @author Aidan Lister + * @author Tom Buskens + */ +class Buffer +{ + + /** + * Constants for the byte code types we need to read as + */ + const NUMBER_TYPE_BIGENDIAN = 'be', + NUMBER_TYPE_LITTLEENDIAN = 'le', + NUMBER_TYPE_MACHINE = 'm'; + + /** + * The number type we use for reading integers. Defaults to little endian + * + * @type string + */ + private $number_type = self::NUMBER_TYPE_LITTLEENDIAN; + + /** + * The original data + * + * @type string + */ + private $data; + + /** + * The original data + * + * @type int + */ + private $length; + + /** + * Position of pointer + * + * @type int + */ + private $index = 0; + + /** + * Constructor + * + * @param string $data + * @param string $number_type + */ + public function __construct($data, $number_type = self::NUMBER_TYPE_LITTLEENDIAN) + { + + $this->number_type = $number_type; + $this->data = $data; + $this->length = strlen($data); + } + + /** + * Return all the data + * + * @return string The data + */ + public function getData() + { + + return $this->data; + } + + /** + * Return data currently in the buffer + * + * @return string The data currently in the buffer + */ + public function getBuffer() + { + + return substr($this->data, $this->index); + } + + /** + * Returns the number of bytes in the buffer + * + * @return int Length of the buffer + */ + public function getLength() + { + + return max($this->length - $this->index, 0); + } + + /** + * Read from the buffer + * + * @param int $length + * + * @return string + * @throws \GameQ\Exception\Protocol + */ + public function read($length = 1) + { + + if (($length + $this->index) > $this->length) { + throw new Exception("Unable to read length={$length} from buffer. Bad protocol format or return?"); + } + + $string = substr($this->data, $this->index, $length); + $this->index += $length; + + return $string; + } + + /** + * Read the last character from the buffer + * + * Unlike the other read functions, this function actually removes + * the character from the buffer. + * + * @return string + */ + public function readLast() + { + + $len = strlen($this->data); + $string = $this->data[strlen($this->data) - 1]; + $this->data = substr($this->data, 0, $len - 1); + $this->length -= 1; + + return $string; + } + + /** + * Look at the buffer, but don't remove + * + * @param int $length + * + * @return string + */ + public function lookAhead($length = 1) + { + + return substr($this->data, $this->index, $length); + } + + /** + * Skip forward in the buffer + * + * @param int $length + */ + public function skip($length = 1) + { + + $this->index += $length; + } + + /** + * Jump to a specific position in the buffer, + * will not jump past end of buffer + * + * @param $index + */ + public function jumpto($index) + { + + $this->index = min($index, $this->length - 1); + } + + /** + * Get the current pointer position + * + * @return int + */ + public function getPosition() + { + + return $this->index; + } + + /** + * Read from buffer until delimiter is reached + * + * If not found, return everything + * + * @param string $delim + * + * @return string + * @throws \GameQ\Exception\Protocol + */ + public function readString($delim = "\x00") + { + + // Get position of delimiter + $len = strpos($this->data, $delim, min($this->index, $this->length)); + + // If it is not found then return whole buffer + if ($len === false) { + return $this->read(strlen($this->data) - $this->index); + } + + // Read the string and remove the delimiter + $string = $this->read($len - $this->index); + ++$this->index; + + return $string; + } + + /** + * Reads a pascal string from the buffer + * + * @param int $offset Number of bits to cut off the end + * @param bool $read_offset True if the data after the offset is to be read + * + * @return string + * @throws \GameQ\Exception\Protocol + */ + public function readPascalString($offset = 0, $read_offset = false) + { + + // Get the proper offset + $len = $this->readInt8(); + $offset = max($len - $offset, 0); + + // Read the data + if ($read_offset) { + return $this->read($offset); + } else { + return substr($this->read($len), 0, $offset); + } + } + + /** + * Read from buffer until any of the delimiters is reached + * + * If not found, return everything + * + * @param $delims + * @param null|string &$delimfound + * + * @return string + * @throws \GameQ\Exception\Protocol + * + * @todo: Check to see if this is even used anymore + */ + public function readStringMulti($delims, &$delimfound = null) + { + + // Get position of delimiters + $pos = []; + foreach ($delims as $delim) { + if ($index = strpos($this->data, $delim, min($this->index, $this->length))) { + $pos[] = $index; + } + } + + // If none are found then return whole buffer + if (empty($pos)) { + return $this->read(strlen($this->data) - $this->index); + } + + // Read the string and remove the delimiter + sort($pos); + $string = $this->read($pos[0] - $this->index); + $delimfound = $this->read(); + + return $string; + } + + /** + * Read an 8-bit unsigned integer + * + * @return int + * @throws \GameQ\Exception\Protocol + */ + public function readInt8() + { + + $int = unpack('Cint', $this->read(1)); + + return $int['int']; + } + + /** + * Read and 8-bit signed integer + * + * @return int + * @throws \GameQ\Exception\Protocol + */ + public function readInt8Signed() + { + + $int = unpack('cint', $this->read(1)); + + return $int['int']; + } + + /** + * Read a 16-bit unsigned integer + * + * @return int + * @throws \GameQ\Exception\Protocol + */ + public function readInt16() + { + + // Change the integer type we are looking up + switch ($this->number_type) { + case self::NUMBER_TYPE_BIGENDIAN: + $type = 'nint'; + break; + + case self::NUMBER_TYPE_LITTLEENDIAN: + $type = 'vint'; + break; + + default: + $type = 'Sint'; + } + + $int = unpack($type, $this->read(2)); + + return $int['int']; + } + + /** + * Read a 16-bit signed integer + * + * @return int + * @throws \GameQ\Exception\Protocol + */ + public function readInt16Signed() + { + + // Read the data into a string + $string = $this->read(2); + + // For big endian we need to reverse the bytes + if ($this->number_type == self::NUMBER_TYPE_BIGENDIAN) { + $string = strrev($string); + } + + $int = unpack('sint', $string); + + unset($string); + + return $int['int']; + } + + /** + * Read a 32-bit unsigned integer + * + * @return int + * @throws \GameQ\Exception\Protocol + */ + public function readInt32($length = 4) + { + // Change the integer type we are looking up + $littleEndian = null; + switch ($this->number_type) { + case self::NUMBER_TYPE_BIGENDIAN: + $type = 'N'; + $littleEndian = false; + break; + + case self::NUMBER_TYPE_LITTLEENDIAN: + $type = 'V'; + $littleEndian = true; + break; + + default: + $type = 'L'; + } + + // read from the buffer and append/prepend empty bytes for shortened int32 + $corrected = $this->read($length); + + // Unpack the number + $int = unpack($type . 'int', self::extendBinaryString($corrected, 4, $littleEndian)); + + return $int['int']; + } + + /** + * Read a 32-bit signed integer + * + * @return int + * @throws \GameQ\Exception\Protocol + */ + public function readInt32Signed() + { + + // Read the data into a string + $string = $this->read(4); + + // For big endian we need to reverse the bytes + if ($this->number_type == self::NUMBER_TYPE_BIGENDIAN) { + $string = strrev($string); + } + + $int = unpack('lint', $string); + + unset($string); + + return $int['int']; + } + + /** + * Read a 64-bit unsigned integer + * + * @return int + * @throws \GameQ\Exception\Protocol + */ + public function readInt64() + { + + // We have the pack 64-bit codes available. See: http://php.net/manual/en/function.pack.php + if (version_compare(PHP_VERSION, '5.6.3') >= 0 && PHP_INT_SIZE == 8) { + // Change the integer type we are looking up + switch ($this->number_type) { + case self::NUMBER_TYPE_BIGENDIAN: + $type = 'Jint'; + break; + + case self::NUMBER_TYPE_LITTLEENDIAN: + $type = 'Pint'; + break; + + default: + $type = 'Qint'; + } + + $int64 = unpack($type, $this->read(8)); + + $int = $int64['int']; + + unset($int64); + } else { + if ($this->number_type == self::NUMBER_TYPE_BIGENDIAN) { + $high = $this->readInt32(); + $low = $this->readInt32(); + } else { + $low = $this->readInt32(); + $high = $this->readInt32(); + } + + // We have to determine the number via bitwise + $int = ($high << 32) | $low; + + unset($low, $high); + } + + return $int; + } + + /** + * Read a 32-bit float + * + * @return float + * @throws \GameQ\Exception\Protocol + */ + public function readFloat32() + { + + // Read the data into a string + $string = $this->read(4); + + // For big endian we need to reverse the bytes + if ($this->number_type == self::NUMBER_TYPE_BIGENDIAN) { + $string = strrev($string); + } + + $float = unpack('ffloat', $string); + + unset($string); + + return $float['float']; + } + + private static function extendBinaryString($input, $length = 4, $littleEndian = null) + { + if (is_null($littleEndian)) { + $littleEndian = self::isLittleEndian(); + } + + $extension = str_repeat(pack($littleEndian ? 'V' : 'N', 0b0000), $length - strlen($input)); + + if ($littleEndian) { + return $input . $extension; + } else { + return $extension . $input; + } + } + + private static function isLittleEndian() + { + return 0x00FF === current(unpack('v', pack('S', 0x00FF))); + } +} diff --git a/third_party/gameq_v3.1/GameQ/Exception/Protocol.php b/third_party/gameq_v3.1/GameQ/Exception/Protocol.php new file mode 100644 index 00000000..035fc6de --- /dev/null +++ b/third_party/gameq_v3.1/GameQ/Exception/Protocol.php @@ -0,0 +1,30 @@ +. + * + * + */ + +namespace GameQ\Exception; + +/** + * Exception + * + * @author Austin Bischoff + */ +class Protocol extends \Exception +{ +} diff --git a/third_party/gameq_v3.1/GameQ/Exception/Query.php b/third_party/gameq_v3.1/GameQ/Exception/Query.php new file mode 100644 index 00000000..cc69f398 --- /dev/null +++ b/third_party/gameq_v3.1/GameQ/Exception/Query.php @@ -0,0 +1,30 @@ +. + * + * + */ + +namespace GameQ\Exception; + +/** + * Exception + * + * @author Austin Bischoff + */ +class Query extends \Exception +{ +} diff --git a/third_party/gameq_v3.1/GameQ/Exception/Server.php b/third_party/gameq_v3.1/GameQ/Exception/Server.php new file mode 100644 index 00000000..4024551e --- /dev/null +++ b/third_party/gameq_v3.1/GameQ/Exception/Server.php @@ -0,0 +1,30 @@ +. + * + * + */ + +namespace GameQ\Exception; + +/** + * Exception + * + * @author Austin Bischoff + */ +class Server extends \Exception +{ +} diff --git a/third_party/gameq_v3.1/GameQ/Filters/Base.php b/third_party/gameq_v3.1/GameQ/Filters/Base.php new file mode 100644 index 00000000..501f77d4 --- /dev/null +++ b/third_party/gameq_v3.1/GameQ/Filters/Base.php @@ -0,0 +1,63 @@ +. + */ + +namespace GameQ\Filters; + +use GameQ\Server; + +/** + * Abstract base class which all filters must inherit + * + * @author Austin Bischoff + */ +abstract class Base +{ + + /** + * Holds the options for this instance of the filter + * + * @type array + */ + protected $options = []; + + /** + * Construct + * + * @param array $options + */ + public function __construct(array $options = []) + { + + $this->options = $options; + } + + public function getOptions() + { + return $this->options; + } + + /** + * Apply the filter to the data + * + * @param array $result + * @param \GameQ\Server $server + * + * @return mixed + */ + abstract public function apply(array $result, Server $server); +} diff --git a/third_party/gameq_v3.1/GameQ/Filters/Normalize.php b/third_party/gameq_v3.1/GameQ/Filters/Normalize.php new file mode 100644 index 00000000..5771c929 --- /dev/null +++ b/third_party/gameq_v3.1/GameQ/Filters/Normalize.php @@ -0,0 +1,133 @@ +. + */ + +namespace GameQ\Filters; + +use GameQ\Server; + +/** + * Class Normalize + * + * @package GameQ\Filters + */ +class Normalize extends Base +{ + + /** + * Holds the protocol specific normalize information + * + * @type array + */ + protected $normalize = []; + + /** + * Apply this filter + * + * @param array $result + * @param \GameQ\Server $server + * + * @return array + */ + public function apply(array $result, Server $server) + { + + // No result passed so just return + if (empty($result)) { + return $result; + } + + //$data = [ ]; + //$data['raw'][$server->id()] = $result; + + // Grab the normalize for this protocol for the specific server + $this->normalize = $server->protocol()->getNormalize(); + + // Do general information + $result = array_merge($result, $this->check('general', $result)); + + // Do player information + if (isset($result['players']) && count($result['players']) > 0) { + // Iterate + foreach ($result['players'] as $key => $player) { + $result['players'][$key] = array_merge($player, $this->check('player', $player)); + } + } else { + $result['players'] = []; + } + + // Do team information + if (isset($result['teams']) && count($result['teams']) > 0) { + // Iterate + foreach ($result['teams'] as $key => $team) { + $result['teams'][$key] = array_merge($team, $this->check('team', $team)); + } + } else { + $result['teams'] = []; + } + + //$data['filtered'][$server->id()] = $result; + /*file_put_contents( + sprintf('%s/../../../tests/Filters/Providers/Normalize/%s_1.json', __DIR__, $server->protocol()->getProtocol()), + json_encode($data, JSON_UNESCAPED_UNICODE | JSON_PARTIAL_OUTPUT_ON_ERROR) + );*/ + + // Return the normalized result + return $result; + } + + /** + * Check a section for normalization + * + * @param $section + * @param $data + * + * @return array + */ + protected function check($section, $data) + { + + // Normalized return array + $normalized = []; + + if (isset($this->normalize[$section]) && !empty($this->normalize[$section])) { + foreach ($this->normalize[$section] as $property => $raw) { + // Default the value for the new key as null + $value = null; + + if (is_array($raw)) { + // Iterate over the raw property we want to use + foreach ($raw as $check) { + if (array_key_exists($check, $data)) { + $value = $data[$check]; + break; + } + } + } else { + // String + if (array_key_exists($raw, $data)) { + $value = $data[$raw]; + } + } + + $normalized['gq_' . $property] = $value; + } + } + + return $normalized; + } +} diff --git a/third_party/gameq_v3.1/GameQ/Filters/Secondstohuman.php b/third_party/gameq_v3.1/GameQ/Filters/Secondstohuman.php new file mode 100644 index 00000000..1b413f74 --- /dev/null +++ b/third_party/gameq_v3.1/GameQ/Filters/Secondstohuman.php @@ -0,0 +1,121 @@ +. + */ + +namespace GameQ\Filters; + +use GameQ\Server; + +/** + * Class Secondstohuman + * + * This class converts seconds into a human readable time string 'hh:mm:ss'. This is mainly for converting + * a player's connected time into a readable string. Note that most game servers DO NOT return a player's connected + * time. Source (A2S) based games generally do but not always. This class can also be used to convert other time + * responses into readable time + * + * @package GameQ\Filters + * @author Austin Bischoff + */ +class Secondstohuman extends Base +{ + + /** + * The options key for setting the data key(s) to look for to convert + */ + const OPTION_TIMEKEYS = 'timekeys'; + + /** + * The result key added when applying this filter to a result + */ + const RESULT_KEY = 'gq_%s_human'; + + /** + * Holds the default 'time' keys from the response array. This is key is usually 'time' from A2S responses + * + * @var array + */ + protected $timeKeysDefault = ['time']; + + /** + * Secondstohuman constructor. + * + * @param array $options + */ + public function __construct(array $options = []) + { + // Check for passed keys + if (!array_key_exists(self::OPTION_TIMEKEYS, $options)) { + // Use default + $options[self::OPTION_TIMEKEYS] = $this->timeKeysDefault; + } else { + // Used passed key(s) and make sure it is an array + $options[self::OPTION_TIMEKEYS] = (!is_array($options[self::OPTION_TIMEKEYS])) ? + [$options[self::OPTION_TIMEKEYS]] : $options[self::OPTION_TIMEKEYS]; + } + + parent::__construct($options); + } + + /** + * Apply this filter to the result data + * + * @param array $result + * @param Server $server + * + * @return array + */ + public function apply(array $result, Server $server) + { + // Send the results off to be iterated and return the updated result + return $this->iterate($result); + } + + /** + * Home grown iterate function. Would like to replace this with an internal PHP method(s) but could not find a way + * to make the iterate classes add new keys to the response. They all seemed to be read-only. + * + * @todo: See if there is a more internal way of handling this instead of foreach looping and recursive calling + * + * @param array $result + * + * @return array + */ + protected function iterate(array &$result) + { + // Iterate over the results + foreach ($result as $key => $value) { + // Offload to itself if we have another array + if (is_array($value)) { + // Iterate and update the result + $result[$key] = $this->iterate($value); + } elseif (in_array($key, $this->options[self::OPTION_TIMEKEYS])) { + // Make sure the value is a float (throws E_WARNING in PHP 7.1+) + $value = floatval($value); + // We match one of the keys we are wanting to convert so add it and move on + $result[sprintf(self::RESULT_KEY, $key)] = sprintf( + "%02d:%02d:%02d", + floor($value / 3600), + ($value / 60) % 60, + $value % 60 + ); + } + } + + return $result; + } +} diff --git a/third_party/gameq_v3.1/GameQ/Filters/Stripcolors.php b/third_party/gameq_v3.1/GameQ/Filters/Stripcolors.php new file mode 100644 index 00000000..1760b73a --- /dev/null +++ b/third_party/gameq_v3.1/GameQ/Filters/Stripcolors.php @@ -0,0 +1,118 @@ +. + */ + +namespace GameQ\Filters; + +use GameQ\Server; + +/** + * Class Strip Colors + * + * Strip color codes from UT and Quake based games + * + * @package GameQ\Filters + */ +class Stripcolors extends Base +{ + + /** + * Apply this filter + * + * @SuppressWarnings(PHPMD.CyclomaticComplexity) + * + * @param array $result + * @param \GameQ\Server $server + * + * @return array + */ + public function apply(array $result, Server $server) + { + + // No result passed so just return + if (empty($result)) { + return $result; + } + + //$data = []; + //$data['raw'][ $server->id() ] = $result; + + // Switch based on the base (not game) protocol + switch ($server->protocol()->getProtocol()) { + case 'quake2': + case 'quake3': + case 'doom3': + array_walk_recursive($result, [$this, 'stripQuake']); + break; + case 'unreal2': + case 'ut3': + case 'gamespy3': //not sure if gamespy3 supports ut colors but won't hurt + case 'gamespy2': + array_walk_recursive($result, [$this, 'stripUnreal']); + break; + case 'source': + array_walk_recursive($result, [$this, 'stripSource']); + break; + case 'gta5m': + array_walk_recursive($result, [$this, 'stripQuake']); + break; + } + + /*$data['filtered'][ $server->id() ] = $result; + file_put_contents( + sprintf( + '%s/../../../tests/Filters/Providers/Stripcolors\%s_1.json', + __DIR__, + $server->protocol()->getProtocol() + ), + json_encode($data, JSON_UNESCAPED_UNICODE | JSON_PARTIAL_OUTPUT_ON_ERROR) + );*/ + + // Return the stripped result + return $result; + } + + /** + * Strip color codes from quake based games + * + * @param string $string + */ + protected function stripQuake(&$string) + { + $string = preg_replace('#(\^.)#', '', $string); + } + + /** + * Strip color codes from Source based games + * + * @param string $string + */ + protected function stripSource(&$string) + { + $string = strip_tags($string); + } + + /** + * Strip color codes from Unreal based games + * + * @param string $string + */ + protected function stripUnreal(&$string) + { + $string = preg_replace('/\x1b.../', '', $string); + } +} diff --git a/third_party/gameq_v3.1/GameQ/Filters/Test.php b/third_party/gameq_v3.1/GameQ/Filters/Test.php new file mode 100644 index 00000000..836ddf3d --- /dev/null +++ b/third_party/gameq_v3.1/GameQ/Filters/Test.php @@ -0,0 +1,47 @@ +. + */ + +namespace GameQ\Filters; + +use GameQ\Server; + +/** + * Class Test + * + * This is a test filter to be used for testing purposes only. + * + * @package GameQ\Filters + */ +class Test extends Base +{ + /** + * Apply the filter. For this we just return whatever is sent + * + * @SuppressWarnings(PHPMD) + * + * @param array $result + * @param \GameQ\Server $server + * + * @return array + */ + public function apply(array $result, Server $server) + { + + return $result; + } +} diff --git a/third_party/gameq_v3.1/GameQ/GameQ.php b/third_party/gameq_v3.1/GameQ/GameQ.php new file mode 100644 index 00000000..a4ec2036 --- /dev/null +++ b/third_party/gameq_v3.1/GameQ/GameQ.php @@ -0,0 +1,659 @@ +. + */ + +namespace GameQ; + +use GameQ\Exception\Protocol as ProtocolException; +use GameQ\Exception\Query as QueryException; + +/** + * Base GameQ Class + * + * This class should be the only one that is included when you use GameQ to query + * any games servers. + * + * Requirements: See wiki or README for more information on the requirements + * - PHP 5.4.14+ + * * Bzip2 - http://www.php.net/manual/en/book.bzip2.php + * + * @author Austin Bischoff + * + * @property bool $debug + * @property string $capture_packets_file + * @property int $stream_timeout + * @property int $timeout + * @property int $write_wait + */ +class GameQ +{ + /* + * Constants + */ + const PROTOCOLS_DIRECTORY = __DIR__ . '/Protocols'; + + /* Static Section */ + + /** + * Holds the instance of itself + * + * @type self + */ + protected static $instance = null; + + /** + * Create a new instance of this class + * + * @return \GameQ\GameQ + */ + public static function factory() + { + + // Create a new instance + self::$instance = new self(); + + // Return this new instance + return self::$instance; + } + + /* Dynamic Section */ + + /** + * Default options + * + * @type array + */ + protected $options = [ + 'debug' => false, + 'timeout' => 3, // Seconds + 'filters' => [ + // Default normalize + 'normalize_d751713988987e9331980363e24189ce' => [ + 'filter' => 'normalize', + 'options' => [], + ], + ], + // Advanced settings + 'stream_timeout' => 200000, // See http://www.php.net/manual/en/function.stream-select.php for more info + 'write_wait' => 500, + // How long (in micro-seconds) to pause between writing to server sockets, helps cpu usage + + // Used for generating protocol test data + 'capture_packets_file' => null, + ]; + + /** + * Array of servers being queried + * + * @type array + */ + protected $servers = []; + + /** + * The query library to use. Default is Native + * + * @type string + */ + protected $queryLibrary = 'GameQ\\Query\\Native'; + + /** + * Holds the instance of the queryLibrary + * + * @type \GameQ\Query\Core|null + */ + protected $query = null; + + /** + * GameQ constructor. + * + * Do some checks as needed so this will operate + */ + public function __construct() + { + // Check for missing utf8_encode function + if (!function_exists('utf8_encode')) { + throw new \Exception("PHP's utf8_encode() function is required - " + . "http://php.net/manual/en/function.utf8-encode.php. Check your php installation."); + } + } + + /** + * Get an option's value + * + * @param mixed $option + * + * @return mixed|null + */ + public function __get($option) + { + + return isset($this->options[$option]) ? $this->options[$option] : null; + } + + /** + * Set an option's value + * + * @param mixed $option + * @param mixed $value + * + * @return bool + */ + public function __set($option, $value) + { + + $this->options[$option] = $value; + + return true; + } + + public function getServers() + { + return $this->servers; + } + + public function getOptions() + { + return $this->options; + } + + /** + * Chainable call to __set, uses set as the actual setter + * + * @param mixed $var + * @param mixed $value + * + * @return $this + */ + public function setOption($var, $value) + { + + // Use magic + $this->{$var} = $value; + + return $this; // Make chainable + } + + /** + * Add a single server + * + * @param array $server_info + * + * @return $this + */ + public function addServer(array $server_info = []) + { + + // Add and validate the server + $this->servers[uniqid()] = new Server($server_info); + + return $this; // Make calls chainable + } + + /** + * Add multiple servers in a single call + * + * @param array $servers + * + * @return $this + */ + public function addServers(array $servers = []) + { + + // Loop through all the servers and add them + foreach ($servers as $server_info) { + $this->addServer($server_info); + } + + return $this; // Make calls chainable + } + + /** + * Add a set of servers from a file or an array of files. + * Supported formats: + * JSON + * + * @param array $files + * + * @return $this + * @throws \Exception + */ + public function addServersFromFiles($files = []) + { + + // Since we expect an array let us turn a string (i.e. single file) into an array + if (!is_array($files)) { + $files = [$files]; + } + + // Iterate over the file(s) and add them + foreach ($files as $file) { + // Check to make sure the file exists and we can read it + if (!file_exists($file) || !is_readable($file)) { + continue; + } + + // See if this file is JSON + if (($servers = json_decode(file_get_contents($file), true)) === null + && json_last_error() !== JSON_ERROR_NONE + ) { + // Type not supported + continue; + } + + // Add this list of servers + $this->addServers($servers); + } + + return $this; + } + + /** + * Clear all of the defined servers + * + * @return $this + */ + public function clearServers() + { + + // Reset all the servers + $this->servers = []; + + return $this; // Make Chainable + } + + /** + * Add a filter to the processing list + * + * @param string $filterName + * @param array $options + * + * @return $this + */ + public function addFilter($filterName, $options = []) + { + // Create the filter hash so we can run multiple versions of the same filter + $filterHash = sprintf('%s_%s', strtolower($filterName), md5(json_encode($options))); + + // Add the filter + $this->options['filters'][$filterHash] = [ + 'filter' => strtolower($filterName), + 'options' => $options, + ]; + + unset($filterHash); + + return $this; + } + + /** + * Remove an added filter + * + * @param string $filterHash + * + * @return $this + */ + public function removeFilter($filterHash) + { + // Make lower case + $filterHash = strtolower($filterHash); + + // Remove this filter if it has been defined + if (array_key_exists($filterHash, $this->options['filters'])) { + unset($this->options['filters'][$filterHash]); + } + + unset($filterHash); + + return $this; + } + + /** + * Return the list of applied filters + * + * @return array + */ + public function listFilters() + { + return $this->options['filters']; + } + + /** + * Main method used to actually process all of the added servers and return the information + * + * @return array + * @throws \Exception + */ + public function process() + { + + // Initialize the query library we are using + $class = new \ReflectionClass($this->queryLibrary); + + // Set the query pointer to the new instance of the library + $this->query = $class->newInstance(); + + unset($class); + + // Define the return + $results = []; + + // @todo: Add break up into loop to split large arrays into smaller chunks + + // Do server challenge(s) first, if any + $this->doChallenges(); + + // Do packets for server(s) and get query responses + $this->doQueries(); + + // Now we should have some information to process for each server + foreach ($this->servers as $server) { + /* @var $server \GameQ\Server */ + + // Parse the responses for this server + $result = $this->doParseResponse($server); + + // Apply the filters + $result = array_merge($result, $this->doApplyFilters($result, $server)); + + // Sort the keys so they are alphabetical and nicer to look at + ksort($result); + + // Add the result to the results array + $results[$server->id()] = $result; + } + + return $results; + } + + /** + * Do server challenges, where required + */ + protected function doChallenges() + { + + // Initialize the sockets for reading + $sockets = []; + + // By default we don't have any challenges to process + $server_challenge = false; + + // Do challenge packets + foreach ($this->servers as $server_id => $server) { + /* @var $server \GameQ\Server */ + + // This protocol has a challenge packet that needs to be sent + if ($server->protocol()->hasChallenge()) { + // We have a challenge, set the flag + $server_challenge = true; + + // Let's make a clone of the query class + $socket = clone $this->query; + + // Set the information for this query socket + $socket->set( + $server->protocol()->transport(), + $server->ip, + $server->port_query, + $this->timeout + ); + + try { + // Now write the challenge packet to the socket. + $socket->write($server->protocol()->getPacket(Protocol::PACKET_CHALLENGE)); + + // Add the socket information so we can reference it easily + $sockets[(int)$socket->get()] = [ + 'server_id' => $server_id, + 'socket' => $socket, + ]; + } catch (QueryException $exception) { + // Check to see if we are in debug, if so bubble up the exception + if ($this->debug) { + throw new \Exception($exception->getMessage(), $exception->getCode(), $exception); + } + } + + unset($socket); + + // Let's sleep shortly so we are not hammering out calls rapid fire style hogging cpu + usleep($this->write_wait); + } + } + + // We have at least one server with a challenge, we need to listen for responses + if ($server_challenge) { + // Now we need to listen for and grab challenge response(s) + $responses = call_user_func_array( + [$this->query, 'getResponses'], + [$sockets, $this->timeout, $this->stream_timeout] + ); + + // Iterate over the challenge responses + foreach ($responses as $socket_id => $response) { + // Back out the server_id we need to update the challenge response for + $server_id = $sockets[$socket_id]['server_id']; + + // Make this into a buffer so it is easier to manipulate + $challenge = new Buffer(implode('', $response)); + + // Grab the server instance + /* @var $server \GameQ\Server */ + $server = $this->servers[$server_id]; + + // Apply the challenge + $server->protocol()->challengeParseAndApply($challenge); + + // Add this socket to be reused, has to be reused in GameSpy3 for example + $server->socketAdd($sockets[$socket_id]['socket']); + + // Clear + unset($server); + } + } + } + + /** + * Run the actual queries and get the response(s) + */ + protected function doQueries() + { + + // Initialize the array of sockets + $sockets = []; + + // Iterate over the server list + foreach ($this->servers as $server_id => $server) { + /* @var $server \GameQ\Server */ + + // Invoke the beforeSend method + $server->protocol()->beforeSend($server); + + // Get all the non-challenge packets we need to send + $packets = $server->protocol()->getPacket('!' . Protocol::PACKET_CHALLENGE); + + if (count($packets) == 0) { + // Skip nothing else to do for some reason. + continue; + } + + // Try to use an existing socket + if (($socket = $server->socketGet()) === null) { + // Let's make a clone of the query class + $socket = clone $this->query; + + // Set the information for this query socket + $socket->set( + $server->protocol()->transport(), + $server->ip, + $server->port_query, + $this->timeout + ); + } + + try { + // Iterate over all the packets we need to send + foreach ($packets as $packet_data) { + // Now write the packet to the socket. + $socket->write($packet_data); + + // Let's sleep shortly so we are not hammering out calls rapid fire style + usleep($this->write_wait); + } + + unset($packets); + + // Add the socket information so we can reference it easily + $sockets[(int)$socket->get()] = [ + 'server_id' => $server_id, + 'socket' => $socket, + ]; + } catch (QueryException $exception) { + // Check to see if we are in debug, if so bubble up the exception + if ($this->debug) { + throw new \Exception($exception->getMessage(), $exception->getCode(), $exception); + } + + continue; + } + + // Clean up the sockets, if any left over + $server->socketCleanse(); + } + + // Now we need to listen for and grab response(s) + $responses = call_user_func_array( + [$this->query, 'getResponses'], + [$sockets, $this->timeout, $this->stream_timeout] + ); + + // Iterate over the responses + foreach ($responses as $socket_id => $response) { + // Back out the server_id + $server_id = $sockets[$socket_id]['server_id']; + + // Grab the server instance + /* @var $server \GameQ\Server */ + $server = $this->servers[$server_id]; + + // Save the response from this packet + $server->protocol()->packetResponse($response); + + unset($server); + } + + // Now we need to close all of the sockets + foreach ($sockets as $socketInfo) { + /* @var $socket \GameQ\Query\Core */ + $socket = $socketInfo['socket']; + + // Close the socket + $socket->close(); + + unset($socket); + } + + unset($sockets); + } + + /** + * Parse the response for a specific server + * + * @param \GameQ\Server $server + * + * @return array + * @throws \Exception + */ + protected function doParseResponse(Server $server) + { + + try { + // @codeCoverageIgnoreStart + // We want to save this server's response to a file (useful for unit testing) + if (!is_null($this->capture_packets_file)) { + file_put_contents( + $this->capture_packets_file, + implode(PHP_EOL . '||' . PHP_EOL, $server->protocol()->packetResponse()) + ); + } + // @codeCoverageIgnoreEnd + + // Get the server response + $results = $server->protocol()->processResponse(); + + // Check for online before we do anything else + $results['gq_online'] = (count($results) > 0); + } catch (ProtocolException $e) { + // Check to see if we are in debug, if so bubble up the exception + if ($this->debug) { + throw new \Exception($e->getMessage(), $e->getCode(), $e); + } + + // We ignore this server + $results = [ + 'gq_online' => false, + ]; + } + + // Now add some default stuff + $results['gq_address'] = (isset($results['gq_address'])) ? $results['gq_address'] : $server->ip(); + $results['gq_port_client'] = $server->portClient(); + $results['gq_port_query'] = (isset($results['gq_port_query'])) ? $results['gq_port_query'] : $server->portQuery(); + $results['gq_protocol'] = $server->protocol()->getProtocol(); + $results['gq_type'] = (string)$server->protocol(); + $results['gq_name'] = $server->protocol()->nameLong(); + $results['gq_transport'] = $server->protocol()->transport(); + + // Process the join link + if (!isset($results['gq_joinlink']) || empty($results['gq_joinlink'])) { + $results['gq_joinlink'] = $server->getJoinLink(); + } + + return $results; + } + + /** + * Apply any filters to the results + * + * @param array $results + * @param \GameQ\Server $server + * + * @return array + */ + protected function doApplyFilters(array $results, Server $server) + { + + // Loop over the filters + foreach ($this->options['filters'] as $filterOptions) { + // Try to do this filter + try { + // Make a new reflection class + $class = new \ReflectionClass(sprintf('GameQ\\Filters\\%s', ucfirst($filterOptions['filter']))); + + // Create a new instance of the filter class specified + $filter = $class->newInstanceArgs([$filterOptions['options']]); + + // Apply the filter to the data + $results = $filter->apply($results, $server); + } catch (\ReflectionException $exception) { + // Invalid, skip it + continue; + } + } + + return $results; + } +} diff --git a/third_party/gameq_v3.1/GameQ/Protocol.php b/third_party/gameq_v3.1/GameQ/Protocol.php new file mode 100644 index 00000000..6d94a45f --- /dev/null +++ b/third_party/gameq_v3.1/GameQ/Protocol.php @@ -0,0 +1,500 @@ +. + * + * + */ + +namespace GameQ; + +/** + * Handles the core functionality for the protocols + * + * @SuppressWarnings(PHPMD.NumberOfChildren) + * + * @author Austin Bischoff + */ +abstract class Protocol +{ + + /** + * Constants for class states + */ + const STATE_TESTING = 1; + + const STATE_BETA = 2; + + const STATE_STABLE = 3; + + const STATE_DEPRECATED = 4; + + /** + * Constants for packet keys + */ + const PACKET_ALL = 'all'; // Some protocols allow all data to be sent back in one call. + + const PACKET_BASIC = 'basic'; + + const PACKET_CHALLENGE = 'challenge'; + + const PACKET_CHANNELS = 'channels'; // Voice servers + + const PACKET_DETAILS = 'details'; + + const PACKET_INFO = 'info'; + + const PACKET_PLAYERS = 'players'; + + const PACKET_STATUS = 'status'; + + const PACKET_RULES = 'rules'; + + const PACKET_VERSION = 'version'; + + /** + * Transport constants + */ + const TRANSPORT_UDP = 'udp'; + + const TRANSPORT_TCP = 'tcp'; + + const TRANSPORT_SSL = 'ssl'; + + const TRANSPORT_TLS = 'tls'; + + /** + * Short name of the protocol + * + * @type string + */ + protected $name = 'unknown'; + + /** + * The longer, fancier name for the protocol + * + * @type string + */ + protected $name_long = 'unknown'; + + /** + * The difference between the client port and query port + * + * @type int + */ + protected $port_diff = 0; + + /** + * The transport method to use to actually send the data + * Default is UDP + * + * @type string + */ + protected $transport = self::TRANSPORT_UDP; + + /** + * The protocol type used when querying the server + * + * @type string + */ + protected $protocol = 'unknown'; + + /** + * Holds the valid packet types this protocol has available. + * + * @type array + */ + protected $packets = []; + + /** + * Holds the response headers and the method to use to process them. + * + * @type array + */ + protected $responses = []; + + /** + * Holds the list of methods to run when parsing the packet response(s) data. These + * methods should provide all the return information. + * + * @type array + */ + protected $process_methods = []; + + /** + * The packet responses received + * + * @type array + */ + protected $packets_response = []; + + /** + * Holds the instance of the result class + * + * @type null + */ + protected $result = null; + + /** + * Options for this protocol + * + * @type array + */ + protected $options = []; + + /** + * Define the state of this class + * + * @type int + */ + protected $state = self::STATE_STABLE; + + /** + * Holds specific normalize settings + * + * @todo: Remove this ugly bulk by moving specific ones to their specific game(s) + * + * @type array + */ + protected $normalize = [ + // General + 'general' => [ + // target => source + 'dedicated' => [ + 'listenserver', + 'dedic', + 'bf2dedicated', + 'netserverdedicated', + 'bf2142dedicated', + 'dedicated', + ], + 'gametype' => ['ggametype', 'sigametype', 'matchtype'], + 'hostname' => ['svhostname', 'servername', 'siname', 'name'], + 'mapname' => ['map', 'simap'], + 'maxplayers' => ['svmaxclients', 'simaxplayers', 'maxclients', 'max_players'], + 'mod' => ['game', 'gamedir', 'gamevariant'], + 'numplayers' => ['clients', 'sinumplayers', 'num_players'], + 'password' => ['protected', 'siusepass', 'sineedpass', 'pswrd', 'gneedpass', 'auth', 'passsord'], + ], + // Indvidual + 'player' => [ + 'name' => ['nick', 'player', 'playername', 'name'], + 'kills' => ['kills'], + 'deaths' => ['deaths'], + 'score' => ['kills', 'frags', 'skill', 'score'], + 'ping' => ['ping'], + ], + // Team + 'team' => [ + 'name' => ['name', 'teamname', 'team_t'], + 'score' => ['score', 'score_t'], + ], + ]; + + /** + * Quick join link + * + * @type string + */ + protected $join_link = ''; + + /** + * @param array $options + */ + public function __construct(array $options = []) + { + + // Set the options for this specific instance of the class + $this->options = $options; + } + + /** + * String name of this class + * + * @return string + */ + public function __toString() + { + + return $this->name; + } + + /** + * Get the port difference between the server's client (game) and query ports + * + * @return int + */ + public function portDiff() + { + + return $this->port_diff; + } + + /** + * "Find" the query port based off of the client port and port_diff + * + * This method is meant to be overloaded for more complex maths or lookup tables + * + * @param int $clientPort + * + * @return int + */ + public function findQueryPort($clientPort) + { + + return $clientPort + $this->port_diff; + } + + /** + * Return the join_link as defined by the protocol class + * + * @return string + */ + public function joinLink() + { + + return $this->join_link; + } + + /** + * Short (callable) name of this class + * + * @return string + */ + public function name() + { + + return $this->name; + } + + /** + * Long name of this class + * + * @return string + */ + public function nameLong() + { + + return $this->name_long; + } + + /** + * Return the status of this Protocol Class + * + * @return int + */ + public function state() + { + + return $this->state; + } + + /** + * Return the protocol property + * + * @return string + */ + public function getProtocol() + { + + return $this->protocol; + } + + /** + * Get/set the transport type for this protocol + * + * @param string|null $type + * + * @return string + */ + public function transport($type = null) + { + + // Act as setter + if (!is_null($type)) { + $this->transport = $type; + } + + return $this->transport; + } + + /** + * Set the options for the protocol call + * + * @param array $options + * + * @return array + */ + public function options($options = []) + { + + // Act as setter + if (!empty($options)) { + $this->options = $options; + } + + return $this->options; + } + + + /* + * Packet Section + */ + + /** + * Return specific packet(s) + * + * @param array $type + * + * @return array + */ + public function getPacket($type = []) + { + + $packets = []; + + + // We want an array of packets back + if (is_array($type) && !empty($type)) { + // Loop the packets + foreach ($this->packets as $packet_type => $packet_data) { + // We want this packet + if (in_array($packet_type, $type)) { + $packets[$packet_type] = $packet_data; + } + } + } elseif ($type == '!challenge') { + // Loop the packets + foreach ($this->packets as $packet_type => $packet_data) { + // Dont want challenge packets + if ($packet_type != self::PACKET_CHALLENGE) { + $packets[$packet_type] = $packet_data; + } + } + } elseif (is_string($type)) { + // Return specific packet type + $packets = $this->packets[$type]; + } else { + // Return all packets + $packets = $this->packets; + } + + // Return the packets + return $packets; + } + + /** + * Get/set the packet response + * + * @param array|null $response + * + * @return array + */ + public function packetResponse(array $response = null) + { + + // Act as setter + if (!empty($response)) { + $this->packets_response = $response; + } + + return $this->packets_response; + } + + + /* + * Challenge section + */ + + /** + * Determine whether or not this protocol has a challenge needed before querying + * + * @return bool + */ + public function hasChallenge() + { + + return (isset($this->packets[self::PACKET_CHALLENGE]) && !empty($this->packets[self::PACKET_CHALLENGE])); + } + + /** + * Parse the challenge response and add it to the buffer items that need it. + * This should be overloaded by extending class + * + * @codeCoverageIgnore + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + * + * @param \GameQ\Buffer $challenge_buffer + * + * @return bool + */ + public function challengeParseAndApply(Buffer $challenge_buffer) + { + + return true; + } + + /** + * Apply the challenge string to all the packets that need it. + * + * @param string $challenge_string + * + * @return bool + */ + protected function challengeApply($challenge_string) + { + + // Let's loop through all the packets and append the challenge where it is needed + foreach ($this->packets as $packet_type => $packet) { + $this->packets[$packet_type] = sprintf($packet, $challenge_string); + } + + return true; + } + + /** + * Get the normalize settings for the protocol + * + * @return array + */ + public function getNormalize() + { + + return $this->normalize; + } + + /* + * General + */ + + /** + * Generic method to allow protocol classes to do work right before the query is sent + * + * @codeCoverageIgnore + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + * + * @param \GameQ\Server $server + */ + public function beforeSend(Server $server) + { + } + + /** + * Method called to process query response data. Each extending class has to have one of these functions. + * + * @return mixed + */ + abstract public function processResponse(); +} diff --git a/third_party/gameq_v3.1/GameQ/Protocols/Aa3.php b/third_party/gameq_v3.1/GameQ/Protocols/Aa3.php new file mode 100644 index 00000000..6ffd412a --- /dev/null +++ b/third_party/gameq_v3.1/GameQ/Protocols/Aa3.php @@ -0,0 +1,53 @@ +. + */ + +namespace GameQ\Protocols; + +/** + * Class Aa3 + * + * @package GameQ\Protocols + * @author Austin Bischoff + */ +class Aa3 extends Source +{ + + /** + * String name of this protocol class + * + * @type string + */ + protected $name = 'aa3'; + + /** + * Longer string name of this protocol class + * + * @type string + */ + protected $name_long = "America's Army 3"; + + /** + * Query port = client_port + 18243 + * + * client_port default 8777 + * query_port default 27020 + * + * @type int + */ + protected $port_diff = 18243; +} diff --git a/third_party/gameq_v3.1/GameQ/Protocols/Aapg.php b/third_party/gameq_v3.1/GameQ/Protocols/Aapg.php new file mode 100644 index 00000000..a207d4fe --- /dev/null +++ b/third_party/gameq_v3.1/GameQ/Protocols/Aapg.php @@ -0,0 +1,42 @@ +. + */ + +namespace GameQ\Protocols; + +/** + * Class Aapg + * + * @package GameQ\Protocols + * @author Austin Bischoff + */ +class Aapg extends Aa3 +{ + /** + * String name of this protocol class + * + * @type string + */ + protected $name = 'aapg'; + + /** + * Longer string name of this protocol class + * + * @type string + */ + protected $name_long = "America's Army: Proving Grounds"; +} diff --git a/third_party/gameq_v3.1/GameQ/Protocols/Arkse.php b/third_party/gameq_v3.1/GameQ/Protocols/Arkse.php new file mode 100644 index 00000000..3193c5a6 --- /dev/null +++ b/third_party/gameq_v3.1/GameQ/Protocols/Arkse.php @@ -0,0 +1,51 @@ +. + */ + +namespace GameQ\Protocols; + +/** + * Class ARK: Survival Evolved + * + * @package GameQ\Protocols + * @author Austin Bischoff + */ +class Arkse extends Source +{ + + /** + * String name of this protocol class + * + * @type string + */ + protected $name = 'arkse'; + + /** + * Longer string name of this protocol class + * + * @type string + */ + protected $name_long = "ARK: Survival Evolved"; + + /** + * query_port = client_port + 19238 + * 27015 = 7777 + 19238 + * + * @type int + */ + protected $port_diff = 19238; +} diff --git a/third_party/gameq_v3.1/GameQ/Protocols/Arma.php b/third_party/gameq_v3.1/GameQ/Protocols/Arma.php new file mode 100644 index 00000000..2653872f --- /dev/null +++ b/third_party/gameq_v3.1/GameQ/Protocols/Arma.php @@ -0,0 +1,43 @@ +. + */ + +namespace GameQ\Protocols; + +/** + * Class Arma + * + * @package GameQ\Protocols + * + * @author Wilson Jesus <> + */ +class Arma extends Gamespy2 +{ + /** + * String name of this protocol class + * + * @type string + */ + protected $name = 'arma'; + + /** + * Longer string name of this protocol class + * + * @type string + */ + protected $name_long = "ArmA Armed Assault"; +} diff --git a/third_party/gameq_v3.1/GameQ/Protocols/Arma3.php b/third_party/gameq_v3.1/GameQ/Protocols/Arma3.php new file mode 100644 index 00000000..fdc2cba4 --- /dev/null +++ b/third_party/gameq_v3.1/GameQ/Protocols/Arma3.php @@ -0,0 +1,221 @@ +. + */ + +namespace GameQ\Protocols; + +use GameQ\Buffer; +use GameQ\Result; + +/** + * Class Armed Assault 3 + * + * Rules protocol reference: https://community.bistudio.com/wiki/Arma_3_ServerBrowserProtocol2 + * + * @package GameQ\Protocols + * @author Austin Bischoff + * @author Memphis017 + */ +class Arma3 extends Source +{ + // Base DLC names + const BASE_DLC_KART = 'Karts'; + const BASE_DLC_MARKSMEN = 'Marksmen'; + const BASE_DLC_HELI = 'Helicopters'; + const BASE_DLC_CURATOR = 'Curator'; + const BASE_DLC_EXPANSION = 'Expansion'; + const BASE_DLC_JETS = 'Jets'; + const BASE_DLC_ORANGE = 'Laws of War'; + const BASE_DLC_ARGO = 'Malden'; + const BASE_DLC_TACOPS = 'Tac-Ops'; + const BASE_DLC_TANKS = 'Tanks'; + const BASE_DLC_CONTACT = 'Contact'; + const BASE_DLC_ENOCH = 'Contact (Platform)'; + + // Special + const BASE_DLC_AOW = 'Art of War'; + + // Creator DLC names + const CREATOR_DLC_GM = 'Global Mobilization'; + const CREATOR_DLC_VN = 'S.O.G. Prairie Fire'; + const CREATOR_DLC_CSLA = 'ČSLA - Iron Curtain'; + const CREATOR_DLC_WS = 'Western Sahara'; + + /** + * DLC Flags/Bits as defined in the documentation. + * + * @see https://community.bistudio.com/wiki/Arma_3:_ServerBrowserProtocol3 + * + * @var array + */ + protected $dlcFlags = [ + 0b0000000000000001 => self::BASE_DLC_KART, + 0b0000000000000010 => self::BASE_DLC_MARKSMEN, + 0b0000000000000100 => self::BASE_DLC_HELI, + 0b0000000000001000 => self::BASE_DLC_CURATOR, + 0b0000000000010000 => self::BASE_DLC_EXPANSION, + 0b0000000000100000 => self::BASE_DLC_JETS, + 0b0000000001000000 => self::BASE_DLC_ORANGE, + 0b0000000010000000 => self::BASE_DLC_ARGO, + 0b0000000100000000 => self::BASE_DLC_TACOPS, + 0b0000001000000000 => self::BASE_DLC_TANKS, + 0b0000010000000000 => self::BASE_DLC_CONTACT, + 0b0000100000000000 => self::BASE_DLC_ENOCH, + 0b0001000000000000 => self::BASE_DLC_AOW, + 0b0010000000000000 => 'Unknown', + 0b0100000000000000 => 'Unknown', + 0b1000000000000000 => 'Unknown', + ]; + + /** + * String name of this protocol class + * + * @type string + */ + protected $name = 'arma3'; + + /** + * Longer string name of this protocol class + * + * @type string + */ + protected $name_long = "Arma3"; + + /** + * Query port = client_port + 1 + * + * @type int + */ + protected $port_diff = 1; + + /** + * Process the rules since Arma3 changed their response for rules + * + * @param Buffer $buffer + * + * @return array + * @throws \GameQ\Exception\Protocol + */ + protected function processRules(Buffer $buffer) + { + // Total number of packets, burn it + $buffer->readInt16(); + + // Will hold the data string + $data = ''; + + // Loop until we run out of strings + while ($buffer->getLength()) { + // Burn the delimiters (i.e. \x01\x04\x00) + $buffer->readString(); + + // Add the data to the string, we are reassembling it + $data .= $buffer->readString(); + } + + // Restore escaped sequences + $data = str_replace(["\x01\x01", "\x01\x02", "\x01\x03"], ["\x01", "\x00", "\xFF"], $data); + + // Make a new buffer with the reassembled data + $responseBuffer = new Buffer($data); + + // Kill the old buffer, should be empty + unset($buffer, $data); + + // Set the result to a new result instance + $result = new Result(); + + // Get results + $result->add('rules_protocol_version', $responseBuffer->readInt8()); // read protocol version + $result->add('overflow', $responseBuffer->readInt8()); // Read overflow flags + $dlcByte = $responseBuffer->readInt8(); // Grab DLC byte 1 and use it later + $dlcByte2 = $responseBuffer->readInt8(); // Grab DLC byte 2 and use it later + $dlcBits = ($dlcByte2 << 8) | $dlcByte; // concatenate DLC bits to 16 Bit int + + // Grab difficulty so we can man handle it... + $difficulty = $responseBuffer->readInt8(); + + // Process difficulty + $result->add('3rd_person', $difficulty >> 7); + $result->add('advanced_flight_mode', ($difficulty >> 6) & 1); + $result->add('difficulty_ai', ($difficulty >> 3) & 3); + $result->add('difficulty_level', $difficulty & 3); + + unset($difficulty); + + // Crosshair + $result->add('crosshair', $responseBuffer->readInt8()); + + // Loop over the base DLC bits so we can pull in the info for the DLC (if enabled) + foreach ($this->dlcFlags as $dlcFlag => $dlcName) { + // Check that the DLC bit is enabled + if (($dlcBits & $dlcFlag) === $dlcFlag) { + // Add the DLC to the list + $result->addSub('dlcs', 'name', $dlcName); + $result->addSub('dlcs', 'hash', dechex($responseBuffer->readInt32())); + } + } + + // Read the mount of mods, these include DLC as well as Creator DLC and custom modifications + $modCount = $responseBuffer->readInt8(); + + // Add mod count + $result->add('mod_count', $modCount); + + // Loop over the mods + while ($modCount) { + // Read the mods hash + $result->addSub('mods', 'hash', dechex($responseBuffer->readInt32())); + + // Get the information byte containing DLC flag and steamId length + $infoByte = $responseBuffer->readInt8(); + + // Determine isDLC by flag, first bit in upper nibble + $result->addSub('mods', 'dlc', ($infoByte & 0b00010000) === 0b00010000); + + // Read the steam id of the mod/CDLC (might be less than 4 bytes) + $result->addSub('mods', 'steam_id', $responseBuffer->readInt32($infoByte & 0x0F)); + + // Read the name of the mod + $result->addSub('mods', 'name', $responseBuffer->readPascalString(0, true) ?: 'Unknown'); + + --$modCount; + } + + // No longer needed + unset($dlcByte, $dlcByte2, $dlcBits); + + // Get the signatures count + $signatureCount = $responseBuffer->readInt8(); + $result->add('signature_count', $signatureCount); + + // Make signatures array + $signatures = []; + + // Loop until we run out of signatures + for ($x = 0; $x < $signatureCount; $x++) { + $signatures[] = $responseBuffer->readPascalString(0, true); + } + + // Add as a simple array + $result->add('signatures', $signatures); + + unset($responseBuffer, $signatureCount, $signatures, $x); + + return $result->fetch(); + } +} diff --git a/third_party/gameq_v3.1/GameQ/Protocols/Armedassault2oa.php b/third_party/gameq_v3.1/GameQ/Protocols/Armedassault2oa.php new file mode 100644 index 00000000..e527a38d --- /dev/null +++ b/third_party/gameq_v3.1/GameQ/Protocols/Armedassault2oa.php @@ -0,0 +1,50 @@ +. + */ + +namespace GameQ\Protocols; + +/** + * Class Armedassault2oa + * + * @package GameQ\Protocols + * @author Austin Bischoff + */ +class Armedassault2oa extends Source +{ + + /** + * String name of this protocol class + * + * @type string + */ + protected $name = "armedassault2oa"; + + /** + * Longer string name of this protocol class + * + * @type string + */ + protected $name_long = "Armed Assault 2: Operation Arrowhead"; + + /** + * Query port = client_port + 1 + * + * @type int + */ + protected $port_diff = 1; +} diff --git a/third_party/gameq_v3.1/GameQ/Protocols/Armedassault3.php b/third_party/gameq_v3.1/GameQ/Protocols/Armedassault3.php new file mode 100644 index 00000000..5bbca429 --- /dev/null +++ b/third_party/gameq_v3.1/GameQ/Protocols/Armedassault3.php @@ -0,0 +1,32 @@ +. + */ + +namespace GameQ\Protocols; + +/** + * Armed assault 3 dummy Protocol Class + * + * Added for backward compatibility, please update to class arma3 + * + * @deprecated v3.0.10 + * @package GameQ\Protocols + * @author Austin Bischoff + */ +class Armedassault3 extends Arma3 +{ +} diff --git a/third_party/gameq_v3.1/GameQ/Protocols/Ase.php b/third_party/gameq_v3.1/GameQ/Protocols/Ase.php new file mode 100644 index 00000000..abc47818 --- /dev/null +++ b/third_party/gameq_v3.1/GameQ/Protocols/Ase.php @@ -0,0 +1,217 @@ +. + */ + +namespace GameQ\Protocols; + +use GameQ\Protocol; +use GameQ\Buffer; +use GameQ\Result; + +/** + * All-Seeing Eye Protocol class + * + * @author Marcel Bößendörfer + * @author Austin Bischoff + */ +class Ase extends Protocol +{ + + /** + * Array of packets we want to look up. + * Each key should correspond to a defined method in this or a parent class + * + * @type array + */ + protected $packets = [ + self::PACKET_ALL => "s", + ]; + + /** + * The query protocol used to make the call + * + * @type string + */ + protected $protocol = 'ase'; + + /** + * String name of this protocol class + * + * @type string + */ + protected $name = 'ase'; + + /** + * Longer string name of this protocol class + * + * @type string + */ + protected $name_long = "All-Seeing Eye"; + + /** + * The client join link + * + * @type string + */ + protected $join_link = null; + + /** + * Normalize settings for this protocol + * + * @type array + */ + protected $normalize = [ + // General + 'general' => [ + // target => source + 'dedicated' => 'dedicated', + 'gametype' => 'gametype', + 'hostname' => 'servername', + 'mapname' => 'map', + 'maxplayers' => 'max_players', + 'mod' => 'game_dir', + 'numplayers' => 'num_players', + 'password' => 'password', + ], + // Individual + 'player' => [ + 'name' => 'name', + 'score' => 'score', + 'team' => 'team', + 'ping' => 'ping', + 'time' => 'time', + ], + ]; + + /** + * Process the response + * + * @return array + * @throws \GameQ\Exception\Protocol + */ + public function processResponse() + { + // Create a new buffer + $buffer = new Buffer(implode('', $this->packets_response)); + + // Check for valid response + if ($buffer->getLength() < 4) { + throw new \GameQ\Exception\Protocol(sprintf('%s The response from the server was empty.', __METHOD__)); + } + + // Read the header + $header = $buffer->read(4); + + // Verify header + if ($header !== 'EYE1') { + throw new \GameQ\Exception\Protocol(sprintf('%s The response header "%s" does not match expected "EYE1"', __METHOD__, $header)); + } + + // Create a new result + $result = new Result(); + + // Variables + $result->add('gamename', $buffer->readPascalString(1, true)); + $result->add('port', $buffer->readPascalString(1, true)); + $result->add('servername', $buffer->readPascalString(1, true)); + $result->add('gametype', $buffer->readPascalString(1, true)); + $result->add('map', $buffer->readPascalString(1, true)); + $result->add('version', $buffer->readPascalString(1, true)); + $result->add('password', $buffer->readPascalString(1, true)); + $result->add('num_players', $buffer->readPascalString(1, true)); + $result->add('max_players', $buffer->readPascalString(1, true)); + $result->add('dedicated', 1); + + // Offload the key/value pair processing + $this->processKeyValuePairs($buffer, $result); + + // Offload processing player and team info + $this->processPlayersAndTeams($buffer, $result); + + unset($buffer); + + return $result->fetch(); + } + + /* + * Internal methods + */ + + /** + * Handles processing the extra key/value pairs for server settings + * + * @param \GameQ\Buffer $buffer + * @param \GameQ\Result $result + */ + protected function processKeyValuePairs(Buffer &$buffer, Result &$result) + { + + // Key / value pairs + while ($buffer->getLength()) { + $key = $buffer->readPascalString(1, true); + + // If we have an empty key, we've reached the end + if (empty($key)) { + break; + } + + // Otherwise, add the pair + $result->add( + $key, + $buffer->readPascalString(1, true) + ); + } + + unset($key); + } + + /** + * Handles processing the player and team data into a usable format + * + * @param \GameQ\Buffer $buffer + * @param \GameQ\Result $result + */ + protected function processPlayersAndTeams(Buffer &$buffer, Result &$result) + { + + // Players and team info + while ($buffer->getLength()) { + // Get the flags + $flags = $buffer->readInt8(); + + // Get data according to the flags + if ($flags & 1) { + $result->addPlayer('name', $buffer->readPascalString(1, true)); + } + if ($flags & 2) { + $result->addPlayer('team', $buffer->readPascalString(1, true)); + } + if ($flags & 4) { + $result->addPlayer('skin', $buffer->readPascalString(1, true)); + } + if ($flags & 8) { + $result->addPlayer('score', $buffer->readPascalString(1, true)); + } + if ($flags & 16) { + $result->addPlayer('ping', $buffer->readPascalString(1, true)); + } + if ($flags & 32) { + $result->addPlayer('time', $buffer->readPascalString(1, true)); + } + } + } +} diff --git a/third_party/gameq_v3.1/GameQ/Protocols/Atlas.php b/third_party/gameq_v3.1/GameQ/Protocols/Atlas.php new file mode 100644 index 00000000..83406bae --- /dev/null +++ b/third_party/gameq_v3.1/GameQ/Protocols/Atlas.php @@ -0,0 +1,55 @@ +. + */ + +namespace GameQ\Protocols; + +/** + * Class Atlas + * + * @package GameQ\Protocols + * @author Wilson Jesus <> + */ +class Atlas extends Source +{ + + /** + * String name of this protocol class + * + * @type string + */ + protected $name = 'atlas'; + + /** + * Longer string name of this protocol class + * + * @type string + */ + protected $name_long = "Atlas"; + + /** + * query_port = client_port + 51800 + * 57561 = 5761 + 51800 + * + * this is the default value for the stock game server, both ports + * can be independently changed from the stock ones, + * making the port_diff logic useless. + * + * @type int + */ + protected $port_diff = 51800; +} diff --git a/third_party/gameq_v3.1/GameQ/Protocols/Avorion.php b/third_party/gameq_v3.1/GameQ/Protocols/Avorion.php new file mode 100644 index 00000000..b4aa2d7a --- /dev/null +++ b/third_party/gameq_v3.1/GameQ/Protocols/Avorion.php @@ -0,0 +1,48 @@ +. + */ + +namespace GameQ\Protocols; + +/** + * Avorion Protocol Class + * + * @package GameQ\Protocols + */ +class Avorion extends Source +{ + /** + * String name of this protocol class + * + * @type string + */ + protected $name = 'avorion'; + + /** + * Longer string name of this protocol class + * + * @type string + */ + protected $name_long = "Avorion"; + + /** + * query_port = client_port + 1 + * + * @type int + * protected $port_diff = 1; + */ +} diff --git a/third_party/gameq_v3.1/GameQ/Protocols/Barotrauma.php b/third_party/gameq_v3.1/GameQ/Protocols/Barotrauma.php new file mode 100644 index 00000000..643428a2 --- /dev/null +++ b/third_party/gameq_v3.1/GameQ/Protocols/Barotrauma.php @@ -0,0 +1,49 @@ +. + */ + +namespace GameQ\Protocols; + +/** + * Barotrauma Protocol Class + * + * @package GameQ\Protocols + * @author Jesse Lukas + */ +class Barotrauma extends Source +{ + /** + * String name of this protocol class + * + * @type string + */ + protected $name = 'barotrauma'; + + /** + * Longer string name of this protocol class + * + * @type string + */ + protected $name_long = "Barotrauma"; + + /** + * query_port = client_port + 1 + * + * @type int + */ + protected $port_diff = 1; +} diff --git a/third_party/gameq_v3.1/GameQ/Protocols/Batt1944.php b/third_party/gameq_v3.1/GameQ/Protocols/Batt1944.php new file mode 100644 index 00000000..f0ff38e6 --- /dev/null +++ b/third_party/gameq_v3.1/GameQ/Protocols/Batt1944.php @@ -0,0 +1,68 @@ +. + */ + +namespace GameQ\Protocols; + +/** + * Class Battalion 1944 + * + * @package GameQ\Protocols + * @author TacTicToe66 + */ +class Batt1944 extends Source +{ + + /** + * String name of this protocol class + * + * @type string + */ + protected $name = 'batt1944'; + + /** + * Longer string name of this protocol class + * + * @type string + */ + protected $name_long = "Battalion 1944"; + + /** + * query_port = client_port + 3 + * + * @type int + */ + protected $port_diff = 3; + + /** + * Normalize main fields + * + * @var array + */ + protected $normalize = [ + // General + 'general' => [ + // target => source + 'gametype' => 'bat_gamemode_s', + 'hostname' => 'bat_name_s', + 'mapname' => 'bat_map_s', + 'maxplayers' => 'bat_max_players_i', + 'numplayers' => 'bat_player_count_s', + 'password' => 'bat_has_password_s', + ], + ]; +} diff --git a/third_party/gameq_v3.1/GameQ/Protocols/Bf1942.php b/third_party/gameq_v3.1/GameQ/Protocols/Bf1942.php new file mode 100644 index 00000000..4cf06c5e --- /dev/null +++ b/third_party/gameq_v3.1/GameQ/Protocols/Bf1942.php @@ -0,0 +1,88 @@ +. + */ + +namespace GameQ\Protocols; + +/** + * Class Battlefield 1942 + * + * @package GameQ\Protocols + * @author Austin Bischoff + */ +class Bf1942 extends Gamespy +{ + + /** + * String name of this protocol class + * + * @type string + */ + protected $name = 'bf1942'; + + /** + * Longer string name of this protocol class + * + * @type string + */ + protected $name_long = "Battlefield 1942"; + + /** + * query_port = client_port + 8433 + * 23000 = 14567 + 8433 + * + * @type int + */ + protected $port_diff = 8433; + + /** + * The client join link + * + * @type string + */ + protected $join_link = "bf1942://%s:%d"; + + /** + * Normalize settings for this protocol + * + * @type array + */ + protected $normalize = [ + // General + 'general' => [ + // target => source + 'dedicated' => 'dedicated', + 'gametype' => 'gametype', + 'hostname' => 'hostname', + 'mapname' => 'mapname', + 'maxplayers' => 'maxplayers', + 'numplayers' => 'numplayers', + 'password' => 'password', + ], + // Individual + 'player' => [ + 'name' => 'playername', + 'kills' => 'kills', + 'deaths' => 'deaths', + 'ping' => 'ping', + 'score' => 'score', + ], + 'team' => [ + 'name' => 'teamname', + ], + ]; +} diff --git a/third_party/gameq_v3.1/GameQ/Protocols/Bf2.php b/third_party/gameq_v3.1/GameQ/Protocols/Bf2.php new file mode 100644 index 00000000..0610f9d0 --- /dev/null +++ b/third_party/gameq_v3.1/GameQ/Protocols/Bf2.php @@ -0,0 +1,98 @@ +. + */ + +namespace GameQ\Protocols; + +/** + * Class Battlefield 2 + * + * @package GameQ\Protocols + * @author Austin Bischoff + */ +class Bf2 extends Gamespy3 +{ + + /** + * String name of this protocol class + * + * @type string + */ + protected $name = 'bf2'; + + /** + * Longer string name of this protocol class + * + * @type string + */ + protected $name_long = "Battlefield 2"; + + /** + * query_port = client_port + 8433 + * 29900 = 16567 + 13333 + * + * @type int + */ + protected $port_diff = 13333; + + /** + * The client join link + * + * @type string + */ + protected $join_link = "bf2://%s:%d"; + + /** + * BF2 has a different query packet to send than "normal" Gamespy 3 + * + * @var array + */ + protected $packets = [ + self::PACKET_ALL => "\xFE\xFD\x00\x10\x20\x30\x40\xFF\xFF\xFF\x01", + ]; + + /** + * Normalize settings for this protocol + * + * @type array + */ + protected $normalize = [ + // General + 'general' => [ + // target => source + 'dedicated' => 'dedicated', + 'gametype' => 'gametype', + 'hostname' => 'hostname', + 'mapname' => 'mapname', + 'maxplayers' => 'maxplayers', + 'numplayers' => 'numplayers', + 'password' => 'password', + ], + // Individual + 'player' => [ + 'name' => 'player', + 'kills' => 'score', + 'deaths' => 'deaths', + 'ping' => 'ping', + 'score' => 'score', + ], + 'team' => [ + 'name' => 'team', + 'score' => 'score', + ], + ]; +} diff --git a/third_party/gameq_v3.1/GameQ/Protocols/Bf3.php b/third_party/gameq_v3.1/GameQ/Protocols/Bf3.php new file mode 100644 index 00000000..90845159 --- /dev/null +++ b/third_party/gameq_v3.1/GameQ/Protocols/Bf3.php @@ -0,0 +1,348 @@ +. + */ + +namespace GameQ\Protocols; + +use GameQ\Protocol; +use GameQ\Buffer; +use GameQ\Result; +use GameQ\Exception\Protocol as Exception; + +/** + * Battlefield 3 Protocol Class + * + * Good place for doc status and info is http://www.fpsadmin.com/forum/showthread.php?t=24134 + * + * @package GameQ\Protocols + * @author Austin Bischoff + */ +class Bf3 extends Protocol +{ + + /** + * Array of packets we want to query. + * + * @type array + */ + protected $packets = [ + self::PACKET_STATUS => "\x00\x00\x00\x21\x1b\x00\x00\x00\x01\x00\x00\x00\x0a\x00\x00\x00serverInfo\x00", + self::PACKET_VERSION => "\x00\x00\x00\x22\x18\x00\x00\x00\x01\x00\x00\x00\x07\x00\x00\x00version\x00", + self::PACKET_PLAYERS => + "\x00\x00\x00\x23\x24\x00\x00\x00\x02\x00\x00\x00\x0b\x00\x00\x00listPlayers\x00\x03\x00\x00\x00\x61ll\x00", + ]; + + /** + * Use the response flag to figure out what method to run + * + * @type array + */ + protected $responses = [ + 1627389952 => "processDetails", // a + 1644167168 => "processVersion", // b + 1660944384 => "processPlayers", // c + ]; + + /** + * The transport mode for this protocol is TCP + * + * @type string + */ + protected $transport = self::TRANSPORT_TCP; + + /** + * The query protocol used to make the call + * + * @type string + */ + protected $protocol = 'bf3'; + + /** + * String name of this protocol class + * + * @type string + */ + protected $name = 'bf3'; + + /** + * Longer string name of this protocol class + * + * @type string + */ + protected $name_long = "Battlefield 3"; + + /** + * The client join link + * + * @type string + */ + protected $join_link = null; + + /** + * query_port = client_port + 22000 + * 47200 = 25200 + 22000 + * + * @type int + */ + protected $port_diff = 22000; + + /** + * Normalize settings for this protocol + * + * @type array + */ + protected $normalize = [ + // General + 'general' => [ + // target => source + 'dedicated' => 'dedicated', + 'hostname' => 'hostname', + 'mapname' => 'map', + 'maxplayers' => 'max_players', + 'numplayers' => 'num_players', + 'password' => 'password', + ], + 'player' => [ + 'name' => 'name', + 'score' => 'score', + 'ping' => 'ping', + ], + 'team' => [ + 'score' => 'tickets', + ], + ]; + + /** + * Process the response for the StarMade server + * + * @return array + * @throws \GameQ\Exception\Protocol + */ + public function processResponse() + { + + // Holds the results sent back + $results = []; + + // Holds the processed packets after having been reassembled + $processed = []; + + // Start up the index for the processed + $sequence_id_last = 0; + + foreach ($this->packets_response as $packet) { + // Create a new buffer + $buffer = new Buffer($packet); + + // Each "good" packet begins with sequence_id (32-bit) + $sequence_id = $buffer->readInt32(); + + // Sequence id is a response + if (array_key_exists($sequence_id, $this->responses)) { + $processed[$sequence_id] = $buffer->getBuffer(); + $sequence_id_last = $sequence_id; + } else { + // This is a continuation of the previous packet, reset the buffer and append + $buffer->jumpto(0); + + // Append + $processed[$sequence_id_last] .= $buffer->getBuffer(); + } + } + + unset($buffer, $sequence_id_last, $sequence_id); + + // Iterate over the combined packets and do some work + foreach ($processed as $sequence_id => $data) { + // Create a new buffer + $buffer = new Buffer($data); + + // Get the length of the packet + $packetLength = $buffer->getLength(); + + // Check to make sure the expected length matches the real length + // Subtract 4 for the sequence_id pulled out earlier + if ($packetLength != ($buffer->readInt32() - 4)) { + throw new Exception(__METHOD__ . " packet length does not match expected length!"); + } + + // Now we need to call the proper method + $results = array_merge( + $results, + call_user_func_array([$this, $this->responses[$sequence_id]], [$buffer]) + ); + } + + return $results; + } + + /* + * Internal Methods + */ + + /** + * Decode the buffer into a usable format + * + * @param \GameQ\Buffer $buffer + * + * @return array + */ + protected function decode(Buffer $buffer) + { + + $items = []; + + // Get the number of words in this buffer + $itemCount = $buffer->readInt32(); + + // Loop over the number of items + for ($i = 0; $i < $itemCount; $i++) { + // Length of the string + $buffer->readInt32(); + + // Just read the string + $items[$i] = $buffer->readString(); + } + + return $items; + } + + /** + * Process the server details + * + * @param \GameQ\Buffer $buffer + * + * @return array + */ + protected function processDetails(Buffer $buffer) + { + + // Decode into items + $items = $this->decode($buffer); + + // Set the result to a new result instance + $result = new Result(); + + // Server is always dedicated + $result->add('dedicated', 1); + + // These are the same no matter what mode the server is in + $result->add('hostname', $items[1]); + $result->add('num_players', (int)$items[2]); + $result->add('max_players', (int)$items[3]); + $result->add('gametype', $items[4]); + $result->add('map', $items[5]); + $result->add('roundsplayed', (int)$items[6]); + $result->add('roundstotal', (int)$items[7]); + $result->add('num_teams', (int)$items[8]); + + // Set the current index + $index_current = 9; + + // Pull the team count + $teamCount = $result->get('num_teams'); + + // Loop for the number of teams found, increment along the way + for ($id = 1; $id <= $teamCount; $id++, $index_current++) { + // Shows the tickets + $result->addTeam('tickets', $items[$index_current]); + // We add an id so we know which team this is + $result->addTeam('id', $id); + } + + // Get and set the rest of the data points. + $result->add('targetscore', (int)$items[$index_current]); + $result->add('online', 1); // Forced true, it seems $words[$index_current + 1] is always empty + $result->add('ranked', (int)$items[$index_current + 2]); + $result->add('punkbuster', (int)$items[$index_current + 3]); + $result->add('password', (int)$items[$index_current + 4]); + $result->add('uptime', (int)$items[$index_current + 5]); + $result->add('roundtime', (int)$items[$index_current + 6]); + // Added in R9 + $result->add('ip_port', $items[$index_current + 7]); + $result->add('punkbuster_version', $items[$index_current + 8]); + $result->add('join_queue', (int)$items[$index_current + 9]); + $result->add('region', $items[$index_current + 10]); + $result->add('pingsite', $items[$index_current + 11]); + $result->add('country', $items[$index_current + 12]); + // Added in R29, No docs as of yet + $result->add('quickmatch', (int)$items[$index_current + 13]); // Guessed from research + + unset($items, $index_current, $teamCount, $buffer); + + return $result->fetch(); + } + + /** + * Process the server version + * + * @param \GameQ\Buffer $buffer + * + * @return array + */ + protected function processVersion(Buffer $buffer) + { + + // Decode into items + $items = $this->decode($buffer); + + // Set the result to a new result instance + $result = new Result(); + + $result->add('version', $items[2]); + + unset($buffer, $items); + + return $result->fetch(); + } + + /** + * Process the players + * + * @param \GameQ\Buffer $buffer + * + * @return array + */ + protected function processPlayers(Buffer $buffer) + { + + // Decode into items + $items = $this->decode($buffer); + + // Set the result to a new result instance + $result = new Result(); + + // Number of data points per player + $numTags = $items[1]; + + // Grab the tags for each player + $tags = array_slice($items, 2, $numTags); + + // Get the player count + $playerCount = $items[$numTags + 2]; + + // Iterate over the index until we run out of players + for ($i = 0, $x = $numTags + 3; $i < $playerCount; $i++, $x += $numTags) { + // Loop over the player tags and extract the info for that tag + foreach ($tags as $index => $tag) { + $result->addPlayer($tag, $items[($x + $index)]); + } + } + + return $result->fetch(); + } +} diff --git a/third_party/gameq_v3.1/GameQ/Protocols/Bf4.php b/third_party/gameq_v3.1/GameQ/Protocols/Bf4.php new file mode 100644 index 00000000..69517529 --- /dev/null +++ b/third_party/gameq_v3.1/GameQ/Protocols/Bf4.php @@ -0,0 +1,114 @@ +. + */ + +namespace GameQ\Protocols; + +use GameQ\Buffer; +use GameQ\Result; + +/** + * Battlefield 4 Protocol class + * + * Good place for doc status and info is http://battlelog.battlefield.com/bf4/forum/view/2955064768683911198/ + * + * @package GameQ\Protocols + * @author Austin Bischoff + */ +class Bf4 extends Bf3 +{ + + /** + * String name of this protocol class + * + * @type string + */ + protected $name = 'bf4'; + + /** + * Longer string name of this protocol class + * + * @type string + */ + protected $name_long = "Battlefield 4"; + + /** + * Handle processing details since they are different than BF3 + * + * @param \GameQ\Buffer $buffer + * + * @return array + */ + protected function processDetails(Buffer $buffer) + { + + // Decode into items + $items = $this->decode($buffer); + + // Set the result to a new result instance + $result = new Result(); + + // Server is always dedicated + $result->add('dedicated', 1); + + // These are the same no matter what mode the server is in + $result->add('hostname', $items[1]); + $result->add('num_players', (int) $items[2]); + $result->add('max_players', (int) $items[3]); + $result->add('gametype', $items[4]); + $result->add('map', $items[5]); + $result->add('roundsplayed', (int) $items[6]); + $result->add('roundstotal', (int) $items[7]); + $result->add('num_teams', (int) $items[8]); + + // Set the current index + $index_current = 9; + + // Pull the team count + $teamCount = $result->get('num_teams'); + + // Loop for the number of teams found, increment along the way + for ($id = 1; $id <= $teamCount; $id++, $index_current++) { + // Shows the tickets + $result->addTeam('tickets', $items[$index_current]); + // We add an id so we know which team this is + $result->addTeam('id', $id); + } + + // Get and set the rest of the data points. + $result->add('targetscore', (int) $items[$index_current]); + $result->add('online', 1); // Forced true, it seems $words[$index_current + 1] is always empty + $result->add('ranked', (int) $items[$index_current + 2]); + $result->add('punkbuster', (int) $items[$index_current + 3]); + $result->add('password', (int) $items[$index_current + 4]); + $result->add('uptime', (int) $items[$index_current + 5]); + $result->add('roundtime', (int) $items[$index_current + 6]); + $result->add('ip_port', $items[$index_current + 7]); + $result->add('punkbuster_version', $items[$index_current + 8]); + $result->add('join_queue', (int) $items[$index_current + 9]); + $result->add('region', $items[$index_current + 10]); + $result->add('pingsite', $items[$index_current + 11]); + $result->add('country', $items[$index_current + 12]); + //$result->add('quickmatch', (int) $items[$index_current + 13]); Supposed to be here according to R42 but is not + $result->add('blaze_player_count', (int) $items[$index_current + 13]); + $result->add('blaze_game_state', (int) $items[$index_current + 14]); + + unset($items, $index_current, $teamCount, $buffer); + + return $result->fetch(); + } +} diff --git a/third_party/gameq_v3.1/GameQ/Protocols/Bfbc2.php b/third_party/gameq_v3.1/GameQ/Protocols/Bfbc2.php new file mode 100644 index 00000000..b7167a02 --- /dev/null +++ b/third_party/gameq_v3.1/GameQ/Protocols/Bfbc2.php @@ -0,0 +1,326 @@ +. + */ + +namespace GameQ\Protocols; + +use GameQ\Protocol; +use GameQ\Buffer; +use GameQ\Result; +use GameQ\Exception\Protocol as Exception; + +/** + * Battlefield Bad Company 2 Protocol Class + * + * NOTE: There are no qualifiers to the response packets sent back from the server as to which response packet + * belongs to which query request. For now this class assumes the responses are in the same order as the order in + * which the packets were sent to the server. If this assumption turns out to be wrong there is easy way to tell which + * response belongs to which query. Hopefully this assumption will hold true as it has in my testing. + * + * @package GameQ\Protocols + * @author Austin Bischoff + */ +class Bfbc2 extends Protocol +{ + + /** + * Array of packets we want to query. + * + * @type array + */ + protected $packets = [ + self::PACKET_VERSION => "\x00\x00\x00\x00\x18\x00\x00\x00\x01\x00\x00\x00\x07\x00\x00\x00version\x00", + self::PACKET_STATUS => "\x00\x00\x00\x00\x1b\x00\x00\x00\x01\x00\x00\x00\x0a\x00\x00\x00serverInfo\x00", + self::PACKET_PLAYERS => "\x00\x00\x00\x00\x24\x00\x00\x00\x02\x00\x00\x00\x0b\x00\x00\x00listPlayers\x00\x03\x00\x00\x00\x61ll\x00", + ]; + + /** + * Use the response flag to figure out what method to run + * + * @type array + */ + protected $responses = [ + "processVersion", + "processDetails", + "processPlayers", + ]; + + /** + * The transport mode for this protocol is TCP + * + * @type string + */ + protected $transport = self::TRANSPORT_TCP; + + /** + * The query protocol used to make the call + * + * @type string + */ + protected $protocol = 'bfbc2'; + + /** + * String name of this protocol class + * + * @type string + */ + protected $name = 'bfbc2'; + + /** + * Longer string name of this protocol class + * + * @type string + */ + protected $name_long = "Battlefield Bad Company 2"; + + /** + * The client join link + * + * @type string + */ + protected $join_link = null; + + /** + * query_port = client_port + 29321 + * 48888 = 19567 + 29321 + * + * @type int + */ + protected $port_diff = 29321; + + /** + * Normalize settings for this protocol + * + * @type array + */ + protected $normalize = [ + // General + 'general' => [ + // target => source + 'dedicated' => 'dedicated', + 'hostname' => 'hostname', + 'mapname' => 'map', + 'maxplayers' => 'max_players', + 'numplayers' => 'num_players', + 'password' => 'password', + ], + 'player' => [ + 'name' => 'name', + 'score' => 'score', + 'ping' => 'ping', + ], + 'team' => [ + 'score' => 'tickets', + ], + ]; + + /** + * Process the response for the StarMade server + * + * @return array + * @throws \GameQ\Exception\Protocol + */ + public function processResponse() + { + + //print_r($this->packets_response); + + // Holds the results sent back + $results = []; + + // Iterate over the response packets + // @todo: This protocol has no packet ordering, ids or anyway to identify which packet coming back belongs to which initial call. + foreach ($this->packets_response as $i => $packet) { + // Create a new buffer + $buffer = new Buffer($packet); + + // Burn first 4 bytes, same across all packets + $buffer->skip(4); + + // Get the packet length + $packetLength = $buffer->getLength(); + + // Check to make sure the expected length matches the real length + // Subtract 4 for the header burn + if ($packetLength != ($buffer->readInt32() - 4)) { + throw new Exception(__METHOD__ . " packet length does not match expected length!"); + } + + // We assume the packets are coming back in the same order as sent, this maybe incorrect... + $results = array_merge( + $results, + call_user_func_array([$this, $this->responses[$i]], [$buffer]) + ); + } + + unset($buffer, $packetLength); + + return $results; + } + + /* + * Internal Methods + */ + + /** + * Decode the buffer into a usable format + * + * @param \GameQ\Buffer $buffer + * + * @return array + */ + protected function decode(Buffer $buffer) + { + + $items = []; + + // Get the number of words in this buffer + $itemCount = $buffer->readInt32(); + + // Loop over the number of items + for ($i = 0; $i < $itemCount; $i++) { + // Length of the string + $buffer->readInt32(); + + // Just read the string + $items[$i] = $buffer->readString(); + } + + return $items; + } + + /** + * Process the server details + * + * @param \GameQ\Buffer $buffer + * + * @return array + */ + protected function processDetails(Buffer $buffer) + { + + // Decode into items + $items = $this->decode($buffer); + + // Set the result to a new result instance + $result = new Result(); + + // Server is always dedicated + $result->add('dedicated', 1); + + // These are the same no matter what mode the server is in + $result->add('hostname', $items[1]); + $result->add('num_players', (int)$items[2]); + $result->add('max_players', (int)$items[3]); + $result->add('gametype', $items[4]); + $result->add('map', $items[5]); + $result->add('roundsplayed', (int)$items[6]); + $result->add('roundstotal', (int)$items[7]); + $result->add('num_teams', (int)$items[8]); + + // Set the current index + $index_current = 9; + + // Pull the team count + $teamCount = $result->get('num_teams'); + + // Loop for the number of teams found, increment along the way + for ($id = 1; $id <= $teamCount; $id++, $index_current++) { + // Shows the tickets + $result->addTeam('tickets', $items[$index_current]); + // We add an id so we know which team this is + $result->addTeam('id', $id); + } + + // Get and set the rest of the data points. + $result->add('targetscore', (int)$items[$index_current]); + $result->add('online', 1); // Forced true, shows accepting players + $result->add('ranked', (($items[$index_current + 2] == 'true') ? 1 : 0)); + $result->add('punkbuster', (($items[$index_current + 3] == 'true') ? 1 : 0)); + $result->add('password', (($items[$index_current + 4] == 'true') ? 1 : 0)); + $result->add('uptime', (int)$items[$index_current + 5]); + $result->add('roundtime', (int)$items[$index_current + 6]); + $result->add('mod', $items[$index_current + 7]); + + $result->add('ip_port', $items[$index_current + 9]); + $result->add('punkbuster_version', $items[$index_current + 10]); + $result->add('join_queue', (($items[$index_current + 11] == 'true') ? 1 : 0)); + $result->add('region', $items[$index_current + 12]); + + unset($items, $index_current, $teamCount, $buffer); + + return $result->fetch(); + } + + /** + * Process the server version + * + * @param \GameQ\Buffer $buffer + * + * @return array + */ + protected function processVersion(Buffer $buffer) + { + // Decode into items + $items = $this->decode($buffer); + + // Set the result to a new result instance + $result = new Result(); + + $result->add('version', $items[2]); + + unset($buffer, $items); + + return $result->fetch(); + } + + /** + * Process the players + * + * @param \GameQ\Buffer $buffer + * + * @return array + */ + protected function processPlayers(Buffer $buffer) + { + + // Decode into items + $items = $this->decode($buffer); + + // Set the result to a new result instance + $result = new Result(); + + // Number of data points per player + $numTags = $items[1]; + + // Grab the tags for each player + $tags = array_slice($items, 2, $numTags); + + // Get the player count + $playerCount = $items[$numTags + 2]; + + // Iterate over the index until we run out of players + for ($i = 0, $x = $numTags + 3; $i < $playerCount; $i++, $x += $numTags) { + // Loop over the player tags and extract the info for that tag + foreach ($tags as $index => $tag) { + $result->addPlayer($tag, $items[($x + $index)]); + } + } + + return $result->fetch(); + } +} diff --git a/third_party/gameq_v3.1/GameQ/Protocols/Bfh.php b/third_party/gameq_v3.1/GameQ/Protocols/Bfh.php new file mode 100644 index 00000000..067d77f9 --- /dev/null +++ b/third_party/gameq_v3.1/GameQ/Protocols/Bfh.php @@ -0,0 +1,43 @@ +. + */ + +namespace GameQ\Protocols; + +/** + * Battlefield Hardline Protocol class + * + * @package GameQ\Protocols + * @author Austin Bischoff + */ +class Bfh extends Bf4 +{ + + /** + * String name of this protocol class + * + * @type string + */ + protected $name = 'bfh'; + + /** + * Longer string name of this protocol class + * + * @type string + */ + protected $name_long = "Battlefield Hardline"; +} diff --git a/third_party/gameq_v3.1/GameQ/Protocols/Blackmesa.php b/third_party/gameq_v3.1/GameQ/Protocols/Blackmesa.php new file mode 100644 index 00000000..efaafdfb --- /dev/null +++ b/third_party/gameq_v3.1/GameQ/Protocols/Blackmesa.php @@ -0,0 +1,42 @@ +. + */ + +namespace GameQ\Protocols; + +/** + * Blackmesa Protocol Class + * + * @package GameQ\Protocols + * @author Jesse Lukas + */ +class Blackmesa extends Source +{ + /** + * String name of this protocol class + * + * @type string + */ + protected $name = 'blackmesa'; + + /** + * Longer string name of this protocol class + * + * @type string + */ + protected $name_long = "Black Mesa"; +} diff --git a/third_party/gameq_v3.1/GameQ/Protocols/Brink.php b/third_party/gameq_v3.1/GameQ/Protocols/Brink.php new file mode 100644 index 00000000..20226525 --- /dev/null +++ b/third_party/gameq_v3.1/GameQ/Protocols/Brink.php @@ -0,0 +1,50 @@ +. + */ + +namespace GameQ\Protocols; + +/** + * Class Brink + * + * @package GameQ\Protocols + * + * @author Wilson Jesus <> + */ +class Brink extends Source +{ + /** + * String name of this protocol class + * + * @type string + */ + protected $name = 'brink'; + + /** + * Longer string name of this protocol class + * + * @type string + */ + protected $name_long = "Brink"; + + /** + * query_port = client_port + 1 + * + * @type int + */ + protected $port_diff = 1; +} diff --git a/third_party/gameq_v3.1/GameQ/Protocols/Citadel.php b/third_party/gameq_v3.1/GameQ/Protocols/Citadel.php new file mode 100644 index 00000000..3d1074b1 --- /dev/null +++ b/third_party/gameq_v3.1/GameQ/Protocols/Citadel.php @@ -0,0 +1,42 @@ +. + */ + +namespace GameQ\Protocols; + +/** + * Citadel Protocol Class + * + * @package GameQ\Protocols + * @author Jesse Lukas + */ +class Citadel extends Source +{ + /** + * String name of this protocol class + * + * @type string + */ + protected $name = 'citadel'; + + /** + * Longer string name of this protocol class + * + * @type string + */ + protected $name_long = "Citadel"; +} diff --git a/third_party/gameq_v3.1/GameQ/Protocols/Cod.php b/third_party/gameq_v3.1/GameQ/Protocols/Cod.php new file mode 100644 index 00000000..2425ea67 --- /dev/null +++ b/third_party/gameq_v3.1/GameQ/Protocols/Cod.php @@ -0,0 +1,43 @@ +. + */ + +namespace GameQ\Protocols; + +/** + * Call of Duty Protocol Class + * + * @package GameQ\Protocols + * + * @author Wilson Jesus <> + */ +class Cod extends Quake3 +{ + /** + * String name of this protocol class + * + * @type string + */ + protected $name = 'cod'; + + /** + * Longer string name of this protocol class + * + * @type string + */ + protected $name_long = "Call of Duty"; +} diff --git a/third_party/gameq_v3.1/GameQ/Protocols/Cod2.php b/third_party/gameq_v3.1/GameQ/Protocols/Cod2.php new file mode 100644 index 00000000..79be7ca2 --- /dev/null +++ b/third_party/gameq_v3.1/GameQ/Protocols/Cod2.php @@ -0,0 +1,42 @@ +. + */ + +namespace GameQ\Protocols; + +/** + * Call of Duty 2 Protocol Class + * + * @package GameQ\Protocols + * @author Austin Bischoff + */ +class Cod2 extends Quake3 +{ + /** + * String name of this protocol class + * + * @type string + */ + protected $name = 'cod2'; + + /** + * Longer string name of this protocol class + * + * @type string + */ + protected $name_long = "Call of Duty 2"; +} diff --git a/third_party/gameq_v3.1/GameQ/Protocols/Cod4.php b/third_party/gameq_v3.1/GameQ/Protocols/Cod4.php new file mode 100644 index 00000000..9838d9cb --- /dev/null +++ b/third_party/gameq_v3.1/GameQ/Protocols/Cod4.php @@ -0,0 +1,42 @@ +. + */ + +namespace GameQ\Protocols; + +/** + * Call of Duty 4 Protocol Class + * + * @package GameQ\Protocols + * @author Austin Bischoff + */ +class Cod4 extends Quake3 +{ + /** + * String name of this protocol class + * + * @type string + */ + protected $name = 'cod4'; + + /** + * Longer string name of this protocol class + * + * @type string + */ + protected $name_long = "Call of Duty 4"; +} diff --git a/third_party/gameq_v3.1/GameQ/Protocols/Codmw2.php b/third_party/gameq_v3.1/GameQ/Protocols/Codmw2.php new file mode 100644 index 00000000..290e43c9 --- /dev/null +++ b/third_party/gameq_v3.1/GameQ/Protocols/Codmw2.php @@ -0,0 +1,89 @@ +. + */ + +namespace GameQ\Protocols; + +use GameQ\Buffer; +use GameQ\Result; + +/** + * Call of Duty: Modern Warfare 2 Protocol Class + * + * @package GameQ\Protocols + * @author Wilson Jesus <> + */ +class Codmw2 extends Quake3 +{ + /** + * String name of this protocol class + * + * @type string + */ + protected $name = 'codmw2'; + + /** + * Longer string name of this protocol class + * + * @type string + */ + protected $name_long = "Call of Duty: Modern Warfare 2"; + + protected function processPlayers(Buffer $buffer) + { + // Temporarily cache players in order to remove last + $players = []; + + // Loop until we are out of data + while ($buffer->getLength()) { + // Make a new buffer with this block + $playerInfo = new Buffer($buffer->readString("\x0A")); + + // Read player info + $player = [ + 'frags' => $playerInfo->readString("\x20"), + 'ping' => $playerInfo->readString("\x20"), + ]; + + // Skip first " + $playerInfo->skip(1); + + // Add player name, encoded + $player['name'] = utf8_encode(trim(($playerInfo->readString('"')))); + + // Add player + $players[] = $player; + } + + // Remove last, empty player + array_pop($players); + + // Set the result to a new result instance + $result = new Result(); + + // Add players + $result->add('players', $players); + + // Add Playercount + $result->add('clients', count($players)); + + // Clear + unset($buffer, $players); + + return $result->fetch(); + } +} diff --git a/third_party/gameq_v3.1/GameQ/Protocols/Codmw3.php b/third_party/gameq_v3.1/GameQ/Protocols/Codmw3.php new file mode 100644 index 00000000..1049b602 --- /dev/null +++ b/third_party/gameq_v3.1/GameQ/Protocols/Codmw3.php @@ -0,0 +1,50 @@ +. + */ + +namespace GameQ\Protocols; + +/** + * Class Codmw3 + * + * @package GameQ\Protocols + * @author Austin Bischoff + */ +class Codmw3 extends Source +{ + + /** + * String name of this protocol class + * + * @type string + */ + protected $name = 'codmw3'; + + /** + * Longer string name of this protocol class + * + * @type string + */ + protected $name_long = "Call of Duty: Modern Warfare 3"; + + /** + * query_port = client_port + 2 + * + * @type int + */ + protected $port_diff = 2; +} diff --git a/third_party/gameq_v3.1/GameQ/Protocols/Coduo.php b/third_party/gameq_v3.1/GameQ/Protocols/Coduo.php new file mode 100644 index 00000000..2dd9a182 --- /dev/null +++ b/third_party/gameq_v3.1/GameQ/Protocols/Coduo.php @@ -0,0 +1,43 @@ +. + */ + +namespace GameQ\Protocols; + +/** + * Call of Duty United Offensive Class + * + * @package GameQ\Protocols + * + * @author Wilson Jesus <> + */ +class Coduo extends Quake3 +{ + /** + * String name of this protocol class + * + * @type string + */ + protected $name = 'coduo'; + + /** + * Longer string name of this protocol class + * + * @type string + */ + protected $name_long = "Call of Duty: United Offensive"; +} diff --git a/third_party/gameq_v3.1/GameQ/Protocols/Codwaw.php b/third_party/gameq_v3.1/GameQ/Protocols/Codwaw.php new file mode 100644 index 00000000..f730678e --- /dev/null +++ b/third_party/gameq_v3.1/GameQ/Protocols/Codwaw.php @@ -0,0 +1,43 @@ +. + */ + +namespace GameQ\Protocols; + +/** + * Call of Duty World at War Class + * + * @package GameQ\Protocols + * @author naXe + * @author Austin Bischoff + */ +class Codwaw extends Quake3 +{ + /** + * String name of this protocol class + * + * @type string + */ + protected $name = 'codwaw'; + + /** + * Longer string name of this protocol class + * + * @type string + */ + protected $name_long = "Call of Duty: World at War"; +} diff --git a/third_party/gameq_v3.1/GameQ/Protocols/Conanexiles.php b/third_party/gameq_v3.1/GameQ/Protocols/Conanexiles.php new file mode 100644 index 00000000..a097e1d8 --- /dev/null +++ b/third_party/gameq_v3.1/GameQ/Protocols/Conanexiles.php @@ -0,0 +1,42 @@ +. + */ + +namespace GameQ\Protocols; + +/** + * Class Conanexiles + * + * @package GameQ\Protocols + * @author Austin Bischoff + */ +class Conanexiles extends Source +{ + /** + * String name of this protocol class + * + * @type string + */ + protected $name = 'conanexiles'; + + /** + * Longer string name of this protocol class + * + * @type string + */ + protected $name_long = "Conan Exiles"; +} diff --git a/third_party/gameq_v3.1/GameQ/Protocols/Contagion.php b/third_party/gameq_v3.1/GameQ/Protocols/Contagion.php new file mode 100644 index 00000000..64d0b76e --- /dev/null +++ b/third_party/gameq_v3.1/GameQ/Protocols/Contagion.php @@ -0,0 +1,42 @@ +. + */ + +namespace GameQ\Protocols; + +/** + * Class Contagion + * + * @package GameQ\Protocols + * @author Nikolay Ipanyuk + * @author Austin Bischoff + */ +class Contagion extends Source +{ + /** + * String name of this protocol class + * + * @type string + */ + protected $name = 'contagion'; + /** + * Longer string name of this protocol class + * + * @type string + */ + protected $name_long = "Contagion"; +} diff --git a/third_party/gameq_v3.1/GameQ/Protocols/Crysis.php b/third_party/gameq_v3.1/GameQ/Protocols/Crysis.php new file mode 100644 index 00000000..e09a673d --- /dev/null +++ b/third_party/gameq_v3.1/GameQ/Protocols/Crysis.php @@ -0,0 +1,43 @@ +. + */ + +namespace GameQ\Protocols; + +/** + * Class Crysis + * + * @package GameQ\Protocols + * + * @author Wilson Jesus <> + */ +class Crysis extends Gamespy3 +{ + /** + * String name of this protocol class + * + * @type string + */ + protected $name = 'crysis'; + + /** + * Longer string name of this protocol class + * + * @type string + */ + protected $name_long = "Crysis"; +} diff --git a/third_party/gameq_v3.1/GameQ/Protocols/Crysis2.php b/third_party/gameq_v3.1/GameQ/Protocols/Crysis2.php new file mode 100644 index 00000000..75c6614a --- /dev/null +++ b/third_party/gameq_v3.1/GameQ/Protocols/Crysis2.php @@ -0,0 +1,43 @@ +. + */ + +namespace GameQ\Protocols; + +/** + * Class Crysis2 + * + * @package GameQ\Protocols + * + * @author Wilson Jesus <> + */ +class Crysis2 extends Gamespy3 +{ + /** + * String name of this protocol class + * + * @type string + */ + protected $name = 'crysis2'; + + /** + * Longer string name of this protocol class + * + * @type string + */ + protected $name_long = "Crysis 2"; +} diff --git a/third_party/gameq_v3.1/GameQ/Protocols/Crysiswars.php b/third_party/gameq_v3.1/GameQ/Protocols/Crysiswars.php new file mode 100644 index 00000000..44dcdcf1 --- /dev/null +++ b/third_party/gameq_v3.1/GameQ/Protocols/Crysiswars.php @@ -0,0 +1,43 @@ +. + */ + +namespace GameQ\Protocols; + +/** + * Class Crysiswars + * + * @package GameQ\Protocols + * + * @author Austin Bischoff + */ +class Crysiswars extends Gamespy3 +{ + /** + * String name of this protocol class + * + * @type string + */ + protected $name = 'crysiswars'; + + /** + * Longer string name of this protocol class + * + * @type string + */ + protected $name_long = "Crysis Wars"; +} diff --git a/third_party/gameq_v3.1/GameQ/Protocols/Cs15.php b/third_party/gameq_v3.1/GameQ/Protocols/Cs15.php new file mode 100644 index 00000000..ba375240 --- /dev/null +++ b/third_party/gameq_v3.1/GameQ/Protocols/Cs15.php @@ -0,0 +1,45 @@ +. + */ + +namespace GameQ\Protocols; + +/** + * Counter-Strike 1.5 Protocol Class + * + * @author Nikolay Ipanyuk + * @author Austin Bischoff + * + * @package GameQ\Protocols + */ +class Cs15 extends Won +{ + + /** + * String name of this protocol class + * + * @type string + */ + protected $name = 'cs15'; + + /** + * Longer string name of this protocol class + * + * @type string + */ + protected $name_long = "Counter-Strike 1.5"; +} diff --git a/third_party/gameq_v3.1/GameQ/Protocols/Cs16.php b/third_party/gameq_v3.1/GameQ/Protocols/Cs16.php new file mode 100644 index 00000000..25a66029 --- /dev/null +++ b/third_party/gameq_v3.1/GameQ/Protocols/Cs16.php @@ -0,0 +1,69 @@ +. + */ + +namespace GameQ\Protocols; + +/** + * Class Cs16 + * + * @package GameQ\Protocols + * @author Austin Bischoff + */ +class Cs16 extends Source +{ + + /** + * String name of this protocol class + * + * @type string + */ + protected $name = 'cs16'; + + /** + * Longer string name of this protocol class + * + * @type string + */ + protected $name_long = "Counter-Strike 1.6"; + + /** + * In the case of cs 1.6 we offload split packets here because the split packet response for rules is in + * the old gold source format + * + * @param $packet_id + * @param array $packets + * + * @return string + * @throws \GameQ\Exception\Protocol + */ + protected function processPackets($packet_id, array $packets = []) + { + + // The response is gold source if the packets are split + $this->source_engine = self::GOLDSOURCE_ENGINE; + + // Offload to the parent + $packs = parent::processPackets($packet_id, $packets); + + // Reset the engine + $this->source_engine = self::SOURCE_ENGINE; + + // Return the result + return $packs; + } +} diff --git a/third_party/gameq_v3.1/GameQ/Protocols/Cs2d.php b/third_party/gameq_v3.1/GameQ/Protocols/Cs2d.php new file mode 100644 index 00000000..0f238fdd --- /dev/null +++ b/third_party/gameq_v3.1/GameQ/Protocols/Cs2d.php @@ -0,0 +1,263 @@ +. + */ + +namespace GameQ\Protocols; + +use GameQ\Protocol; +use GameQ\Buffer; +use GameQ\Result; +use GameQ\Exception\Protocol as Exception; + +/** + * Counter-Strike 2d Protocol Class + * + * Note: + * Unable to make player information calls work as the protocol does not like parallel requests + * + * @author Austin Bischoff + */ +class Cs2d extends Protocol +{ + + /** + * Array of packets we want to query. + * + * @type array + */ + protected $packets = [ + self::PACKET_STATUS => "\x01\x00\xFB\x01", + //self::PACKET_STATUS => "\x01\x00\x03\x10\x21\xFB\x01\x75\x00", + self::PACKET_PLAYERS => "\x01\x00\xFB\x05", + ]; + + /** + * Use the response flag to figure out what method to run + * + * @type array + */ + protected $responses = [ + "\x01\x00\xFB\x01" => "processDetails", + "\x01\x00\xFB\x05" => "processPlayers", + ]; + + /** + * The query protocol used to make the call + * + * @type string + */ + protected $protocol = 'cs2d'; + + /** + * String name of this protocol class + * + * @type string + */ + protected $name = 'cs2d'; + + /** + * Longer string name of this protocol class + * + * @type string + */ + protected $name_long = "Counter-Strike 2d"; + + /** + * The client join link + * + * @type string + */ + protected $join_link = "cs2d://%s:%d/"; + + /** + * Normalize settings for this protocol + * + * @type array + */ + protected $normalize = [ + // General + 'general' => [ + // target => source + 'dedicated' => 'dedicated', + 'gametype' => 'game_mode', + 'hostname' => 'hostname', + 'mapname' => 'mapname', + 'maxplayers' => 'max_players', + 'mod' => 'game_dir', + 'numplayers' => 'num_players', + 'password' => 'password', + ], + // Individual + 'player' => [ + 'name' => 'name', + 'deaths' => 'deaths', + 'score' => 'score', + ], + ]; + + /** + * Process the response for the Tibia server + * + * @return array + * @throws \GameQ\Exception\Protocol + */ + public function processResponse() + { + + // We have a merged packet, try to split it back up + if (count($this->packets_response) == 1) { + // Temp buffer to make string manipulation easier + $buffer = new Buffer($this->packets_response[0]); + + // Grab the header and set the packet we need to split with + $packet = (($buffer->lookAhead(4) === $this->packets[self::PACKET_PLAYERS]) ? + self::PACKET_STATUS : self::PACKET_PLAYERS); + + // Explode the merged packet as the response + $responses = explode(substr($this->packets[$packet], 2), $buffer->getData()); + + // Try to rebuild the second packet to the same as if it was sent as two separate responses + $responses[1] = $this->packets[$packet] . ((count($responses) === 2) ? $responses[1] : ""); + + unset($buffer); + } else { + $responses = $this->packets_response; + } + + // Will hold the packets after sorting + $packets = []; + + // We need to pre-sort these for split packets so we can do extra work where needed + foreach ($responses as $response) { + $buffer = new Buffer($response); + + // Pull out the header + $header = $buffer->read(4); + + // Add the packet to the proper section, we will combine later + $packets[$header][] = $buffer->getBuffer(); + } + + unset($buffer); + + $results = []; + + // Now let's iterate and process + foreach ($packets as $header => $packetGroup) { + // Figure out which packet response this is + if (!array_key_exists($header, $this->responses)) { + throw new Exception(__METHOD__ . " response type '" . bin2hex($header) . "' is not valid"); + } + + // Now we need to call the proper method + $results = array_merge( + $results, + call_user_func_array([$this, $this->responses[$header]], [new Buffer(implode($packetGroup))]) + ); + } + + unset($packets); + + return $results; + } + + /** + * Handles processing the details data into a usable format + * + * @param Buffer $buffer + * + * @return array + * @throws Exception + */ + protected function processDetails(Buffer $buffer) + { + // Set the result to a new result instance + $result = new Result(); + + // First int is the server flags + $serverFlags = $buffer->readInt8(); + + // Read server flags + $result->add('password', (int)$this->readFlag($serverFlags, 0)); + $result->add('registered_only', (int)$this->readFlag($serverFlags, 1)); + $result->add('fog_of_war', (int)$this->readFlag($serverFlags, 2)); + $result->add('friendly_fire', (int)$this->readFlag($serverFlags, 3)); + $result->add('bots_enabled', (int)$this->readFlag($serverFlags, 5)); + $result->add('lua_scripts', (int)$this->readFlag($serverFlags, 6)); + + // Read the rest of the buffer data + $result->add('servername', utf8_encode($buffer->readPascalString(0))); + $result->add('mapname', utf8_encode($buffer->readPascalString(0))); + $result->add('num_players', $buffer->readInt8()); + $result->add('max_players', $buffer->readInt8()); + $result->add('game_mode', $buffer->readInt8()); + $result->add('num_bots', (($this->readFlag($serverFlags, 5)) ? $buffer->readInt8() : 0)); + $result->add('dedicated', 1); + + unset($buffer); + + return $result->fetch(); + } + + /** + * Handles processing the player data into a usable format + * + * @param Buffer $buffer + * + * @return array + * @throws Exception + */ + protected function processPlayers(Buffer $buffer) + { + + // Set the result to a new result instance + $result = new Result(); + + // First entry is the number of players in this list. Don't care + $buffer->read(); + + // Parse players + while ($buffer->getLength()) { + // Player id + if (($id = $buffer->readInt8()) !== 0) { + // Add the results + $result->addPlayer('id', $id); + $result->addPlayer('name', utf8_encode($buffer->readPascalString(0))); + $result->addPlayer('team', $buffer->readInt8()); + $result->addPlayer('score', $buffer->readInt32()); + $result->addPlayer('deaths', $buffer->readInt32()); + } + } + + unset($buffer, $id); + + return $result->fetch(); + } + + /** + * Read flags from stored value + * + * @param $flags + * @param $offset + * + * @return bool + */ + protected function readFlag($flags, $offset) + { + return !!($flags & (1 << $offset)); + } +} diff --git a/third_party/gameq_v3.1/GameQ/Protocols/Cscz.php b/third_party/gameq_v3.1/GameQ/Protocols/Cscz.php new file mode 100644 index 00000000..b539128f --- /dev/null +++ b/third_party/gameq_v3.1/GameQ/Protocols/Cscz.php @@ -0,0 +1,45 @@ +. + */ + +namespace GameQ\Protocols; + +/** + * Class Cscz + * + * Based off of CS 1.6 + * + * @package GameQ\Protocols + * @author Austin Bischoff + */ +class Cscz extends Cs16 +{ + + /** + * String name of this protocol class + * + * @type string + */ + protected $name = 'cscz'; + + /** + * Longer string name of this protocol class + * + * @type string + */ + protected $name_long = "Counter-Strike: Condition Zero"; +} diff --git a/third_party/gameq_v3.1/GameQ/Protocols/Csgo.php b/third_party/gameq_v3.1/GameQ/Protocols/Csgo.php new file mode 100644 index 00000000..41af7352 --- /dev/null +++ b/third_party/gameq_v3.1/GameQ/Protocols/Csgo.php @@ -0,0 +1,43 @@ +. + */ + +namespace GameQ\Protocols; + +/** + * Class Csgo + * + * @package GameQ\Protocols + * @author Austin Bischoff + */ +class Csgo extends Source +{ + + /** + * String name of this protocol class + * + * @type string + */ + protected $name = 'csgo'; + + /** + * Longer string name of this protocol class + * + * @type string + */ + protected $name_long = "Counter-Strike: Global Offensive"; +} diff --git a/third_party/gameq_v3.1/GameQ/Protocols/Css.php b/third_party/gameq_v3.1/GameQ/Protocols/Css.php new file mode 100644 index 00000000..be75da3d --- /dev/null +++ b/third_party/gameq_v3.1/GameQ/Protocols/Css.php @@ -0,0 +1,42 @@ +. + */ + +namespace GameQ\Protocols; + +/** + * Class Css + * + * @package GameQ\Protocols + * @author Austin Bischoff + */ +class Css extends Source +{ + /** + * String name of this protocol class + * + * @type string + */ + protected $name = 'css'; + + /** + * Longer string name of this protocol class + * + * @type string + */ + protected $name_long = "Counter-Strike: Source"; +} diff --git a/third_party/gameq_v3.1/GameQ/Protocols/Dal.php b/third_party/gameq_v3.1/GameQ/Protocols/Dal.php new file mode 100644 index 00000000..6b05037d --- /dev/null +++ b/third_party/gameq_v3.1/GameQ/Protocols/Dal.php @@ -0,0 +1,43 @@ +. + */ + +namespace GameQ\Protocols; + +/** + * Class Dark and Light + * + * @package GameQ\Protocols + * @author Austin Bischoff + */ +class Dal extends Arkse +{ + + /** + * String name of this protocol class + * + * @type string + */ + protected $name = 'dal'; + + /** + * Longer string name of this protocol class + * + * @type string + */ + protected $name_long = "Dark and Light"; +} diff --git a/third_party/gameq_v3.1/GameQ/Protocols/Dayz.php b/third_party/gameq_v3.1/GameQ/Protocols/Dayz.php new file mode 100644 index 00000000..01c7c28d --- /dev/null +++ b/third_party/gameq_v3.1/GameQ/Protocols/Dayz.php @@ -0,0 +1,66 @@ +. + */ + +namespace GameQ\Protocols; + +/** + * Class Dayz + * + * @package GameQ\Protocols + * @author Austin Bischoff + */ +class Dayz extends Source +{ + + /** + * String name of this protocol class + * + * @type string + */ + protected $name = 'dayz'; + + /** + * Longer string name of this protocol class + * + * @type string + */ + protected $name_long = "DayZ Standalone"; + + /** + * Overload the math used to guess at the Query Port + * + * @param int $clientPort + * + * @return int + */ + public function findQueryPort($clientPort) + { + + /* + * Port layout: + * 2302 - 27016 + * 2402 - 27017 + * 2502 - 27018 + * 2602 - 27019 + * 2702 - 27020 + * ... + */ + + return 27016 + (($clientPort - 2302) / 100); + } +} diff --git a/third_party/gameq_v3.1/GameQ/Protocols/Dayzmod.php b/third_party/gameq_v3.1/GameQ/Protocols/Dayzmod.php new file mode 100644 index 00000000..2ce1076d --- /dev/null +++ b/third_party/gameq_v3.1/GameQ/Protocols/Dayzmod.php @@ -0,0 +1,44 @@ +. + */ + +namespace GameQ\Protocols; + +/** + * Class Dayzmod + * + * @package GameQ\Protocols + * @author Marcel Bößendörfer + * @author Austin Bischoff + */ +class Dayzmod extends Armedassault2oa +{ + + /** + * String name of this protocol class + * + * @type string + */ + protected $name = 'dayzmod'; + + /** + * Longer string name of this protocol class + * + * @type string + */ + protected $name_long = "DayZ Mod"; +} diff --git a/third_party/gameq_v3.1/GameQ/Protocols/Dod.php b/third_party/gameq_v3.1/GameQ/Protocols/Dod.php new file mode 100644 index 00000000..0c7baf69 --- /dev/null +++ b/third_party/gameq_v3.1/GameQ/Protocols/Dod.php @@ -0,0 +1,45 @@ +. + */ + +namespace GameQ\Protocols; + +/** + * Class Dod + * + * Based off of CS 1.6 + * + * @package GameQ\Protocols + * @author Austin Bischoff + */ +class Dod extends Cs16 +{ + + /** + * String name of this protocol class + * + * @type string + */ + protected $name = 'dod'; + + /** + * Longer string name of this protocol class + * + * @type string + */ + protected $name_long = "Day of Defeat"; +} diff --git a/third_party/gameq_v3.1/GameQ/Protocols/Dods.php b/third_party/gameq_v3.1/GameQ/Protocols/Dods.php new file mode 100644 index 00000000..898d75b9 --- /dev/null +++ b/third_party/gameq_v3.1/GameQ/Protocols/Dods.php @@ -0,0 +1,42 @@ +. + */ + +namespace GameQ\Protocols; + +/** + * Class Dods + * + * @package GameQ\Protocols + * @author Austin Bischoff + */ +class Dods extends Source +{ + /** + * String name of this protocol class + * + * @type string + */ + protected $name = 'dods'; + + /** + * Longer string name of this protocol class + * + * @type string + */ + protected $name_long = "Day of Defeat: Source"; +} diff --git a/third_party/gameq_v3.1/GameQ/Protocols/Doom3.php b/third_party/gameq_v3.1/GameQ/Protocols/Doom3.php new file mode 100644 index 00000000..2e00f5f1 --- /dev/null +++ b/third_party/gameq_v3.1/GameQ/Protocols/Doom3.php @@ -0,0 +1,221 @@ +. + */ + +namespace GameQ\Protocols; + +use GameQ\Protocol; +use GameQ\Buffer; +use GameQ\Result; +use GameQ\Exception\Protocol as Exception; + +/** + * Doom3 Protocol Class + * + * Handles processing DOOM 3 servers + * + * @package GameQ\Protocols + * @author Wilson Jesus <> + */ +class Doom3 extends Protocol +{ + /** + * Array of packets we want to look up. + * Each key should correspond to a defined method in this or a parent class + * + * @type array + */ + protected $packets = [ + self::PACKET_ALL => "\xFF\xFFgetInfo\x00PiNGPoNG\x00", + ]; + + /** + * Use the response flag to figure out what method to run + * + * @type array + */ + protected $responses = [ + "\xFF\xFFinfoResponse" => 'processStatus', + ]; + + /** + * The query protocol used to make the call + * + * @type string + */ + protected $protocol = 'doom3'; + + /** + * String name of this protocol class + * + * @type string + */ + protected $name = 'doom3'; + + /** + * Longer string name of this protocol class + * + * @type string + */ + protected $name_long = "Doom 3"; + + /** + * The client join link + * + * @type string + */ + protected $join_link = null; + + /** + * Normalize settings for this protocol + * + * @type array + */ + protected $normalize = [ + // General + 'general' => [ + // target => source + 'hostname' => 'si_name', + 'gametype' => 'gamename', + 'mapname' => 'si_map', + 'maxplayers' => 'si_maxPlayers', + 'numplayers' => 'clients', + 'password' => 'si_usepass', + ], + // Individual + 'player' => [ + 'name' => 'name', + 'ping' => 'ping', + ], + ]; + + /** + * Handle response from the server + * + * @return mixed + * @throws Exception + */ + public function processResponse() + { + // Make a buffer + $buffer = new Buffer(implode('', $this->packets_response)); + + // Grab the header + $header = $buffer->readString(); + + // Header + // Figure out which packet response this is + if (empty($header) || !array_key_exists($header, $this->responses)) { + throw new Exception(__METHOD__ . " response type '" . bin2hex($header) . "' is not valid"); + } + + return call_user_func_array([$this, $this->responses[$header]], [$buffer]); + } + + /** + * Process the status response + * + * @param Buffer $buffer + * + * @return array + */ + protected function processStatus(Buffer $buffer) + { + // We need to split the data and offload + $results = $this->processServerInfo($buffer); + + $results = array_merge_recursive( + $results, + $this->processPlayers($buffer) + ); + + unset($buffer); + + // Return results + return $results; + } + + /** + * Handle processing the server information + * + * @param Buffer $buffer + * + * @return array + */ + protected function processServerInfo(Buffer $buffer) + { + // Set the result to a new result instance + $result = new Result(); + + $result->add('version', $buffer->readInt8() . '.' . $buffer->readInt8()); + + // Key / value pairs, delimited by an empty pair + while ($buffer->getLength()) { + $key = trim($buffer->readString()); + $val = utf8_encode(trim($buffer->readString())); + + // Something is empty so we are done + if (empty($key) && empty($val)) { + break; + } + + $result->add($key, $val); + } + + unset($buffer); + + return $result->fetch(); + } + + /** + * Handle processing of player data + * + * @param Buffer $buffer + * + * @return array + */ + protected function processPlayers(Buffer $buffer) + { + // Some games do not have a number of current players + $playerCount = 0; + + // Set the result to a new result instance + $result = new Result(); + + // Parse players + // Loop thru the buffer until we run out of data + while (($id = $buffer->readInt8()) != 32) { + // Add player info results + $result->addPlayer('id', $id); + $result->addPlayer('ping', $buffer->readInt16()); + $result->addPlayer('rate', $buffer->readInt32()); + // Add player name, encoded + $result->addPlayer('name', utf8_encode(trim($buffer->readString()))); + + // Increment + $playerCount++; + } + + // Add the number of players to the result + $result->add('clients', $playerCount); + + // Clear + unset($buffer, $playerCount); + + return $result->fetch(); + } +} diff --git a/third_party/gameq_v3.1/GameQ/Protocols/Dow.php b/third_party/gameq_v3.1/GameQ/Protocols/Dow.php new file mode 100644 index 00000000..b66512a7 --- /dev/null +++ b/third_party/gameq_v3.1/GameQ/Protocols/Dow.php @@ -0,0 +1,69 @@ +. + */ + +namespace GameQ\Protocols; + +use GameQ\Buffer; + +/** + * Class Dow + * + * Apparently the player response is incomplete as there is no information being returned for that packet + * + * @package GameQ\Protocols + * @author Austin Bischoff + */ +class Dow extends Source +{ + /** + * String name of this protocol class + * + * @type string + */ + protected $name = 'dow'; + + /** + * Longer string name of this protocol class + * + * @type string + */ + protected $name_long = "Days of War"; + + /** + * Normalize main fields + * + * @var array + */ + protected $normalize = [ + // General + 'general' => [ + // target => source + 'gametype' => 'G_s', + 'hostname' => 'ONM_s', + 'mapname' => 'MPN_s', + 'maxplayers' => 'P_i', + 'numplayers' => 'N_i', + ], + // Individual + 'player' => [ + 'name' => 'name', + 'score' => 'score', + 'time' => 'time', + ], + ]; +} diff --git a/third_party/gameq_v3.1/GameQ/Protocols/Eco.php b/third_party/gameq_v3.1/GameQ/Protocols/Eco.php new file mode 100644 index 00000000..a2292e90 --- /dev/null +++ b/third_party/gameq_v3.1/GameQ/Protocols/Eco.php @@ -0,0 +1,123 @@ +. + */ + +namespace GameQ\Protocols; + +use GameQ\Exception\Protocol as Exception; +use GameQ\Result; + +/** + * ECO Global Survival Protocol Class + * + * @author Austin Bischoff + */ +class Eco extends Http +{ + /** + * Packets to send + * + * @var array + */ + protected $packets = [ + self::PACKET_STATUS => "GET /frontpage HTTP/1.0\r\nAccept: */*\r\n\r\n", + ]; + + /** + * Http protocol is SSL + * + * @var string + */ + protected $transport = self::TRANSPORT_TCP; + + /** + * The protocol being used + * + * @var string + */ + protected $protocol = 'eco'; + + /** + * String name of this protocol class + * + * @var string + */ + protected $name = 'eco'; + + /** + * Longer string name of this protocol class + * + * @var string + */ + protected $name_long = "ECO Global Survival"; + + /** + * query_port = client_port + 1 + * + * @type int + */ + protected $port_diff = 1; + + /** + * Normalize some items + * + * @var array + */ + protected $normalize = [ + // General + 'general' => [ + // target => source + 'dedicated' => 'dedicated', + 'hostname' => 'description', + 'maxplayers' => 'totalplayers', + 'numplayers' => 'onlineplayers', + 'password' => 'haspassword', + ], + ]; + + /** + * Process the response + * + * @return array + * @throws Exception + */ + public function processResponse() + { + if (empty($this->packets_response)) { + return []; + } + + // Implode and rip out the JSON + preg_match('/\{(.*)\}/ms', implode('', $this->packets_response), $matches); + + // Return should be JSON, let's validate + if (!isset($matches[0]) || ($json = json_decode($matches[0])) === null) { + throw new Exception("JSON response from Eco server is invalid."); + } + + $result = new Result(); + + // Server is always dedicated + $result->add('dedicated', 1); + + foreach ($json->Info as $info => $setting) { + $result->add(strtolower($info), $setting); + } + + return $result->fetch(); + } +} diff --git a/third_party/gameq_v3.1/GameQ/Protocols/Egs.php b/third_party/gameq_v3.1/GameQ/Protocols/Egs.php new file mode 100644 index 00000000..aab79aea --- /dev/null +++ b/third_party/gameq_v3.1/GameQ/Protocols/Egs.php @@ -0,0 +1,51 @@ +. + */ + +namespace GameQ\Protocols; + +/** + * Class Empyrion - Galactic Survival + * + * @package GameQ\Protocols + * @author Austin Bischoff + * @author TacTicToe66 + */ +class Egs extends Source +{ + + /** + * String name of this protocol class + * + * @type string + */ + protected $name = 'egs'; + + /** + * Longer string name of this protocol class + * + * @type string + */ + protected $name_long = "Empyrion - Galactic Survival"; + + /** + * query_port = client_port + 1 + * + * @type int + */ + protected $port_diff = 1; +} diff --git a/third_party/gameq_v3.1/GameQ/Protocols/Et.php b/third_party/gameq_v3.1/GameQ/Protocols/Et.php new file mode 100644 index 00000000..63b5beb7 --- /dev/null +++ b/third_party/gameq_v3.1/GameQ/Protocols/Et.php @@ -0,0 +1,43 @@ +. + */ + +namespace GameQ\Protocols; + +/** + * Wolfenstein Enemy Territory Protocol Class + * + * @package GameQ\Protocols + * + * @author Wilson Jesus <> + */ +class Et extends Quake3 +{ + /** + * String name of this protocol class + * + * @type string + */ + protected $name = 'et'; + + /** + * Longer string name of this protocol class + * + * @type string + */ + protected $name_long = "Wolfenstein Enemy Territory"; +} diff --git a/third_party/gameq_v3.1/GameQ/Protocols/Etqw.php b/third_party/gameq_v3.1/GameQ/Protocols/Etqw.php new file mode 100644 index 00000000..1f3a446c --- /dev/null +++ b/third_party/gameq_v3.1/GameQ/Protocols/Etqw.php @@ -0,0 +1,234 @@ +. + */ + +namespace GameQ\Protocols; + +use GameQ\Buffer; +use GameQ\Exception\Protocol as Exception; +use GameQ\Protocol; +use GameQ\Result; + +/** + * Enemy Territory Quake Wars Protocol Class + * + * @author Austin Bischoff + */ +class Etqw extends Protocol +{ + + /** + * Array of packets we want to look up. + * Each key should correspond to a defined method in this or a parent class + * + * @type array + */ + protected $packets = [ + self::PACKET_STATUS => "\xFF\xFFgetInfoEx\x00\x00\x00\x00", + //self::PACKET_STATUS => "\xFF\xFFgetInfo\x00\x00\x00\x00\x00", + ]; + + /** + * Use the response flag to figure out what method to run + * + * @type array + */ + protected $responses = [ + "\xFF\xFFinfoExResponse" => "processStatus", + ]; + + /** + * The query protocol used to make the call + * + * @type string + */ + protected $protocol = 'etqw'; + + /** + * String name of this protocol class + * + * @type string + */ + protected $name = 'etqw'; + + /** + * Longer string name of this protocol class + * + * @type string + */ + protected $name_long = "Enemy Territory Quake Wars"; + + /** + * Normalize settings for this protocol + * + * @type array + */ + protected $normalize = [ + // General + 'general' => [ + // target => source + 'gametype' => 'campaign', + 'hostname' => 'name', + 'mapname' => 'map', + 'maxplayers' => 'maxPlayers', + 'mod' => 'gamename', + 'numplayers' => 'numplayers', + 'password' => 'privateClients', + ], + // Individual + 'player' => [ + 'name' => 'name', + 'score' => 'score', + 'time' => 'time', + ], + ]; + + /** + * Process the response + * + * @return array + * @throws \GameQ\Exception\Protocol + */ + public function processResponse() + { + // In case it comes back as multiple packets (it shouldn't) + $buffer = new Buffer(implode('', $this->packets_response)); + + // Figure out what packet response this is for + $response_type = $buffer->readString(); + + // Figure out which packet response this is + if (!array_key_exists($response_type, $this->responses)) { + throw new Exception(__METHOD__ . " response type '{$response_type}' is not valid"); + } + + // Offload the call + $results = call_user_func_array([$this, $this->responses[$response_type]], [$buffer]); + + return $results; + } + + /* + * Internal methods + */ + + /** + * Handle processing the status response + * + * @param Buffer $buffer + * + * @return array + */ + protected function processStatus(Buffer $buffer) + { + // Set the result to a new result instance + $result = new Result(); + + // Defaults + $result->add('dedicated', 1); + + // Now burn the challenge, version and size + $buffer->skip(16); + + // Key / value pairs + while ($buffer->getLength()) { + $var = str_replace('si_', '', $buffer->readString()); + $val = $buffer->readString(); + if (empty($var) && empty($val)) { + break; + } + // Add the server prop + $result->add($var, $val); + } + // Now let's do the basic player info + $this->parsePlayers($buffer, $result); + + // Now grab the rest of the server info + $result->add('osmask', $buffer->readInt32()); + $result->add('ranked', $buffer->readInt8()); + $result->add('timeleft', $buffer->readInt32()); + $result->add('gamestate', $buffer->readInt8()); + $result->add('servertype', $buffer->readInt8()); + + // 0: regular server + if ($result->get('servertype') == 0) { + $result->add('interested_clients', $buffer->readInt8()); + } else { + // 1: tv server + $result->add('connected_clients', $buffer->readInt32()); + $result->add('max_clients', $buffer->readInt32()); + } + + // Now let's parse the extended player info + $this->parsePlayersExtra($buffer, $result); + + unset($buffer); + + return $result->fetch(); + } + + /** + * Parse players out of the status ex response + * + * @param Buffer $buffer + * @param Result $result + */ + protected function parsePlayers(Buffer &$buffer, Result &$result) + { + // By default there are 0 players + $players = 0; + + // Iterate over the players until we run out + while (($id = $buffer->readInt8()) != 32) { + $result->addPlayer('id', $id); + $result->addPlayer('ping', $buffer->readInt16()); + $result->addPlayer('name', $buffer->readString()); + $result->addPlayer('clantag_pos', $buffer->readInt8()); + $result->addPlayer('clantag', $buffer->readString()); + $result->addPlayer('bot', $buffer->readInt8()); + $players++; + } + + // Let's add in the current players as a result + $result->add('numplayers', $players); + + // Free some memory + unset($id); + } + + /** + * Handle parsing extra player data + * + * @param Buffer $buffer + * @param Result $result + */ + protected function parsePlayersExtra(Buffer &$buffer, Result &$result) + { + // Iterate over the extra player info + while (($id = $buffer->readInt8()) != 32) { + $result->addPlayer('total_xp', $buffer->readFloat32()); + $result->addPlayer('teamname', $buffer->readString()); + $result->addPlayer('total_kills', $buffer->readInt32()); + $result->addPlayer('total_deaths', $buffer->readInt32()); + } + + // @todo: Add team stuff + + // Free some memory + unset($id); + } +} diff --git a/third_party/gameq_v3.1/GameQ/Protocols/Ffe.php b/third_party/gameq_v3.1/GameQ/Protocols/Ffe.php new file mode 100644 index 00000000..c0947bdc --- /dev/null +++ b/third_party/gameq_v3.1/GameQ/Protocols/Ffe.php @@ -0,0 +1,43 @@ +. + */ + +namespace GameQ\Protocols; + +/** + * Class Ffe - Fortress Forever + * + * @package GameQ\Protocols + * @author Austin Bischoff + */ +class Ffe extends Source +{ + + /** + * String name of this protocol class + * + * @type string + */ + protected $name = 'ffe'; + + /** + * Longer string name of this protocol class + * + * @type string + */ + protected $name_long = "Fortress Forever"; +} diff --git a/third_party/gameq_v3.1/GameQ/Protocols/Ffow.php b/third_party/gameq_v3.1/GameQ/Protocols/Ffow.php new file mode 100644 index 00000000..00c33d47 --- /dev/null +++ b/third_party/gameq_v3.1/GameQ/Protocols/Ffow.php @@ -0,0 +1,243 @@ + "\xFF\xFF\xFF\xFF\x57", + self::PACKET_RULES => "\xFF\xFF\xFF\xFF\x56%s", + self::PACKET_PLAYERS => "\xFF\xFF\xFF\xFF\x55%s", + self::PACKET_INFO => "\xFF\xFF\xFF\xFF\x46\x4C\x53\x51", + ]; + + /** + * Use the response flag to figure out what method to run + * + * @type array + */ + protected $responses = [ + "\xFF\xFF\xFF\xFF\x49\x02" => 'processInfo', // I + "\xFF\xFF\xFF\xFF\x45\x00" => 'processRules', // E + "\xFF\xFF\xFF\xFF\x44\x00" => 'processPlayers', // D + ]; + + /** + * The query protocol used to make the call + * + * @type string + */ + protected $protocol = 'ffow'; + + /** + * String name of this protocol class + * + * @type string + */ + protected $name = 'ffow'; + + /** + * Longer string name of this protocol class + * + * @type string + */ + protected $name_long = "Frontlines Fuel of War"; + + /** + * The client join link + * + * @type string + */ + protected $join_link = null; + + /** + * query_port = client_port + 2 + * + * @type int + */ + protected $port_diff = 2; + + /** + * Normalize settings for this protocol + * + * @type array + */ + protected $normalize = [ + // General + 'general' => [ + // target => source + 'gametype' => 'gamemode', + 'hostname' => 'servername', + 'mapname' => 'mapname', + 'maxplayers' => 'max_players', + 'mod' => 'modname', + 'numplayers' => 'num_players', + 'password' => 'password', + ], + // Individual + 'player' => [ + 'name' => 'name', + 'ping' => 'ping', + 'score' => 'frags', + ], + ]; + + /** + * Parse the challenge response and apply it to all the packet types + * + * @param \GameQ\Buffer $challenge_buffer + * + * @return bool + * @throws \GameQ\Exception\Protocol + */ + public function challengeParseAndApply(Buffer $challenge_buffer) + { + // Burn padding + $challenge_buffer->skip(5); + + // Apply the challenge and return + return $this->challengeApply($challenge_buffer->read(4)); + } + + /** + * Handle response from the server + * + * @return mixed + * @throws Exception + */ + public function processResponse() + { + // Init results + $results = []; + + foreach ($this->packets_response as $response) { + $buffer = new Buffer($response); + + // Figure out what packet response this is for + $response_type = $buffer->read(6); + + // Figure out which packet response this is + if (!array_key_exists($response_type, $this->responses)) { + throw new Exception(__METHOD__ . " response type '" . bin2hex($response_type) . "' is not valid"); + } + + // Now we need to call the proper method + $results = array_merge( + $results, + call_user_func_array([$this, $this->responses[$response_type]], [$buffer]) + ); + + unset($buffer); + } + + return $results; + } + + /** + * Handle processing the server information + * + * @param Buffer $buffer + * + * @return array + */ + protected function processInfo(Buffer $buffer) + { + // Set the result to a new result instance + $result = new Result(); + + $result->add('servername', $buffer->readString()); + $result->add('mapname', $buffer->readString()); + $result->add('modname', $buffer->readString()); + $result->add('gamemode', $buffer->readString()); + $result->add('description', $buffer->readString()); + $result->add('version', $buffer->readString()); + $result->add('port', $buffer->readInt16()); + $result->add('num_players', $buffer->readInt8()); + $result->add('max_players', $buffer->readInt8()); + $result->add('dedicated', $buffer->readInt8()); + $result->add('os', $buffer->readInt8()); + $result->add('password', $buffer->readInt8()); + $result->add('anticheat', $buffer->readInt8()); + $result->add('average_fps', $buffer->readInt8()); + $result->add('round', $buffer->readInt8()); + $result->add('max_rounds', $buffer->readInt8()); + $result->add('time_left', $buffer->readInt16()); + + unset($buffer); + + return $result->fetch(); + } + + /** + * Handle processing the server rules + * + * @param Buffer $buffer + * + * @return array + */ + protected function processRules(Buffer $buffer) + { + // Set the result to a new result instance + $result = new Result(); + + // Burn extra header + $buffer->skip(1); + + // Read rules until we run out of buffer + while ($buffer->getLength()) { + $key = $buffer->readString(); + // Check for map + if (strstr($key, "Map:")) { + $result->addSub("maplist", "name", $buffer->readString()); + } else // Regular rule + { + $result->add($key, $buffer->readString()); + } + } + + unset($buffer); + + return $result->fetch(); + } + + /** + * Handle processing of player data + * + * @todo: Build this out when there is a server with players to test against + * + * @param Buffer $buffer + * + * @return array + */ + protected function processPlayers(Buffer $buffer) + { + // Set the result to a new result instance + $result = new Result(); + + unset($buffer); + + return $result->fetch(); + } +} diff --git a/third_party/gameq_v3.1/GameQ/Protocols/Fof.php b/third_party/gameq_v3.1/GameQ/Protocols/Fof.php new file mode 100644 index 00000000..a35c4c0a --- /dev/null +++ b/third_party/gameq_v3.1/GameQ/Protocols/Fof.php @@ -0,0 +1,43 @@ +. + */ + +namespace GameQ\Protocols; + +/** + * Class Fistful of Frags + * + * @package GameQ\Protocols + * @author Austin Bischoff + * @author Jesse Lukas +*/ +class Fof extends Source +{ + /** + * String name of this protocol class + * + * @type string + */ + protected $name = 'fof'; + + /** + * Longer string name of this protocol class + * + * @type string + */ + protected $name_long = "Fistful of Frags"; +} diff --git a/third_party/gameq_v3.1/GameQ/Protocols/Gamespy.php b/third_party/gameq_v3.1/GameQ/Protocols/Gamespy.php new file mode 100644 index 00000000..b1a1e4fa --- /dev/null +++ b/third_party/gameq_v3.1/GameQ/Protocols/Gamespy.php @@ -0,0 +1,181 @@ +. + */ + +namespace GameQ\Protocols; + +use GameQ\Protocol; +use GameQ\Buffer; +use GameQ\Result; +use \GameQ\Exception\Protocol as Exception; + +/** + * GameSpy Protocol class + * + * @author Austin Bischoff + */ +class Gamespy extends Protocol +{ + + /** + * Array of packets we want to look up. + * Each key should correspond to a defined method in this or a parent class + * + * @type array + */ + protected $packets = [ + self::PACKET_STATUS => "\x5C\x73\x74\x61\x74\x75\x73\x5C", + ]; + + /** + * The query protocol used to make the call + * + * @type string + */ + protected $protocol = 'gamespy'; + + /** + * String name of this protocol class + * + * @type string + */ + protected $name = 'gamespy'; + + /** + * Longer string name of this protocol class + * + * @type string + */ + protected $name_long = "GameSpy Server"; + + /** + * The client join link + * + * @type string + */ + protected $join_link = null; + + /** + * Process the response for this protocol + * + * @return array + * @throws Exception + */ + public function processResponse() + { + // Holds the processed packets so we can sort them in case they come in an unordered + $processed = []; + + // Iterate over the packets + foreach ($this->packets_response as $response) { + // Check to see if we had a preg_match error + if (($match = preg_match("#^(.*)\\\\queryid\\\\([^\\\\]+)(\\\\|$)#", $response, $matches)) === false + || $match != 1 + ) { + throw new Exception(__METHOD__ . " An error occurred while parsing the packets for 'queryid'"); + } + + // Multiply so we move the decimal point out of the way, if there is one + $key = (int)(floatval($matches[2]) * 1000); + + // Add this packet to the processed + $processed[$key] = $matches[1]; + } + + // Sort the new array to make sure the keys (query ids) are in the proper order + ksort($processed, SORT_NUMERIC); + + // Create buffer and offload processing + return $this->processStatus(new Buffer(implode('', $processed))); + } + + /* + * Internal methods + */ + + /** + * Handle processing the status buffer + * + * @param Buffer $buffer + * + * @return array + */ + protected function processStatus(Buffer $buffer) + { + // Set the result to a new result instance + $result = new Result(); + + // By default dedicted + $result->add('dedicated', 1); + + // Lets peek and see if the data starts with a \ + if ($buffer->lookAhead(1) == '\\') { + // Burn the first one + $buffer->skip(1); + } + + // Explode the data + $data = explode('\\', $buffer->getBuffer()); + + // No longer needed + unset($buffer); + + // Init some vars + $numPlayers = 0; + $numTeams = 0; + + $itemCount = count($data); + + // Check to make sure we have more than 1 item in the array before trying to loop + if (count($data) > 1) { + // Now lets loop the array since we have items + for ($x = 0; $x < $itemCount; $x += 2) { + // Set some local vars + $key = $data[$x]; + $val = $data[$x + 1]; + + // Check for _ variable (i.e players) + if (($suffix = strrpos($key, '_')) !== false && is_numeric(substr($key, $suffix + 1))) { + // See if this is a team designation + if (substr($key, 0, $suffix) == 'teamname') { + $result->addTeam('teamname', $val); + $numTeams++; + } else { + // Its a player + if (substr($key, 0, $suffix) == 'playername') { + $numPlayers++; + } + $result->addPlayer(substr($key, 0, $suffix), utf8_encode($val)); + } + } else { + // Regular variable so just add the value. + $result->add($key, $val); + } + } + } + + // Add the player and team count + $result->add('num_players', $numPlayers); + $result->add('num_teams', $numTeams); + + // Unset some stuff to free up memory + unset($data, $key, $val, $suffix, $x, $itemCount); + + // Return the result + return $result->fetch(); + } +} diff --git a/third_party/gameq_v3.1/GameQ/Protocols/Gamespy2.php b/third_party/gameq_v3.1/GameQ/Protocols/Gamespy2.php new file mode 100644 index 00000000..c7788d9e --- /dev/null +++ b/third_party/gameq_v3.1/GameQ/Protocols/Gamespy2.php @@ -0,0 +1,269 @@ +. + */ + +namespace GameQ\Protocols; + +use GameQ\Exception\Protocol as Exception; +use GameQ\Protocol; +use GameQ\Buffer; +use GameQ\Result; + +/** + * GameSpy2 Protocol class + * + * Given the ability for non utf-8 characters to be used as hostnames, player names, etc... this + * version returns all strings utf-8 encoded (utf8_encode). To access the proper version of a + * string response you must use utf8_decode() on the specific response. + * + * @author Austin Bischoff + */ +class Gamespy2 extends Protocol +{ + + /** + * Define the state of this class + * + * @type int + */ + protected $state = self::STATE_BETA; + + /** + * Array of packets we want to look up. + * Each key should correspond to a defined method in this or a parent class + * + * @type array + */ + protected $packets = [ + self::PACKET_DETAILS => "\xFE\xFD\x00\x43\x4F\x52\x59\xFF\x00\x00", + self::PACKET_PLAYERS => "\xFE\xFD\x00\x43\x4F\x52\x58\x00\xFF\xFF", + ]; + + /** + * Use the response flag to figure out what method to run + * + * @type array + */ + protected $responses = [ + "\x00\x43\x4F\x52\x59" => "processDetails", + "\x00\x43\x4F\x52\x58" => "processPlayers", + ]; + + /** + * The query protocol used to make the call + * + * @type string + */ + protected $protocol = 'gamespy2'; + + /** + * String name of this protocol class + * + * @type string + */ + protected $name = 'gamespy2'; + + /** + * Longer string name of this protocol class + * + * @type string + */ + protected $name_long = "GameSpy2 Server"; + + /** + * The client join link + * + * @type string + */ + protected $join_link = null; + + /** + * Normalize settings for this protocol + * + * @type array + */ + protected $normalize = [ + // General + 'general' => [ + // target => source + 'dedicated' => 'dedicated', + 'gametype' => 'gametype', + 'hostname' => 'hostname', + 'mapname' => 'mapname', + 'maxplayers' => 'maxplayers', + 'mod' => 'mod', + 'numplayers' => 'numplayers', + 'password' => 'password', + ], + ]; + + + /** + * Process the response + * + * @return array + * @throws Exception + */ + public function processResponse() + { + + // Will hold the packets after sorting + $packets = []; + + // We need to pre-sort these for split packets so we can do extra work where needed + foreach ($this->packets_response as $response) { + $buffer = new Buffer($response); + + // Pull out the header + $header = $buffer->read(5); + + // Add the packet to the proper section, we will combine later + $packets[$header][] = $buffer->getBuffer(); + } + + unset($buffer); + + $results = []; + + // Now let's iterate and process + foreach ($packets as $header => $packetGroup) { + // Figure out which packet response this is + if (!array_key_exists($header, $this->responses)) { + throw new Exception(__METHOD__ . " response type '" . bin2hex($header) . "' is not valid"); + } + + // Now we need to call the proper method + $results = array_merge( + $results, + call_user_func_array([$this, $this->responses[$header]], [new Buffer(implode($packetGroup))]) + ); + } + + unset($packets); + + return $results; + } + + /* + * Internal methods + */ + + /** + * Handles processing the details data into a usable format + * + * @param \GameQ\Buffer $buffer + * + * @return array + * @throws Exception + */ + protected function processDetails(Buffer $buffer) + { + + // Set the result to a new result instance + $result = new Result(); + + // We go until we hit an empty key + while ($buffer->getLength()) { + $key = $buffer->readString(); + if (strlen($key) == 0) { + break; + } + $result->add($key, utf8_encode($buffer->readString())); + } + + unset($buffer); + + return $result->fetch(); + } + + /** + * Handles processing the players data into a usable format + * + * @param \GameQ\Buffer $buffer + * + * @return array + * @throws Exception + */ + protected function processPlayers(Buffer $buffer) + { + + // Set the result to a new result instance + $result = new Result(); + + // Skip the header + $buffer->skip(1); + + // Players are first + $this->parsePlayerTeam('players', $buffer, $result); + + // Teams are next + $this->parsePlayerTeam('teams', $buffer, $result); + + unset($buffer); + + return $result->fetch(); + } + + /** + * Parse the player/team info returned from the player call + * + * @param string $dataType + * @param \GameQ\Buffer $buffer + * @param \GameQ\Result $result + * + * @throws Exception + */ + protected function parsePlayerTeam($dataType, Buffer &$buffer, Result &$result) + { + + // Do count + $result->add('num_' . $dataType, $buffer->readInt8()); + + // Variable names + $varNames = []; + + // Loop until we run out of length + while ($buffer->getLength()) { + $varNames[] = str_replace('_', '', $buffer->readString()); + + if ($buffer->lookAhead() === "\x00") { + $buffer->skip(); + break; + } + } + + // Check if there are any value entries + if ($buffer->lookAhead() == "\x00") { + $buffer->skip(); + + return; + } + + // Get the values + while ($buffer->getLength() > 4) { + foreach ($varNames as $varName) { + $result->addSub($dataType, utf8_encode($varName), utf8_encode($buffer->readString())); + } + if ($buffer->lookAhead() === "\x00") { + $buffer->skip(); + break; + } + } + + return; + } +} diff --git a/third_party/gameq_v3.1/GameQ/Protocols/Gamespy3.php b/third_party/gameq_v3.1/GameQ/Protocols/Gamespy3.php new file mode 100644 index 00000000..2df0a4bd --- /dev/null +++ b/third_party/gameq_v3.1/GameQ/Protocols/Gamespy3.php @@ -0,0 +1,340 @@ +. + */ + +namespace GameQ\Protocols; + +use GameQ\Protocol; +use GameQ\Buffer; +use GameQ\Result; + +/** + * GameSpy3 Protocol class + * + * Given the ability for non utf-8 characters to be used as hostnames, player names, etc... this + * version returns all strings utf-8 encoded (utf8_encode). To access the proper version of a + * string response you must use utf8_decode() on the specific response. + * + * @author Austin Bischoff + */ +class Gamespy3 extends Protocol +{ + + /** + * Array of packets we want to look up. + * Each key should correspond to a defined method in this or a parent class + * + * @type array + */ + protected $packets = [ + self::PACKET_CHALLENGE => "\xFE\xFD\x09\x10\x20\x30\x40", + self::PACKET_ALL => "\xFE\xFD\x00\x10\x20\x30\x40%s\xFF\xFF\xFF\x01", + ]; + + /** + * The query protocol used to make the call + * + * @type string + */ + protected $protocol = 'gamespy3'; + + /** + * String name of this protocol class + * + * @type string + */ + protected $name = 'gamespy3'; + + /** + * Longer string name of this protocol class + * + * @type string + */ + protected $name_long = "GameSpy3 Server"; + + /** + * The client join link + * + * @type string + */ + protected $join_link = null; + + /** + * This defines the split between the server info and player/team info. + * This value can vary by game. This value is the default split. + * + * @var string + */ + protected $packetSplit = "/\\x00\\x00\\x01/m"; + + /** + * Parse the challenge response and apply it to all the packet types + * + * @param \GameQ\Buffer $challenge_buffer + * + * @return bool + * @throws \GameQ\Exception\Protocol + */ + public function challengeParseAndApply(Buffer $challenge_buffer) + { + // Pull out the challenge + $challenge = substr(preg_replace("/[^0-9\-]/si", "", $challenge_buffer->getBuffer()), 1); + + // By default, no challenge result (see #197) + $challenge_result = ''; + + // Check for valid challenge (see #197) + if ($challenge) { + // Encode chellenge result + $challenge_result = sprintf( + "%c%c%c%c", + ($challenge >> 24), + ($challenge >> 16), + ($challenge >> 8), + ($challenge >> 0) + ); + } + + // Apply the challenge and return + return $this->challengeApply($challenge_result); + } + + /** + * Process the response + * + * @return array + */ + public function processResponse() + { + + // Holds the processed packets + $processed = []; + + // Iterate over the packets + foreach ($this->packets_response as $response) { + // Make a buffer + $buffer = new Buffer($response, Buffer::NUMBER_TYPE_BIGENDIAN); + + // Packet type = 0 + $buffer->readInt8(); + + // Session Id + $buffer->readInt32(); + + // We need to burn the splitnum\0 because it is not used + $buffer->skip(9); + + // Get the id + $id = $buffer->readInt8(); + + // Burn next byte not sure what it is used for + $buffer->skip(1); + + // Add this packet to the processed + $processed[$id] = $buffer->getBuffer(); + + unset($buffer, $id); + } + + // Sort packets, reset index + ksort($processed); + + // Offload cleaning up the packets if they happen to be split + $packets = $this->cleanPackets(array_values($processed)); + + // Split the packets by type general and the rest (i.e. players & teams) + $split = preg_split($this->packetSplit, implode('', $packets)); + + // Create a new result + $result = new Result(); + + // Assign variable due to pass by reference in PHP 7+ + $buffer = new Buffer($split[0], Buffer::NUMBER_TYPE_BIGENDIAN); + + // First key should be server details and rules + $this->processDetails($buffer, $result); + + // The rest should be the player and team information, if it exists + if (array_key_exists(1, $split)) { + $buffer = new Buffer($split[1], Buffer::NUMBER_TYPE_BIGENDIAN); + $this->processPlayersAndTeams($buffer, $result); + } + + unset($buffer); + + return $result->fetch(); + } + + /* + * Internal methods + */ + + /** + * Handles cleaning up packets since the responses can be a bit "dirty" + * + * @param array $packets + * + * @return array + */ + protected function cleanPackets(array $packets = []) + { + + // Get the number of packets + $packetCount = count($packets); + + // Compare last var of current packet with first var of next packet + // On a partial match, remove last var from current packet, + // variable header from next packet + for ($i = 0, $x = $packetCount; $i < $x - 1; $i++) { + // First packet + $fst = substr($packets[$i], 0, -1); + // Second packet + $snd = $packets[$i + 1]; + // Get last variable from first packet + $fstvar = substr($fst, strrpos($fst, "\x00") + 1); + // Get first variable from last packet + $snd = substr($snd, strpos($snd, "\x00") + 2); + $sndvar = substr($snd, 0, strpos($snd, "\x00")); + // Check if fstvar is a substring of sndvar + // If so, remove it from the first string + if (!empty($fstvar) && strpos($sndvar, $fstvar) !== false) { + $packets[$i] = preg_replace("#(\\x00[^\\x00]+\\x00)$#", "\x00", $packets[$i]); + } + } + + // Now let's loop the return and remove any dupe prefixes + for ($x = 1; $x < $packetCount; $x++) { + $buffer = new Buffer($packets[$x], Buffer::NUMBER_TYPE_BIGENDIAN); + + $prefix = $buffer->readString(); + + // Check to see if the return before has the same prefix present + if ($prefix != null && strstr($packets[($x - 1)], $prefix)) { + // Update the return by removing the prefix plus 2 chars + $packets[$x] = substr(str_replace($prefix, '', $packets[$x]), 2); + } + + unset($buffer); + } + + unset($x, $i, $snd, $sndvar, $fst, $fstvar); + + // Return cleaned packets + return $packets; + } + + /** + * Handles processing the details data into a usable format + * + * @param \GameQ\Buffer $buffer + * @param \GameQ\Result $result + */ + protected function processDetails(Buffer &$buffer, Result &$result) + { + + // We go until we hit an empty key + while ($buffer->getLength()) { + $key = $buffer->readString(); + if (strlen($key) == 0) { + break; + } + $result->add($key, utf8_encode($buffer->readString())); + } + } + + /** + * Handles processing the player and team data into a usable format + * + * @param \GameQ\Buffer $buffer + * @param \GameQ\Result $result + */ + protected function processPlayersAndTeams(Buffer &$buffer, Result &$result) + { + + /* + * Explode the data into groups. First is player, next is team (item_t) + * Each group should be as follows: + * + * [0] => item_ + * [1] => information for item_ + * ... + */ + $data = explode("\x00\x00", $buffer->getBuffer()); + + // By default item_group is blank, this will be set for each loop thru the data + $item_group = ''; + + // By default the item_type is blank, this will be set on each loop + $item_type = ''; + + // Save count as variable + $count = count($data); + + // Loop through all of the $data for information and pull it out into the result + for ($x = 0; $x < $count - 1; $x++) { + // Pull out the item + $item = $data[$x]; + // If this is an empty item, move on + if ($item == '' || $item == "\x00") { + continue; + } + /* + * Left as reference: + * + * Each block of player_ and team_t have preceding junk chars + * + * player_ is actually \x01player_ + * team_t is actually \x00\x02team_t + * + * Probably a by-product of the change to exploding the data from the original. + * + * For now we just strip out these characters + */ + // Check to see if $item has a _ at the end, this is player info + if (substr($item, -1) == '_') { + // Set the item group + $item_group = 'players'; + // Set the item type, rip off any trailing stuff and bad chars + $item_type = rtrim(str_replace("\x01", '', $item), '_'); + } elseif (substr($item, -2) == '_t') { + // Check to see if $item has a _t at the end, this is team info + // Set the item group + $item_group = 'teams'; + // Set the item type, rip off any trailing stuff and bad chars + $item_type = rtrim(str_replace(["\x00", "\x02"], '', $item), '_t'); + } else { + // We can assume it is data belonging to a previously defined item + + // Make a temp buffer so we have easier access to the data + $buf_temp = new Buffer($item, Buffer::NUMBER_TYPE_BIGENDIAN); + // Get the values + while ($buf_temp->getLength()) { + // No value so break the loop, end of string + if (($val = $buf_temp->readString()) === '') { + break; + } + // Add the value to the proper item in the correct group + $result->addSub($item_group, $item_type, utf8_encode(trim($val))); + } + // Unset our buffer + unset($buf_temp); + } + } + // Free up some memory + unset($count, $data, $item, $item_group, $item_type, $val); + } +} diff --git a/third_party/gameq_v3.1/GameQ/Protocols/Gamespy4.php b/third_party/gameq_v3.1/GameQ/Protocols/Gamespy4.php new file mode 100644 index 00000000..e28755f1 --- /dev/null +++ b/third_party/gameq_v3.1/GameQ/Protocols/Gamespy4.php @@ -0,0 +1,34 @@ +. + */ + +namespace GameQ\Protocols; + +/** + * GameSpy4 Protocol Class + * + * By all accounts GameSpy 4 seems to be GameSpy 3. + * + * References: + * http://www.deletedscreen.com/?p=951 + * http://pastebin.com/2zZFDuTd + * + * @author Austin Bischoff + */ +class Gamespy4 extends Gamespy3 +{ +} diff --git a/third_party/gameq_v3.1/GameQ/Protocols/Gmod.php b/third_party/gameq_v3.1/GameQ/Protocols/Gmod.php new file mode 100644 index 00000000..65967247 --- /dev/null +++ b/third_party/gameq_v3.1/GameQ/Protocols/Gmod.php @@ -0,0 +1,42 @@ +. + */ + +namespace GameQ\Protocols; + +/** + * Class Gmod + * + * @package GameQ\Protocols + * @author Austin Bischoff + */ +class Gmod extends Source +{ + /** + * String name of this protocol class + * + * @type string + */ + protected $name = 'gmod'; + + /** + * Longer string name of this protocol class + * + * @type string + */ + protected $name_long = "Garry's Mod"; +} diff --git a/third_party/gameq_v3.1/GameQ/Protocols/Grav.php b/third_party/gameq_v3.1/GameQ/Protocols/Grav.php new file mode 100644 index 00000000..e025075a --- /dev/null +++ b/third_party/gameq_v3.1/GameQ/Protocols/Grav.php @@ -0,0 +1,42 @@ +. + */ + +namespace GameQ\Protocols; + +/** + * Grav Online Protocol Class + * + * @package GameQ\Protocols + * @author Austin Bischoff + */ +class Grav extends Source +{ + /** + * String name of this protocol class + * + * @type string + */ + protected $name = 'grav'; + + /** + * Longer string name of this protocol class + * + * @type string + */ + protected $name_long = "GRAV Online"; +} diff --git a/third_party/gameq_v3.1/GameQ/Protocols/Gta5m.php b/third_party/gameq_v3.1/GameQ/Protocols/Gta5m.php new file mode 100644 index 00000000..0f0c50a6 --- /dev/null +++ b/third_party/gameq_v3.1/GameQ/Protocols/Gta5m.php @@ -0,0 +1,173 @@ +. + */ + +namespace GameQ\Protocols; + +use GameQ\Buffer; +use GameQ\Exception\Protocol as Exception; +use GameQ\Protocol; +use GameQ\Result; + +/** + * GTA Five M Protocol Class + * + * Server base can be found at https://fivem.net/ + * + * Based on code found at https://github.com/LiquidObsidian/fivereborn-query + * + * @author Austin Bischoff + */ +class Gta5m extends Protocol +{ + + /** + * Array of packets we want to look up. + * Each key should correspond to a defined method in this or a parent class + * + * @type array + */ + protected $packets = [ + self::PACKET_STATUS => "\xFF\xFF\xFF\xFFgetinfo xxx", + ]; + + /** + * Use the response flag to figure out what method to run + * + * @type array + */ + protected $responses = [ + "\xFF\xFF\xFF\xFFinfoResponse" => "processStatus", + ]; + + /** + * The query protocol used to make the call + * + * @type string + */ + protected $protocol = 'gta5m'; + + /** + * String name of this protocol class + * + * @type string + */ + protected $name = 'gta5m'; + + /** + * Longer string name of this protocol class + * + * @type string + */ + protected $name_long = "GTA Five M"; + + /** + * Normalize settings for this protocol + * + * @type array + */ + protected $normalize = [ + // General + 'general' => [ + // target => source + 'gametype' => 'gametype', + 'hostname' => 'hostname', + 'mapname' => 'mapname', + 'maxplayers' => 'sv_maxclients', + 'mod' => 'gamename', + 'numplayers' => 'clients', + 'password' => 'privateClients', + ], + ]; + + /** + * Process the response + * + * @return array + * @throws \GameQ\Exception\Protocol + */ + public function processResponse() + { + // In case it comes back as multiple packets (it shouldn't) + $buffer = new Buffer(implode('', $this->packets_response)); + + // Figure out what packet response this is for + $response_type = $buffer->readString(PHP_EOL); + + // Figure out which packet response this is + if (empty($response_type) || !array_key_exists($response_type, $this->responses)) { + throw new Exception(__METHOD__ . " response type '{$response_type}' is not valid"); + } + + // Offload the call + $results = call_user_func_array([$this, $this->responses[$response_type]], [$buffer]); + + return $results; + } + + /* + * Internal methods + */ + + /** + * Handle processing the status response + * + * @param Buffer $buffer + * + * @return array + */ + protected function processStatus(Buffer $buffer) + { + // Set the result to a new result instance + $result = new Result(); + + // Lets peek and see if the data starts with a \ + if ($buffer->lookAhead(1) == '\\') { + // Burn the first one + $buffer->skip(1); + } + + // Explode the data + $data = explode('\\', $buffer->getBuffer()); + + // No longer needed + unset($buffer); + + $itemCount = count($data); + + // Now lets loop the array + for ($x = 0; $x < $itemCount; $x += 2) { + // Set some local vars + $key = $data[$x]; + $val = $data[$x + 1]; + + if (in_array($key, ['challenge'])) { + continue; // skip + } + + // Regular variable so just add the value. + $result->add($key, $val); + } + + /*var_dump($data); + var_dump($result->fetch()); + + exit;*/ + + return $result->fetch(); + } +} diff --git a/third_party/gameq_v3.1/GameQ/Protocols/Gtan.php b/third_party/gameq_v3.1/GameQ/Protocols/Gtan.php new file mode 100644 index 00000000..f7b531ee --- /dev/null +++ b/third_party/gameq_v3.1/GameQ/Protocols/Gtan.php @@ -0,0 +1,163 @@ +. + */ +namespace GameQ\Protocols; + +use GameQ\Exception\Protocol as Exception; +use GameQ\Result; +use GameQ\Server; + +/** + * Grand Theft Auto Network Protocol Class + * https://stats.gtanet.work/ + * + * Result from this call should be a header + JSON response + * + * References: + * - https://master.gtanet.work/apiservers + * + * @author Austin Bischoff + */ +class Gtan extends Http +{ + /** + * Packets to send + * + * @var array + */ + protected $packets = [ + //self::PACKET_STATUS => "GET /apiservers HTTP/1.0\r\nHost: master.gtanet.work\r\nAccept: */*\r\n\r\n", + self::PACKET_STATUS => "GET /gtan/api.php?ip=%s&raw HTTP/1.0\r\nHost: multiplayerhosting.info\r\nAccept: */*\r\n\r\n", + ]; + + /** + * Http protocol is SSL + * + * @var string + */ + protected $transport = self::TRANSPORT_SSL; + + /** + * The protocol being used + * + * @var string + */ + protected $protocol = 'gtan'; + + /** + * String name of this protocol class + * + * @var string + */ + protected $name = 'gtan'; + + /** + * Longer string name of this protocol class + * + * @var string + */ + protected $name_long = "Grand Theft Auto Network"; + + /** + * Holds the real ip so we can overwrite it back + * + * @var string + */ + protected $realIp = null; + + protected $realPortQuery = null; + + /** + * Normalize some items + * + * @var array + */ + protected $normalize = [ + // General + 'general' => [ + // target => source + 'dedicated' => 'dedicated', + 'hostname' => 'hostname', + 'mapname' => 'map', + 'mod' => 'mod', + 'maxplayers' => 'maxplayers', + 'numplayers' => 'numplayers', + 'password' => 'password', + ], + ]; + + public function beforeSend(Server $server) + { + // Loop over the packets and update them + foreach ($this->packets as $packetType => $packet) { + // Fill out the packet with the server info + $this->packets[$packetType] = sprintf($packet, $server->ip . ':' . $server->port_query); + } + + $this->realIp = $server->ip; + $this->realPortQuery = $server->port_query; + + // Override the existing settings + //$server->ip = 'master.gtanet.work'; + $server->ip = 'multiplayerhosting.info'; + $server->port_query = 443; + } + + /** + * Process the response + * + * @return array + * @throws Exception + */ + public function processResponse() + { + // No response, assume offline + if (empty($this->packets_response)) { + return [ + 'gq_address' => $this->realIp, + 'gq_port_query' => $this->realPortQuery, + ]; + } + + // Implode and rip out the JSON + preg_match('/\{(.*)\}/ms', implode('', $this->packets_response), $matches); + + // Return should be JSON, let's validate + if (!isset($matches[0]) || ($json = json_decode($matches[0])) === null) { + throw new Exception("JSON response from Gtan protocol is invalid."); + } + + $result = new Result(); + + // Server is always dedicated + $result->add('dedicated', 1); + + $result->add('gq_address', $this->realIp); + $result->add('gq_port_query', $this->realPortQuery); + + // Add server items + $result->add('hostname', $json->ServerName); + $result->add('serverversion', $json->ServerVersion); + $result->add('map', ((!empty($json->Map)) ? $json->Map : 'Los Santos/Blaine Country')); + $result->add('mod', $json->Gamemode); + $result->add('password', (int)$json->Passworded); + $result->add('numplayers', $json->CurrentPlayers); + $result->add('maxplayers', $json->MaxPlayers); + + return $result->fetch(); + } +} diff --git a/third_party/gameq_v3.1/GameQ/Protocols/Gtar.php b/third_party/gameq_v3.1/GameQ/Protocols/Gtar.php new file mode 100644 index 00000000..2121e07c --- /dev/null +++ b/third_party/gameq_v3.1/GameQ/Protocols/Gtar.php @@ -0,0 +1,164 @@ +. + */ +namespace GameQ\Protocols; + +use GameQ\Exception\Protocol as Exception; +use GameQ\Result; +use GameQ\Server; + +/** + * Grand Theft Auto Rage Protocol Class + * https://rage.mp/masterlist/ + * + * Result from this call should be a header + JSON response + * + * @author K700 + * @author Austin Bischoff + */ +class Gtar extends Http +{ + /** + * Packets to send + * + * @var array + */ + protected $packets = [ + self::PACKET_STATUS => "GET /master/ HTTP/1.0\r\nHost: cdn.rage.mp\r\nAccept: */*\r\n\r\n", + ]; + + /** + * Http protocol is SSL + * + * @var string + */ + protected $transport = self::TRANSPORT_SSL; + + /** + * The protocol being used + * + * @var string + */ + protected $protocol = 'gtar'; + + /** + * String name of this protocol class + * + * @var string + */ + protected $name = 'gtar'; + + /** + * Longer string name of this protocol class + * + * @var string + */ + protected $name_long = "Grand Theft Auto Rage"; + + /** + * Holds the real ip so we can overwrite it back + * + * @var string + */ + protected $realIp = null; + + protected $realPortQuery = null; + + /** + * Normalize some items + * + * @var array + */ + protected $normalize = [ + // General + 'general' => [ + // target => source + 'dedicated' => 'dedicated', + 'hostname' => 'hostname', + 'mod' => 'mod', + 'maxplayers' => 'maxplayers', + 'numplayers' => 'numplayers', + ], + ]; + + public function beforeSend(Server $server) + { + // Loop over the packets and update them + foreach ($this->packets as $packetType => $packet) { + // Fill out the packet with the server info + $this->packets[$packetType] = sprintf($packet, $server->ip . ':' . $server->port_query); + } + + $this->realIp = $server->ip; + $this->realPortQuery = $server->port_query; + + // Override the existing settings + $server->ip = 'cdn.rage.mp'; + $server->port_query = 443; + } + + /** + * Process the response + * + * @return array + * @throws Exception + */ + public function processResponse() + { + // No response, assume offline + if (empty($this->packets_response)) { + return [ + 'gq_address' => $this->realIp, + 'gq_port_query' => $this->realPortQuery, + ]; + } + + // Implode and rip out the JSON + preg_match('/\{(.*)\}/ms', implode('', $this->packets_response), $matches); + + // Return should be JSON, let's validate + if (!isset($matches[0]) || ($json = json_decode($matches[0])) === null) { + throw new Exception("JSON response from Gtar protocol is invalid."); + } + + $address = $this->realIp.':'.$this->realPortQuery; + $server = $json->$address; + + if (empty($server)) { + return [ + 'gq_address' => $this->realIp, + 'gq_port_query' => $this->realPortQuery, + ]; + } + + $result = new Result(); + + // Server is always dedicated + $result->add('dedicated', 1); + + $result->add('gq_address', $this->realIp); + $result->add('gq_port_query', $this->realPortQuery); + + // Add server items + $result->add('hostname', $server->name); + $result->add('mod', $server->gamemode); + $result->add('numplayers', $server->players); + $result->add('maxplayers', $server->maxplayers); + + return $result->fetch(); + } +} diff --git a/third_party/gameq_v3.1/GameQ/Protocols/Had2.php b/third_party/gameq_v3.1/GameQ/Protocols/Had2.php new file mode 100644 index 00000000..92134351 --- /dev/null +++ b/third_party/gameq_v3.1/GameQ/Protocols/Had2.php @@ -0,0 +1,75 @@ +. + */ + +namespace GameQ\Protocols; + +/** + * Hidden & Dangerous 2 Protocol Class + * + * @author Wilson Jesus <> + */ +class Had2 extends Gamespy2 +{ + + /** + * String name of this protocol class + * + * @type string + */ + protected $name = 'had2'; + + /** + * Longer string name of this protocol class + * + * @type string + */ + protected $name_long = "Hidden & Dangerous 2"; + + /** + * The difference between the client port and query port + * + * @type int + */ + protected $port_diff = 3; + + /** + * Normalize settings for this protocol + * + * @type array + */ + protected $normalize = [ + // General + 'general' => [ + // target => source + 'dedicated' => 'isdedicated', + 'gametype' => 'gametype', + 'hostname' => 'hostname', + 'mapname' => 'mapname', + 'maxplayers' => 'maxplayers', + 'numplayers' => 'numplayers', + 'password' => 'password', + ], + // Individual + 'player' => [ + 'name' => 'player', + 'score' => 'score', + 'deaths' => 'deaths', + 'ping' => 'ping', + ], + ]; +} diff --git a/third_party/gameq_v3.1/GameQ/Protocols/Halo.php b/third_party/gameq_v3.1/GameQ/Protocols/Halo.php new file mode 100644 index 00000000..f402f94d --- /dev/null +++ b/third_party/gameq_v3.1/GameQ/Protocols/Halo.php @@ -0,0 +1,42 @@ +. + */ + +namespace GameQ\Protocols; + +/** + * Halo: Combat Evolved Protocol Class + * + * @author Wilson Jesus <> + */ +class Halo extends Gamespy2 +{ + + /** + * String name of this protocol class + * + * @type string + */ + protected $name = 'halo'; + + /** + * Longer string name of this protocol class + * + * @type string + */ + protected $name_long = "Halo: Combat Evolved"; +} diff --git a/third_party/gameq_v3.1/GameQ/Protocols/Hl1.php b/third_party/gameq_v3.1/GameQ/Protocols/Hl1.php new file mode 100644 index 00000000..e17667b6 --- /dev/null +++ b/third_party/gameq_v3.1/GameQ/Protocols/Hl1.php @@ -0,0 +1,43 @@ +. + */ + +namespace GameQ\Protocols; + +/** + * Class Hl1 + * + * @package GameQ\Protocols + * @author Austin Bischoff + * @author Jesse Lukas + */ +class Hl1 extends Source +{ + /** + * String name of this protocol class + * + * @type string + */ + protected $name = 'hl1'; + + /** + * Longer string name of this protocol class + * + * @type string + */ + protected $name_long = "Half Life"; +} diff --git a/third_party/gameq_v3.1/GameQ/Protocols/Hl2dm.php b/third_party/gameq_v3.1/GameQ/Protocols/Hl2dm.php new file mode 100644 index 00000000..15f881aa --- /dev/null +++ b/third_party/gameq_v3.1/GameQ/Protocols/Hl2dm.php @@ -0,0 +1,42 @@ +. + */ + +namespace GameQ\Protocols; + +/** + * Class Hl2dm + * + * @package GameQ\Protocols + * @author Austin Bischoff + */ +class Hl2dm extends Source +{ + /** + * String name of this protocol class + * + * @type string + */ + protected $name = 'hl2dm'; + + /** + * Longer string name of this protocol class + * + * @type string + */ + protected $name_long = "Half Life 2: Deathmatch"; +} diff --git a/third_party/gameq_v3.1/GameQ/Protocols/Hll.php b/third_party/gameq_v3.1/GameQ/Protocols/Hll.php new file mode 100644 index 00000000..bf0b00c1 --- /dev/null +++ b/third_party/gameq_v3.1/GameQ/Protocols/Hll.php @@ -0,0 +1,68 @@ +. + */ + +namespace GameQ\Protocols; + +/** + * Class Hll + * + * @package GameQ\Protocols + * @author Wilson Jesus <> + */ +class Hll extends Source +{ + /** + * String name of this protocol class + * + * @type string + */ + protected $name = 'hll'; + + /** + * Longer string name of this protocol class + * + * @type string + */ + protected $name_long = "Hell Let Loose"; + + /** + * query_port = client_port + 15 + * 64015 = 64000 + 15 + * + * @type int + */ + protected $port_diff = 15; + + /** + * Normalize settings for this protocol + * + * @type array + */ + /*protected $normalize = [ + 'general' => [ + // target => source + 'dedicated' => 'dedicated', + 'gametype' => 'gametype', + 'servername' => 'hostname', + 'mapname' => 'mapname', + 'maxplayers' => 'maxplayers', + 'numplayers' => 'numplayers', + 'password' => 'password', + ], + ];*/ +} diff --git a/third_party/gameq_v3.1/GameQ/Protocols/Http.php b/third_party/gameq_v3.1/GameQ/Protocols/Http.php new file mode 100644 index 00000000..2a86d8d1 --- /dev/null +++ b/third_party/gameq_v3.1/GameQ/Protocols/Http.php @@ -0,0 +1,67 @@ +. + */ + +namespace GameQ\Protocols; + +use GameQ\Protocol; + +/** + * Class Http + * + * Generic HTTP protocol class. Useful for making http based requests + * + * @package GameQ\Protocols + * @author Austin Bischoff + */ +abstract class Http extends Protocol +{ + /** + * The query protocol used to make the call + * + * @type string + */ + protected $protocol = 'http'; + + /** + * String name of this protocol class + * + * @type string + */ + protected $name = 'http'; + + /** + * Longer string name of this protocol class + * + * @type string + */ + protected $name_long = "Generic HTTP protocol"; + + /** + * Http protocol is TCP + * + * @var string + */ + protected $transport = self::TRANSPORT_TCP; + + /** + * The client join link + * + * @type string + */ + protected $join_link = null; +} diff --git a/third_party/gameq_v3.1/GameQ/Protocols/Hurtworld.php b/third_party/gameq_v3.1/GameQ/Protocols/Hurtworld.php new file mode 100644 index 00000000..fa54654a --- /dev/null +++ b/third_party/gameq_v3.1/GameQ/Protocols/Hurtworld.php @@ -0,0 +1,42 @@ +. + */ + +namespace GameQ\Protocols; + +/** + * Class Hurtworld + * + * @package GameQ\Protocols + * @author Nikolay Ipanyuk + * @author Austin Bischoff + */ +class Hurtworld extends Source +{ + /** + * String name of this protocol class + * + * @type string + */ + protected $name = 'hurtworld'; + /** + * Longer string name of this protocol class + * + * @type string + */ + protected $name_long = "Hurtworld"; +} diff --git a/third_party/gameq_v3.1/GameQ/Protocols/Insurgency.php b/third_party/gameq_v3.1/GameQ/Protocols/Insurgency.php new file mode 100644 index 00000000..77b8329e --- /dev/null +++ b/third_party/gameq_v3.1/GameQ/Protocols/Insurgency.php @@ -0,0 +1,42 @@ +. + */ + +namespace GameQ\Protocols; + +/** + * Class Insurgency + * + * @package GameQ\Protocols + * @author Austin Bischoff + */ +class Insurgency extends Source +{ + /** + * String name of this protocol class + * + * @type string + */ + protected $name = 'insurgency'; + + /** + * Longer string name of this protocol class + * + * @type string + */ + protected $name_long = "Insurgency"; +} diff --git a/third_party/gameq_v3.1/GameQ/Protocols/Insurgencysand.php b/third_party/gameq_v3.1/GameQ/Protocols/Insurgencysand.php new file mode 100644 index 00000000..407d6e26 --- /dev/null +++ b/third_party/gameq_v3.1/GameQ/Protocols/Insurgencysand.php @@ -0,0 +1,49 @@ +. + */ + +namespace GameQ\Protocols; + +/** + * Insurgency Sandstorm Class + * + * @package GameQ\Protocols + * @author Austin Bischoff + */ +class Insurgencysand extends Source +{ + /** + * String name of this protocol class + * + * @type string + */ + protected $name = 'insurgencysand'; + + /** + * Longer string name of this protocol class + * + * @type string + */ + protected $name_long = "Insurgency: Sandstorm"; + + /** + * query_port = client_port + 29 + * + * @type int + */ + protected $port_diff = 29; +} diff --git a/third_party/gameq_v3.1/GameQ/Protocols/Jediacademy.php b/third_party/gameq_v3.1/GameQ/Protocols/Jediacademy.php new file mode 100644 index 00000000..a051a3a9 --- /dev/null +++ b/third_party/gameq_v3.1/GameQ/Protocols/Jediacademy.php @@ -0,0 +1,42 @@ +. + */ + +namespace GameQ\Protocols; + +/** + * Jedi Academy Protocol Class + * + * @package GameQ\Protocols + * @author Austin Bischoff + */ +class Jediacademy extends Quake3 +{ + /** + * String name of this protocol class + * + * @type string + */ + protected $name = 'jediacademy'; + + /** + * Longer string name of this protocol class + * + * @type string + */ + protected $name_long = "Star Wars Jedi Knight: Jedi Academy"; +} diff --git a/third_party/gameq_v3.1/GameQ/Protocols/Jedioutcast.php b/third_party/gameq_v3.1/GameQ/Protocols/Jedioutcast.php new file mode 100644 index 00000000..1afd9afe --- /dev/null +++ b/third_party/gameq_v3.1/GameQ/Protocols/Jedioutcast.php @@ -0,0 +1,42 @@ +. + */ + +namespace GameQ\Protocols; + +/** + * Jedi Outcast Protocol Class + * + * @package GameQ\Protocols + * @author Austin Bischoff + */ +class Jedioutcast extends Quake3 +{ + /** + * String name of this protocol class + * + * @type string + */ + protected $name = 'jedioutcast'; + + /** + * Longer string name of this protocol class + * + * @type string + */ + protected $name_long = "Star Wars Jedi Knight II: Jedi Outcast"; +} diff --git a/third_party/gameq_v3.1/GameQ/Protocols/Justcause2.php b/third_party/gameq_v3.1/GameQ/Protocols/Justcause2.php new file mode 100644 index 00000000..648cb6d5 --- /dev/null +++ b/third_party/gameq_v3.1/GameQ/Protocols/Justcause2.php @@ -0,0 +1,127 @@ +. + */ + +namespace GameQ\Protocols; + +use GameQ\Buffer; +use GameQ\Result; + +/** + * Just Cause 2 Multiplayer Protocol Class + * + * Special thanks to Woet for some insight on packing + * + * @package GameQ\Protocols + * @author Austin Bischoff + */ +class Justcause2 extends Gamespy4 +{ + /** + * String name of this protocol class + * + * @type string + */ + protected $name = 'justcause2'; + + /** + * Longer string name of this protocol class + * + * @type string + */ + protected $name_long = "Just Cause 2 Multiplayer"; + + /** + * The client join link + * + * @type string + */ + protected $join_link = "steam://connect/%s:%d/"; + + /** + * Change the packets used + * + * @var array + */ + protected $packets = [ + self::PACKET_CHALLENGE => "\xFE\xFD\x09\x10\x20\x30\x40", + self::PACKET_ALL => "\xFE\xFD\x00\x10\x20\x30\x40%s\xFF\xFF\xFF\x02", + ]; + + /** + * Override the packet split + * + * @var string + */ + protected $packetSplit = "/\\x00\\x00\\x00/m"; + + /** + * Normalize settings for this protocol + * + * @type array + */ + protected $normalize = [ + 'general' => [ + // target => source + 'dedicated' => 'dedicated', + 'gametype' => 'gametype', + 'hostname' => 'hostname', + 'mapname' => 'mapname', + 'maxplayers' => 'maxplayers', + 'numplayers' => 'numplayers', + 'password' => 'password', + ], + // Individual + 'player' => [ + 'name' => 'name', + 'ping' => 'ping', + ], + ]; + + /** + * Overload so we can add in some static data points + * + * @param Buffer $buffer + * @param Result $result + */ + protected function processDetails(Buffer &$buffer, Result &$result) + { + parent::processDetails($buffer, $result); + + // Add in map + $result->add('mapname', 'Panau'); + $result->add('dedicated', 'true'); + } + + /** + * Override the parent, this protocol is returned differently + * + * @param Buffer $buffer + * @param Result $result + * + * @see Gamespy3::processPlayersAndTeams() + */ + protected function processPlayersAndTeams(Buffer &$buffer, Result &$result) + { + // Loop until we run out of data + while ($buffer->getLength()) { + $result->addPlayer('name', $buffer->readString()); + $result->addPlayer('steamid', $buffer->readString()); + $result->addPlayer('ping', $buffer->readInt16()); + } + } +} diff --git a/third_party/gameq_v3.1/GameQ/Protocols/Justcause3.php b/third_party/gameq_v3.1/GameQ/Protocols/Justcause3.php new file mode 100644 index 00000000..c4e901d9 --- /dev/null +++ b/third_party/gameq_v3.1/GameQ/Protocols/Justcause3.php @@ -0,0 +1,50 @@ +. + */ + +namespace GameQ\Protocols; + +/** + * Class Just Cause 3 + * + * @package GameQ\Protocols + * @author Austin Bischoff + */ +class Justcause3 extends Source +{ + + /** + * String name of this protocol class + * + * @type string + */ + protected $name = 'justcause3'; + + /** + * Longer string name of this protocol class + * + * @type string + */ + protected $name_long = "Just Cause 3"; + + /** + * Query port = client_port + 1 + * + * @type int + */ + protected $port_diff = 1; +} diff --git a/third_party/gameq_v3.1/GameQ/Protocols/Killingfloor.php b/third_party/gameq_v3.1/GameQ/Protocols/Killingfloor.php new file mode 100644 index 00000000..9cc19643 --- /dev/null +++ b/third_party/gameq_v3.1/GameQ/Protocols/Killingfloor.php @@ -0,0 +1,96 @@ +. + */ + +namespace GameQ\Protocols; + +use GameQ\Buffer; +use GameQ\Result; + +/** + * Class Killing floor + * + * @package GameQ\Protocols + * @author Austin Bischoff + */ +class Killingfloor extends Unreal2 +{ + + /** + * String name of this protocol class + * + * @type string + */ + protected $name = 'killing floor'; + + /** + * Longer string name of this protocol class + * + * @type string + */ + protected $name_long = "Killing Floor"; + + /** + * query_port = client_port + 1 + * + * @type int + */ + protected $port_diff = 1; + + /** + * The client join link + * + * @type string + */ + protected $join_link = "steam://connect/%s:%d/"; + + /** + * Overload the default detail process since this version is different + * + * @param \GameQ\Buffer $buffer + * + * @return array + */ + protected function processDetails(Buffer $buffer) + { + + // Set the result to a new result instance + $result = new Result(); + + $result->add('serverid', $buffer->readInt32()); // 0 + $result->add('serverip', $buffer->readPascalString(1)); // empty + $result->add('gameport', $buffer->readInt32()); + $result->add('queryport', $buffer->readInt32()); // 0 + + // We burn the first char since it is not always correct with the hostname + $buffer->skip(1); + + // Read as a regular string since the length is incorrect (what we skipped earlier) + $result->add('servername', utf8_encode($buffer->readString())); + + // The rest is read as normal + $result->add('mapname', utf8_encode($buffer->readPascalString(1))); + $result->add('gametype', $buffer->readPascalString(1)); + $result->add('numplayers', $buffer->readInt32()); + $result->add('maxplayers', $buffer->readInt32()); + $result->add('currentwave', $buffer->readInt32()); + + unset($buffer); + + return $result->fetch(); + } +} diff --git a/third_party/gameq_v3.1/GameQ/Protocols/Killingfloor2.php b/third_party/gameq_v3.1/GameQ/Protocols/Killingfloor2.php new file mode 100644 index 00000000..a134f258 --- /dev/null +++ b/third_party/gameq_v3.1/GameQ/Protocols/Killingfloor2.php @@ -0,0 +1,51 @@ +. + */ + +namespace GameQ\Protocols; + +/** + * Class Killing floor + * + * @package GameQ\Protocols + * @author Austin Bischoff + */ +class Killingfloor2 extends Source +{ + + /** + * String name of this protocol class + * + * @type string + */ + protected $name = 'killing floor 2'; + + /** + * Longer string name of this protocol class + * + * @type string + */ + protected $name_long = "Killing Floor 2"; + + /** + * query_port = client_port + 19238 + * 27015 = 7777 + 19238 + * + * @type int + */ + protected $port_diff = 19238; +} diff --git a/third_party/gameq_v3.1/GameQ/Protocols/Kingpin.php b/third_party/gameq_v3.1/GameQ/Protocols/Kingpin.php new file mode 100644 index 00000000..87007d91 --- /dev/null +++ b/third_party/gameq_v3.1/GameQ/Protocols/Kingpin.php @@ -0,0 +1,43 @@ +. + */ + +namespace GameQ\Protocols; + +/** + * Kingpin: Life of Crime Protocol Class + * + * @package GameQ\Protocols + * + * @author Wilson Jesus <> + */ +class Kingpin extends Quake2 +{ + /** + * String name of this protocol class + * + * @type string + */ + protected $name = 'kingpin'; + + /** + * Longer string name of this protocol class + * + * @type string + */ + protected $name_long = "Kingpin: Life of Crime"; +} diff --git a/third_party/gameq_v3.1/GameQ/Protocols/L4d.php b/third_party/gameq_v3.1/GameQ/Protocols/L4d.php new file mode 100644 index 00000000..596452a7 --- /dev/null +++ b/third_party/gameq_v3.1/GameQ/Protocols/L4d.php @@ -0,0 +1,42 @@ +. + */ + +namespace GameQ\Protocols; + +/** + * Class L4d + * + * @package GameQ\Protocols + * @author Austin Bischoff + */ +class L4d extends Source +{ + /** + * String name of this protocol class + * + * @type string + */ + protected $name = 'l4d'; + + /** + * Longer string name of this protocol class + * + * @type string + */ + protected $name_long = "Left 4 Dead"; +} diff --git a/third_party/gameq_v3.1/GameQ/Protocols/L4d2.php b/third_party/gameq_v3.1/GameQ/Protocols/L4d2.php new file mode 100644 index 00000000..475514c9 --- /dev/null +++ b/third_party/gameq_v3.1/GameQ/Protocols/L4d2.php @@ -0,0 +1,42 @@ +. + */ + +namespace GameQ\Protocols; + +/** + * Class L4d2 + * + * @package GameQ\Protocols + * @author Austin Bischoff + */ +class L4d2 extends Source +{ + /** + * String name of this protocol class + * + * @type string + */ + protected $name = 'l4d2'; + + /** + * Longer string name of this protocol class + * + * @type string + */ + protected $name_long = "Left 4 Dead 2"; +} diff --git a/third_party/gameq_v3.1/GameQ/Protocols/Lhmp.php b/third_party/gameq_v3.1/GameQ/Protocols/Lhmp.php new file mode 100644 index 00000000..3d5e81f3 --- /dev/null +++ b/third_party/gameq_v3.1/GameQ/Protocols/Lhmp.php @@ -0,0 +1,214 @@ +. + */ + +namespace GameQ\Protocols; + +use GameQ\Protocol; +use GameQ\Buffer; +use GameQ\Result; +use GameQ\Exception\Protocol as Exception; + +/** + * Lost Heaven Protocol class + * + * Reference: http://lh-mp.eu/wiki/index.php/Query_System + * + * @author Austin Bischoff + */ +class Lhmp extends Protocol +{ + + /** + * Array of packets we want to look up. + * Each key should correspond to a defined method in this or a parent class + * + * @type array + */ + protected $packets = [ + self::PACKET_DETAILS => "LHMPo", + self::PACKET_PLAYERS => "LHMPp", + ]; + + /** + * Use the response flag to figure out what method to run + * + * @type array + */ + protected $responses = [ + "LHMPo" => "processDetails", + "LHMPp" => "processPlayers", + ]; + + /** + * The query protocol used to make the call + * + * @type string + */ + protected $protocol = 'lhmp'; + + /** + * String name of this protocol class + * + * @type string + */ + protected $name = 'lhmp'; + + /** + * Longer string name of this protocol class + * + * @type string + */ + protected $name_long = "Lost Heaven"; + + /** + * query_port = client_port + 1 + * + * @type int + */ + protected $port_diff = 1; + + /** + * Normalize settings for this protocol + * + * @type array + */ + protected $normalize = [ + // General + 'general' => [ + // target => source + 'gametype' => 'gamemode', + 'hostname' => 'servername', + 'mapname' => 'mapname', + 'maxplayers' => 'maxplayers', + 'numplayers' => 'numplayers', + 'password' => 'password', + ], + // Individual + 'player' => [ + 'name' => 'name', + ], + ]; + + /** + * Process the response + * + * @return array + * @throws \GameQ\Exception\Protocol + */ + public function processResponse() + { + // Will hold the packets after sorting + $packets = []; + + // We need to pre-sort these for split packets so we can do extra work where needed + foreach ($this->packets_response as $response) { + $buffer = new Buffer($response); + + // Pull out the header + $header = $buffer->read(5); + + // Add the packet to the proper section, we will combine later + $packets[$header][] = $buffer->getBuffer(); + } + + unset($buffer); + + $results = []; + + // Now let's iterate and process + foreach ($packets as $header => $packetGroup) { + // Figure out which packet response this is + if (!array_key_exists($header, $this->responses)) { + throw new Exception(__METHOD__ . " response type '{$header}' is not valid"); + } + + // Now we need to call the proper method + $results = array_merge( + $results, + call_user_func_array([$this, $this->responses[$header]], [new Buffer(implode($packetGroup))]) + ); + } + + unset($packets); + + return $results; + } + + /* + * Internal methods + */ + + /** + * Handles processing the details data into a usable format + * + * @param Buffer $buffer + * + * @return array + * @throws Exception + */ + protected function processDetails(Buffer $buffer) + { + + // Set the result to a new result instance + $result = new Result(); + + $result->add('protocol', $buffer->readString()); + $result->add('password', $buffer->readString()); + $result->add('numplayers', $buffer->readInt16()); + $result->add('maxplayers', $buffer->readInt16()); + $result->add('servername', utf8_encode($buffer->readPascalString())); + $result->add('gamemode', $buffer->readPascalString()); + $result->add('website', utf8_encode($buffer->readPascalString())); + $result->add('mapname', utf8_encode($buffer->readPascalString())); + + unset($buffer); + + return $result->fetch(); + } + + /** + * Handles processing the player data into a usable format + * + * @param Buffer $buffer + * + * @return array + */ + protected function processPlayers(Buffer $buffer) + { + + // Set the result to a new result instance + $result = new Result(); + + // Get the number of players + $result->add('numplayers', $buffer->readInt16()); + + // Parse players + while ($buffer->getLength()) { + // Player id + if (($id = $buffer->readInt16()) !== 0) { + // Add the results + $result->addPlayer('id', $id); + $result->addPlayer('name', utf8_encode($buffer->readPascalString())); + } + } + + unset($buffer, $id); + + return $result->fetch(); + } +} diff --git a/third_party/gameq_v3.1/GameQ/Protocols/M2mp.php b/third_party/gameq_v3.1/GameQ/Protocols/M2mp.php new file mode 100644 index 00000000..a6076e3a --- /dev/null +++ b/third_party/gameq_v3.1/GameQ/Protocols/M2mp.php @@ -0,0 +1,219 @@ +. + */ + +namespace GameQ\Protocols; + +use GameQ\Protocol; +use GameQ\Buffer; +use GameQ\Result; +use GameQ\Exception\Protocol as Exception; + +/** + * Mafia 2 Multiplayer Protocol Class + * + * Loosely based on SAMP protocol + * + * Query port = server port + 1 + * + * Handles processing Mafia 2 Multiplayer servers + * + * @package GameQ\Protocols + * @author Wilson Jesus <> + */ +class M2mp extends Protocol +{ + /** + * Array of packets we want to look up. + * Each key should correspond to a defined method in this or a parent class + * + * @type array + */ + protected $packets = [ + self::PACKET_ALL => "M2MP", + ]; + + /** + * Use the response flag to figure out what method to run + * + * @type array + */ + protected $responses = [ + "M2MP" => 'processStatus', + ]; + + /** + * The query protocol used to make the call + * + * @type string + */ + protected $protocol = 'm2mp'; + + /** + * String name of this protocol class + * + * @type string + */ + protected $name = 'm2mp'; + + /** + * Longer string name of this protocol class + * + * @type string + */ + protected $name_long = "Mafia 2 Multiplayer"; + + /** + * The client join link + * + * @type string + */ + protected $join_link = null; + + /** + * The difference between the client port and query port + * + * @type int + */ + protected $port_diff = 1; + + /** + * Normalize settings for this protocol + * + * @type array + */ + protected $normalize = [ + // General + 'general' => [ + // target => source + 'hostname' => 'servername', + 'gametype' => 'gamemode', + 'maxplayers' => 'max_players', + 'numplayers' => 'num_players', + 'password' => 'password', + ], + // Individual + 'player' => [ + 'name' => 'name', + ], + ]; + + /** + * Handle response from the server + * + * @return mixed + * @throws Exception + */ + public function processResponse() + { + // Make a buffer + $buffer = new Buffer(implode('', $this->packets_response)); + + // Grab the header + $header = $buffer->read(4); + + // Header + // Figure out which packet response this is + if ($header != "M2MP") { + throw new Exception(__METHOD__ . " response type '" . bin2hex($header) . "' is not valid"); + } + + return call_user_func_array([$this, $this->responses[$header]], [$buffer]); + } + + /** + * Process the status response + * + * @param Buffer $buffer + * + * @return array + */ + protected function processStatus(Buffer $buffer) + { + // We need to split the data and offload + $results = $this->processServerInfo($buffer); + + $results = array_merge_recursive( + $results, + $this->processPlayers($buffer) + ); + + unset($buffer); + + // Return results + return $results; + } + + /** + * Handle processing the server information + * + * @param Buffer $buffer + * + * @return array + */ + protected function processServerInfo(Buffer $buffer) + { + // Set the result to a new result instance + $result = new Result(); + + // Always dedicated + $result->add('dedicated', 1); + + // Pull out the server information + // Note the length information is incorrect, we correct using offset options in pascal method + $result->add('servername', $buffer->readPascalString(1, true)); + $result->add('num_players', $buffer->readPascalString(1, true)); + $result->add('max_players', $buffer->readPascalString(1, true)); + $result->add('gamemode', $buffer->readPascalString(1, true)); + $result->add('password', (bool) $buffer->readInt8()); + + unset($buffer); + + return $result->fetch(); + } + + /** + * Handle processing of player data + * + * @param Buffer $buffer + * + * @return array + */ + protected function processPlayers(Buffer $buffer) + { + // Set the result to a new result instance + $result = new Result(); + + // Parse players + // Read the player info, it's in the same query response for some odd reason. + while ($buffer->getLength()) { + // Check to see if we ran out of info, length bug from response + if ($buffer->getLength() <= 1) { + break; + } + + // Only player name information is available + // Add player name, encoded + $result->addPlayer('name', utf8_encode(trim($buffer->readPascalString(1, true)))); + } + + // Clear + unset($buffer); + + return $result->fetch(); + } +} diff --git a/third_party/gameq_v3.1/GameQ/Protocols/Minecraft.php b/third_party/gameq_v3.1/GameQ/Protocols/Minecraft.php new file mode 100644 index 00000000..a895cb87 --- /dev/null +++ b/third_party/gameq_v3.1/GameQ/Protocols/Minecraft.php @@ -0,0 +1,87 @@ +. + */ + +namespace GameQ\Protocols; + +/** + * Minecraft Protocol Class + * + * Thanks to https://github.com/xPaw/PHP-Minecraft-Query for helping me realize this is + * Gamespy 3 Protocol. Make sure you enable the items below for it to work. + * + * Information from original author: + * Instructions + * + * Before using this class, you need to make sure that your server is running GS4 status listener. + * + * Look for those settings in server.properties: + * + * enable-query=true + * query.port=25565 + * + * @package GameQ\Protocols + * + * @author Austin Bischoff + */ +class Minecraft extends Gamespy3 +{ + + /** + * String name of this protocol class + * + * @type string + */ + protected $name = 'minecraft'; + + /** + * Longer string name of this protocol class + * + * @type string + */ + protected $name_long = "Minecraft"; + + /** + * The client join link + * + * @type string + */ + protected $join_link = "minecraft://%s:%d/"; + + /** + * Normalize settings for this protocol + * + * @type array + */ + protected $normalize = [ + // General + 'general' => [ + // target => source + 'dedicated' => 'dedicated', + 'gametype' => 'game_id', + 'hostname' => 'hostname', + 'mapname' => 'map', + 'maxplayers' => 'maxplayers', + 'numplayers' => 'numplayers', + 'password' => 'password', + ], + // Individual + 'player' => [ + 'name' => 'player', + ], + ]; +} diff --git a/third_party/gameq_v3.1/GameQ/Protocols/Minecraftpe.php b/third_party/gameq_v3.1/GameQ/Protocols/Minecraftpe.php new file mode 100644 index 00000000..21d11868 --- /dev/null +++ b/third_party/gameq_v3.1/GameQ/Protocols/Minecraftpe.php @@ -0,0 +1,44 @@ +. + */ + +namespace GameQ\Protocols; + +/** + * Minecraft PE (BE) Protocol Class + * + * @package GameQ\Protocols + * + * @author Austin Bischoff + */ +class Minecraftpe extends Minecraft +{ + + /** + * String name of this protocol class + * + * @type string + */ + protected $name = 'minecraftpe'; + + /** + * Longer string name of this protocol class + * + * @type string + */ + protected $name_long = "MinecraftPE"; +} diff --git a/third_party/gameq_v3.1/GameQ/Protocols/Miscreated.php b/third_party/gameq_v3.1/GameQ/Protocols/Miscreated.php new file mode 100644 index 00000000..d59fed12 --- /dev/null +++ b/third_party/gameq_v3.1/GameQ/Protocols/Miscreated.php @@ -0,0 +1,68 @@ +. + */ + +namespace GameQ\Protocols; + +/** + * Class Miscreated + * + * @package GameQ\Protocols + * @author Wilson Jesus <> + */ +class Miscreated extends Source +{ + /** + * String name of this protocol class + * + * @type string + */ + protected $name = 'miscreated'; + + /** + * Longer string name of this protocol class + * + * @type string + */ + protected $name_long = "Miscreated"; + + /** + * query_port = client_port + 2 + * 64092 = 64090 + 2 + * + * @type int + */ + protected $port_diff = 2; + + /** + * Normalize settings for this protocol + * + * @type array + */ + protected $normalize = [ + 'general' => [ + // target => source + 'dedicated' => 'dedicated', + 'gametype' => 'gametype', + 'servername' => 'hostname', + 'mapname' => 'mapname', + 'maxplayers' => 'maxplayers', + 'numplayers' => 'numplayers', + 'password' => 'password', + ], + ]; +} diff --git a/third_party/gameq_v3.1/GameQ/Protocols/Modiverse.php b/third_party/gameq_v3.1/GameQ/Protocols/Modiverse.php new file mode 100644 index 00000000..64b41ed5 --- /dev/null +++ b/third_party/gameq_v3.1/GameQ/Protocols/Modiverse.php @@ -0,0 +1,43 @@ +. + */ + +namespace GameQ\Protocols; + +/** + * Class Modiverse + * + * @package GameQ\Protocols + * @author Austin Bischoff + * @author Jesse Lukas + */ +class Modiverse extends Source +{ + /** + * String name of this protocol class + * + * @type string + */ + protected $name = 'modiverse'; + + /** + * Longer string name of this protocol class + * + * @type string + */ + protected $name_long = "Modiverse"; +} diff --git a/third_party/gameq_v3.1/GameQ/Protocols/Mohaa.php b/third_party/gameq_v3.1/GameQ/Protocols/Mohaa.php new file mode 100644 index 00000000..66ddd7e7 --- /dev/null +++ b/third_party/gameq_v3.1/GameQ/Protocols/Mohaa.php @@ -0,0 +1,79 @@ +. + */ + +namespace GameQ\Protocols; + +/** + * Medal of honor: Allied Assault Protocol Class + * + * @package GameQ\Protocols + * @author Bram + * @author Austin Bischoff + */ +class Mohaa extends Gamespy +{ + /** + * String name of this protocol class + * + * @type string + */ + protected $name = 'mohaa'; + + /** + * Longer string name of this protocol class + * + * @type string + */ + protected $name_long = "Medal of honor: Allied Assault"; + + /** + * Normalize settings for this protocol + * + * @type array + */ + protected $normalize = [ + 'general' => [ + // target => source + 'dedicated' => 'dedicated', + 'gametype' => 'gametype', + 'hostname' => 'hostname', + 'mapname' => 'mapname', + 'maxplayers' => 'maxplayers', + 'numplayers' => 'numplayers', + 'password' => 'password', + ], + // Individual + 'player' => [ + 'name' => 'player', + 'score' => 'frags', + 'ping' => 'ping', + ], + ]; + + /** + * Query port is always the client port + 97 in MOHAA + * + * @param int $clientPort + * + * @return int + */ + public function findQueryPort($clientPort) + { + return $clientPort + 97; + } +} diff --git a/third_party/gameq_v3.1/GameQ/Protocols/Mordhau.php b/third_party/gameq_v3.1/GameQ/Protocols/Mordhau.php new file mode 100644 index 00000000..fa305ce1 --- /dev/null +++ b/third_party/gameq_v3.1/GameQ/Protocols/Mordhau.php @@ -0,0 +1,53 @@ +. + */ + +namespace GameQ\Protocols; + +/** + * Class MORDHAU + * + * @package GameQ\Protocols + * @author Wilson Jesus <> + */ +class Mordhau extends Source +{ + + /** + * String name of this protocol class + * + * @type string + */ + protected $name = 'mordhau'; + + /** + * Longer string name of this protocol class + * + * @type string + */ + protected $name_long = "MORDHAU"; + + #protected $port = 7777; + + /** + * query_port = client_port + 19238 + * 27015 = 7777 + 19238 + * + * @type int + */ + #protected $port_diff = 19238; +} diff --git a/third_party/gameq_v3.1/GameQ/Protocols/Mta.php b/third_party/gameq_v3.1/GameQ/Protocols/Mta.php new file mode 100644 index 00000000..b95dc4c8 --- /dev/null +++ b/third_party/gameq_v3.1/GameQ/Protocols/Mta.php @@ -0,0 +1,59 @@ +. + */ + +namespace GameQ\Protocols; + +/** + * Class Multi Theft Auto + * + * @package GameQ\Protocols + * + * @author Marcel Bößendörfer + * @author Austin Bischoff + */ +class Mta extends Ase +{ + + /** + * String name of this protocol class + * + * @type string + */ + protected $name = 'mta'; + + /** + * Longer string name of this protocol class + * + * @type string + */ + protected $name_long = "Multi Theft Auto"; + + /** + * query_port = client_port + 123 + * + * @type int + */ + protected $port_diff = 123; + + /** + * The client join link + * + * @type string + */ + protected $join_link = "mtasa://%s:%d/"; +} diff --git a/third_party/gameq_v3.1/GameQ/Protocols/Mumble.php b/third_party/gameq_v3.1/GameQ/Protocols/Mumble.php new file mode 100644 index 00000000..299389cf --- /dev/null +++ b/third_party/gameq_v3.1/GameQ/Protocols/Mumble.php @@ -0,0 +1,194 @@ +. + */ + +namespace GameQ\Protocols; + +use GameQ\Protocol; +use GameQ\Result; +use GameQ\Exception\Protocol as Exception; + +/** + * Mumble Protocol class + * + * References: + * https://github.com/edmundask/MurmurQuery - Thanks to skylord123 + * + * @author Austin Bischoff + */ +class Mumble extends Protocol +{ + + /** + * Array of packets we want to look up. + * Each key should correspond to a defined method in this or a parent class + * + * @type array + */ + protected $packets = [ + self::PACKET_ALL => "\x6A\x73\x6F\x6E", // JSON packet + ]; + + /** + * The transport mode for this protocol is TCP + * + * @type string + */ + protected $transport = self::TRANSPORT_TCP; + + /** + * The query protocol used to make the call + * + * @type string + */ + protected $protocol = 'mumble'; + + /** + * String name of this protocol class + * + * @type string + */ + protected $name = 'mumble'; + + /** + * Longer string name of this protocol class + * + * @type string + */ + protected $name_long = "Mumble Server"; + + /** + * The client join link + * + * @type string + */ + protected $join_link = "mumble://%s:%d/"; + + /** + * 27800 = 64738 - 36938 + * + * @type int + */ + protected $port_diff = -36938; + + /** + * Normalize settings for this protocol + * + * @type array + */ + protected $normalize = [ + // General + 'general' => [ + 'dedicated' => 'dedicated', + 'gametype' => 'gametype', + 'hostname' => 'name', + 'numplayers' => 'numplayers', + 'maxplayers' => 'x_gtmurmur_max_users', + ], + // Player + 'player' => [ + 'name' => 'name', + 'ping' => 'tcpPing', + 'team' => 'channel', + 'time' => 'onlinesecs', + ], + // Team + 'team' => [ + 'name' => 'name', + ], + ]; + + /** + * Process the response + * + * @return array + * @throws \GameQ\Exception\Protocol + */ + public function processResponse() + { + + // Try to json_decode, make it into an array + if (($data = json_decode(implode('', $this->packets_response), true)) === null) { + throw new Exception(__METHOD__ . " Unable to decode JSON data."); + } + + // Set the result to a new result instance + $result = new Result(); + + // Always dedicated + $result->add('dedicated', 1); + + // Let's iterate over the response items, there are a lot + foreach ($data as $key => $value) { + // Ignore root for now, that is where all of the channel/player info is housed + if (in_array($key, ['root'])) { + continue; + } + + // Add them as is + $result->add($key, $value); + } + + // Offload the channel and user parsing + $this->processChannelsAndUsers($data['root'], $result); + + unset($data); + + // Manually set the number of players + $result->add('numplayers', count($result->get('players'))); + + return $result->fetch(); + } + + /* + * Internal methods + */ + + /** + * Handles processing the the channels and user info + * + * @param array $data + * @param \GameQ\Result $result + */ + protected function processChannelsAndUsers(array $data, Result &$result) + { + + // Let's add all of the channel information + foreach ($data as $key => $value) { + // We will handle these later + if (in_array($key, ['channels', 'users'])) { + // skip + continue; + } + + // Add the channel property as a team + $result->addTeam($key, $value); + } + + // Itereate over the users in this channel + foreach ($data['users'] as $user) { + foreach ($user as $key => $value) { + $result->addPlayer($key, $value); + } + } + + // Offload more channels to parse + foreach ($data['channels'] as $channel) { + $this->processChannelsAndUsers($channel, $result); + } + } +} diff --git a/third_party/gameq_v3.1/GameQ/Protocols/Nmrih.php b/third_party/gameq_v3.1/GameQ/Protocols/Nmrih.php new file mode 100644 index 00000000..acae3b6e --- /dev/null +++ b/third_party/gameq_v3.1/GameQ/Protocols/Nmrih.php @@ -0,0 +1,43 @@ +. + */ + +namespace GameQ\Protocols; + +/** + * Class No More Room in Hell + * + * @package GameQ\Protocols + * @author Austin Bischoff + * @author Jesse Lukas + */ +class Nmrih extends Source +{ + /** + * No More Room in Hell protocol class + * + * @type string + */ + protected $name = 'nmrih'; + + /** + * Longer string name of this protocol class + * + * @type string + */ + protected $name_long = "No More Room in Hell"; +} diff --git a/third_party/gameq_v3.1/GameQ/Protocols/Ns2.php b/third_party/gameq_v3.1/GameQ/Protocols/Ns2.php new file mode 100644 index 00000000..4c323929 --- /dev/null +++ b/third_party/gameq_v3.1/GameQ/Protocols/Ns2.php @@ -0,0 +1,49 @@ +. + */ + +namespace GameQ\Protocols; + +/** + * Class Ns2 + * + * @package GameQ\Protocols + * @author Austin Bischoff + */ +class Ns2 extends Source +{ + /** + * String name of this protocol class + * + * @type string + */ + protected $name = 'ns2'; + + /** + * Longer string name of this protocol class + * + * @type string + */ + protected $name_long = "Natural Selection 2"; + + /** + * query_port = client_port + 1 + * + * @type int + */ + protected $port_diff = 1; +} diff --git a/third_party/gameq_v3.1/GameQ/Protocols/Of.php b/third_party/gameq_v3.1/GameQ/Protocols/Of.php new file mode 100644 index 00000000..bce7612d --- /dev/null +++ b/third_party/gameq_v3.1/GameQ/Protocols/Of.php @@ -0,0 +1,43 @@ +. + */ + +namespace GameQ\Protocols; + +/** + * Class Open Fortress + * + * @package GameQ\Protocols + * @author Austin Bischoff + * @author Jesse Lukas + */ +class Of extends Source +{ + /** + * Open Fortress protocol class + * + * @type string + */ + protected $name = 'of'; + + /** + * Longer string name of this protocol class + * + * @type string + */ + protected $name_long = "Open Fortress"; +} diff --git a/third_party/gameq_v3.1/GameQ/Protocols/Openttd.php b/third_party/gameq_v3.1/GameQ/Protocols/Openttd.php new file mode 100644 index 00000000..75c44fe1 --- /dev/null +++ b/third_party/gameq_v3.1/GameQ/Protocols/Openttd.php @@ -0,0 +1,183 @@ +. + */ + +namespace GameQ\Protocols; + +use GameQ\Protocol; +use GameQ\Buffer; +use GameQ\Result; +use GameQ\Exception\Protocol as Exception; + +/** + * OpenTTD Protocol Class + * + * Handles processing Open Transport Tycoon Deluxe servers + * + * @package GameQ\Protocols + * @author Wilson Jesus <> + */ +class Openttd extends Protocol +{ + /** + * Array of packets we want to look up. + * Each key should correspond to a defined method in this or a parent class + * + * @type array + */ + protected $packets = [ + self::PACKET_ALL => "\x03\x00\x00", + ]; + + /** + * The query protocol used to make the call + * + * @type string + */ + protected $protocol = 'openttd'; + + /** + * String name of this protocol class + * + * @type string + */ + protected $name = 'openttd'; + + /** + * Longer string name of this protocol class + * + * @type string + */ + protected $name_long = "Open Transport Tycoon Deluxe"; + + /** + * The client join link + * + * @type string + */ + protected $join_link = null; + + /** + * Normalize settings for this protocol + * + * @type array + */ + protected $normalize = [ + // General + 'general' => [ + // target => source + 'hostname' => 'hostname', + 'mapname' => 'map', + 'maxplayers' => 'max_clients', + 'numplayers' => 'clients', + 'password' => 'password', + 'dedicated' => 'dedicated', + ], + ]; + + /** + * Handle response from the server + * + * @return mixed + * @throws Exception + */ + public function processResponse() + { + // Make a buffer + $buffer = new Buffer(implode('', $this->packets_response)); + + // Get the length of the packet + $packetLength = $buffer->getLength(); + + // Grab the header + $length = $buffer->readInt16(); + //$type = $buffer->readInt8(); + $buffer->skip(1); // Skip the "$type" as its not used in the code, and to comply with phpmd it cant be assigned and not used. + + // Header + // Figure out which packet response this is + if ($packetLength != $length) { + throw new Exception(__METHOD__ . " response type '" . bin2hex($length) . "' is not valid"); + } + + return call_user_func_array([$this, 'processServerInfo'], [$buffer]); + } + + /** + * Handle processing the server information + * + * @param Buffer $buffer + * + * @return array + */ + protected function processServerInfo(Buffer $buffer) + { + // Set the result to a new result instance + $result = new Result(); + + $protocol_version = $buffer->readInt8(); + $result->add('protocol_version', $protocol_version); + + switch ($protocol_version) { + case 4: + $num_grfs = $buffer->readInt8(); #number of grfs + $result->add('num_grfs', $num_grfs); + //$buffer->skip ($num_grfs * 20); #skip grfs id and md5 hash + + for ($i=0; $i<$num_grfs; $i++) { + $result->add('grfs_'.$i.'_ID', strtoupper(bin2hex($buffer->read(4)))); + $result->add('grfs_'.$i.'_MD5', strtoupper(bin2hex($buffer->read(16)))); + } + // No break, cascades all the down even if case is meet + case 3: + $result->add('game_date', $buffer->readInt32()); + $result->add('start_date', $buffer->readInt32()); + // Cascades all the way down even if case is meet + case 2: + $result->add('companies_max', $buffer->readInt8()); + $result->add('companies_on', $buffer->readInt8()); + $result->add('spectators_max', $buffer->readInt8()); + // Cascades all the way down even if case is meet + case 1: + $result->add('hostname', $buffer->readString()); + $result->add('version', $buffer->readString()); + + $language = $buffer->readInt8(); + $result->add('language', $language); + $result->add('language_icon', '//media.openttd.org/images/server/'.$language.'_lang.gif'); + + $result->add('password', $buffer->readInt8()); + $result->add('max_clients', $buffer->readInt8()); + $result->add('clients', $buffer->readInt8()); + $result->add('spectators', $buffer->readInt8()); + if ($protocol_version < 3) { + $days = ( 365 * 1920 + 1920 / 4 - 1920 / 100 + 1920 / 400 ); + $result->add('game_date', $buffer->readInt16() + $days); + $result->add('start_date', $buffer->readInt16() + $days); + } + $result->add('map', $buffer->readString()); + $result->add('map_width', $buffer->readInt16()); + $result->add('map_height', $buffer->readInt16()); + $result->add('map_type', $buffer->readInt8()); + $result->add('dedicated', $buffer->readInt8()); + // Cascades all the way down even if case is meet + } + unset($buffer); + + return $result->fetch(); + } +} diff --git a/third_party/gameq_v3.1/GameQ/Protocols/Pixark.php b/third_party/gameq_v3.1/GameQ/Protocols/Pixark.php new file mode 100644 index 00000000..2e67af04 --- /dev/null +++ b/third_party/gameq_v3.1/GameQ/Protocols/Pixark.php @@ -0,0 +1,43 @@ +. + */ + +namespace GameQ\Protocols; + +/** + * Class PixARK + * + * @package GameQ\Protocols + * @author Austin Bischoff + */ +class Pixark extends Arkse +{ + + /** + * String name of this protocol class + * + * @type string + */ + protected $name = 'pixark'; + + /** + * Longer string name of this protocol class + * + * @type string + */ + protected $name_long = "PixARK"; +} diff --git a/third_party/gameq_v3.1/GameQ/Protocols/Postscriptum.php b/third_party/gameq_v3.1/GameQ/Protocols/Postscriptum.php new file mode 100644 index 00000000..555ba7d1 --- /dev/null +++ b/third_party/gameq_v3.1/GameQ/Protocols/Postscriptum.php @@ -0,0 +1,50 @@ +. + */ + +namespace GameQ\Protocols; + +/** + * Class Postscriptum + * + * @package GameQ\Protocols + * @author Austin Bischoff + */ +class Postscriptum extends Source +{ + /** + * String name of this protocol class + * + * @type string + */ + protected $name = 'postscriptum'; + + /** + * Longer string name of this protocol class + * + * @type string + */ + protected $name_long = "Post Scriptum"; + + /** + * query_port = client_port + 10 + * 64092 = 64090 + 10 + * + * @type int + */ + protected $port_diff = 10; +} diff --git a/third_party/gameq_v3.1/GameQ/Protocols/Projectrealitybf2.php b/third_party/gameq_v3.1/GameQ/Protocols/Projectrealitybf2.php new file mode 100644 index 00000000..6f4b5ce0 --- /dev/null +++ b/third_party/gameq_v3.1/GameQ/Protocols/Projectrealitybf2.php @@ -0,0 +1,45 @@ +. + */ + +namespace GameQ\Protocols; + +/** + * Class Projectrealitybf2 + * + * Based off of BF2 + * + * @package GameQ\Protocols + * @author Austin Bischoff + */ +class Projectrealitybf2 extends Bf2 +{ + + /** + * String name of this protocol class + * + * @type string + */ + protected $name = 'projectrealitybf2'; + + /** + * Longer string name of this protocol class + * + * @type string + */ + protected $name_long = "Project Reality: Battlefield 2"; +} diff --git a/third_party/gameq_v3.1/GameQ/Protocols/Quake2.php b/third_party/gameq_v3.1/GameQ/Protocols/Quake2.php new file mode 100644 index 00000000..f0366c2c --- /dev/null +++ b/third_party/gameq_v3.1/GameQ/Protocols/Quake2.php @@ -0,0 +1,219 @@ + "\xFF\xFF\xFF\xFFstatus\x00", + ]; + + /** + * Use the response flag to figure out what method to run + * + * @type array + */ + protected $responses = [ + "\xFF\xFF\xFF\xFF\x70\x72\x69\x6e\x74" => 'processStatus', + ]; + + /** + * The query protocol used to make the call + * + * @type string + */ + protected $protocol = 'quake2'; + + /** + * String name of this protocol class + * + * @type string + */ + protected $name = 'quake2'; + + /** + * Longer string name of this protocol class + * + * @type string + */ + protected $name_long = "Quake 2 Server"; + + /** + * The client join link + * + * @type string + */ + protected $join_link = null; + + /** + * Normalize settings for this protocol + * + * @type array + */ + protected $normalize = [ + // General + 'general' => [ + // target => source + 'gametype' => 'gamename', + 'hostname' => 'hostname', + 'mapname' => 'mapname', + 'maxplayers' => 'maxclients', + 'mod' => 'g_gametype', + 'numplayers' => 'clients', + 'password' => 'password', + ], + // Individual + 'player' => [ + 'name' => 'name', + 'ping' => 'ping', + 'score' => 'frags', + ], + ]; + + /** + * Handle response from the server + * + * @return mixed + * @throws Exception + */ + public function processResponse() + { + // Make a buffer + $buffer = new Buffer(implode('', $this->packets_response)); + + // Grab the header + $header = $buffer->readString("\x0A"); + + // Figure out which packet response this is + if (empty($header) || !array_key_exists($header, $this->responses)) { + throw new Exception(__METHOD__ . " response type '" . bin2hex($header) . "' is not valid"); + } + + return call_user_func_array([$this, $this->responses[$header]], [$buffer]); + } + + /** + * Process the status response + * + * @param Buffer $buffer + * + * @return array + */ + protected function processStatus(Buffer $buffer) + { + // We need to split the data and offload + $results = $this->processServerInfo(new Buffer($buffer->readString("\x0A"))); + + $results = array_merge_recursive( + $results, + $this->processPlayers(new Buffer($buffer->getBuffer())) + ); + + unset($buffer); + + // Return results + return $results; + } + + /** + * Handle processing the server information + * + * @param Buffer $buffer + * + * @return array + */ + protected function processServerInfo(Buffer $buffer) + { + // Set the result to a new result instance + $result = new Result(); + + // Burn leading \ if one exists + $buffer->readString('\\'); + + // Key / value pairs + while ($buffer->getLength()) { + // Add result + $result->add( + trim($buffer->readString('\\')), + utf8_encode(trim($buffer->readStringMulti(['\\', "\x0a"]))) + ); + } + + $result->add('password', 0); + $result->add('mod', 0); + + unset($buffer); + + return $result->fetch(); + } + + /** + * Handle processing of player data + * + * @param Buffer $buffer + * + * @return array + */ + protected function processPlayers(Buffer $buffer) + { + // Some games do not have a number of current players + $playerCount = 0; + + // Set the result to a new result instance + $result = new Result(); + + // Loop until we are out of data + while ($buffer->getLength()) { + // Make a new buffer with this block + $playerInfo = new Buffer($buffer->readString("\x0A")); + + // Add player info + $result->addPlayer('frags', $playerInfo->readString("\x20")); + $result->addPlayer('ping', $playerInfo->readString("\x20")); + + // Skip first " + $playerInfo->skip(1); + + // Add player name, encoded + $result->addPlayer('name', utf8_encode(trim(($playerInfo->readString('"'))))); + + // Skip first " + $playerInfo->skip(2); + + // Add address + $result->addPlayer('address', trim($playerInfo->readString('"'))); + + // Increment + $playerCount++; + + // Clear + unset($playerInfo); + } + + $result->add('clients', $playerCount); + + // Clear + unset($buffer, $playerCount); + + return $result->fetch(); + } +} diff --git a/third_party/gameq_v3.1/GameQ/Protocols/Quake3.php b/third_party/gameq_v3.1/GameQ/Protocols/Quake3.php new file mode 100644 index 00000000..6269b927 --- /dev/null +++ b/third_party/gameq_v3.1/GameQ/Protocols/Quake3.php @@ -0,0 +1,214 @@ + "\xFF\xFF\xFF\xFF\x67\x65\x74\x73\x74\x61\x74\x75\x73\x0A", + ]; + + /** + * Use the response flag to figure out what method to run + * + * @type array + */ + protected $responses = [ + "\xFF\xFF\xFF\xFFstatusResponse" => 'processStatus', + ]; + + /** + * The query protocol used to make the call + * + * @type string + */ + protected $protocol = 'quake3'; + + /** + * String name of this protocol class + * + * @type string + */ + protected $name = 'quake3'; + + /** + * Longer string name of this protocol class + * + * @type string + */ + protected $name_long = "Quake 3 Server"; + + /** + * The client join link + * + * @type string + */ + protected $join_link = null; + + /** + * Normalize settings for this protocol + * + * @type array + */ + protected $normalize = [ + // General + 'general' => [ + // target => source + 'gametype' => 'gamename', + 'hostname' => 'sv_hostname', + 'mapname' => 'mapname', + 'maxplayers' => 'sv_maxclients', + 'mod' => 'g_gametype', + 'numplayers' => 'clients', + 'password' => ['g_needpass', 'pswrd'], + ], + // Individual + 'player' => [ + 'name' => 'name', + 'ping' => 'ping', + 'score' => 'frags', + ], + ]; + + /** + * Handle response from the server + * + * @return mixed + * @throws Exception + */ + public function processResponse() + { + // Make a buffer + $buffer = new Buffer(implode('', $this->packets_response)); + + // Grab the header + $header = $buffer->readString("\x0A"); + + // Figure out which packet response this is + if (empty($header) || !array_key_exists($header, $this->responses)) { + throw new Exception(__METHOD__ . " response type '" . bin2hex($header) . "' is not valid"); + } + + return call_user_func_array([$this, $this->responses[$header]], [$buffer]); + } + + protected function processStatus(Buffer $buffer) + { + // We need to split the data and offload + $results = $this->processServerInfo(new Buffer($buffer->readString("\x0A"))); + + $results = array_merge_recursive( + $results, + $this->processPlayers(new Buffer($buffer->getBuffer())) + ); + + unset($buffer); + + // Return results + return $results; + } + + /** + * Handle processing the server information + * + * @param Buffer $buffer + * + * @return array + */ + protected function processServerInfo(Buffer $buffer) + { + // Set the result to a new result instance + $result = new Result(); + + // Burn leading \ if one exists + $buffer->readString('\\'); + + // Key / value pairs + while ($buffer->getLength()) { + // Add result + $result->add( + trim($buffer->readString('\\')), + utf8_encode(trim($buffer->readStringMulti(['\\', "\x0a"]))) + ); + } + + unset($buffer); + + return $result->fetch(); + } + + /** + * Handle processing of player data + * + * @param Buffer $buffer + * + * @return array + * @throws Exception + */ + protected function processPlayers(Buffer $buffer) + { + // Some games do not have a number of current players + $playerCount = 0; + + // Set the result to a new result instance + $result = new Result(); + + // Loop until we are out of data + while ($buffer->getLength()) { + // Add player info + $result->addPlayer('frags', $buffer->readString("\x20")); + $result->addPlayer('ping', $buffer->readString("\x20")); + + // Look ahead to see if we have a name or team + $checkTeam = $buffer->lookAhead(1); + + // We have team info + if ($checkTeam != '' and $checkTeam != '"') { + $result->addPlayer('team', $buffer->readString("\x20")); + } + + // Check to make sure we have player name + $checkPlayerName = $buffer->read(); + + // Bad response + if ($checkPlayerName !== '"') { + throw new Exception('Expected " but got ' . $checkPlayerName . ' for beginning of player name string!'); + } + + // Add player name, encoded + $result->addPlayer('name', utf8_encode(trim($buffer->readString('"')))); + + // Burn ending delimiter + $buffer->read(); + + // Increment + $playerCount++; + } + + $result->add('clients', $playerCount); + + // Clear + unset($buffer, $playerCount); + + return $result->fetch(); + } +} diff --git a/third_party/gameq_v3.1/GameQ/Protocols/Quake4.php b/third_party/gameq_v3.1/GameQ/Protocols/Quake4.php new file mode 100644 index 00000000..6a5f5c7e --- /dev/null +++ b/third_party/gameq_v3.1/GameQ/Protocols/Quake4.php @@ -0,0 +1,84 @@ +. + */ + +namespace GameQ\Protocols; + +use GameQ\Buffer; +use GameQ\Result; + +/** + * Quake 4 Protocol Class + * + * @package GameQ\Protocols + * + * @author Wilson Jesus <> + */ +class Quake4 extends Doom3 +{ + /** + * String name of this protocol class + * + * @type string + */ + protected $name = 'quake4'; + + /** + * Longer string name of this protocol class + * + * @type string + */ + protected $name_long = "Quake 4"; + + /** + * Handle processing of player data + * + * @param \GameQ\Buffer $buffer + * + * @return array + */ + protected function processPlayers(Buffer $buffer) + { + // Some games do not have a number of current players + $playerCount = 0; + + // Set the result to a new result instance + $result = new Result(); + + // Parse players + // Loop thru the buffer until we run out of data + while (($id = $buffer->readInt8()) != 32) { + // Add player info results + $result->addPlayer('id', $id); + $result->addPlayer('ping', $buffer->readInt16()); + $result->addPlayer('rate', $buffer->readInt32()); + // Add player name, encoded + $result->addPlayer('name', utf8_encode(trim($buffer->readString()))); + $result->addPlayer('clantag', $buffer->readString()); + // Increment + $playerCount++; + } + + // Add the number of players to the result + $result->add('numplayers', $playerCount); + + // Clear + unset($buffer, $playerCount); + + return $result->fetch(); + } +} diff --git a/third_party/gameq_v3.1/GameQ/Protocols/Quakelive.php b/third_party/gameq_v3.1/GameQ/Protocols/Quakelive.php new file mode 100644 index 00000000..d5df3501 --- /dev/null +++ b/third_party/gameq_v3.1/GameQ/Protocols/Quakelive.php @@ -0,0 +1,42 @@ +. + */ + +namespace GameQ\Protocols; + +/** + * Class Quake Live + * + * @package GameQ\Protocols + * @author Austin Bischoff + */ +class Quakelive extends Source +{ + /** + * String name of this protocol class + * + * @type string + */ + protected $name = 'quakelive'; + + /** + * Longer string name of this protocol class + * + * @type string + */ + protected $name_long = "Quake Live"; +} diff --git a/third_party/gameq_v3.1/GameQ/Protocols/Redorchestra2.php b/third_party/gameq_v3.1/GameQ/Protocols/Redorchestra2.php new file mode 100644 index 00000000..67330167 --- /dev/null +++ b/third_party/gameq_v3.1/GameQ/Protocols/Redorchestra2.php @@ -0,0 +1,50 @@ +. + */ + +namespace GameQ\Protocols; + +/** + * Class Redorchestra2 + * + * @package GameQ\Protocols + * @author Austin Bischoff + */ +class Redorchestra2 extends Source +{ + /** + * String name of this protocol class + * + * @type string + */ + protected $name = 'redorchestra2'; + + /** + * Longer string name of this protocol class + * + * @type string + */ + protected $name_long = "Red Orchestra 2"; + + /** + * query_port = client_port + 19238 + * 27015 = 7777 + 19238 + * + * @type int + */ + protected $port_diff = 19238; +} diff --git a/third_party/gameq_v3.1/GameQ/Protocols/Redorchestraostfront.php b/third_party/gameq_v3.1/GameQ/Protocols/Redorchestraostfront.php new file mode 100644 index 00000000..4c83b7eb --- /dev/null +++ b/third_party/gameq_v3.1/GameQ/Protocols/Redorchestraostfront.php @@ -0,0 +1,43 @@ +. + */ + +namespace GameQ\Protocols; + +/** + * Red Orchestra: Ostfront 41-45 Class + * + * @package GameQ\Protocols + * @author naXe + * @author Austin Bischoff + */ +class Redorchestraostfront extends Source +{ + /** + * String name of this protocol class + * + * @type string + */ + protected $name = 'redorchestraostfront'; + + /** + * Longer string name of this protocol class + * + * @type string + */ + protected $name_long = "Red Orchestra: Ostfront 41-45"; +} diff --git a/third_party/gameq_v3.1/GameQ/Protocols/Rf2.php b/third_party/gameq_v3.1/GameQ/Protocols/Rf2.php new file mode 100644 index 00000000..9901c425 --- /dev/null +++ b/third_party/gameq_v3.1/GameQ/Protocols/Rf2.php @@ -0,0 +1,50 @@ +. + */ + +namespace GameQ\Protocols; + +/** + * Class rFactor2 + * + * @package GameQ\Protocols + * @author Wilson Jesus <> + */ +class Rf2 extends Source +{ + /** + * String name of this protocol class + * + * @type string + */ + protected $name = 'rf2'; + + /** + * Longer string name of this protocol class + * + * @type string + */ + protected $name_long = "rFactor 2"; + + /** + * query_port = client_port + 2 + * 64092 = 64090 + 2 + * + * @type int + */ + protected $port_diff = 2; +} diff --git a/third_party/gameq_v3.1/GameQ/Protocols/Risingstorm2.php b/third_party/gameq_v3.1/GameQ/Protocols/Risingstorm2.php new file mode 100644 index 00000000..ddb82a53 --- /dev/null +++ b/third_party/gameq_v3.1/GameQ/Protocols/Risingstorm2.php @@ -0,0 +1,55 @@ +. + */ + +namespace GameQ\Protocols; + +/** + * Class Rising Storm 2 + * + * @package GameQ\Protocols + * @author Austin Bischoff + */ +class Risingstorm2 extends Source +{ + + /** + * String name of this protocol class + * + * @type string + */ + protected $name = 'rising storm 2'; + + /** + * Longer string name of this protocol class + * + * @type string + */ + protected $name_long = "Rising Storm 2"; + + /** + * Query port is always 27015 + * + * @param int $clientPort + * + * @return int + */ + public function findQueryPort($clientPort) + { + return 27015; + } +} diff --git a/third_party/gameq_v3.1/GameQ/Protocols/Rust.php b/third_party/gameq_v3.1/GameQ/Protocols/Rust.php new file mode 100644 index 00000000..356cc19f --- /dev/null +++ b/third_party/gameq_v3.1/GameQ/Protocols/Rust.php @@ -0,0 +1,64 @@ +. + */ + +namespace GameQ\Protocols; + +use GameQ\Buffer; + +/** + * Class Rust + * + * @package GameQ\Protocols + * @author Austin Bischoff + */ +class Rust extends Source +{ + + /** + * String name of this protocol class + * + * @type string + */ + protected $name = 'rust'; + + /** + * Longer string name of this protocol class + * + * @type string + */ + protected $name_long = "Rust"; + + /** + * Overload so we can get max players from mp of keywords and num players from cp keyword + * + * @param Buffer $buffer + */ + protected function processDetails(Buffer $buffer) + { + $results = parent::processDetails($buffer); + + if ($results['keywords']) { + //get max players from mp of keywords and num players from cp keyword + preg_match_all('/(mp|cp)([\d]+)/', $results['keywords'], $matches); + $results['max_players'] = intval($matches[2][0]); + $results['num_players'] = intval($matches[2][1]); + } + + return $results; + } +} diff --git a/third_party/gameq_v3.1/GameQ/Protocols/Samp.php b/third_party/gameq_v3.1/GameQ/Protocols/Samp.php new file mode 100644 index 00000000..cf01f834 --- /dev/null +++ b/third_party/gameq_v3.1/GameQ/Protocols/Samp.php @@ -0,0 +1,279 @@ +. + */ + +namespace GameQ\Protocols; + +use GameQ\Protocol; +use GameQ\Buffer; +use GameQ\Result; +use GameQ\Server; +use GameQ\Exception\Protocol as Exception; + +/** + * San Andreas Multiplayer Protocol Class (samp) + * + * Note: + * Player information will not be returned if player count is over 256 + * + * @author Austin Bischoff + */ +class Samp extends Protocol +{ + + /** + * Array of packets we want to look up. + * Each key should correspond to a defined method in this or a parent class + * + * @type array + */ + protected $packets = [ + self::PACKET_STATUS => "SAMP%si", + self::PACKET_PLAYERS => "SAMP%sd", + self::PACKET_RULES => "SAMP%sr", + ]; + + /** + * Use the response flag to figure out what method to run + * + * @type array + */ + protected $responses = [ + "\x69" => "processStatus", // i + "\x64" => "processPlayers", // d + "\x72" => "processRules", // r + ]; + + /** + * The query protocol used to make the call + * + * @type string + */ + protected $protocol = 'samp'; + + /** + * String name of this protocol class + * + * @type string + */ + protected $name = 'samp'; + + /** + * Longer string name of this protocol class + * + * @type string + */ + protected $name_long = "San Andreas Multiplayer"; + + /** + * Holds the calculated server code that is passed when querying for information + * + * @type string + */ + protected $server_code = null; + + /** + * The client join link + * + * @type string + */ + protected $join_link = "samp://%s:%d/"; + + /** + * Normalize settings for this protocol + * + * @type array + */ + protected $normalize = [ + // General + 'general' => [ + // target => source + 'dedicated' => 'dedicated', + 'hostname' => ['hostname', 'servername'], + 'mapname' => 'mapname', + 'maxplayers' => 'max_players', + 'numplayers' => 'num_players', + 'password' => 'password', + ], + // Individual + 'player' => [ + 'name' => 'name', + 'score' => 'score', + 'ping' => 'ping', + ], + ]; + + /** + * Handle some work before sending the packets out to the server + * + * @param \GameQ\Server $server + */ + public function beforeSend(Server $server) + { + + // Build the server code + $this->server_code = implode('', array_map('chr', explode('.', $server->ip()))) . + pack("S", $server->portClient()); + + // Loop over the packets and update them + foreach ($this->packets as $packetType => $packet) { + // Fill out the packet with the server info + $this->packets[$packetType] = sprintf($packet, $this->server_code); + } + } + + /** + * Process the response + * + * @return array + * @throws \GameQ\Exception\Protocol + */ + public function processResponse() + { + + // Results that will be returned + $results = []; + + // Get the length of the server code so we can figure out how much to read later + $serverCodeLength = strlen($this->server_code); + + // We need to pre-sort these for split packets so we can do extra work where needed + foreach ($this->packets_response as $response) { + // Make new buffer + $buffer = new Buffer($response); + + // Check the header, should be SAMP + if (($header = $buffer->read(4)) !== 'SAMP') { + throw new Exception(__METHOD__ . " header response '{$header}' is not valid"); + } + + // Check to make sure the server response code matches what we sent + if ($buffer->read($serverCodeLength) !== $this->server_code) { + throw new Exception(__METHOD__ . " code check failed."); + } + + // Figure out what packet response this is for + $response_type = $buffer->read(1); + + // Figure out which packet response this is + if (!array_key_exists($response_type, $this->responses)) { + throw new Exception(__METHOD__ . " response type '{$response_type}' is not valid"); + } + + // Now we need to call the proper method + $results = array_merge( + $results, + call_user_func_array([$this, $this->responses[$response_type]], [$buffer]) + ); + + unset($buffer); + } + + return $results; + } + + /* + * Internal methods + */ + + /** + * Handles processing the server status data + * + * @param \GameQ\Buffer $buffer + * + * @return array + * @throws \GameQ\Exception\Protocol + */ + protected function processStatus(Buffer $buffer) + { + + // Set the result to a new result instance + $result = new Result(); + + // Always dedicated + $result->add('dedicated', 1); + + // Pull out the server information + $result->add('password', $buffer->readInt8()); + $result->add('num_players', $buffer->readInt16()); + $result->add('max_players', $buffer->readInt16()); + + // These are read differently for these last 3 + $result->add('servername', utf8_encode($buffer->read($buffer->readInt32()))); + $result->add('gametype', $buffer->read($buffer->readInt32())); + $result->add('language', $buffer->read($buffer->readInt32())); + + unset($buffer); + + return $result->fetch(); + } + + /** + * Handles processing the player data into a usable format + * + * @param \GameQ\Buffer $buffer + * + * @return array + */ + protected function processPlayers(Buffer $buffer) + { + + // Set the result to a new result instance + $result = new Result(); + + // Number of players + $result->add('num_players', $buffer->readInt16()); + + // Run until we run out of buffer + while ($buffer->getLength()) { + $result->addPlayer('id', $buffer->readInt8()); + $result->addPlayer('name', utf8_encode($buffer->readPascalString())); + $result->addPlayer('score', $buffer->readInt32()); + $result->addPlayer('ping', $buffer->readInt32()); + } + + unset($buffer); + + return $result->fetch(); + } + + /** + * Handles processing the rules data into a usable format + * + * @param \GameQ\Buffer $buffer + * + * @return array + */ + protected function processRules(Buffer $buffer) + { + + // Set the result to a new result instance + $result = new Result(); + + // Number of rules + $result->add('num_rules', $buffer->readInt16()); + + // Run until we run out of buffer + while ($buffer->getLength()) { + $result->add($buffer->readPascalString(), $buffer->readPascalString()); + } + + unset($buffer); + + return $result->fetch(); + } +} diff --git a/third_party/gameq_v3.1/GameQ/Protocols/Sco.php b/third_party/gameq_v3.1/GameQ/Protocols/Sco.php new file mode 100644 index 00000000..a920fbd8 --- /dev/null +++ b/third_party/gameq_v3.1/GameQ/Protocols/Sco.php @@ -0,0 +1,50 @@ +. + */ + +namespace GameQ\Protocols; + +/** + * Class Sven Co-op + * + * @package GameQ\Protocols + * @author Austin Bischoff + * @author Jesse Lukas + */ +class Sco extends Source +{ + /** + * Sven Co-op protocol class + * + * @type string + */ + protected $name = 'sco'; + + /** + * Longer string name of this protocol class + * + * @type string + */ + protected $name_long = "Sven Co-op"; + + /** + * query_port = client_port + 1 + * + * @type int + */ + protected $port_diff = 1; +} diff --git a/third_party/gameq_v3.1/GameQ/Protocols/Serioussam.php b/third_party/gameq_v3.1/GameQ/Protocols/Serioussam.php new file mode 100644 index 00000000..64a03cf7 --- /dev/null +++ b/third_party/gameq_v3.1/GameQ/Protocols/Serioussam.php @@ -0,0 +1,75 @@ +. + */ + +namespace GameQ\Protocols; + +/** + * Serious Sam Protocol Class + * + * @author ZCaliptium + */ +class Serioussam extends Gamespy +{ + + /** + * String name of this protocol class + * + * @type string + */ + protected $name = 'serioussam'; + + /** + * Longer string name of this protocol class + * + * @type string + */ + protected $name_long = "Serious Sam"; + + /** + * query_port = client_port + 1 + * + * @type int + */ + protected $port_diff = 1; + + /** + * Normalize settings for this protocol + * + * @type array + */ + protected $normalize = [ + // General + 'general' => [ + // target => source + 'dedicated' => 'dedicated', + 'gametype' => 'gametype', + 'hostname' => 'hostname', + 'mapname' => 'mapname', + 'maxplayers' => 'maxplayers', + 'mod' => 'activemod', + 'numplayers' => 'numplayers', + 'password' => 'password', + ], + // Individual + 'player' => [ + 'name' => 'player', + 'ping' => 'ping', + 'score' => 'frags', + ], + ]; +} diff --git a/third_party/gameq_v3.1/GameQ/Protocols/Sevendaystodie.php b/third_party/gameq_v3.1/GameQ/Protocols/Sevendaystodie.php new file mode 100644 index 00000000..8919b97f --- /dev/null +++ b/third_party/gameq_v3.1/GameQ/Protocols/Sevendaystodie.php @@ -0,0 +1,49 @@ +. + */ + +namespace GameQ\Protocols; + +/** + * Class 7 Days to Die + * + * @package GameQ\Protocols + * @author Austin Bischoff + */ +class Sevendaystodie extends Source +{ + /** + * String name of this protocol class + * + * @type string + */ + protected $name = 'sevendaystodie'; + + /** + * Longer string name of this protocol class + * + * @type string + */ + protected $name_long = "7 Days to Die"; + + /** + * query_port = client_port + 0 + * + * @type int + */ + protected $port_diff = 0; +} diff --git a/third_party/gameq_v3.1/GameQ/Protocols/Ship.php b/third_party/gameq_v3.1/GameQ/Protocols/Ship.php new file mode 100644 index 00000000..9c3bee9e --- /dev/null +++ b/third_party/gameq_v3.1/GameQ/Protocols/Ship.php @@ -0,0 +1,95 @@ +. + */ + +namespace GameQ\Protocols; + +use GameQ\Buffer; +use GameQ\Result; + +/** + * Class Ship + * + * @package GameQ\Protocols + * + * @author Nikolay Ipanyuk + * @author Austin Bischoff + */ +class Ship extends Source +{ + + /** + * String name of this protocol class + * + * @type string + */ + protected $name = 'ship'; + + /** + * Longer string name of this protocol class + * + * @type string + */ + protected $name_long = "The Ship"; + + /** + * Specific player parse for The Ship + * + * Player response has unknown data after the last real player + * + * @param \GameQ\Buffer $buffer + * + * @return array + */ + protected function processPlayers(Buffer $buffer) + { + + // Set the result to a new result instance + $result = new Result(); + + // We need to read the number of players because this response has other data at the end usually + $num_players = $buffer->readInt8(); + + // Player count + $result->add('num_players', $num_players); + + // No players, no work + if ($num_players == 0) { + return $result->fetch(); + } + + // Players list + for ($player = 0; $player < $num_players; $player++) { + $result->addPlayer('id', $buffer->readInt8()); + $result->addPlayer('name', $buffer->readString()); + $result->addPlayer('score', $buffer->readInt32Signed()); + $result->addPlayer('time', $buffer->readFloat32()); + } + + // Extra data + if ($buffer->getLength() > 0) { + for ($player = 0; $player < $num_players; $player++) { + $result->addPlayer('deaths', $buffer->readInt32Signed()); + $result->addPlayer('money', $buffer->readInt32Signed()); + } + } + + unset($buffer); + + return $result->fetch(); + } +} diff --git a/third_party/gameq_v3.1/GameQ/Protocols/Sof2.php b/third_party/gameq_v3.1/GameQ/Protocols/Sof2.php new file mode 100644 index 00000000..96a4db25 --- /dev/null +++ b/third_party/gameq_v3.1/GameQ/Protocols/Sof2.php @@ -0,0 +1,49 @@ +. + */ + +namespace GameQ\Protocols; + +/** + * Soldier of Fortune 2 Class + * + * @package GameQ\Protocols + * @author Austin Bischoff + */ +class Sof2 extends Quake3 +{ + /** + * String name of this protocol class + * + * @type string + */ + protected $name = 'sof2'; + + /** + * Longer string name of this protocol class + * + * @type string + */ + protected $name_long = "Solder of Fortune II"; + + /** + * The client join link + * + * @type string + */ + protected $join_link = "sof2mp://%s:%d/"; +} diff --git a/third_party/gameq_v3.1/GameQ/Protocols/Soldat.php b/third_party/gameq_v3.1/GameQ/Protocols/Soldat.php new file mode 100644 index 00000000..a9dbbc4e --- /dev/null +++ b/third_party/gameq_v3.1/GameQ/Protocols/Soldat.php @@ -0,0 +1,59 @@ +. + */ + +namespace GameQ\Protocols; + +/** + * Class Soldat + * + * @package GameQ\Protocols + * + * @author Marcel Bößendörfer + * @author Austin Bischoff + */ +class Soldat extends Ase +{ + + /** + * String name of this protocol class + * + * @type string + */ + protected $name = 'soldat'; + + /** + * Longer string name of this protocol class + * + * @type string + */ + protected $name_long = "Soldat"; + + /** + * query_port = client_port + 123 + * + * @type int + */ + protected $port_diff = 123; + + /** + * The client join link + * + * @type string + */ + protected $join_link = "soldat://%s:%d/"; +} diff --git a/third_party/gameq_v3.1/GameQ/Protocols/Source.php b/third_party/gameq_v3.1/GameQ/Protocols/Source.php new file mode 100644 index 00000000..dbf9212a --- /dev/null +++ b/third_party/gameq_v3.1/GameQ/Protocols/Source.php @@ -0,0 +1,522 @@ +. + */ + +namespace GameQ\Protocols; + +use GameQ\Buffer; +use GameQ\Exception\Protocol as Exception; +use GameQ\Protocol; +use GameQ\Result; + +/** + * Valve Source Engine Protocol Class (A2S) + * + * This class is used as the basis for all other source based servers + * that rely on the source protocol for game querying. + * + * @SuppressWarnings(PHPMD.NumberOfChildren) + * + * @author Austin Bischoff + */ +class Source extends Protocol +{ + + /* + * Source engine type constants + */ + const SOURCE_ENGINE = 0, + GOLDSOURCE_ENGINE = 1; + + /** + * Array of packets we want to look up. + * Each key should correspond to a defined method in this or a parent class + * + * @type array + */ + protected $packets = [ + self::PACKET_CHALLENGE => "\xFF\xFF\xFF\xFF\x56\x00\x00\x00\x00", + self::PACKET_DETAILS => "\xFF\xFF\xFF\xFFTSource Engine Query\x00%s", + self::PACKET_PLAYERS => "\xFF\xFF\xFF\xFF\x55%s", + self::PACKET_RULES => "\xFF\xFF\xFF\xFF\x56%s", + ]; + + /** + * Use the response flag to figure out what method to run + * + * @type array + */ + protected $responses = [ + "\x49" => "processDetails", // I + "\x6d" => "processDetailsGoldSource", // m, goldsource + "\x44" => "processPlayers", // D + "\x45" => "processRules", // E + ]; + + /** + * The query protocol used to make the call + * + * @type string + */ + protected $protocol = 'source'; + + /** + * String name of this protocol class + * + * @type string + */ + protected $name = 'source'; + + /** + * Longer string name of this protocol class + * + * @type string + */ + protected $name_long = "Source Server"; + + /** + * Define the Source engine type. By default it is assumed to be Source + * + * @type int + */ + protected $source_engine = self::SOURCE_ENGINE; + + /** + * The client join link + * + * @type string + */ + protected $join_link = "steam://connect/%s:%d/"; + + /** + * Normalize settings for this protocol + * + * @type array + */ + protected $normalize = [ + // General + 'general' => [ + // target => source + 'dedicated' => 'dedicated', + 'gametype' => 'game_descr', + 'hostname' => 'hostname', + 'mapname' => 'map', + 'maxplayers' => 'max_players', + 'mod' => 'game_dir', + 'numplayers' => 'num_players', + 'password' => 'password', + ], + // Individual + 'player' => [ + 'name' => 'name', + 'score' => 'score', + 'time' => 'time', + ], + ]; + + /** + * Parse the challenge response and apply it to all the packet types + * + * @param \GameQ\Buffer $challenge_buffer + * + * @return bool + * @throws \GameQ\Exception\Protocol + */ + public function challengeParseAndApply(Buffer $challenge_buffer) + { + + // Skip the header + $challenge_buffer->skip(5); + + // Apply the challenge and return + return $this->challengeApply($challenge_buffer->read(4)); + } + + /** + * Process the response + * + * @return array + * @throws \GameQ\Exception\Protocol + */ + public function processResponse() + { + // Will hold the results when complete + $results = []; + + // Holds sorted response packets + $packets = []; + + // We need to pre-sort these for split packets so we can do extra work where needed + foreach ($this->packets_response as $response) { + $buffer = new Buffer($response); + + // Get the header of packet (long) + $header = $buffer->readInt32Signed(); + + // Single packet + if ($header == -1) { + // We need to peek and see what kind of engine this is for later processing + if ($buffer->lookAhead(1) == "\x6d") { + $this->source_engine = self::GOLDSOURCE_ENGINE; + } + + $packets[] = $buffer->getBuffer(); + continue; + } else { + // Split packet + + // Packet Id (long) + $packet_id = $buffer->readInt32Signed() + 10; + + // Add the buffer to the packet as another array + $packets[$packet_id][] = $buffer->getBuffer(); + } + } + + // Free up memory + unset($response, $packet_id, $buffer, $header); + + // Now that we have the packets sorted we need to iterate and process them + foreach ($packets as $packet_id => $packet) { + // We first need to off load split packets to combine them + if (is_array($packet)) { + $buffer = new Buffer($this->processPackets($packet_id, $packet)); + } else { + $buffer = new Buffer($packet); + } + + // Figure out what packet response this is for + $response_type = $buffer->read(1); + + // Figure out which packet response this is + if (!array_key_exists($response_type, $this->responses)) { + throw new Exception(__METHOD__ . " response type '{$response_type}' is not valid"); + } + + // Now we need to call the proper method + $results = array_merge( + $results, + call_user_func_array([$this, $this->responses[$response_type]], [$buffer]) + ); + + unset($buffer); + } + + // Free up memory + unset($packets, $packet, $packet_id, $response_type); + + return $results; + } + + /* + * Internal methods + */ + + /** + * Process the split packets and decompress if necessary + * + * @SuppressWarnings(PHPMD.UnusedLocalVariable) + * + * @param $packet_id + * @param array $packets + * + * @return string + * @throws \GameQ\Exception\Protocol + */ + protected function processPackets($packet_id, array $packets = []) + { + + // Init array so we can order + $packs = []; + + // We have multiple packets so we need to get them and order them + foreach ($packets as $i => $packet) { + // Make a buffer so we can read this info + $buffer = new Buffer($packet); + + // Gold source + if ($this->source_engine == self::GOLDSOURCE_ENGINE) { + // Grab the packet number (byte) + $packet_number = $buffer->readInt8(); + + // We need to burn the extra header (\xFF\xFF\xFF\xFF) on first loop + if ($i == 0) { + $buffer->read(4); + } + + // Now add the rest of the packet to the new array with the packet_number as the id so we can order it + $packs[$packet_number] = $buffer->getBuffer(); + } else { + // Number of packets in this set (byte) + $buffer->readInt8(); + + // The current packet number (byte) + $packet_number = $buffer->readInt8(); + + // Check to see if this is compressed + // @todo: Check to make sure these decompress correctly, new changes may affect this loop. + if ($packet_id & 0x80000000) { + // Check to see if we have Bzip2 installed + if (!function_exists('bzdecompress')) { + // @codeCoverageIgnoreStart + throw new Exception( + 'Bzip2 is not installed. See http://www.php.net/manual/en/book.bzip2.php for more info.', + 0 + ); + // @codeCoverageIgnoreEnd + } + + // Get the length of the packet (long) + $packet_length = $buffer->readInt32Signed(); + + // Checksum for the decompressed packet (long), burn it - doesnt work in split responses + $buffer->readInt32Signed(); + + // Try to decompress + $result = bzdecompress($buffer->getBuffer()); + + // Now verify the length + if (strlen($result) != $packet_length) { + // @codeCoverageIgnoreStart + throw new Exception( + "Checksum for compressed packet failed! Length expected: {$packet_length}, length + returned: " . strlen($result) + ); + // @codeCoverageIgnoreEnd + } + + // We need to burn the extra header (\xFF\xFF\xFF\xFF) on first loop + if ($i == 0) { + $result = substr($result, 4); + } + } else { + // Get the packet length (short), burn it + $buffer->readInt16Signed(); + + // We need to burn the extra header (\xFF\xFF\xFF\xFF) on first loop + if ($i == 0) { + $buffer->read(4); + } + + // Grab the rest of the buffer as a result + $result = $buffer->getBuffer(); + } + + // Add this packet to the list + $packs[$packet_number] = $result; + } + + unset($buffer); + } + + // Free some memory + unset($packets, $packet); + + // Sort the packets by packet number + ksort($packs); + + // Now combine the packs into one and return + return implode("", $packs); + } + + /** + * Handles processing the details data into a usable format + * + * @param \GameQ\Buffer $buffer + * + * @return mixed + * @throws \GameQ\Exception\Protocol + */ + protected function processDetails(Buffer $buffer) + { + + // Set the result to a new result instance + $result = new Result(); + + $result->add('protocol', $buffer->readInt8()); + $result->add('hostname', $buffer->readString()); + $result->add('map', $buffer->readString()); + $result->add('game_dir', $buffer->readString()); + $result->add('game_descr', $buffer->readString()); + $result->add('steamappid', $buffer->readInt16()); + $result->add('num_players', $buffer->readInt8()); + $result->add('max_players', $buffer->readInt8()); + $result->add('num_bots', $buffer->readInt8()); + $result->add('dedicated', $buffer->read()); + $result->add('os', $buffer->read()); + $result->add('password', $buffer->readInt8()); + $result->add('secure', $buffer->readInt8()); + + // Special result for The Ship only (appid=2400) + if ($result->get('steamappid') == 2400) { + $result->add('game_mode', $buffer->readInt8()); + $result->add('witness_count', $buffer->readInt8()); + $result->add('witness_time', $buffer->readInt8()); + } + + $result->add('version', $buffer->readString()); + + // Because of php 5.4... + $edfCheck = $buffer->lookAhead(1); + + // Extra data flag + if (!empty($edfCheck)) { + $edf = $buffer->readInt8(); + + if ($edf & 0x80) { + $result->add('port', $buffer->readInt16Signed()); + } + + if ($edf & 0x10) { + $result->add('steam_id', $buffer->readInt64()); + } + + if ($edf & 0x40) { + $result->add('sourcetv_port', $buffer->readInt16Signed()); + $result->add('sourcetv_name', $buffer->readString()); + } + + if ($edf & 0x20) { + $result->add('keywords', $buffer->readString()); + } + + if ($edf & 0x01) { + $result->add('game_id', $buffer->readInt64()); + } + + unset($edf); + } + + unset($buffer); + + return $result->fetch(); + } + + /** + * Handles processing the server details from goldsource response + * + * @param \GameQ\Buffer $buffer + * + * @return array + * @throws \GameQ\Exception\Protocol + */ + protected function processDetailsGoldSource(Buffer $buffer) + { + + // Set the result to a new result instance + $result = new Result(); + + $result->add('address', $buffer->readString()); + $result->add('hostname', $buffer->readString()); + $result->add('map', $buffer->readString()); + $result->add('game_dir', $buffer->readString()); + $result->add('game_descr', $buffer->readString()); + $result->add('num_players', $buffer->readInt8()); + $result->add('max_players', $buffer->readInt8()); + $result->add('version', $buffer->readInt8()); + $result->add('dedicated', $buffer->read()); + $result->add('os', $buffer->read()); + $result->add('password', $buffer->readInt8()); + + // Mod section + $result->add('ismod', $buffer->readInt8()); + + // We only run these if ismod is 1 (true) + if ($result->get('ismod') == 1) { + $result->add('mod_urlinfo', $buffer->readString()); + $result->add('mod_urldl', $buffer->readString()); + $buffer->skip(); + $result->add('mod_version', $buffer->readInt32Signed()); + $result->add('mod_size', $buffer->readInt32Signed()); + $result->add('mod_type', $buffer->readInt8()); + $result->add('mod_cldll', $buffer->readInt8()); + } + + $result->add('secure', $buffer->readInt8()); + $result->add('num_bots', $buffer->readInt8()); + + unset($buffer); + + return $result->fetch(); + } + + /** + * Handles processing the player data into a usable format + * + * @param \GameQ\Buffer $buffer + * + * @return mixed + */ + protected function processPlayers(Buffer $buffer) + { + + // Set the result to a new result instance + $result = new Result(); + + // Pull out the number of players + $num_players = $buffer->readInt8(); + + // Player count + $result->add('num_players', $num_players); + + // No players so no need to look any further + if ($num_players == 0) { + return $result->fetch(); + } + + // Players list + while ($buffer->getLength()) { + $result->addPlayer('id', $buffer->readInt8()); + $result->addPlayer('name', $buffer->readString()); + $result->addPlayer('score', $buffer->readInt32Signed()); + $result->addPlayer('time', $buffer->readFloat32()); + } + + unset($buffer); + + return $result->fetch(); + } + + /** + * Handles processing the rules data into a usable format + * + * @param \GameQ\Buffer $buffer + * + * @return mixed + */ + protected function processRules(Buffer $buffer) + { + + // Set the result to a new result instance + $result = new Result(); + + // Count the number of rules + $num_rules = $buffer->readInt16Signed(); + + // Add the count of the number of rules this server has + $result->add('num_rules', $num_rules); + + // Rules + while ($buffer->getLength()) { + $result->add($buffer->readString(), $buffer->readString()); + } + + unset($buffer); + + return $result->fetch(); + } +} diff --git a/third_party/gameq_v3.1/GameQ/Protocols/Spaceengineers.php b/third_party/gameq_v3.1/GameQ/Protocols/Spaceengineers.php new file mode 100644 index 00000000..ddf8567d --- /dev/null +++ b/third_party/gameq_v3.1/GameQ/Protocols/Spaceengineers.php @@ -0,0 +1,42 @@ +. + */ + +namespace GameQ\Protocols; + +/** + * Space Engineers Protocol Class + * + * @package GameQ\Protocols + * @author Austin Bischoff + */ +class Spaceengineers extends Source +{ + /** + * String name of this protocol class + * + * @type string + */ + protected $name = 'spaceengineers'; + + /** + * Longer string name of this protocol class + * + * @type string + */ + protected $name_long = "Space Engineers"; +} diff --git a/third_party/gameq_v3.1/GameQ/Protocols/Squad.php b/third_party/gameq_v3.1/GameQ/Protocols/Squad.php new file mode 100644 index 00000000..3c021885 --- /dev/null +++ b/third_party/gameq_v3.1/GameQ/Protocols/Squad.php @@ -0,0 +1,53 @@ +. + */ + +namespace GameQ\Protocols; + +/** + * Class Squad + * + * Port reference: http://forums.joinsquad.com/topic/9559-query-ports/ + * + * @package GameQ\Protocols + * @author Austin Bischoff + */ +class Squad extends Source +{ + + /** + * String name of this protocol class + * + * @type string + */ + protected $name = 'squad'; + + /** + * Longer string name of this protocol class + * + * @type string + */ + protected $name_long = "Squad"; + + /** + * query_port = client_port + 19378 + * 27165 = 7787 + 19378 + * + * @type int + */ + protected $port_diff = 19378; +} diff --git a/third_party/gameq_v3.1/GameQ/Protocols/Starmade.php b/third_party/gameq_v3.1/GameQ/Protocols/Starmade.php new file mode 100644 index 00000000..09a033fb --- /dev/null +++ b/third_party/gameq_v3.1/GameQ/Protocols/Starmade.php @@ -0,0 +1,226 @@ +. + */ + +namespace GameQ\Protocols; + +use GameQ\Protocol; +use GameQ\Buffer; +use GameQ\Result; +use GameQ\Exception\Protocol as Exception; + +/** + * StarMade Protocol Class + * + * StarMade server query protocol class + * + * Credit to Robin Promesberger for providing Java based querying as a roadmap + * + * @author Austin Bischoff + */ +class Starmade extends Protocol +{ + + /** + * Array of packets we want to query. + * + * @type array + */ + protected $packets = [ + self::PACKET_STATUS => "\x00\x00\x00\x09\x2a\xff\xff\x01\x6f\x00\x00\x00\x00", + ]; + + /** + * The transport mode for this protocol is TCP + * + * @type string + */ + protected $transport = self::TRANSPORT_TCP; + + /** + * The query protocol used to make the call + * + * @type string + */ + protected $protocol = 'starmade'; + + /** + * String name of this protocol class + * + * @type string + */ + protected $name = 'starmade'; + + /** + * Longer string name of this protocol class + * + * @type string + */ + protected $name_long = "StarMade"; + + /** + * The client join link + * + * @type string + */ + protected $join_link = null; + + /** + * Normalize settings for this protocol + * + * @type array + */ + protected $normalize = [ + // General + 'general' => [ + // target => source + 'dedicated' => 'dedicated', + 'hostname' => 'hostname', + 'maxplayers' => 'max_players', + 'numplayers' => 'num_players', + 'password' => 'password', + ], + ]; + + /** + * Process the response for the StarMade server + * + * @return array + * @throws \GameQ\Exception\Protocol + */ + public function processResponse() + { + + // Implode the packets, not sure if there is any split logic for multiple packets + $buffer = new Buffer(implode('', $this->packets_response), Buffer::NUMBER_TYPE_BIGENDIAN); + + // Get the passed length in the data side of the packet + $buffer->readInt32Signed(); + + // Read off the timestamp (in milliseconds) + $buffer->readInt64(); + + // Burn the check id == 42 + $buffer->readInt8(); + + // Read packetId, unused + $buffer->readInt16Signed(); + + // Read commandId, unused + $buffer->readInt8Signed(); + + // Read type, unused + $buffer->readInt8Signed(); + + $parsed = $this->parseServerParameters($buffer); + + // Set the result to a new result instance + $result = new Result(); + + // Best guess info version is the type of response to expect. As of this commit the version is "2". + $result->add('info_version', $parsed[0]); + $result->add('version', $parsed[1]); + $result->add('hostname', $parsed[2]); + $result->add('game_descr', $parsed[3]); + $result->add('start_time', $parsed[4]); + $result->add('num_players', $parsed[5]); + $result->add('max_players', $parsed[6]); + $result->add('dedicated', 1); // All servers are dedicated as far as I can tell + $result->add('password', 0); // Unsure if you can password servers, cant read that value + //$result->add('map', 'Unknown'); + + unset($parsed); + + return $result->fetch(); + } + + /** + * Parse the server response parameters + * + * @SuppressWarnings(PHPMD.CyclomaticComplexity) + * + * @param \GameQ\Buffer $buffer + * + * @return array + * @throws \GameQ\Exception\Protocol + */ + protected function parseServerParameters(Buffer &$buffer) + { + + // Init the parsed data array + $parsed = []; + + // Read the number of parameters to parse + $parameterSize = $buffer->readInt32Signed(); + + // Iterate over the parameter size + for ($i = 0; $i < $parameterSize; $i++) { + // Read the type of return this is + $dataType = $buffer->readInt8Signed(); + + switch ($dataType) { + // 32-bit int + case 1: + $parsed[$i] = $buffer->readInt32Signed(); + break; + + // 64-bit int + case 2: + $parsed[$i] = $buffer->readInt64(); + break; + + // Float + case 3: + $parsed[$i] = $buffer->readFloat32(); + break; + + // String + case 4: + // The first 2 bytes are the string length + $strLength = $buffer->readInt16Signed(); + + // Read the above length from the buffer + $parsed[$i] = $buffer->read($strLength); + + unset($strLength); + break; + + // Boolean + case 5: + $parsed[$i] = (bool)$buffer->readInt8Signed(); + break; + + // 8-bit int + case 6: + $parsed[$i] = $buffer->readInt8Signed(); + break; + + // 16-bit int + case 7: + $parsed[$i] = $buffer->readInt16Signed(); + break; + + // Array + case 8: + // Not implemented + throw new Exception("StarMade array parsing is not implemented!"); + } + } + + return $parsed; + } +} diff --git a/third_party/gameq_v3.1/GameQ/Protocols/Stormworks.php b/third_party/gameq_v3.1/GameQ/Protocols/Stormworks.php new file mode 100644 index 00000000..735b5776 --- /dev/null +++ b/third_party/gameq_v3.1/GameQ/Protocols/Stormworks.php @@ -0,0 +1,50 @@ +. + */ + +namespace GameQ\Protocols; + +/** + * Class Stormworks + * + * @package GameQ\Protocols + * @author Austin Bischoff + * @author Jesse Lukas + */ +class Stormworks extends Source +{ + /** + * Stormworks protocol class + * + * @type string + */ + protected $name = 'stormworks'; + + /** + * Longer string name of this protocol class + * + * @type string + */ + protected $name_long = "Stormworks"; + + /** + * query_port = client_port + 1 + * + * @type int + */ + protected $port_diff = 1; +} diff --git a/third_party/gameq_v3.1/GameQ/Protocols/Swat4.php b/third_party/gameq_v3.1/GameQ/Protocols/Swat4.php new file mode 100644 index 00000000..7b8e1200 --- /dev/null +++ b/third_party/gameq_v3.1/GameQ/Protocols/Swat4.php @@ -0,0 +1,50 @@ +. + */ + +namespace GameQ\Protocols; + +/** + * Class Swat4 + * + * @package GameQ\Protocols + * + * @author Wilson Jesus <> + */ +class Swat4 extends Gamespy2 +{ + /** + * String name of this protocol class + * + * @type string + */ + protected $name = 'swat4'; + + /** + * Longer string name of this protocol class + * + * @type string + */ + protected $name_long = "SWAT 4"; + + /** + * query_port = client_port + 1 + * + * @type int + */ + protected $port_diff = 1; +} diff --git a/third_party/gameq_v3.1/GameQ/Protocols/Teamspeak2.php b/third_party/gameq_v3.1/GameQ/Protocols/Teamspeak2.php new file mode 100644 index 00000000..df0d59aa --- /dev/null +++ b/third_party/gameq_v3.1/GameQ/Protocols/Teamspeak2.php @@ -0,0 +1,290 @@ +. + */ + +namespace GameQ\Protocols; + +use GameQ\Protocol; +use GameQ\Buffer; +use GameQ\Result; +use GameQ\Server; +use GameQ\Exception\Protocol as Exception; + +/** + * Teamspeak 2 Protocol Class + * + * All values are utf8 encoded upon processing + * + * This code ported from GameQ v1/v2. Credit to original author(s) as I just updated it to + * work within this new system. + * + * @author Austin Bischoff + */ +class Teamspeak2 extends Protocol +{ + + /** + * Array of packets we want to look up. + * Each key should correspond to a defined method in this or a parent class + * + * @type array + */ + protected $packets = [ + self::PACKET_DETAILS => "sel %d\x0asi\x0a", + self::PACKET_CHANNELS => "sel %d\x0acl\x0a", + self::PACKET_PLAYERS => "sel %d\x0apl\x0a", + ]; + + /** + * The transport mode for this protocol is TCP + * + * @type string + */ + protected $transport = self::TRANSPORT_TCP; + + /** + * The query protocol used to make the call + * + * @type string + */ + protected $protocol = 'teamspeak2'; + + /** + * String name of this protocol class + * + * @type string + */ + protected $name = 'teamspeak2'; + + /** + * Longer string name of this protocol class + * + * @type string + */ + protected $name_long = "Teamspeak 2"; + + /** + * The client join link + * + * @type string + */ + protected $join_link = "teamspeak://%s:%d/"; + + /** + * Normalize settings for this protocol + * + * @type array + */ + protected $normalize = [ + // General + 'general' => [ + 'dedicated' => 'dedicated', + 'hostname' => 'server_name', + 'password' => 'server_password', + 'numplayers' => 'server_currentusers', + 'maxplayers' => 'server_maxusers', + ], + // Player + 'player' => [ + 'id' => 'p_id', + 'team' => 'c_id', + 'name' => 'nick', + ], + // Team + 'team' => [ + 'id' => 'id', + 'name' => 'name', + ], + ]; + + /** + * Before we send off the queries we need to update the packets + * + * @param \GameQ\Server $server + * + * @throws \GameQ\Exception\Protocol + */ + public function beforeSend(Server $server) + { + + // Check to make sure we have a query_port because it is required + if (!isset($this->options[Server::SERVER_OPTIONS_QUERY_PORT]) + || empty($this->options[Server::SERVER_OPTIONS_QUERY_PORT]) + ) { + throw new Exception(__METHOD__ . " Missing required setting '" . Server::SERVER_OPTIONS_QUERY_PORT . "'."); + } + + // Let's loop the packets and set the proper pieces + foreach ($this->packets as $packet_type => $packet) { + // Update with the client port for the server + $this->packets[$packet_type] = sprintf($packet, $server->portClient()); + } + } + + /** + * Process the response + * + * @return array + * @throws \GameQ\Exception\Protocol + */ + public function processResponse() + { + + // Make a new buffer out of all of the packets + $buffer = new Buffer(implode('', $this->packets_response)); + + // Check the header [TS] + if (($header = trim($buffer->readString("\n"))) !== '[TS]') { + throw new Exception(__METHOD__ . " Expected header '{$header}' does not match expected '[TS]'."); + } + + // Split this buffer as the data blocks are bound by "OK" and drop any empty values + $sections = array_filter(explode("OK", $buffer->getBuffer()), function ($value) { + + $value = trim($value); + + return !empty($value); + }); + + // Trim up the values to remove extra whitespace + $sections = array_map('trim', $sections); + + // Set the result to a new result instance + $result = new Result(); + + // Now we need to iterate over the sections and off load the processing + foreach ($sections as $section) { + // Grab a snip of the data so we can figure out what it is + $check = substr($section, 0, 7); + + // Offload to the proper method + if ($check == 'server_') { + // Server settings and info + $this->processDetails($section, $result); + } elseif ($check == "id\tcode") { + // Channel info + $this->processChannels($section, $result); + } elseif ($check == "p_id\tc_") { + // Player info + $this->processPlayers($section, $result); + } + } + + unset($buffer, $sections, $section, $check); + + return $result->fetch(); + } + + /* + * Internal methods + */ + + + /** + * Handles processing the details data into a usable format + * + * @param string $data + * @param \GameQ\Result $result + */ + protected function processDetails($data, Result &$result) + { + + // Create a buffer + $buffer = new Buffer($data); + + // Always dedicated + $result->add('dedicated', 1); + + // Let's loop until we run out of data + while ($buffer->getLength()) { + // Grab the row, which is an item + $row = trim($buffer->readString("\n")); + + // Split out the information + list($key, $value) = explode('=', $row, 2); + + // Add this to the result + $result->add($key, utf8_encode($value)); + } + + unset($data, $buffer, $row, $key, $value); + } + + /** + * Process the channel listing + * + * @param string $data + * @param \GameQ\Result $result + */ + protected function processChannels($data, Result &$result) + { + + // Create a buffer + $buffer = new Buffer($data); + + // The first line holds the column names, data returned is in column/row format + $columns = explode("\t", trim($buffer->readString("\n")), 9); + + // Loop through the rows until we run out of information + while ($buffer->getLength()) { + // Grab the row, which is a tabbed list of items + $row = trim($buffer->readString("\n")); + + // Explode and merge the data with the columns, then parse + $data = array_combine($columns, explode("\t", $row, 9)); + + foreach ($data as $key => $value) { + // Now add the data to the result + $result->addTeam($key, utf8_encode($value)); + } + } + + unset($data, $buffer, $row, $columns, $key, $value); + } + + /** + * Process the user listing + * + * @param string $data + * @param \GameQ\Result $result + */ + protected function processPlayers($data, Result &$result) + { + + // Create a buffer + $buffer = new Buffer($data); + + // The first line holds the column names, data returned is in column/row format + $columns = explode("\t", trim($buffer->readString("\n")), 16); + + // Loop through the rows until we run out of information + while ($buffer->getLength()) { + // Grab the row, which is a tabbed list of items + $row = trim($buffer->readString("\n")); + + // Explode and merge the data with the columns, then parse + $data = array_combine($columns, explode("\t", $row, 16)); + + foreach ($data as $key => $value) { + // Now add the data to the result + $result->addPlayer($key, utf8_encode($value)); + } + } + + unset($data, $buffer, $row, $columns, $key, $value); + } +} diff --git a/third_party/gameq_v3.1/GameQ/Protocols/Teamspeak3.php b/third_party/gameq_v3.1/GameQ/Protocols/Teamspeak3.php new file mode 100644 index 00000000..c66f6a44 --- /dev/null +++ b/third_party/gameq_v3.1/GameQ/Protocols/Teamspeak3.php @@ -0,0 +1,328 @@ +. + */ + +namespace GameQ\Protocols; + +use GameQ\Protocol; +use GameQ\Buffer; +use GameQ\Result; +use GameQ\Server; +use GameQ\Exception\Protocol as Exception; + +/** + * Teamspeak 3 Protocol Class + * + * All values are utf8 encoded upon processing + * + * This code ported from GameQ v1/v2. Credit to original author(s) as I just updated it to + * work within this new system. + * + * @author Austin Bischoff + */ +class Teamspeak3 extends Protocol +{ + + /** + * Array of packets we want to look up. + * Each key should correspond to a defined method in this or a parent class + * + * @type array + */ + protected $packets = [ + self::PACKET_DETAILS => "use port=%d\x0Aserverinfo\x0A", + self::PACKET_PLAYERS => "use port=%d\x0Aclientlist\x0A", + self::PACKET_CHANNELS => "use port=%d\x0Achannellist -topic\x0A", + ]; + + /** + * The transport mode for this protocol is TCP + * + * @type string + */ + protected $transport = self::TRANSPORT_TCP; + + /** + * The query protocol used to make the call + * + * @type string + */ + protected $protocol = 'teamspeak3'; + + /** + * String name of this protocol class + * + * @type string + */ + protected $name = 'teamspeak3'; + + /** + * Longer string name of this protocol class + * + * @type string + */ + protected $name_long = "Teamspeak 3"; + + /** + * The client join link + * + * @type string + */ + protected $join_link = "ts3server://%s?port=%d"; + + /** + * Normalize settings for this protocol + * + * @type array + */ + protected $normalize = [ + // General + 'general' => [ + 'dedicated' => 'dedicated', + 'hostname' => 'virtualserver_name', + 'password' => 'virtualserver_flag_password', + 'numplayers' => 'numplayers', + 'maxplayers' => 'virtualserver_maxclients', + ], + // Player + 'player' => [ + 'id' => 'clid', + 'team' => 'cid', + 'name' => 'client_nickname', + ], + // Team + 'team' => [ + 'id' => 'cid', + 'name' => 'channel_name', + ], + ]; + + /** + * Before we send off the queries we need to update the packets + * + * @param \GameQ\Server $server + * + * @throws \GameQ\Exception\Protocol + */ + public function beforeSend(Server $server) + { + + // Check to make sure we have a query_port because it is required + if (!isset($this->options[Server::SERVER_OPTIONS_QUERY_PORT]) + || empty($this->options[Server::SERVER_OPTIONS_QUERY_PORT]) + ) { + throw new Exception(__METHOD__ . " Missing required setting '" . Server::SERVER_OPTIONS_QUERY_PORT . "'."); + } + + // Let's loop the packets and set the proper pieces + foreach ($this->packets as $packet_type => $packet) { + // Update with the client port for the server + $this->packets[$packet_type] = sprintf($packet, $server->portClient()); + } + } + + /** + * Process the response + * + * @return array + * @throws \GameQ\Exception\Protocol + */ + public function processResponse() + { + + // Make a new buffer out of all of the packets + $buffer = new Buffer(implode('', $this->packets_response)); + + // Check the header TS3 + if (($header = trim($buffer->readString("\n"))) !== 'TS3') { + throw new Exception(__METHOD__ . " Expected header '{$header}' does not match expected 'TS3'."); + } + + // Convert all the escaped characters + $raw = str_replace( + [ + '\\\\', // Translate escaped \ + '\\/', // Translate escaped / + ], + [ + '\\', + '/', + ], + $buffer->getBuffer() + ); + + // Explode the sections and filter to remove empty, junk ones + $sections = array_filter(explode("\n", $raw), function ($value) { + + $value = trim($value); + + // Not empty string or a message response for "error id=\d" + return !empty($value) && substr($value, 0, 5) !== 'error'; + }); + + // Trim up the values to remove extra whitespace + $sections = array_map('trim', $sections); + + // Set the result to a new result instance + $result = new Result(); + + // Iterate over the sections and offload the parsing + foreach ($sections as $section) { + // Grab a snip of the data so we can figure out what it is + $check = substr(trim($section), 0, 4); + + // Use the first part of the response to figure out where we need to go + if ($check == 'virt') { + // Server info + $this->processDetails($section, $result); + } elseif ($check == 'cid=') { + // Channels + $this->processChannels($section, $result); + } elseif ($check == 'clid') { + // Clients (players) + $this->processPlayers($section, $result); + } + } + + unset($buffer, $sections, $section, $check); + + return $result->fetch(); + } + + /* + * Internal methods + */ + + /** + * Process the properties of the data. + * + * Takes data in "key1=value1 key2=value2 ..." and processes it into a usable format + * + * @param $data + * + * @return array + */ + protected function processProperties($data) + { + + // Will hold the properties we are sending back + $properties = []; + + // All of these are split on space + $items = explode(' ', $data); + + // Iterate over the items + foreach ($items as $item) { + // Explode and make sure we always have 2 items in the array + list($key, $value) = array_pad(explode('=', $item, 2), 2, ''); + + // Convert spaces and other character changes + $properties[$key] = utf8_encode(str_replace( + [ + '\\s', // Translate spaces + ], + [ + ' ', + ], + $value + )); + } + + return $properties; + } + + /** + * Handles processing the details data into a usable format + * + * @param string $data + * @param \GameQ\Result $result + */ + protected function processDetails($data, Result &$result) + { + + // Offload the parsing for these values + $properties = $this->processProperties($data); + + // Always dedicated + $result->add('dedicated', 1); + + // Iterate over the properties + foreach ($properties as $key => $value) { + $result->add($key, $value); + } + + // We need to manually figure out the number of players + $result->add( + 'numplayers', + ($properties['virtualserver_clientsonline'] - $properties['virtualserver_queryclientsonline']) + ); + + unset($data, $properties, $key, $value); + } + + /** + * Process the channel listing + * + * @param string $data + * @param \GameQ\Result $result + */ + protected function processChannels($data, Result &$result) + { + + // We need to split the data at the pipe + $channels = explode('|', $data); + + // Iterate over the channels + foreach ($channels as $channel) { + // Offload the parsing for these values + $properties = $this->processProperties($channel); + + // Iterate over the properties + foreach ($properties as $key => $value) { + $result->addTeam($key, $value); + } + } + + unset($data, $channel, $channels, $properties, $key, $value); + } + + /** + * Process the user listing + * + * @param string $data + * @param \GameQ\Result $result + */ + protected function processPlayers($data, Result &$result) + { + + // We need to split the data at the pipe + $players = explode('|', $data); + + // Iterate over the channels + foreach ($players as $player) { + // Offload the parsing for these values + $properties = $this->processProperties($player); + + // Iterate over the properties + foreach ($properties as $key => $value) { + $result->addPlayer($key, $value); + } + } + + unset($data, $player, $players, $properties, $key, $value); + } +} diff --git a/third_party/gameq_v3.1/GameQ/Protocols/Teeworlds.php b/third_party/gameq_v3.1/GameQ/Protocols/Teeworlds.php new file mode 100644 index 00000000..1bdaa472 --- /dev/null +++ b/third_party/gameq_v3.1/GameQ/Protocols/Teeworlds.php @@ -0,0 +1,181 @@ +. + */ + +namespace GameQ\Protocols; + +use GameQ\Protocol; +use GameQ\Buffer; +use GameQ\Result; +use GameQ\Exception\Protocol as Exception; + +/** + * Teeworlds Protocol class + * + * Only supports versions > 0.5 + * + * @author Austin Bischoff + * @author Marcel Bößendörfer + */ +class Teeworlds extends Protocol +{ + + /** + * Array of packets we want to look up. + * Each key should correspond to a defined method in this or a parent class + * + * @type array + */ + protected $packets = [ + self::PACKET_ALL => "\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\x67\x69\x65\x33\x05", + // 0.5 Packet (not compatible, maybe some wants to implement "Teeworldsold") + //self::PACKET_STATUS => "\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFFgief", + ]; + + /** + * Use the response flag to figure out what method to run + * + * @type array + */ + protected $responses = [ + "\xff\xff\xff\xff\xff\xff\xff\xff\xff\xffinf35" => "processAll", + ]; + + /** + * The query protocol used to make the call + * + * @type string + */ + protected $protocol = 'teeworlds'; + + /** + * String name of this protocol class + * + * @type string + */ + protected $name = 'teeworlds'; + + /** + * Longer string name of this protocol class + * + * @type string + */ + protected $name_long = "Teeworlds Server"; + + /** + * The client join link + * + * @type string + */ + protected $join_link = "steam://connect/%s:%d/"; + + /** + * Normalize settings for this protocol + * + * @type array + */ + protected $normalize = [ + // General + 'general' => [ + // target => source + 'dedicated' => 'dedicated', + 'hostname' => 'hostname', + 'mapname' => 'map', + 'maxplayers' => 'num_players_total', + ], + // Individual + 'player' => [ + 'name' => 'name', + 'score' => 'score', + ], + ]; + + /** + * Process the response + * + * @return array + * @throws Exception + */ + public function processResponse() + { + // Holds the results + $results = []; + + // Iterate over the packets + foreach ($this->packets_response as $response) { + // Make a buffer + $buffer = new Buffer($response); + + // Grab the header + $header = $buffer->readString(); + + // Figure out which packet response this is + if (!array_key_exists($header, $this->responses)) { + throw new Exception(__METHOD__ . " response type '" . bin2hex($header) . "' is not valid"); + } + + // Now we need to call the proper method + $results = array_merge( + $results, + call_user_func_array([$this, $this->responses[$header]], [$buffer]) + ); + } + + unset($buffer); + + return $results; + } + + /** + * Handle processing all of the data returned + * + * @param Buffer $buffer + * + * @return array + */ + protected function processAll(Buffer $buffer) + { + // Set the result to a new result instance + $result = new Result(); + + // Always dedicated + $result->add('dedicated', 1); + + $result->add('version', $buffer->readString()); + $result->add('hostname', $buffer->readString()); + $result->add('map', $buffer->readString()); + $result->add('game_descr', $buffer->readString()); + $result->add('flags', $buffer->readString()); // not sure about that + $result->add('num_players', $buffer->readString()); + $result->add('maxplayers', $buffer->readString()); + $result->add('num_players_total', $buffer->readString()); + $result->add('maxplayers_total', $buffer->readString()); + + // Players + while ($buffer->getLength()) { + $result->addPlayer('name', $buffer->readString()); + $result->addPlayer('clan', $buffer->readString()); + $result->addPlayer('flag', $buffer->readString()); + $result->addPlayer('score', $buffer->readString()); + $result->addPlayer('team', $buffer->readString()); + } + + unset($buffer); + + return $result->fetch(); + } +} diff --git a/third_party/gameq_v3.1/GameQ/Protocols/Terraria.php b/third_party/gameq_v3.1/GameQ/Protocols/Terraria.php new file mode 100644 index 00000000..d9455ef5 --- /dev/null +++ b/third_party/gameq_v3.1/GameQ/Protocols/Terraria.php @@ -0,0 +1,59 @@ +. + */ + +namespace GameQ\Protocols; + +/** + * Class Terraria + * + * @package GameQ\Protocols + * + * @author Austin Bischoff + */ +class Terraria extends Tshock +{ + + /** + * String name of this protocol class + * + * @type string + */ + protected $name = 'terraria'; + + /** + * Longer string name of this protocol class + * + * @type string + */ + protected $name_long = "Terraria"; + + /** + * query_port = client_port + 101 + * 7878 = 7777 + 101 + * + * @type int + */ + protected $port_diff = 101; + + /** + * The client join link + * + * @type string + */ + protected $join_link = "steam://connect/%s:%d/"; +} diff --git a/third_party/gameq_v3.1/GameQ/Protocols/Tf2.php b/third_party/gameq_v3.1/GameQ/Protocols/Tf2.php new file mode 100644 index 00000000..e08411b7 --- /dev/null +++ b/third_party/gameq_v3.1/GameQ/Protocols/Tf2.php @@ -0,0 +1,42 @@ +. + */ + +namespace GameQ\Protocols; + +/** + * Class Tf2 + * + * @package GameQ\Protocols + * @author Austin Bischoff + */ +class Tf2 extends Source +{ + /** + * String name of this protocol class + * + * @type string + */ + protected $name = 'tf2'; + + /** + * Longer string name of this protocol class + * + * @type string + */ + protected $name_long = "Team Fortress 2"; +} diff --git a/third_party/gameq_v3.1/GameQ/Protocols/Theforrest.php b/third_party/gameq_v3.1/GameQ/Protocols/Theforrest.php new file mode 100644 index 00000000..975c3f6f --- /dev/null +++ b/third_party/gameq_v3.1/GameQ/Protocols/Theforrest.php @@ -0,0 +1,50 @@ +. + */ + +namespace GameQ\Protocols; + +/** + * Class Theforrest + * + * @package GameQ\Protocols + * @author Austin Bischoff + */ +class Theforrest extends Source +{ + + /** + * String name of this protocol class + * + * @type string + */ + protected $name = 'theforrest'; + + /** + * Longer string name of this protocol class + * + * @type string + */ + protected $name_long = "The Forrest"; + + /** + * query_port = client_port + 1 + * + * @type int + */ + protected $port_diff = 1; +} diff --git a/third_party/gameq_v3.1/GameQ/Protocols/Tibia.php b/third_party/gameq_v3.1/GameQ/Protocols/Tibia.php new file mode 100644 index 00000000..8702bfa3 --- /dev/null +++ b/third_party/gameq_v3.1/GameQ/Protocols/Tibia.php @@ -0,0 +1,142 @@ +. + */ + +namespace GameQ\Protocols; + +use GameQ\Protocol; +use GameQ\Buffer; +use GameQ\Result; +use GameQ\Exception\Protocol as Exception; + +/** + * Tibia Protocol Class + * + * Tibia server query protocol class + * + * Credit to Ahmad Fatoum for providing Perl based querying as a roadmap + * + * @author Yive + * @author Austin Bischoff + */ +class Tibia extends Protocol +{ + + /** + * Array of packets we want to query. + * + * @type array + */ + protected $packets = [ + self::PACKET_STATUS => "\x06\x00\xFF\xFF\x69\x6E\x66\x6F", + ]; + + /** + * The transport mode for this protocol is TCP + * + * @type string + */ + protected $transport = self::TRANSPORT_TCP; + + /** + * The query protocol used to make the call + * + * @type string + */ + protected $protocol = 'tibia'; + + /** + * String name of this protocol class + * + * @type string + */ + protected $name = 'tibia'; + + /** + * Longer string name of this protocol class + * + * @type string + */ + protected $name_long = "Tibia"; + + /** + * The client join link + * + * @type string + */ + protected $join_link = "otserv://%s/%d/"; + + /** + * Normalize settings for this protocol + * + * @type array + */ + protected $normalize = [ + // General + 'general' => [ + // target => source + 'dedicated' => 'dedicated', + 'gametype' => 'server', + 'hostname' => 'servername', + 'motd' => 'motd', + 'maxplayers' => 'players_max', + 'numplayers' => 'players_online', + 'map' => 'map_name', + ], + ]; + + /** + * Process the response for the Tibia server + * + * @return array + * @throws \GameQ\Exception\Protocol + */ + public function processResponse() + { + // Merge the response packets + $xmlString = implode('', $this->packets_response); + + // Check to make sure this is will decode into a valid XML Document + if (($xmlDoc = @simplexml_load_string($xmlString)) === false) { + throw new Exception(__METHOD__ . " Unable to load XML string."); + } + + // Set the result to a new result instance + $result = new Result(); + + // All servers are dedicated as far as I can tell + $result->add('dedicated', 1); + + // Iterate over the info + foreach (['serverinfo', 'owner', 'map', 'npcs', 'monsters', 'players'] as $property) { + foreach ($xmlDoc->{$property}->attributes() as $key => $value) { + if (!in_array($property, ['serverinfo'])) { + $key = $property . '_' . $key; + } + + // Add the result + $result->add($key, (string)$value); + } + } + + $result->add("motd", (string)$xmlDoc->motd); + + unset($xmlDoc, $xmlDoc); + + return $result->fetch(); + } +} diff --git a/third_party/gameq_v3.1/GameQ/Protocols/Tshock.php b/third_party/gameq_v3.1/GameQ/Protocols/Tshock.php new file mode 100644 index 00000000..551a09e4 --- /dev/null +++ b/third_party/gameq_v3.1/GameQ/Protocols/Tshock.php @@ -0,0 +1,157 @@ +. + */ +namespace GameQ\Protocols; + +use GameQ\Exception\Protocol as Exception; +use GameQ\Result; + +/** + * Tshock Protocol Class + * + * Result from this call should be a header + JSON response + * + * References: + * - https://tshock.atlassian.net/wiki/display/TSHOCKPLUGINS/REST+API+Endpoints#RESTAPIEndpoints-/status + * - http://tshock.co/xf/index.php?threads/rest-tshock-server-status-image.430/ + * + * Special thanks to intradox and Ruok2bu for game & protocol references + * + * @author Austin Bischoff + */ +class Tshock extends Http +{ + /** + * Packets to send + * + * @var array + */ + protected $packets = [ + self::PACKET_STATUS => "GET /v2/server/status?players=true&rules=true HTTP/1.0\r\nAccept: */*\r\n\r\n", + ]; + + /** + * The protocol being used + * + * @var string + */ + protected $protocol = 'tshock'; + + /** + * String name of this protocol class + * + * @var string + */ + protected $name = 'tshock'; + + /** + * Longer string name of this protocol class + * + * @var string + */ + protected $name_long = "Tshock"; + + /** + * Normalize some items + * + * @var array + */ + protected $normalize = [ + // General + 'general' => [ + // target => source + 'dedicated' => 'dedicated', + 'hostname' => 'hostname', + 'mapname' => 'world', + 'maxplayers' => 'maxplayers', + 'numplayers' => 'numplayers', + 'password' => 'password', + ], + // Individual + 'player' => [ + 'name' => 'nickname', + 'team' => 'team', + ], + ]; + + /** + * Process the response + * + * @return array + * @throws Exception + */ + public function processResponse() + { + if (empty($this->packets_response)) { + return []; + } + + // Implode and rip out the JSON + preg_match('/\{(.*)\}/ms', implode('', $this->packets_response), $matches); + + // Return should be JSON, let's validate + if (!isset($matches[0]) || ($json = json_decode($matches[0])) === null) { + throw new Exception("JSON response from Tshock protocol is invalid."); + } + + // Check the status response + if ($json->status != 200) { + throw new Exception("JSON status from Tshock protocol response was '{$json->status}', expected '200'."); + } + + $result = new Result(); + + // Server is always dedicated + $result->add('dedicated', 1); + + // Add server items + $result->add('hostname', $json->name); + $result->add('game_port', $json->port); + $result->add('serverversion', $json->serverversion); + $result->add('world', $json->world); + $result->add('uptime', $json->uptime); + $result->add('password', (int)$json->serverpassword); + $result->add('numplayers', $json->playercount); + $result->add('maxplayers', $json->maxplayers); + + // Parse players + foreach ($json->players as $player) { + $result->addPlayer('nickname', $player->nickname); + $result->addPlayer('username', $player->username); + $result->addPlayer('group', $player->group); + $result->addPlayer('active', (int)$player->active); + $result->addPlayer('state', $player->state); + $result->addPlayer('team', $player->team); + } + + // Make rules into simple array + $rules = []; + + // Parse rules + foreach ($json->rules as $rule => $value) { + // Add rule but convert boolean into int (0|1) + $rules[$rule] = (is_bool($value)) ? (int)$value : $value; + } + + // Add rules + $result->add('rules', $rules); + + unset($rules, $rule, $player, $value); + + return $result->fetch(); + } +} diff --git a/third_party/gameq_v3.1/GameQ/Protocols/Unreal2.php b/third_party/gameq_v3.1/GameQ/Protocols/Unreal2.php new file mode 100644 index 00000000..0ef06757 --- /dev/null +++ b/third_party/gameq_v3.1/GameQ/Protocols/Unreal2.php @@ -0,0 +1,246 @@ +. + */ + +namespace GameQ\Protocols; + +use GameQ\Protocol; +use GameQ\Buffer; +use GameQ\Result; +use GameQ\Exception\Protocol as Exception; + +/** + * Unreal 2 Protocol class + * + * @author Austin Bischoff + */ +class Unreal2 extends Protocol +{ + + /** + * Array of packets we want to look up. + * Each key should correspond to a defined method in this or a parent class + * + * @type array + */ + protected $packets = [ + self::PACKET_DETAILS => "\x79\x00\x00\x00\x00", + self::PACKET_RULES => "\x79\x00\x00\x00\x01", + self::PACKET_PLAYERS => "\x79\x00\x00\x00\x02", + ]; + + /** + * Use the response flag to figure out what method to run + * + * @type array + */ + protected $responses = [ + "\x80\x00\x00\x00\x00" => "processDetails", // 0 + "\x80\x00\x00\x00\x01" => "processRules", // 1 + "\x80\x00\x00\x00\x02" => "processPlayers", // 2 + ]; + + /** + * The query protocol used to make the call + * + * @type string + */ + protected $protocol = 'unreal2'; + + /** + * String name of this protocol class + * + * @type string + */ + protected $name = 'unreal2'; + + /** + * Longer string name of this protocol class + * + * @type string + */ + protected $name_long = "Unreal 2"; + + /** + * Normalize settings for this protocol + * + * @type array + */ + protected $normalize = [ + // General + 'general' => [ + // target => source + 'dedicated' => 'ServerMode', + 'gametype' => 'gametype', + 'hostname' => 'servername', + 'mapname' => 'mapname', + 'maxplayers' => 'maxplayers', + 'numplayers' => 'numplayers', + 'password' => 'password', + ], + // Individual + 'player' => [ + 'name' => 'name', + 'score' => 'score', + ], + ]; + + /** + * Process the response + * + * @return array + * @throws \GameQ\Exception\Protocol + */ + public function processResponse() + { + + // Will hold the packets after sorting + $packets = []; + + // We need to pre-sort these for split packets so we can do extra work where needed + foreach ($this->packets_response as $response) { + $buffer = new Buffer($response); + + // Pull out the header + $header = $buffer->read(5); + + // Add the packet to the proper section, we will combine later + $packets[$header][] = $buffer->getBuffer(); + } + + unset($buffer); + + $results = []; + + // Now let's iterate and process + foreach ($packets as $header => $packetGroup) { + // Figure out which packet response this is + if (!array_key_exists($header, $this->responses)) { + throw new Exception(__METHOD__ . " response type '" . bin2hex($header) . "' is not valid"); + } + + // Now we need to call the proper method + $results = array_merge( + $results, + call_user_func_array([$this, $this->responses[$header]], [new Buffer(implode($packetGroup))]) + ); + } + + unset($packets); + + return $results; + } + + /* + * Internal methods + */ + + /** + * Handles processing the details data into a usable format + * + * @param \GameQ\Buffer $buffer + * + * @return mixed + * @throws \GameQ\Exception\Protocol + */ + protected function processDetails(Buffer $buffer) + { + + // Set the result to a new result instance + $result = new Result(); + + $result->add('serverid', $buffer->readInt32()); // 0 + $result->add('serverip', $buffer->readPascalString(1)); // empty + $result->add('gameport', $buffer->readInt32()); + $result->add('queryport', $buffer->readInt32()); // 0 + $result->add('servername', utf8_encode($buffer->readPascalString(1))); + $result->add('mapname', utf8_encode($buffer->readPascalString(1))); + $result->add('gametype', $buffer->readPascalString(1)); + $result->add('numplayers', $buffer->readInt32()); + $result->add('maxplayers', $buffer->readInt32()); + $result->add('ping', $buffer->readInt32()); // 0 + + unset($buffer); + + return $result->fetch(); + } + + /** + * Handles processing the player data into a usable format + * + * @param \GameQ\Buffer $buffer + * + * @return mixed + */ + protected function processPlayers(Buffer $buffer) + { + + // Set the result to a new result instance + $result = new Result(); + + // Parse players + while ($buffer->getLength()) { + // Player id + if (($id = $buffer->readInt32()) !== 0) { + // Add the results + $result->addPlayer('id', $id); + $result->addPlayer('name', utf8_encode($buffer->readPascalString(1))); + $result->addPlayer('ping', $buffer->readInt32()); + $result->addPlayer('score', $buffer->readInt32()); + + // Skip the next 4, unsure what they are for + $buffer->skip(4); + } + } + + unset($buffer, $id); + + return $result->fetch(); + } + + /** + * Handles processing the rules data into a usable format + * + * @param \GameQ\Buffer $buffer + * + * @return mixed + */ + protected function processRules(Buffer $buffer) + { + + // Set the result to a new result instance + $result = new Result(); + + // Named values + $inc = -1; + while ($buffer->getLength()) { + // Grab the key + $key = $buffer->readPascalString(1); + + // Make sure mutators don't overwrite each other + if ($key === 'Mutator') { + $key .= ++$inc; + } + + $result->add(strtolower($key), utf8_encode($buffer->readPascalString(1))); + } + + unset($buffer); + + return $result->fetch(); + } +} diff --git a/third_party/gameq_v3.1/GameQ/Protocols/Unturned.php b/third_party/gameq_v3.1/GameQ/Protocols/Unturned.php new file mode 100644 index 00000000..4829b37a --- /dev/null +++ b/third_party/gameq_v3.1/GameQ/Protocols/Unturned.php @@ -0,0 +1,49 @@ +. + */ + +namespace GameQ\Protocols; + +/** + * Unturned Protocol Class + * + * @package GameQ\Protocols + * @author Austin Bischoff + */ +class Unturned extends Source +{ + /** + * String name of this protocol class + * + * @type string + */ + protected $name = 'unturned'; + + /** + * Longer string name of this protocol class + * + * @type string + */ + protected $name_long = "Unturned"; + + /** + * query_port = client_port + 1 + * + * @type int + */ + protected $port_diff = 1; +} diff --git a/third_party/gameq_v3.1/GameQ/Protocols/Urbanterror.php b/third_party/gameq_v3.1/GameQ/Protocols/Urbanterror.php new file mode 100644 index 00000000..682f91e6 --- /dev/null +++ b/third_party/gameq_v3.1/GameQ/Protocols/Urbanterror.php @@ -0,0 +1,49 @@ +. + */ + +namespace GameQ\Protocols; + +/** + * Urban Terror Class + * + * @package GameQ\Protocols + * @author Austin Bischoff + */ +class Urbanterror extends Quake3 +{ + /** + * String name of this protocol class + * + * @type string + */ + protected $name = 'urbanterror'; + + /** + * Longer string name of this protocol class + * + * @type string + */ + protected $name_long = "Urban Terror"; + + /** + * The client join link + * + * @type string + */ + protected $join_link = "urt://%s:%d/"; +} diff --git a/third_party/gameq_v3.1/GameQ/Protocols/Ut.php b/third_party/gameq_v3.1/GameQ/Protocols/Ut.php new file mode 100644 index 00000000..75722ce1 --- /dev/null +++ b/third_party/gameq_v3.1/GameQ/Protocols/Ut.php @@ -0,0 +1,73 @@ +. + */ + +namespace GameQ\Protocols; + +/** + * Unreal Tournament Protocol Class + * + * @author Austin Bischoff + */ +class Ut extends Gamespy +{ + + /** + * String name of this protocol class + * + * @type string + */ + protected $name = 'ut'; + + /** + * Longer string name of this protocol class + * + * @type string + */ + protected $name_long = "Unreal Tournament"; + + /** + * query_port = client_port + 1 + * + * @type int + */ + protected $port_diff = 1; + + /** + * Normalize settings for this protocol + * + * @type array + */ + protected $normalize = [ + // General + 'general' => [ + // target => source + 'dedicated' => 'dedicated', + 'gametype' => 'gametype', + 'hostname' => 'hostname', + 'mapname' => 'mapname', + 'maxplayers' => 'maxplayers', + 'numplayers' => 'numplayers', + 'password' => 'password', + ], + // Individual + 'player' => [ + 'name' => 'name', + 'score' => 'frags', + ], + ]; +} diff --git a/third_party/gameq_v3.1/GameQ/Protocols/Ut2004.php b/third_party/gameq_v3.1/GameQ/Protocols/Ut2004.php new file mode 100644 index 00000000..953089f9 --- /dev/null +++ b/third_party/gameq_v3.1/GameQ/Protocols/Ut2004.php @@ -0,0 +1,42 @@ +. + */ + +namespace GameQ\Protocols; + +/** + * Unreal Tournament 2004 Protocol Class + * + * @author Austin Bischoff + */ +class Ut2004 extends Unreal2 +{ + + /** + * String name of this protocol class + * + * @type string + */ + protected $name = 'ut2004'; + + /** + * Longer string name of this protocol class + * + * @type string + */ + protected $name_long = "Unreal Tournament 2004"; +} diff --git a/third_party/gameq_v3.1/GameQ/Protocols/Ut3.php b/third_party/gameq_v3.1/GameQ/Protocols/Ut3.php new file mode 100644 index 00000000..b55cc340 --- /dev/null +++ b/third_party/gameq_v3.1/GameQ/Protocols/Ut3.php @@ -0,0 +1,133 @@ +. + */ + +namespace GameQ\Protocols; + +/** + * Unreal Tournament 3 Protocol Class + * + * Note: The response from UT3 appears to not be consistent. Many times packets are incomplete or there are extra + * "echoes" in the responses. This may cause issues like odd characters showing up in the keys for the player and team + * array responses. Not sure much can be done about it. + * + * @author Austin Bischoff + */ +class Ut3 extends Gamespy3 +{ + + /** + * String name of this protocol class + * + * @type string + */ + protected $name = 'ut3'; + + /** + * Longer string name of this protocol class + * + * @type string + */ + protected $name_long = "Unreal Tournament 3"; + + /** + * Normalize settings for this protocol + * + * @type array + */ + protected $normalize = [ + // General + 'general' => [ + 'dedicated' => 'bIsDedicated', + 'hostname' => 'hostname', + 'numplayers' => 'numplayers', + ], + ]; + + /** + * Overload the response process so we can make some changes + * + * @return array + */ + public function processResponse() + { + + // Grab the result from the parent + /** @type array $result */ + $result = parent::processResponse(); + + // Move some stuff around + $this->renameResult($result, 'OwningPlayerName', 'hostname'); + $this->renameResult($result, 'p1073741825', 'mapname'); + $this->renameResult($result, 'p1073741826', 'gametype'); + $this->renameResult($result, 'p1073741827', 'servername'); + $this->renameResult($result, 'p1073741828', 'custom_mutators'); + $this->renameResult($result, 'gamemode', 'open'); + $this->renameResult($result, 's32779', 'gamemode'); + $this->renameResult($result, 's0', 'bot_skill'); + $this->renameResult($result, 's6', 'pure_server'); + $this->renameResult($result, 's7', 'password'); + $this->renameResult($result, 's8', 'vs_bots'); + $this->renameResult($result, 's10', 'force_respawn'); + $this->renameResult($result, 'p268435704', 'frag_limit'); + $this->renameResult($result, 'p268435705', 'time_limit'); + $this->renameResult($result, 'p268435703', 'numbots'); + $this->renameResult($result, 'p268435717', 'stock_mutators'); + + // Put custom mutators into an array + if (isset($result['custom_mutators'])) { + $result['custom_mutators'] = explode("\x1c", $result['custom_mutators']); + } + + // Delete some unknown stuff + $this->deleteResult($result, ['s1', 's9', 's11', 's12', 's13', 's14']); + + // Return the result + return $result; + } + + /** + * Dirty hack to rename result entries into something more useful + * + * @param array $result + * @param string $old + * @param string $new + */ + protected function renameResult(array &$result, $old, $new) + { + + // Check to see if the old item is there + if (isset($result[$old])) { + $result[$new] = $result[$old]; + unset($result[$old]); + } + } + + /** + * Dirty hack to delete result items + * + * @param array $result + * @param array $array + */ + protected function deleteResult(array &$result, array $array) + { + + foreach ($array as $key) { + unset($result[$key]); + } + } +} diff --git a/third_party/gameq_v3.1/GameQ/Protocols/Valheim.php b/third_party/gameq_v3.1/GameQ/Protocols/Valheim.php new file mode 100644 index 00000000..18469229 --- /dev/null +++ b/third_party/gameq_v3.1/GameQ/Protocols/Valheim.php @@ -0,0 +1,48 @@ +. + */ + +namespace GameQ\Protocols; + +/** + * Valheim Protocol Class + * + * @package GameQ\Protocols + */ +class Valheim extends Source +{ + /** + * String name of this protocol class + * + * @type string + */ + protected $name = 'valheim'; + + /** + * Longer string name of this protocol class + * + * @type string + */ + protected $name_long = "Valheim"; + + /** + * query_port = client_port + 1 + * + * @type int + */ + protected $port_diff = 1; +} diff --git a/third_party/gameq_v3.1/GameQ/Protocols/Ventrilo.php b/third_party/gameq_v3.1/GameQ/Protocols/Ventrilo.php new file mode 100644 index 00000000..6986bedc --- /dev/null +++ b/third_party/gameq_v3.1/GameQ/Protocols/Ventrilo.php @@ -0,0 +1,877 @@ +. + */ + +namespace GameQ\Protocols; + +use GameQ\Protocol; +use GameQ\Result; +use GameQ\Exception\Protocol as Exception; + +/** + * Ventrilo Protocol Class + * + * Note that a password is not required for versions >= 3.0.3 + * + * All values are utf8 encoded upon processing + * + * This code ported from GameQ v1/v2. Credit to original author(s) as I just updated it to + * work within this new system. + * + * @author Austin Bischoff + */ +class Ventrilo extends Protocol +{ + + /** + * Array of packets we want to look up. + * Each key should correspond to a defined method in this or a parent class + * + * @type array + */ + protected $packets = [ + self::PACKET_ALL => + "V\xc8\xf4\xf9`\xa2\x1e\xa5M\xfb\x03\xccQN\xa1\x10\x95\xaf\xb2g\x17g\x812\xfbW\xfd\x8e\xd2\x22r\x034z\xbb\x98", + ]; + + /** + * The query protocol used to make the call + * + * @type string + */ + protected $protocol = 'ventrilo'; + + /** + * String name of this protocol class + * + * @type string + */ + protected $name = 'ventrilo'; + + /** + * Longer string name of this protocol class + * + * @type string + */ + protected $name_long = "Ventrilo"; + + /** + * The client join link + * + * @type string + */ + protected $join_link = "ventrilo://%s:%d/"; + + /** + * Normalize settings for this protocol + * + * @type array + */ + protected $normalize = [ + // General + 'general' => [ + 'dedicated' => 'dedicated', + 'password' => 'auth', + 'hostname' => 'name', + 'numplayers' => 'clientcount', + 'maxplayers' => 'maxclients', + ], + // Player + 'player' => [ + 'team' => 'cid', + 'name' => 'name', + ], + // Team + 'team' => [ + 'id' => 'cid', + 'name' => 'name', + ], + ]; + + /** + * Encryption table for the header + * + * @type array + */ + private $head_encrypt_table = [ + 0x80, + 0xe5, + 0x0e, + 0x38, + 0xba, + 0x63, + 0x4c, + 0x99, + 0x88, + 0x63, + 0x4c, + 0xd6, + 0x54, + 0xb8, + 0x65, + 0x7e, + 0xbf, + 0x8a, + 0xf0, + 0x17, + 0x8a, + 0xaa, + 0x4d, + 0x0f, + 0xb7, + 0x23, + 0x27, + 0xf6, + 0xeb, + 0x12, + 0xf8, + 0xea, + 0x17, + 0xb7, + 0xcf, + 0x52, + 0x57, + 0xcb, + 0x51, + 0xcf, + 0x1b, + 0x14, + 0xfd, + 0x6f, + 0x84, + 0x38, + 0xb5, + 0x24, + 0x11, + 0xcf, + 0x7a, + 0x75, + 0x7a, + 0xbb, + 0x78, + 0x74, + 0xdc, + 0xbc, + 0x42, + 0xf0, + 0x17, + 0x3f, + 0x5e, + 0xeb, + 0x74, + 0x77, + 0x04, + 0x4e, + 0x8c, + 0xaf, + 0x23, + 0xdc, + 0x65, + 0xdf, + 0xa5, + 0x65, + 0xdd, + 0x7d, + 0xf4, + 0x3c, + 0x4c, + 0x95, + 0xbd, + 0xeb, + 0x65, + 0x1c, + 0xf4, + 0x24, + 0x5d, + 0x82, + 0x18, + 0xfb, + 0x50, + 0x86, + 0xb8, + 0x53, + 0xe0, + 0x4e, + 0x36, + 0x96, + 0x1f, + 0xb7, + 0xcb, + 0xaa, + 0xaf, + 0xea, + 0xcb, + 0x20, + 0x27, + 0x30, + 0x2a, + 0xae, + 0xb9, + 0x07, + 0x40, + 0xdf, + 0x12, + 0x75, + 0xc9, + 0x09, + 0x82, + 0x9c, + 0x30, + 0x80, + 0x5d, + 0x8f, + 0x0d, + 0x09, + 0xa1, + 0x64, + 0xec, + 0x91, + 0xd8, + 0x8a, + 0x50, + 0x1f, + 0x40, + 0x5d, + 0xf7, + 0x08, + 0x2a, + 0xf8, + 0x60, + 0x62, + 0xa0, + 0x4a, + 0x8b, + 0xba, + 0x4a, + 0x6d, + 0x00, + 0x0a, + 0x93, + 0x32, + 0x12, + 0xe5, + 0x07, + 0x01, + 0x65, + 0xf5, + 0xff, + 0xe0, + 0xae, + 0xa7, + 0x81, + 0xd1, + 0xba, + 0x25, + 0x62, + 0x61, + 0xb2, + 0x85, + 0xad, + 0x7e, + 0x9d, + 0x3f, + 0x49, + 0x89, + 0x26, + 0xe5, + 0xd5, + 0xac, + 0x9f, + 0x0e, + 0xd7, + 0x6e, + 0x47, + 0x94, + 0x16, + 0x84, + 0xc8, + 0xff, + 0x44, + 0xea, + 0x04, + 0x40, + 0xe0, + 0x33, + 0x11, + 0xa3, + 0x5b, + 0x1e, + 0x82, + 0xff, + 0x7a, + 0x69, + 0xe9, + 0x2f, + 0xfb, + 0xea, + 0x9a, + 0xc6, + 0x7b, + 0xdb, + 0xb1, + 0xff, + 0x97, + 0x76, + 0x56, + 0xf3, + 0x52, + 0xc2, + 0x3f, + 0x0f, + 0xb6, + 0xac, + 0x77, + 0xc4, + 0xbf, + 0x59, + 0x5e, + 0x80, + 0x74, + 0xbb, + 0xf2, + 0xde, + 0x57, + 0x62, + 0x4c, + 0x1a, + 0xff, + 0x95, + 0x6d, + 0xc7, + 0x04, + 0xa2, + 0x3b, + 0xc4, + 0x1b, + 0x72, + 0xc7, + 0x6c, + 0x82, + 0x60, + 0xd1, + 0x0d, + ]; + + /** + * Encryption table for the data + * + * @type array + */ + private $data_encrypt_table = [ + 0x82, + 0x8b, + 0x7f, + 0x68, + 0x90, + 0xe0, + 0x44, + 0x09, + 0x19, + 0x3b, + 0x8e, + 0x5f, + 0xc2, + 0x82, + 0x38, + 0x23, + 0x6d, + 0xdb, + 0x62, + 0x49, + 0x52, + 0x6e, + 0x21, + 0xdf, + 0x51, + 0x6c, + 0x76, + 0x37, + 0x86, + 0x50, + 0x7d, + 0x48, + 0x1f, + 0x65, + 0xe7, + 0x52, + 0x6a, + 0x88, + 0xaa, + 0xc1, + 0x32, + 0x2f, + 0xf7, + 0x54, + 0x4c, + 0xaa, + 0x6d, + 0x7e, + 0x6d, + 0xa9, + 0x8c, + 0x0d, + 0x3f, + 0xff, + 0x6c, + 0x09, + 0xb3, + 0xa5, + 0xaf, + 0xdf, + 0x98, + 0x02, + 0xb4, + 0xbe, + 0x6d, + 0x69, + 0x0d, + 0x42, + 0x73, + 0xe4, + 0x34, + 0x50, + 0x07, + 0x30, + 0x79, + 0x41, + 0x2f, + 0x08, + 0x3f, + 0x42, + 0x73, + 0xa7, + 0x68, + 0xfa, + 0xee, + 0x88, + 0x0e, + 0x6e, + 0xa4, + 0x70, + 0x74, + 0x22, + 0x16, + 0xae, + 0x3c, + 0x81, + 0x14, + 0xa1, + 0xda, + 0x7f, + 0xd3, + 0x7c, + 0x48, + 0x7d, + 0x3f, + 0x46, + 0xfb, + 0x6d, + 0x92, + 0x25, + 0x17, + 0x36, + 0x26, + 0xdb, + 0xdf, + 0x5a, + 0x87, + 0x91, + 0x6f, + 0xd6, + 0xcd, + 0xd4, + 0xad, + 0x4a, + 0x29, + 0xdd, + 0x7d, + 0x59, + 0xbd, + 0x15, + 0x34, + 0x53, + 0xb1, + 0xd8, + 0x50, + 0x11, + 0x83, + 0x79, + 0x66, + 0x21, + 0x9e, + 0x87, + 0x5b, + 0x24, + 0x2f, + 0x4f, + 0xd7, + 0x73, + 0x34, + 0xa2, + 0xf7, + 0x09, + 0xd5, + 0xd9, + 0x42, + 0x9d, + 0xf8, + 0x15, + 0xdf, + 0x0e, + 0x10, + 0xcc, + 0x05, + 0x04, + 0x35, + 0x81, + 0xb2, + 0xd5, + 0x7a, + 0xd2, + 0xa0, + 0xa5, + 0x7b, + 0xb8, + 0x75, + 0xd2, + 0x35, + 0x0b, + 0x39, + 0x8f, + 0x1b, + 0x44, + 0x0e, + 0xce, + 0x66, + 0x87, + 0x1b, + 0x64, + 0xac, + 0xe1, + 0xca, + 0x67, + 0xb4, + 0xce, + 0x33, + 0xdb, + 0x89, + 0xfe, + 0xd8, + 0x8e, + 0xcd, + 0x58, + 0x92, + 0x41, + 0x50, + 0x40, + 0xcb, + 0x08, + 0xe1, + 0x15, + 0xee, + 0xf4, + 0x64, + 0xfe, + 0x1c, + 0xee, + 0x25, + 0xe7, + 0x21, + 0xe6, + 0x6c, + 0xc6, + 0xa6, + 0x2e, + 0x52, + 0x23, + 0xa7, + 0x20, + 0xd2, + 0xd7, + 0x28, + 0x07, + 0x23, + 0x14, + 0x24, + 0x3d, + 0x45, + 0xa5, + 0xc7, + 0x90, + 0xdb, + 0x77, + 0xdd, + 0xea, + 0x38, + 0x59, + 0x89, + 0x32, + 0xbc, + 0x00, + 0x3a, + 0x6d, + 0x61, + 0x4e, + 0xdb, + 0x29, + ]; + + /** + * Process the response + * + * @return array + * @throws \GameQ\Exception\Protocol + */ + public function processResponse() + { + + // We need to decrypt the packets + $decrypted = $this->decryptPackets($this->packets_response); + + // Now let us convert special characters from hex to ascii all at once + $decrypted = preg_replace_callback( + '|%([0-9A-F]{2})|', + function ($matches) { + + // Pack this into ascii + return pack('H*', $matches[1]); + }, + $decrypted + ); + + // Explode into lines + $lines = explode("\n", $decrypted); + + // Set the result to a new result instance + $result = new Result(); + + // Always dedicated + $result->add('dedicated', 1); + + // Defaults + $channelFields = 5; + $playerFields = 7; + + // Iterate over the lines + foreach ($lines as $line) { + // Trim all the outlying space + $line = trim($line); + + // We dont have anything in this line + if (strlen($line) == 0) { + continue; + } + + /** + * Everything is in this format: ITEM: VALUE + * + * Example: + * ... + * MAXCLIENTS: 175 + * VOICECODEC: 3,Speex + * VOICEFORMAT: 31,32 KHz%2C 16 bit%2C 9 Qlty + * UPTIME: 9167971 + * PLATFORM: Linux-i386 + * VERSION: 3.0.6 + * ... + */ + + // Check to see if we have a colon, every line should + if (($colon_pos = strpos($line, ":")) !== false && $colon_pos > 0) { + // Split the line into key/value pairs + list($key, $value) = explode(':', $line, 2); + + // Lower the font of the key + $key = strtolower($key); + + // Trim the value of extra space + $value = trim($value); + + // Switch and offload items as needed + switch ($key) { + case 'client': + $this->processPlayer($value, $playerFields, $result); + break; + + case 'channel': + $this->processChannel($value, $channelFields, $result); + break; + + // Find the number of fields for the channels + case 'channelfields': + $channelFields = count(explode(',', $value)); + break; + + // Find the number of fields for the players + case 'clientfields': + $playerFields = count(explode(',', $value)); + break; + + // By default we just add they key as an item + default: + $result->add($key, utf8_encode($value)); + break; + } + } + } + + unset($decrypted, $line, $lines, $colon_pos, $key, $value); + + return $result->fetch(); + } + + /* + * Internal methods + */ + + /** + * Decrypt the incoming packets + * + * @codeCoverageIgnore + * + * @param array $packets + * + * @return string + * @throws \GameQ\Exception\Protocol + */ + protected function decryptPackets(array $packets = []) + { + + // This will be returned + $decrypted = []; + + foreach ($packets as $packet) { + # Header : + $header = substr($packet, 0, 20); + + $header_items = []; + + $header_key = unpack("n1", $header); + + $key = array_shift($header_key); + + $chars = unpack("C*", substr($header, 2)); + + $a1 = $key & 0xFF; + $a2 = $key >> 8; + + if ($a1 == 0) { + throw new Exception(__METHOD__ . ": Header key is invalid"); + } + + $table = $this->head_encrypt_table; + + $characterCount = count($chars); + + $key = 0; + for ($index = 1; $index <= $characterCount; $index++) { + $chars[$index] -= ($table[$a2] + (($index - 1) % 5)) & 0xFF; + $a2 = ($a2 + $a1) & 0xFF; + if (($index % 2) == 0) { + $short_array = unpack("n1", pack("C2", $chars[$index - 1], $chars[$index])); + $header_items[$key] = $short_array[1]; + ++$key; + } + } + + $header_items = array_combine([ + 'zero', + 'cmd', + 'id', + 'totlen', + 'len', + 'totpck', + 'pck', + 'datakey', + 'crc', + ], $header_items); + + // Check to make sure the number of packets match + if ($header_items['totpck'] != count($packets)) { + throw new Exception(__METHOD__ . ": Too few packets received"); + } + + # Data : + $table = $this->data_encrypt_table; + $a1 = $header_items['datakey'] & 0xFF; + $a2 = $header_items['datakey'] >> 8; + + if ($a1 == 0) { + throw new Exception(__METHOD__ . ": Data key is invalid"); + } + + $chars = unpack("C*", substr($packet, 20)); + $data = ""; + $characterCount = count($chars); + + for ($index = 1; $index <= $characterCount; $index++) { + $chars[$index] -= ($table[$a2] + (($index - 1) % 72)) & 0xFF; + $a2 = ($a2 + $a1) & 0xFF; + $data .= chr($chars[$index]); + } + //@todo: Check CRC ??? + $decrypted[$header_items['pck']] = $data; + } + + // Return the decrypted packets as one string + return implode('', $decrypted); + } + + /** + * Process the channel listing + * + * @param string $data + * @param int $fieldCount + * @param \GameQ\Result $result + */ + protected function processChannel($data, $fieldCount, Result &$result) + { + + // Split the items on the comma + $items = explode(",", $data, $fieldCount); + + // Iterate over the items for this channel + foreach ($items as $item) { + // Split the key=value pair + list($key, $value) = explode("=", $item, 2); + + $result->addTeam(strtolower($key), utf8_encode($value)); + } + } + + /** + * Process the user listing + * + * @param string $data + * @param int $fieldCount + * @param \GameQ\Result $result + */ + protected function processPlayer($data, $fieldCount, Result &$result) + { + + // Split the items on the comma + $items = explode(",", $data, $fieldCount); + + // Iterate over the items for this player + foreach ($items as $item) { + // Split the key=value pair + list($key, $value) = explode("=", $item, 2); + + $result->addPlayer(strtolower($key), utf8_encode($value)); + } + } +} diff --git a/third_party/gameq_v3.1/GameQ/Protocols/Vrising.php b/third_party/gameq_v3.1/GameQ/Protocols/Vrising.php new file mode 100644 index 00000000..08549469 --- /dev/null +++ b/third_party/gameq_v3.1/GameQ/Protocols/Vrising.php @@ -0,0 +1,48 @@ +. + */ + +namespace GameQ\Protocols; + +/** + * V Rining Protocol Class + * + * @package GameQ\Protocols + */ +class Vrising extends Source +{ + /** + * String name of this protocol class + * + * @type string + */ + protected $name = 'vrising'; + + /** + * Longer string name of this protocol class + * + * @type string + */ + protected $name_long = "V Rising"; + + /** + * query_port = client_port + 1 + * + * @type int + */ + protected $port_diff = 1; +} diff --git a/third_party/gameq_v3.1/GameQ/Protocols/Warsow.php b/third_party/gameq_v3.1/GameQ/Protocols/Warsow.php new file mode 100644 index 00000000..f1d629a9 --- /dev/null +++ b/third_party/gameq_v3.1/GameQ/Protocols/Warsow.php @@ -0,0 +1,96 @@ +. + */ + +namespace GameQ\Protocols; + +use GameQ\Buffer; +use GameQ\Result; + +/** + * Warsow Protocol Class + * + * @package GameQ\Protocols + * @author Austin Bischoff + */ +class Warsow extends Quake3 +{ + /** + * String name of this protocol class + * + * @type string + */ + protected $name = 'warsow'; + + /** + * Longer string name of this protocol class + * + * @type string + */ + protected $name_long = "Warsow"; + + /** + * The client join link + * + * @type string + */ + protected $join_link = "warsow://%s:%d/"; + + /** + * Handle player info, different than quake3 base + * + * @param Buffer $buffer + * + * @return array + * @throws \GameQ\Exception\Protocol + */ + protected function processPlayers(Buffer $buffer) + { + // Set the result to a new result instance + $result = new Result(); + + // Loop until we are out of data + while ($buffer->getLength()) { + // Make a new buffer with this block + $playerInfo = new Buffer($buffer->readString("\x0A")); + + // Add player info + $result->addPlayer('frags', $playerInfo->readString("\x20")); + $result->addPlayer('ping', $playerInfo->readString("\x20")); + + // Skip first " + $playerInfo->skip(1); + + // Add player name, encoded + $result->addPlayer('name', utf8_encode(trim(($playerInfo->readString('"'))))); + + // Skip space + $playerInfo->skip(1); + + // Add team + $result->addPlayer('team', $playerInfo->read()); + + // Clear + unset($playerInfo); + } + + // Clear + unset($buffer); + + return $result->fetch(); + } +} diff --git a/third_party/gameq_v3.1/GameQ/Protocols/Won.php b/third_party/gameq_v3.1/GameQ/Protocols/Won.php new file mode 100644 index 00000000..bef09841 --- /dev/null +++ b/third_party/gameq_v3.1/GameQ/Protocols/Won.php @@ -0,0 +1,66 @@ +. + */ + +namespace GameQ\Protocols; + +/** + * World Opponent Network (WON) class + * + * Pre-cursor to the A2S (source) protocol system + * + * @author Nikolay Ipanyuk + * @author Austin Bischoff + * + * @package GameQ\Protocols + */ +class Won extends Source +{ + + /** + * Array of packets we want to look up. + * Each key should correspond to a defined method in this or a parent class + * + * @type array + */ + protected $packets = [ + self::PACKET_DETAILS => "\xFF\xFF\xFF\xFFdetails\x00", + self::PACKET_PLAYERS => "\xFF\xFF\xFF\xFFplayers", + self::PACKET_RULES => "\xFF\xFF\xFF\xFFrules", + ]; + + /** + * The query protocol used to make the call + * + * @type string + */ + protected $protocol = 'won'; + + /** + * String name of this protocol class + * + * @type string + */ + protected $name = 'won'; + + /** + * Longer string name of this protocol class + * + * @type string + */ + protected $name_long = "World Opponent Network"; +} diff --git a/third_party/gameq_v3.1/GameQ/Protocols/Wurm.php b/third_party/gameq_v3.1/GameQ/Protocols/Wurm.php new file mode 100644 index 00000000..c5936522 --- /dev/null +++ b/third_party/gameq_v3.1/GameQ/Protocols/Wurm.php @@ -0,0 +1,42 @@ +. + */ + +namespace GameQ\Protocols; + +/** + * Wurm Unlimited Protocol Class + * + * @package GameQ\Protocols + * @author Austin Bischoff + */ +class Wurm extends Source +{ + /** + * String name of this protocol class + * + * @type string + */ + protected $name = 'wurm'; + + /** + * Longer string name of this protocol class + * + * @type string + */ + protected $name_long = "Wurm Unlimited"; +} diff --git a/third_party/gameq_v3.1/GameQ/Protocols/Zomboid.php b/third_party/gameq_v3.1/GameQ/Protocols/Zomboid.php new file mode 100644 index 00000000..4733d97a --- /dev/null +++ b/third_party/gameq_v3.1/GameQ/Protocols/Zomboid.php @@ -0,0 +1,42 @@ +. + */ + +namespace GameQ\Protocols; + +/** + * Project Zomboid Protocol Class + * + * @package GameQ\Protocols + * @author Austin Bischoff + */ +class Zomboid extends Source +{ + /** + * String name of this protocol class + * + * @type string + */ + protected $name = 'zomboid'; + + /** + * Longer string name of this protocol class + * + * @type string + */ + protected $name_long = "Project Zomboid"; +} diff --git a/third_party/gameq_v3.1/GameQ/Query/Core.php b/third_party/gameq_v3.1/GameQ/Query/Core.php new file mode 100644 index 00000000..fd1949da --- /dev/null +++ b/third_party/gameq_v3.1/GameQ/Query/Core.php @@ -0,0 +1,189 @@ +. + */ + +namespace GameQ\Query; + +/** + * Core for the query mechanisms + * + * @author Austin Bischoff + */ +abstract class Core +{ + + /** + * The socket used by this resource + * + * @type null|resource + */ + public $socket = null; + + /** + * The transport type (udp, tcp, etc...) + * See http://php.net/manual/en/transports.php for the supported list + * + * @type string + */ + protected $transport = null; + + /** + * Connection IP address + * + * @type string + */ + protected $ip = null; + + /** + * Connection port + * + * @type int + */ + protected $port = null; + + /** + * The time in seconds to wait before timing out while connecting to the socket + * + * @type int + */ + protected $timeout = 3; // Seconds + + /** + * Socket is blocking? + * + * @type bool + */ + protected $blocking = false; + + /** + * Called when the class is cloned + */ + public function __clone() + { + + // Reset the properties for this class when cloned + $this->reset(); + } + + /** + * Set the connection information for the socket + * + * @param string $transport + * @param string $ip + * @param int $port + * @param int $timeout seconds + * @param bool $blocking + */ + public function set($transport, $ip, $port, $timeout = 3, $blocking = false) + { + + $this->transport = $transport; + + $this->ip = $ip; + + $this->port = $port; + + $this->timeout = $timeout; + + $this->blocking = $blocking; + } + + /** + * Reset this instance's properties + */ + public function reset() + { + + $this->transport = null; + + $this->ip = null; + + $this->port = null; + + $this->timeout = 3; + + $this->blocking = false; + } + + public function getTransport() + { + return $this->transport; + } + + public function getIp() + { + return $this->ip; + } + + public function getPort() + { + return $this->port; + } + + public function getTimeout() + { + return $this->timeout; + } + + public function getBlocking() + { + return $this->blocking; + } + + /** + * Create a new socket + * + * @return void + */ + abstract protected function create(); + + /** + * Get the socket + * + * @return mixed + */ + abstract public function get(); + + /** + * Write data to the socket + * + * @param string $data + * + * @return int The number of bytes written + */ + abstract public function write($data); + + /** + * Close the socket + * + * @return void + */ + abstract public function close(); + + /** + * Read the responses from the socket(s) + * + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + * + * @param array $sockets + * @param int $timeout + * @param int $stream_timeout + * + * @return array + */ + abstract public function getResponses(array $sockets, $timeout, $stream_timeout); +} diff --git a/third_party/gameq_v3.1/GameQ/Query/Native.php b/third_party/gameq_v3.1/GameQ/Query/Native.php new file mode 100644 index 00000000..24200b0c --- /dev/null +++ b/third_party/gameq_v3.1/GameQ/Query/Native.php @@ -0,0 +1,227 @@ +. + */ + +namespace GameQ\Query; + +use GameQ\Exception\Query as Exception; + +/** + * Native way of querying servers + * + * @author Austin Bischoff + */ +class Native extends Core +{ + /** + * Get the current socket or create one and return + * + * @return resource|null + * @throws \GameQ\Exception\Query + */ + public function get() + { + + // No socket for this server, make one + if (is_null($this->socket)) { + $this->create(); + } + + return $this->socket; + } + + /** + * Write data to the socket + * + * @param string $data + * + * @return int The number of bytes written + * @throws \GameQ\Exception\Query + */ + public function write($data) + { + + try { + // No socket for this server, make one + if (is_null($this->socket)) { + $this->create(); + } + + // Send the packet + return fwrite($this->socket, $data); + } catch (\Exception $e) { + throw new Exception($e->getMessage(), $e->getCode(), $e); + } + } + + /** + * Close the current socket + */ + public function close() + { + + if ($this->socket) { + fclose($this->socket); + $this->socket = null; + } + } + + /** + * Create a new socket for this query + * + * @throws \GameQ\Exception\Query + */ + protected function create() + { + + // Create the remote address + $remote_addr = sprintf("%s://%s:%d", $this->transport, $this->ip, $this->port); + + // Create context + $context = stream_context_create([ + 'socket' => [ + 'bindto' => '0:0', // Bind to any available IP and OS decided port + ], + ]); + + // Define these first + $errno = null; + $errstr = null; + + // Create the socket + if (($this->socket = + @stream_socket_client($remote_addr, $errno, $errstr, $this->timeout, STREAM_CLIENT_CONNECT, $context)) + !== false + ) { + // Set the read timeout on the streams + stream_set_timeout($this->socket, $this->timeout); + + // Set blocking mode + stream_set_blocking($this->socket, $this->blocking); + + // Set the read buffer + stream_set_read_buffer($this->socket, 0); + + // Set the write buffer + stream_set_write_buffer($this->socket, 0); + } else { + // Reset socket + $this->socket = null; + + // Something bad happened, throw query exception + throw new Exception( + __METHOD__ . " - Error creating socket to server {$this->ip}:{$this->port}. Error: " . $errstr, + $errno + ); + } + } + + /** + * Pull the responses out of the stream + * + * @SuppressWarnings(PHPMD.CyclomaticComplexity) + * @SuppressWarnings(PHPMD.NPathComplexity) + * + * @param array $sockets + * @param int $timeout + * @param int $stream_timeout + * + * @return array Raw responses + */ + public function getResponses(array $sockets, $timeout, $stream_timeout) + { + + // Set the loop to active + $loop_active = true; + + // Will hold the responses read from the sockets + $responses = []; + + // To store the sockets + $sockets_tmp = []; + + // Loop and pull out all the actual sockets we need to listen on + foreach ($sockets as $socket_id => $socket_data) { + // Get the socket + /* @var $socket \GameQ\Query\Core */ + $socket = $socket_data['socket']; + + // Append the actual socket we are listening to + $sockets_tmp[$socket_id] = $socket->get(); + + unset($socket); + } + + // Init some variables + $read = $sockets_tmp; + $write = null; + $except = null; + + // Check to see if $read is empty, if so stream_select() will throw a warning + if (empty($read)) { + return $responses; + } + + // This is when it should stop + $time_stop = microtime(true) + $timeout; + + // Let's loop until we break something. + while ($loop_active && microtime(true) < $time_stop) { + // Check to make sure $read is not empty, if so we are done + if (empty($read)) { + break; + } + + // Now lets listen for some streams, but do not cross the streams! + $streams = stream_select($read, $write, $except, 0, $stream_timeout); + + // We had error or no streams left, kill the loop + if ($streams === false || ($streams <= 0)) { + break; + } + + // Loop the sockets that received data back + foreach ($read as $socket) { + /* @var $socket resource */ + + // See if we have a response + if (($response = fread($socket, 32768)) === false) { + continue; // No response yet so lets continue. + } + + // Check to see if the response is empty, if so we are done with this server + if (strlen($response) == 0) { + // Remove this server from any future read loops + unset($sockets_tmp[(int)$socket]); + continue; + } + + // Add the response we got back + $responses[(int)$socket][] = $response; + } + + // Because stream_select modifies read we need to reset it each time to the original array of sockets + $read = $sockets_tmp; + } + + // Free up some memory + unset($streams, $read, $write, $except, $sockets_tmp, $time_stop, $response); + + // Return all of the responses, may be empty if something went wrong + return $responses; + } +} diff --git a/third_party/gameq_v3.1/GameQ/Result.php b/third_party/gameq_v3.1/GameQ/Result.php new file mode 100644 index 00000000..7023f17a --- /dev/null +++ b/third_party/gameq_v3.1/GameQ/Result.php @@ -0,0 +1,130 @@ +. + */ + +namespace GameQ; + +/** + * Provide an interface for easy storage of a parsed server response + * + * @author Aidan Lister + * @author Tom Buskens + */ +class Result +{ + + /** + * Formatted server response + * + * @var array + */ + protected $result = []; + + /** + * Adds variable to results + * + * @param string $name Variable name + * @param string|array $value Variable value + */ + public function add($name, $value) + { + + $this->result[$name] = $value; + } + + /** + * Adds player variable to output + * + * @param string $name Variable name + * @param string $value Variable value + */ + public function addPlayer($name, $value) + { + + $this->addSub('players', $name, $value); + } + + /** + * Adds player variable to output + * + * @param string $name Variable name + * @param string $value Variable value + */ + public function addTeam($name, $value) + { + + $this->addSub('teams', $name, $value); + } + + /** + * Add a variable to a category + * + * @param $sub string The category + * @param $key string The variable name + * @param $value string The variable value + */ + public function addSub($sub, $key, $value) + { + + // Nothing of this type yet, set an empty array + if (!isset($this->result[$sub]) or !is_array($this->result[$sub])) { + $this->result[$sub] = []; + } + + // Find the first entry that doesn't have this variable + $found = false; + $count = count($this->result[$sub]); + for ($i = 0; $i != $count; $i++) { + if (!isset($this->result[$sub][$i][$key])) { + $this->result[$sub][$i][$key] = $value; + $found = true; + break; + } + } + + // Not found, create a new entry + if (!$found) { + $this->result[$sub][][$key] = $value; + } + + unset($count); + } + + /** + * Return all stored results + * + * @return array All results + */ + public function fetch() + { + + return $this->result; + } + + /** + * Return a single variable + * + * @param string $var The variable name + * + * @return mixed The variable value + */ + public function get($var) + { + + return isset($this->result[$var]) ? $this->result[$var] : null; + } +} diff --git a/third_party/gameq_v3.1/GameQ/Server.php b/third_party/gameq_v3.1/GameQ/Server.php new file mode 100644 index 00000000..1725d461 --- /dev/null +++ b/third_party/gameq_v3.1/GameQ/Server.php @@ -0,0 +1,389 @@ +. + */ + +namespace GameQ; + +use GameQ\Exception\Server as Exception; + +/** + * Server class to represent each server entity + * + * @author Austin Bischoff + */ +class Server +{ + /* + * Server array keys + */ + const SERVER_TYPE = 'type'; + + const SERVER_HOST = 'host'; + + const SERVER_ID = 'id'; + + const SERVER_OPTIONS = 'options'; + + /* + * Server options keys + */ + + /* + * Use this option when the query_port and client connect ports are different + */ + const SERVER_OPTIONS_QUERY_PORT = 'query_port'; + + /** + * The protocol class for this server + * + * @type \GameQ\Protocol + */ + protected $protocol = null; + + /** + * Id of this server + * + * @type string + */ + public $id = null; + + /** + * IP Address of this server + * + * @type string + */ + public $ip = null; + + /** + * The server's client port (connect port) + * + * @type int + */ + public $port_client = null; + + /** + * The server's query port + * + * @type int + */ + public $port_query = null; + + /** + * Holds other server specific options + * + * @type array + */ + protected $options = []; + + /** + * Holds the sockets already open for this server + * + * @type array + */ + protected $sockets = []; + + /** + * Construct the class with the passed options + * + * @param array $server_info + * + * @throws \GameQ\Exception\Server + */ + public function __construct(array $server_info = []) + { + + // Check for server type + if (!array_key_exists(self::SERVER_TYPE, $server_info) || empty($server_info[self::SERVER_TYPE])) { + throw new Exception("Missing server info key '" . self::SERVER_TYPE . "'!"); + } + + // Check for server host + if (!array_key_exists(self::SERVER_HOST, $server_info) || empty($server_info[self::SERVER_HOST])) { + throw new Exception("Missing server info key '" . self::SERVER_HOST . "'!"); + } + + // IP address and port check + $this->checkAndSetIpPort($server_info[self::SERVER_HOST]); + + // Check for server id + if (array_key_exists(self::SERVER_ID, $server_info) && !empty($server_info[self::SERVER_ID])) { + // Set the server id + $this->id = $server_info[self::SERVER_ID]; + } else { + // Make an id so each server has an id when returned + $this->id = sprintf('%s:%d', $this->ip, $this->port_client); + } + + // Check and set server options + if (array_key_exists(self::SERVER_OPTIONS, $server_info)) { + // Set the options + $this->options = $server_info[self::SERVER_OPTIONS]; + } + + try { + // Make the protocol class for this type + $class = new \ReflectionClass( + sprintf('GameQ\\Protocols\\%s', ucfirst(strtolower($server_info[self::SERVER_TYPE]))) + ); + + $this->protocol = $class->newInstanceArgs([$this->options]); + } catch (\ReflectionException $e) { + throw new Exception("Unable to locate Protocols class for '{$server_info[self::SERVER_TYPE]}'!"); + } + + // Check and set any server options + $this->checkAndSetServerOptions(); + + unset($server_info, $class); + } + + /** + * Check and set the ip address for this server + * + * @param $ip_address + * + * @throws \GameQ\Exception\Server + */ + protected function checkAndSetIpPort($ip_address) + { + + // Test for IPv6 + if (substr_count($ip_address, ':') > 1) { + // See if we have a port, input should be in the format [::1]:27015 or similar + if (strstr($ip_address, ']:')) { + // Explode to get port + $server_addr = explode(':', $ip_address); + + // Port is the last item in the array, remove it and save + $this->port_client = (int)array_pop($server_addr); + + // The rest is the address, recombine + $this->ip = implode(':', $server_addr); + + unset($server_addr); + } else { + // Just the IPv6 address, no port defined, fail + throw new Exception( + "The host address '{$ip_address}' is missing the port. All " + . "servers must have a port defined!" + ); + } + + // Now let's validate the IPv6 value sent, remove the square brackets ([]) first + if (!filter_var(trim($this->ip, '[]'), FILTER_VALIDATE_IP, ['flags' => FILTER_FLAG_IPV6,])) { + throw new Exception("The IPv6 address '{$this->ip}' is invalid."); + } + } else { + // We have IPv4 with a port defined + if (strstr($ip_address, ':')) { + list($this->ip, $this->port_client) = explode(':', $ip_address); + + // Type case the port + $this->port_client = (int)$this->port_client; + } else { + // No port, fail + throw new Exception( + "The host address '{$ip_address}' is missing the port. All " + . "servers must have a port defined!" + ); + } + + // Validate the IPv4 value, if FALSE is not a valid IP, maybe a hostname. + if (! filter_var($this->ip, FILTER_VALIDATE_IP, ['flags' => FILTER_FLAG_IPV4,])) { + // Try to resolve the hostname to IPv4 + $resolved = gethostbyname($this->ip); + + // When gethostbyname() fails it returns the original string + if ($this->ip === $resolved) { + // so if ip and the result from gethostbyname() are equal this failed. + throw new Exception("Unable to resolve the host '{$this->ip}' to an IP address."); + } else { + $this->ip = $resolved; + } + } + } + } + + /** + * Check and set any server specific options + */ + protected function checkAndSetServerOptions() + { + + // Specific query port defined + if (array_key_exists(self::SERVER_OPTIONS_QUERY_PORT, $this->options)) { + $this->port_query = (int)$this->options[self::SERVER_OPTIONS_QUERY_PORT]; + } else { + // Do math based on the protocol class + $this->port_query = $this->protocol->findQueryPort($this->port_client); + } + } + + /** + * Set an option for this server + * + * @param $key + * @param $value + * + * @return $this + */ + public function setOption($key, $value) + { + + $this->options[$key] = $value; + + return $this; // Make chainable + } + + /** + * Return set option value + * + * @param mixed $key + * + * @return mixed + */ + public function getOption($key) + { + + return (array_key_exists($key, $this->options)) ? $this->options[$key] : null; + } + + public function getOptions() + { + return $this->options; + } + + /** + * Get the ID for this server + * + * @return string + */ + public function id() + { + + return $this->id; + } + + /** + * Get the IP address for this server + * + * @return string + */ + public function ip() + { + + return $this->ip; + } + + /** + * Get the client port for this server + * + * @return int + */ + public function portClient() + { + + return $this->port_client; + } + + /** + * Get the query port for this server + * + * @return int + */ + public function portQuery() + { + + return $this->port_query; + } + + /** + * Return the protocol class for this server + * + * @return \GameQ\Protocol + */ + public function protocol() + { + + return $this->protocol; + } + + /** + * Get the join link for this server + * + * @return string + */ + public function getJoinLink() + { + + return sprintf($this->protocol->joinLink(), $this->ip, $this->portClient()); + } + + /* + * Socket holding + */ + + /** + * Add a socket for this server to be reused + * + * @codeCoverageIgnore + * + * @param \GameQ\Query\Core $socket + */ + public function socketAdd(Query\Core $socket) + { + + $this->sockets[] = $socket; + } + + /** + * Get a socket from the list to reuse, if any are available + * + * @codeCoverageIgnore + * + * @return \GameQ\Query\Core|null + */ + public function socketGet() + { + + $socket = null; + + if (count($this->sockets) > 0) { + $socket = array_pop($this->sockets); + } + + return $socket; + } + + /** + * Clear any sockets still listed and attempt to close them + * + * @codeCoverageIgnore + */ + public function socketCleanse() + { + + // Close all of the sockets available + foreach ($this->sockets as $socket) { + /* @var $socket \GameQ\Query\Core */ + $socket->close(); + } + + // Reset the sockets list + $this->sockets = []; + } +}