├── LICENSE ├── README.md ├── docs └── SourceQuery.md ├── src └── SourceQuery.php └── tests └── test-query.php /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (C) 2011 by Yannick Croissant 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | SourceQuery 2 | =========== 3 | 4 | A simple PHP class to query a [Source engine](http://en.wikipedia.org/wiki/Source_%28game_engine%29) server. 5 | 6 | How to use 7 | ---------- 8 | 9 | ### Example 10 | 11 | PHP: 12 | 13 | require_once 'lib/SourceQuery.php'; 14 | 15 | $server = new SourceQuery('217.70.184.250', 27015); 16 | $infos = $server->getInfos(); 17 | echo 'There is ' . $infos['players'] . ' player(s) on the server "' .$infos['name'] . '".'; -------------------------------------------------------------------------------- /docs/SourceQuery.md: -------------------------------------------------------------------------------- 1 | Class: SourceQuery {#SourceQuery} 2 | ========================================= 3 | 4 | SourceQuery is a simple PHP class to query a [Source engine](http://en.wikipedia.org/wiki/Source_%28game_engine%29) server. 5 | 6 | SourceQuery Method: constructor {#SourceQuery:constructor} 7 | ------------------------------------------------------------------- 8 | 9 | The class constructor 10 | 11 | ### Syntax: 12 | 13 | $server = new SourceQuery(ip, port); 14 | 15 | ### Arguments: 16 | 17 | 1. ip - (string) The IP address of the game server 18 | 2. port - (number: defaults to `27015`) The port the game server is running on 19 | 20 | ### Example: 21 | 22 | #### PHP: 23 | 24 | $server = new SourceQuery('217.70.184.250', 27030); 25 | 26 | SourceQuery Method: getInfos {#SourceQuery:getInfos} 27 | ------------------------------------------------------------------- 28 | 29 | Request informations about the server ([A2S_INFO query](http://developer.valvesoftware.com/wiki/Server_queries#A2S_INFO)). 30 | 31 | ### Syntax: 32 | 33 | $server->getInfos(); 34 | 35 | ### Returns: 36 | 37 | * (array) An array containing the following fields: 38 | 39 | * bots - (number) Number of bot players currently on the server (Unsupported on GoldSource servers) 40 | * dedie - (string) 'l' for listen, 'd' for dedicated, 'p' for HLTV 41 | * id - (number) The [Steam Application ID](http://developer.valvesoftware.com/wiki/Steam_Application_IDs) of the game (Unsupported on GoldSource servers) 42 | * ip - (string) The IP address of the game server 43 | * map - (string) The current map being played 44 | * mod - (string) The name of the folder containing the game files 45 | * modname - (string) A friendly string name for the game type 46 | * name - (string) The Source server's name 47 | * os - (string) Host operating system. 'l' for Linux, 'w' for Windows 48 | * pass - (number) Set to 1 if the server has a password 49 | * places - (number) Maximum allowed players for the server 50 | * players - (number) The number of players currently on the server 51 | * port - (number) The port the game server is running on 52 | * protocol - (number) Server type (Source server: 73, GoldSource server: 109) 53 | 54 | ### Example: 55 | 56 | #### PHP: 57 | 58 | $infos = $server->getInfos(); 59 | 60 | ### Notes: 61 | 62 | * The server type (Source or GoldSource) is automatically detected 63 | 64 | SourceQuery Method: getPlayers {#SourceQuery:getPlayers} 65 | ------------------------------------------------------------------- 66 | 67 | Request informations about the players ([A2S_PLAYER query](http://developer.valvesoftware.com/wiki/Server_queries#A2S_PLAYER)). 68 | 69 | ### Syntax: 70 | 71 | $server->getPlayers(); 72 | 73 | ### Returns: 74 | 75 | * (array) An array containing one entry by player. Then for each player an array containing the following fields: 76 | 77 | * id - (number) The index into [0.. Num Players] for this entry 78 | * name - (string) Player's name 79 | * score - (string) Player's score 80 | * time - (number) The time in seconds this player has been connected 81 | 82 | ### Example: 83 | 84 | #### PHP: 85 | 86 | $playersInfos = $server->getPlayers(); 87 | 88 | ### Notes: 89 | 90 | * For more informations, please read the [Server queries documentation](http://developer.valvesoftware.com/wiki/Server_queries) -------------------------------------------------------------------------------- /src/SourceQuery.php: -------------------------------------------------------------------------------- 1 | ip = $ip; 6 | $this->port = $port; 7 | } 8 | 9 | protected function query($query) { 10 | $fp = fsockopen('udp://' . $this->ip, $this->port, $errno, $errstr, 2); 11 | if (!$fp) { 12 | trigger_error('The server does not respond', E_USER_NOTICE); 13 | return false; 14 | } else { 15 | fwrite($fp, $query); 16 | stream_set_timeout($fp, 2); 17 | $result = ''; 18 | do { 19 | $result .= fread ($fp, 1); 20 | $fpstatus = socket_get_status($fp); 21 | } while ($fpstatus['unread_bytes']); 22 | fclose($fp); 23 | return $result; 24 | } 25 | } 26 | 27 | protected function getChallenge() { 28 | $challenge = $this->query("\xFF\xFF\xFF\xFFU\xFF\xFF\xFF\xFF"); 29 | return substr($challenge, 5); 30 | } 31 | 32 | // Hex to Signed Dec http://fr2.php.net/manual/en/function.hexdec.php#97172 33 | private function hexdecs($hex){ 34 | $dec = hexdec($hex); 35 | $max = pow(2, 4 * (strlen($hex) + (strlen($hex) % 2))); 36 | $_dec = $max - $dec; 37 | return $dec > $_dec ? -$_dec : $dec; 38 | } 39 | 40 | public function getInfos() { 41 | $infos = $this->query("\xFF\xFF\xFF\xFFTSource Engine Query\x00"); 42 | 43 | // Détermine le protocole utilisé 44 | $protocol = hexdec(substr(bin2hex($infos), 8, 2)); 45 | 46 | if($protocol == 109) return $this->getInfos1($infos); 47 | else if($protocol == 73) return $this->getInfos2($infos); 48 | 49 | trigger_error('Unknown server type', E_USER_NOTICE); 50 | return false; 51 | } 52 | 53 | protected function getInfos1($infos) { 54 | // Split informations 55 | $infos = chunk_split(substr(bin2hex($infos), 10), 2, '\\'); 56 | @list($serveur['ip'], $serveur['name'], $serveur['map'], $serveur['mod'], $serveur['modname'], $serveur['params']) = explode('\\00', $infos); 57 | 58 | // Split parameters 59 | $serveur['params'] = substr($serveur['params'],0,18); 60 | 61 | $serveur['params'] = chunk_split(str_replace('\\', '', $serveur['params']), 2, ' '); 62 | list($params['players'], $params['places'], $params['protocol'], $params['dedie'], $params['os'], $params['pass']) = explode(' ', $serveur['params']); 63 | $params = array( 64 | 'id' => 0, // Unsupported 65 | 'bots' => 0, // Unsupported 66 | 'ip' => $this->ip, 67 | 'port' => $this->port, 68 | 'players' => hexdec($params['players']), 69 | 'places' => hexdec($params['places']), 70 | 'protocol' => hexdec($params['protocol']), 71 | 'dedie' => chr(hexdec($params['dedie'])), 72 | 'os' => chr(hexdec($params['os'])), 73 | 'pass' => hexdec($params['pass']) 74 | ); 75 | unset($serveur['ip']); 76 | unset($serveur['params']); 77 | 78 | $serveur = array_map(function($item){ 79 | return pack("H*", str_replace('\\', '', $item)); 80 | }, $serveur); 81 | 82 | $infos = ($params + $serveur); 83 | return $infos; 84 | } 85 | 86 | protected function getInfos2($infos) { 87 | // Split informations 88 | $infos = chunk_split(substr(bin2hex($infos), 12), 2, '\\'); 89 | @list($serveur['name'], $serveur['map'], $serveur['mod'], $serveur['modname'], $serveur['params']) = explode('\\00', $infos, 5); 90 | 91 | // Split parameters 92 | $serveur['params'] = substr($serveur['params'], 0); 93 | 94 | $serveur['params'] = chunk_split(str_replace('\\', '', $serveur['params']), 2, ' '); 95 | list($params['id1'], $params['id2'], $params['players'], $params['places'], $params['bots'], $params['dedie'], $params['os'], $params['pass']) = explode(' ', $serveur['params']); 96 | $params=array( 97 | 'id' => hexdec($params['id2'] . $params['id1']), 98 | 'ip' => $this->ip, 99 | 'port' => $this->port, 100 | 'players' => hexdec($params['players']), 101 | 'places' => hexdec($params['places']), 102 | 'bots' => hexdec($params['bots']), 103 | 'protocol' => 73, 104 | 'dedie' => chr(hexdec($params['dedie'])), 105 | 'os' => chr(hexdec($params['os'])), 106 | 'pass' => hexdec($params['pass']) 107 | ); 108 | unset($serveur['params']); 109 | 110 | $serveur = array_map(function($item){ 111 | return pack("H*", str_replace('\\', '', $item)); 112 | }, $serveur); 113 | 114 | $infos = ($serveur + $params); 115 | return $infos; 116 | } 117 | 118 | public function getPlayers() { 119 | $challenge = $this->getChallenge(); 120 | 121 | $infos = $this->query("\xFF\xFF\xFF\xFFU" . $challenge); 122 | 123 | $infos = chunk_split(substr(bin2hex($infos), 12), 2, '\\'); 124 | 125 | $infos = explode('\\', $infos); 126 | 127 | $players = array(); 128 | for ($i = 0; isset($infos[$i + 1]); $i = $j + 9) { 129 | 130 | // Player name 131 | $name = ''; 132 | for ($j = $i + 1; isset($infos[$j]) && $infos[$j] != '00'; $j++) $name .= chr(hexdec($infos[$j])); 133 | 134 | if (!isset($infos[$j + 8])) break; 135 | 136 | // Gametime 137 | eval('$time="\x'.trim(chunk_split($infos[$j + 5] . $infos[$j + 6] . $infos[$j + 7] . $infos[$j + 8], 2,"\x"), "\x") . '";'); 138 | list(,$time) = unpack('f', $time); 139 | 140 | // Score 141 | $score = ltrim($infos[$j + 4] . $infos[$j + 3] . $infos[$j + 2] . $infos[$j + 1], '0'); 142 | 143 | $players[] = array( 144 | 'id' => hexdec($infos[$i]), 145 | 'name' => $name, 146 | 'score' => empty($score)? 0 : $this->hexdecs($score), 147 | 'time' => $time 148 | ); 149 | } 150 | return $players; 151 | } 152 | } 153 | ?> -------------------------------------------------------------------------------- /tests/test-query.php: -------------------------------------------------------------------------------- 1 | assertType('SourceQuery', $server); 24 | } 25 | 26 | public function testGetServerInfos(){ 27 | $server = new SourceQuery(IP, PORT); 28 | 29 | $this->assertType(PHPUnit_Framework_Constraint_IsType::TYPE_ARRAY, $server->getInfos()); 30 | } 31 | 32 | public function testGetServerInfos2(){ 33 | $server = new SourceQuery(IP, PORT); 34 | $infos = $server->getInfos(); 35 | 36 | $this->assertNotNull($infos['id']); 37 | $this->assertType(PHPUnit_Framework_Constraint_IsType::TYPE_INT, $infos['id']); 38 | 39 | $this->assertNotNull($infos['ip']); 40 | $this->assertType(PHPUnit_Framework_Constraint_IsType::TYPE_STRING, $infos['ip']); 41 | 42 | $this->assertNotNull($infos['port']); 43 | $this->assertType(PHPUnit_Framework_Constraint_IsType::TYPE_INT, $infos['port']); 44 | 45 | $this->assertNotNull($infos['players']); 46 | $this->assertType(PHPUnit_Framework_Constraint_IsType::TYPE_INT, $infos['players']); 47 | 48 | $this->assertNotNull($infos['places']); 49 | $this->assertType(PHPUnit_Framework_Constraint_IsType::TYPE_INT, $infos['places']); 50 | 51 | $this->assertNotNull($infos['bots']); 52 | $this->assertType(PHPUnit_Framework_Constraint_IsType::TYPE_INT, $infos['bots']); 53 | 54 | $this->assertNotNull($infos['protocol']); 55 | $this->assertType(PHPUnit_Framework_Constraint_IsType::TYPE_INT, $infos['protocol']); 56 | 57 | $this->assertNotNull($infos['dedie']); 58 | $this->assertType(PHPUnit_Framework_Constraint_IsType::TYPE_STRING, $infos['dedie']); 59 | 60 | $this->assertNotNull($infos['os']); 61 | $this->assertType(PHPUnit_Framework_Constraint_IsType::TYPE_STRING, $infos['os']); 62 | 63 | $this->assertNotNull($infos['pass']); 64 | $this->assertType(PHPUnit_Framework_Constraint_IsType::TYPE_INT, $infos['pass']); 65 | } 66 | 67 | public function testGetPlayersInfos(){ 68 | $server = new SourceQuery(IP, PORT); 69 | 70 | $this->assertType(PHPUnit_Framework_Constraint_IsType::TYPE_ARRAY, $server->getPlayers()); 71 | } 72 | 73 | public function testGetPlayersInfos2(){ 74 | $server = new SourceQuery(IP, PORT); 75 | $infos = $server->getPlayers(); 76 | 77 | if(count($infos) == 0) return true; 78 | 79 | $this->assertNotNull($infos[0]['id']); 80 | $this->assertType(PHPUnit_Framework_Constraint_IsType::TYPE_INT, $infos[0]['id']); 81 | 82 | $this->assertNotNull($infos[0]['name']); 83 | $this->assertType(PHPUnit_Framework_Constraint_IsType::TYPE_STRING, $infos[0]['name']); 84 | 85 | $this->assertNotNull($infos[0]['score']); 86 | $this->assertType(PHPUnit_Framework_Constraint_IsType::TYPE_INT, $infos[0]['score']); 87 | 88 | $this->assertNotNull($infos[0]['time']); 89 | $this->assertType(PHPUnit_Framework_Constraint_IsType::TYPE_FLOAT, $infos[0]['time']); 90 | } 91 | } 92 | 93 | QueryTests::main(); --------------------------------------------------------------------------------