├── gameq3 ├── protocols │ ├── dayz.php │ ├── brink.php │ ├── aa3.php │ ├── aoc.php │ ├── dod.php │ ├── ffe.php │ ├── gmod.php │ ├── ns.php │ ├── tf2.php │ ├── css.php │ ├── ns2.php │ ├── tfc.php │ ├── zps.php │ ├── dods.php │ ├── hldm.php │ ├── homefront.php │ ├── alienswarm.php │ ├── hl2dm.php │ ├── insurgency.php │ ├── csgo.php │ ├── left4dead2.php │ ├── codmw3.php │ ├── zombiemaster.php │ ├── arma2oa.php │ ├── cod.php │ ├── cod2.php │ ├── cod4.php │ ├── coduo.php │ ├── codwaw.php │ ├── ut.php │ ├── ut2004.php │ ├── minecraftquery.php │ ├── bf4.php │ ├── killingfloor.php │ ├── cs.php │ ├── rust.php │ ├── cscz.php │ ├── left4dead.php │ ├── ship.php │ ├── arma3.php │ ├── arma2.php │ ├── il2.php │ ├── arma.php │ ├── fear.php │ ├── minecraft16.php │ ├── bfv.php │ ├── bf2142.php │ ├── bf2.php │ ├── bf1942.php │ ├── minecraft.php │ ├── mohwf.php │ ├── kerbalmultiplayer.php │ ├── tshock.php │ ├── quake3.php │ ├── samp.php │ ├── squad.php │ ├── unreal2.php │ ├── gamespy2.php │ ├── bf3.php │ ├── lfs.php │ ├── ut3.php │ ├── teamspeak3.php │ ├── gamespy.php │ └── source.php ├── filters │ ├── strip_badchars.php │ ├── sortplayers.php │ └── colorize.php ├── result.php ├── log.php ├── buffer.php └── gameq3.php ├── examples ├── simple.php ├── cli.php ├── list.php └── index.php └── README.md /gameq3/protocols/dayz.php: -------------------------------------------------------------------------------- 1 | . 17 | */ 18 | 19 | 20 | namespace GameQ3\protocols; 21 | 22 | class Dayz extends \GameQ3\Protocols\Arma2 { 23 | protected $name = "dayz"; 24 | protected $name_long = "DayZ Mod"; 25 | } -------------------------------------------------------------------------------- /gameq3/protocols/brink.php: -------------------------------------------------------------------------------- 1 | . 17 | */ 18 | 19 | 20 | namespace GameQ3\protocols; 21 | 22 | class Brink extends \GameQ3\Protocols\Source { 23 | protected $name = "brink"; 24 | protected $name_long = "Brink"; 25 | } 26 | -------------------------------------------------------------------------------- /gameq3/protocols/aa3.php: -------------------------------------------------------------------------------- 1 | . 17 | */ 18 | 19 | 20 | namespace GameQ3\protocols; 21 | 22 | class Aa3 extends \GameQ3\Protocols\Source { 23 | protected $name = "aa3"; 24 | protected $name_long = " America's Army 3"; 25 | } 26 | -------------------------------------------------------------------------------- /gameq3/protocols/aoc.php: -------------------------------------------------------------------------------- 1 | . 17 | */ 18 | 19 | 20 | namespace GameQ3\protocols; 21 | 22 | class Aoc extends \GameQ3\Protocols\Source { 23 | protected $name = "aoc"; 24 | protected $name_long = "Age of Chivalry"; 25 | } 26 | -------------------------------------------------------------------------------- /gameq3/protocols/dod.php: -------------------------------------------------------------------------------- 1 | . 17 | */ 18 | 19 | 20 | namespace GameQ3\protocols; 21 | 22 | class Dod extends \GameQ3\Protocols\Source { 23 | protected $name = "dod"; 24 | protected $name_long = "Day of Defeat"; 25 | } 26 | -------------------------------------------------------------------------------- /gameq3/protocols/ffe.php: -------------------------------------------------------------------------------- 1 | . 17 | */ 18 | 19 | 20 | namespace GameQ3\protocols; 21 | 22 | class Ffe extends \GameQ3\Protocols\Source { 23 | protected $name = "ffe"; 24 | protected $name_long = "Fortress Forever"; 25 | } 26 | -------------------------------------------------------------------------------- /gameq3/protocols/gmod.php: -------------------------------------------------------------------------------- 1 | . 17 | */ 18 | 19 | 20 | namespace GameQ3\protocols; 21 | 22 | class Gmod extends \GameQ3\Protocols\Source { 23 | protected $name = "gmod"; 24 | protected $name_long = "Garry's Mod"; 25 | } 26 | -------------------------------------------------------------------------------- /gameq3/protocols/ns.php: -------------------------------------------------------------------------------- 1 | . 17 | */ 18 | 19 | 20 | namespace GameQ3\protocols; 21 | 22 | class Ns extends \GameQ3\Protocols\Source { 23 | protected $name = "ns"; 24 | protected $name_long = "Natural Selection"; 25 | } 26 | -------------------------------------------------------------------------------- /gameq3/protocols/tf2.php: -------------------------------------------------------------------------------- 1 | . 17 | */ 18 | 19 | 20 | namespace GameQ3\protocols; 21 | 22 | class Tf2 extends \GameQ3\Protocols\Source { 23 | protected $name = "tf2"; 24 | protected $name_long = "Team Fortress 2"; 25 | } 26 | -------------------------------------------------------------------------------- /gameq3/protocols/css.php: -------------------------------------------------------------------------------- 1 | . 17 | */ 18 | 19 | 20 | namespace GameQ3\protocols; 21 | 22 | class Css extends \GameQ3\Protocols\Source { 23 | protected $name = "css"; 24 | protected $name_long = "Counter-Strike: Source"; 25 | } 26 | -------------------------------------------------------------------------------- /gameq3/protocols/ns2.php: -------------------------------------------------------------------------------- 1 | . 17 | */ 18 | 19 | 20 | namespace GameQ3\protocols; 21 | 22 | class Ns2 extends \GameQ3\Protocols\Source { 23 | protected $name = "ns2"; 24 | protected $name_long = "Natural Selection 2"; 25 | } 26 | -------------------------------------------------------------------------------- /gameq3/protocols/tfc.php: -------------------------------------------------------------------------------- 1 | . 17 | */ 18 | 19 | 20 | namespace GameQ3\protocols; 21 | 22 | class Tfc extends \GameQ3\Protocols\Source { 23 | protected $name = "tfc"; 24 | protected $name_long = "Team Fortress Classic"; 25 | } 26 | -------------------------------------------------------------------------------- /gameq3/protocols/zps.php: -------------------------------------------------------------------------------- 1 | . 17 | */ 18 | 19 | 20 | namespace GameQ3\protocols; 21 | 22 | class Zps extends \GameQ3\Protocols\Source { 23 | protected $name = "zps"; 24 | protected $name_long = "Zombie Panic Source"; 25 | } 26 | -------------------------------------------------------------------------------- /gameq3/protocols/dods.php: -------------------------------------------------------------------------------- 1 | . 17 | */ 18 | 19 | 20 | namespace GameQ3\protocols; 21 | 22 | class Dods extends \GameQ3\Protocols\Source { 23 | protected $name = "dods"; 24 | protected $name_long = "Day of Defeat: Source"; 25 | } 26 | -------------------------------------------------------------------------------- /gameq3/protocols/hldm.php: -------------------------------------------------------------------------------- 1 | . 17 | */ 18 | 19 | 20 | namespace GameQ3\protocols; 21 | 22 | class Hldm extends \GameQ3\Protocols\Source { 23 | protected $name = "hldm"; 24 | protected $name_long = "Half Life: Deathmatch"; 25 | } 26 | -------------------------------------------------------------------------------- /gameq3/protocols/homefront.php: -------------------------------------------------------------------------------- 1 | . 17 | */ 18 | 19 | 20 | namespace GameQ3\protocols; 21 | 22 | class Homefront extends \GameQ3\Protocols\Source { 23 | protected $name = "homefront"; 24 | protected $name_long = "Homefront"; 25 | } 26 | -------------------------------------------------------------------------------- /gameq3/protocols/alienswarm.php: -------------------------------------------------------------------------------- 1 | . 17 | */ 18 | 19 | 20 | namespace GameQ3\protocols; 21 | 22 | class Alienswarm extends \GameQ3\Protocols\Source { 23 | protected $name = "alienswarm"; 24 | protected $name_long = "Alien Swarm"; 25 | } 26 | -------------------------------------------------------------------------------- /gameq3/protocols/hl2dm.php: -------------------------------------------------------------------------------- 1 | . 17 | */ 18 | 19 | 20 | namespace GameQ3\protocols; 21 | 22 | class Hl2dm extends \GameQ3\Protocols\Source { 23 | protected $name = "hl2dm"; 24 | protected $name_long = "Half Life 2: Deathmatch"; 25 | } 26 | -------------------------------------------------------------------------------- /gameq3/protocols/insurgency.php: -------------------------------------------------------------------------------- 1 | . 17 | */ 18 | 19 | 20 | namespace GameQ3\protocols; 21 | 22 | class Insurgency extends \GameQ3\Protocols\Source { 23 | protected $name = "insurgency"; 24 | protected $name_long = "Insurgency"; 25 | } 26 | -------------------------------------------------------------------------------- /gameq3/protocols/csgo.php: -------------------------------------------------------------------------------- 1 | . 17 | */ 18 | 19 | 20 | namespace GameQ3\protocols; 21 | 22 | class Csgo extends \GameQ3\Protocols\Source { 23 | protected $name = "csgo"; 24 | protected $name_long = "Counter-Strike: Global Offensive"; 25 | } 26 | -------------------------------------------------------------------------------- /gameq3/protocols/left4dead2.php: -------------------------------------------------------------------------------- 1 | . 17 | */ 18 | 19 | 20 | namespace GameQ3\protocols; 21 | 22 | class Left4dead2 extends \GameQ3\Protocols\Source { 23 | protected $name = "left4dead2"; 24 | protected $name_long = "Left 4 Dead 2"; 25 | } 26 | -------------------------------------------------------------------------------- /gameq3/protocols/codmw3.php: -------------------------------------------------------------------------------- 1 | . 17 | */ 18 | 19 | 20 | namespace GameQ3\protocols; 21 | 22 | class Codmw3 extends \GameQ3\Protocols\Source { 23 | protected $name = "codmw3"; 24 | protected $name_long = "Call of Duty: Modern Warfare 3"; 25 | } 26 | -------------------------------------------------------------------------------- /gameq3/protocols/zombiemaster.php: -------------------------------------------------------------------------------- 1 | . 17 | */ 18 | 19 | 20 | namespace GameQ3\protocols; 21 | 22 | class Zombiemaster extends \GameQ3\Protocols\Source { 23 | protected $name = "zombiemaster"; 24 | protected $name_long = "Zombie Master"; 25 | } 26 | -------------------------------------------------------------------------------- /gameq3/protocols/arma2oa.php: -------------------------------------------------------------------------------- 1 | . 17 | */ 18 | 19 | 20 | namespace GameQ3\protocols; 21 | 22 | class Arma2oa extends \GameQ3\Protocols\Arma2 { 23 | protected $name = "arma2oa"; 24 | protected $name_long = "Armed Assault 2: Operation Arrowhead"; 25 | 26 | } -------------------------------------------------------------------------------- /gameq3/protocols/cod.php: -------------------------------------------------------------------------------- 1 | . 17 | */ 18 | 19 | 20 | namespace GameQ3\protocols; 21 | 22 | class Cod extends \GameQ3\Protocols\Quake3 { 23 | 24 | protected $name = "cod"; 25 | protected $name_long = "Call of Duty"; 26 | 27 | protected $query_port = 28960; 28 | protected $ports_type = self::PT_SAME; 29 | } 30 | -------------------------------------------------------------------------------- /gameq3/protocols/cod2.php: -------------------------------------------------------------------------------- 1 | . 17 | */ 18 | 19 | 20 | namespace GameQ3\protocols; 21 | 22 | class Cod2 extends \GameQ3\Protocols\Quake3 { 23 | 24 | protected $name = "cod2"; 25 | protected $name_long = "Call of Duty 2"; 26 | 27 | protected $query_port = 28960; 28 | protected $ports_type = self::PT_SAME; 29 | } 30 | -------------------------------------------------------------------------------- /gameq3/protocols/cod4.php: -------------------------------------------------------------------------------- 1 | . 17 | */ 18 | 19 | 20 | namespace GameQ3\protocols; 21 | 22 | class Cod4 extends \GameQ3\Protocols\Quake3 { 23 | 24 | protected $name = "cod4"; 25 | protected $name_long = "Call of Duty 4"; 26 | 27 | protected $query_port = 28960; 28 | protected $ports_type = self::PT_SAME; 29 | } 30 | -------------------------------------------------------------------------------- /gameq3/protocols/coduo.php: -------------------------------------------------------------------------------- 1 | . 17 | */ 18 | 19 | 20 | namespace GameQ3\protocols; 21 | 22 | class Coduo extends \GameQ3\Protocols\Quake3 { 23 | 24 | protected $name = "coduo"; 25 | protected $name_long = "Call of Duty: United Offensive"; 26 | 27 | protected $query_port = 28960; 28 | protected $ports_type = self::PT_SAME; 29 | } 30 | -------------------------------------------------------------------------------- /gameq3/protocols/codwaw.php: -------------------------------------------------------------------------------- 1 | . 17 | */ 18 | 19 | 20 | namespace GameQ3\protocols; 21 | 22 | class Codwaw extends \GameQ3\Protocols\Quake3 { 23 | 24 | protected $name = "codwaw"; 25 | protected $name_long = "Call of Duty: World at War"; 26 | 27 | protected $query_port = 28960; 28 | protected $ports_type = self::PT_SAME; 29 | } 30 | -------------------------------------------------------------------------------- /gameq3/protocols/ut.php: -------------------------------------------------------------------------------- 1 | . 17 | */ 18 | 19 | 20 | namespace GameQ3\protocols; 21 | 22 | class Ut extends \GameQ3\Protocols\Gamespy { 23 | protected $name = "ut"; 24 | protected $name_long = "Unreal Tournament"; 25 | 26 | protected $query_port = 7778; 27 | protected $connect_port = 7777; 28 | protected $ports_type = self::PT_DIFFERENT_COMPUTABLE; 29 | } 30 | -------------------------------------------------------------------------------- /examples/simple.php: -------------------------------------------------------------------------------- 1 | 'CS 1.6', 9 | 'type' => 'cs', 10 | 'connect_host' => 'simhost.org:27015', 11 | // 'unset' => array('players', 'settings'), 12 | ); 13 | 14 | 15 | $gq = new \GameQ3\GameQ3(); 16 | 17 | $gq->setLogLevel(true, true, true, true); 18 | $gq->setFilter('colorize', array( 19 | 'format' => 'strip' 20 | )); 21 | 22 | $gq->setFilter('strip_badchars'); 23 | 24 | $gq->setFilter('sortplayers', array( 25 | 'sortkeys' => array( 26 | array('key' => 'is_bot', 'order' => 'asc'), 27 | array('key' => 'score', 'order' => 'desc'), 28 | array('key' => 'name', 'order' => 'asc'), 29 | ) 30 | )); 31 | 32 | try { 33 | $gq->addServer($server); 34 | } 35 | catch(Exception $e) { 36 | die($e->getMessage()); 37 | } 38 | 39 | $t = microtime(true); 40 | $results = $gq->requestAllData(); 41 | $t = (microtime(true) - $t); 42 | 43 | echo "Time elapsed: " . sprintf("%.2f", $t * 1000) . "ms.\n"; 44 | 45 | var_dump($results); -------------------------------------------------------------------------------- /gameq3/protocols/ut2004.php: -------------------------------------------------------------------------------- 1 | . 17 | */ 18 | 19 | 20 | namespace GameQ3\protocols; 21 | 22 | class Ut2004 extends \GameQ3\Protocols\Unreal2 { 23 | 24 | protected $name = "ut2004"; 25 | protected $name_long = "Unreal Tournament 2004"; 26 | 27 | protected $query_port = 7778; 28 | protected $connect_port = 7777; 29 | protected $ports_type = self::PT_DIFFERENT_COMPUTABLE; 30 | } 31 | -------------------------------------------------------------------------------- /gameq3/protocols/minecraftquery.php: -------------------------------------------------------------------------------- 1 | . 17 | * 18 | * 19 | */ 20 | 21 | namespace GameQ3\protocols; 22 | 23 | class Minecraftquery extends \GameQ3\Protocols\Gamespy3 { 24 | protected $query_port = 25565; 25 | protected $connect_port = 25565; 26 | protected $ports_type = self::PT_DIFFERENT_NONCOMPUTABLE_VARIABLE; 27 | 28 | protected $name = 'minecraft'; 29 | protected $name_long = "Minecraft"; 30 | 31 | } -------------------------------------------------------------------------------- /gameq3/protocols/bf4.php: -------------------------------------------------------------------------------- 1 | . 17 | */ 18 | 19 | 20 | namespace GameQ3\protocols; 21 | 22 | /* 23 | * Sometimes BF4 sends last players packet after ~100ms from previous, leading to "length OOB exception". Make sure to set higher timeouts in this case. For example, 300ms: $gq->setOption('read_got_timeout', 300); 24 | */ 25 | 26 | class Bf4 extends \GameQ3\Protocols\Bf3 { 27 | protected $name = "bf4"; 28 | protected $name_long = "Battlefield 4"; 29 | 30 | } -------------------------------------------------------------------------------- /gameq3/protocols/killingfloor.php: -------------------------------------------------------------------------------- 1 | . 17 | */ 18 | 19 | 20 | namespace GameQ3\protocols; 21 | 22 | class Killingfloor extends \GameQ3\Protocols\Unreal2 { 23 | protected $name = "killingfloor"; 24 | protected $name_long = "Killing Floor"; 25 | 26 | protected $query_port = 7708; 27 | protected $connect_port = 7707; 28 | protected $ports_type = self::PT_DIFFERENT_COMPUTABLE; 29 | 30 | protected $connect_string = 'steam://connect/{IDENTIFIER}'; // same as source 31 | } 32 | -------------------------------------------------------------------------------- /gameq3/filters/strip_badchars.php: -------------------------------------------------------------------------------- 1 | . 17 | * 18 | * 19 | */ 20 | 21 | namespace GameQ3\filters; 22 | 23 | class Strip_badchars { 24 | 25 | public static function filter(&$data, $args) { 26 | 27 | array_walk_recursive($data, function(&$val, $key) { 28 | if (is_string($val)) { 29 | $val = trim($val); 30 | 31 | // http://stackoverflow.com/a/1401716 32 | // http://stackoverflow.com/a/13695364 33 | $val = htmlspecialchars_decode(htmlspecialchars($val, \ENT_SUBSTITUTE, 'UTF-8')); 34 | } 35 | }); 36 | 37 | } 38 | } -------------------------------------------------------------------------------- /gameq3/protocols/cs.php: -------------------------------------------------------------------------------- 1 | . 17 | */ 18 | 19 | 20 | namespace GameQ3\protocols; 21 | 22 | class Cs extends \GameQ3\Protocols\Source { 23 | protected $name = "cs"; 24 | protected $name_long = "Counter-Strike 1.6"; 25 | 26 | 27 | protected function _process_rules($packets) { 28 | // CS 1.6 sends A2S_INFO in new source format, but rules in old format. Durty workaround for this. 29 | 30 | $os = $this->source_engine; 31 | $this->source_engine = false; 32 | 33 | $packet = $this->_preparePackets($packets); 34 | 35 | $this->source_engine = $os; 36 | 37 | 38 | if (!$packet) return false; 39 | 40 | $this->_parse_rules($packet); 41 | 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /gameq3/protocols/rust.php: -------------------------------------------------------------------------------- 1 | . 17 | */ 18 | 19 | 20 | namespace GameQ3\protocols; 21 | 22 | 23 | // Players array is unusable, as it consists of "hidden"-0 rows for each player. 24 | 25 | class Rust extends \GameQ3\Protocols\Source { 26 | protected $name = "rust"; 27 | protected $name_long = "Rust"; 28 | 29 | protected $query_port = 28016; 30 | protected $connect_port = 28015; 31 | protected $ports_type = self::PT_DIFFERENT_COMPUTABLE; 32 | 33 | public function init() { 34 | $this->forceRequested('settings', false); 35 | 36 | $this->forceRequested('players', false); 37 | $this->result->setIgnore('players', true); 38 | 39 | parent::init(); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /gameq3/protocols/cscz.php: -------------------------------------------------------------------------------- 1 | . 17 | */ 18 | 19 | 20 | namespace GameQ3\protocols; 21 | 22 | class Cscz extends \GameQ3\Protocols\Source { 23 | protected $name = "cscz"; 24 | protected $name_long = "Counter-Strike: Condition Zero"; 25 | 26 | 27 | protected function _process_rules($packets) { 28 | // CS 1.6 sends A2S_INFO in new source format, but rules in old format. Durty workaround for this. 29 | 30 | $os = $this->source_engine; 31 | $this->source_engine = false; 32 | 33 | $packet = $this->_preparePackets($packets); 34 | 35 | $this->source_engine = $os; 36 | 37 | 38 | if (!$packet) return false; 39 | 40 | $this->_parse_rules($packet); 41 | 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /gameq3/protocols/left4dead.php: -------------------------------------------------------------------------------- 1 | . 17 | */ 18 | 19 | 20 | namespace GameQ3\protocols; 21 | 22 | class Left4dead extends \GameQ3\Protocols\Source { 23 | protected $name = "left4dead"; 24 | protected $name_long = "Left 4 Dead"; 25 | 26 | protected function _detectMode($game_description, $appid) { 27 | if ($appid == 500) { 28 | if (strpos($game_description, '- Co-op')) { 29 | $this->result->addGeneral('mode', 'coop'); 30 | } else 31 | if (strpos($game_description, '- Survival')) { 32 | $this->result->addGeneral('mode', 'survival'); 33 | } else 34 | if (strpos($game_description, '- Versus')) { 35 | $this->result->addGeneral('mode', 'versus'); 36 | } 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /gameq3/protocols/ship.php: -------------------------------------------------------------------------------- 1 | . 17 | */ 18 | 19 | 20 | namespace GameQ3\protocols; 21 | 22 | use GameQ3\Buffer; 23 | 24 | class Ship extends \GameQ3\Protocols\Source { 25 | protected $name = "ship"; 26 | protected $name_long = "The Ship"; 27 | 28 | protected function _parseDetailsExtension(Buffer &$buf, $appid) { 29 | if ($appid == 2400) { 30 | // mode 31 | $m = $buf->readInt8(); 32 | switch ($m) { 33 | case 0: $ms = "Hunt"; break; 34 | case 1: $ms = "Elimination"; break; 35 | case 2: $ms = "Duel"; break; 36 | case 3: $ms = "Deathmatch"; break; 37 | case 4: $ms = "VIP Team"; break; 38 | case 5: $ms = "Team Elimination"; break; 39 | default: $ms = false; 40 | } 41 | if ($ms) 42 | $this->result->addGeneral('mode', $ms); 43 | 44 | $this->result->addSetting('the_ship_witnesses', $buf->readInt8()); 45 | $this->result->addSetting('the_ship_duration', $buf->readInt8()); 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /gameq3/protocols/arma3.php: -------------------------------------------------------------------------------- 1 | . 17 | */ 18 | 19 | namespace GameQ3\protocols; 20 | 21 | class Arma3 extends \GameQ3\Protocols\Gamespy2 { 22 | protected $name = "arma3"; 23 | protected $name_long = "Armed Assault 3"; 24 | 25 | protected $query_port = 2302; 26 | protected $ports_type = self::PT_SAME; 27 | 28 | protected function _put_var($key, $val) { 29 | switch($key) { 30 | case 'hostname': 31 | $this->result->addGeneral('hostname', $val); 32 | break; 33 | case 'mission': 34 | $this->result->addGeneral('map', $val); 35 | $this->result->addSetting($key, $val); 36 | break; 37 | case 'gamever': 38 | $this->result->addGeneral('version', $val); 39 | break; 40 | case 'gametype': 41 | $this->result->addGeneral('mode', $val); 42 | break; 43 | case 'numplayers': 44 | $this->result->addGeneral('num_players', $val); 45 | break; 46 | case 'maxplayers': 47 | $this->result->addGeneral('max_players', $val); 48 | break; 49 | case 'password': 50 | $this->result->addGeneral('password', $val == 1); 51 | break; 52 | default: 53 | $this->result->addSetting($key, $val); 54 | } 55 | } 56 | } -------------------------------------------------------------------------------- /gameq3/protocols/arma2.php: -------------------------------------------------------------------------------- 1 | . 17 | */ 18 | 19 | 20 | namespace GameQ3\protocols; 21 | 22 | class Arma2 extends \GameQ3\Protocols\Gamespy3 { 23 | protected $name = "arma2"; 24 | protected $name_long = "Armed Assault 2"; 25 | 26 | protected $query_port = 2302; 27 | protected $ports_type = self::PT_SAME; 28 | 29 | protected function _put_var($key, $val) { 30 | switch($key) { 31 | case 'hostname': 32 | $this->result->addGeneral('hostname', $val); 33 | break; 34 | case 'mission': 35 | $this->result->addGeneral('map', $val); 36 | $this->result->addSetting($key, $val); 37 | break; 38 | case 'gamever': 39 | $this->result->addGeneral('version', $val); 40 | break; 41 | case 'gametype': 42 | $this->result->addGeneral('mode', $val); 43 | break; 44 | case 'numplayers': 45 | $this->result->addGeneral('num_players', $val); 46 | break; 47 | case 'maxplayers': 48 | $this->result->addGeneral('max_players', $val); 49 | break; 50 | case 'password': 51 | $this->result->addGeneral('password', $val == 1); 52 | break; 53 | default: 54 | $this->result->addSetting($key, $val); 55 | } 56 | } 57 | 58 | } -------------------------------------------------------------------------------- /gameq3/protocols/il2.php: -------------------------------------------------------------------------------- 1 | . 17 | */ 18 | 19 | 20 | namespace GameQ3\protocols; 21 | 22 | class Il2 extends \GameQ3\Protocols\Gamespy { 23 | protected $name = "il2"; 24 | protected $name_long = "IL-2 Sturmovik"; 25 | 26 | protected $query_port = 21000; 27 | protected $ports_type = self::PT_SAME; 28 | 29 | protected function _put_var($key, $val) { 30 | switch($key) { 31 | case 'hostname': 32 | $this->result->addGeneral('hostname', iconv("ISO-8859-1//IGNORE", "utf-8", $val)); 33 | break; 34 | case 'mapname': 35 | $this->result->addGeneral('map', basename($val, '.mis')); 36 | $this->result->addSetting($key, $val); 37 | break; 38 | case 'gamever': 39 | $this->result->addGeneral('version', $val); 40 | break; 41 | case 'gametype': 42 | $this->result->addGeneral('mode', $val); 43 | break; 44 | case 'numplayers': 45 | $this->result->addGeneral('num_players', $val); 46 | break; 47 | case 'maxplayers': 48 | $this->result->addGeneral('max_players', $val); 49 | break; 50 | case 'password': 51 | $this->result->addGeneral('password', $val == 1); 52 | break; 53 | default: 54 | $this->result->addSetting($key, $val); 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /gameq3/protocols/arma.php: -------------------------------------------------------------------------------- 1 | . 17 | */ 18 | 19 | 20 | namespace GameQ3\protocols; 21 | 22 | class Arma extends \GameQ3\Protocols\Gamespy2 { 23 | protected $name = "arma"; 24 | protected $name_long = "Armed Assault"; 25 | 26 | protected $query_port = 2302; 27 | protected $ports_type = self::PT_SAME; 28 | 29 | /// TODO: teamids are not teams at all. It is something like rank. Find this out. 30 | 31 | protected function _put_var($key, $val) { 32 | switch($key) { 33 | case 'hostname': 34 | $this->result->addGeneral('hostname', iconv("ISO-8859-1//IGNORE", "utf-8", $val)); 35 | break; 36 | case 'mission': 37 | $this->result->addGeneral('map', $val); 38 | break; 39 | case 'gamever': 40 | $this->result->addGeneral('version', $val); 41 | break; 42 | case 'gametype': 43 | $this->result->addGeneral('mode', $val); 44 | break; 45 | case 'numplayers': 46 | $this->result->addGeneral('num_players', $val); 47 | break; 48 | case 'maxplayers': 49 | $this->result->addGeneral('max_players', $val); 50 | break; 51 | case 'password': 52 | $this->result->addGeneral('password', $val == 1); 53 | break; 54 | default: 55 | $this->result->addSetting($key, $val); 56 | } 57 | } 58 | } -------------------------------------------------------------------------------- /gameq3/protocols/fear.php: -------------------------------------------------------------------------------- 1 | . 17 | */ 18 | 19 | 20 | namespace GameQ3\protocols; 21 | 22 | class Fear extends \GameQ3\Protocols\Gamespy2 { 23 | protected $name = "fear"; 24 | protected $name_long = "F.E.A.R."; 25 | 26 | protected $query_port = 27888; 27 | protected $ports_type = self::PT_SAME; 28 | 29 | protected function _put_var($key, $val) { 30 | switch($key) { 31 | case 'hostname': 32 | $this->result->addGeneral('hostname', iconv("ISO-8859-1//IGNORE", "utf-8", $val)); 33 | break; 34 | case 'mapname': 35 | $this->result->addGeneral('map', $val); 36 | break; 37 | case 'gamever': 38 | $this->result->addGeneral('version', $val); 39 | break; 40 | case 'gametype': 41 | $this->result->addGeneral('mode', $val); 42 | break; 43 | case 'numplayers': 44 | $this->result->addGeneral('num_players', $val); 45 | break; 46 | case 'maxplayers': 47 | $this->result->addGeneral('max_players', $val); 48 | break; 49 | case 'password': 50 | $this->result->addGeneral('password', $val == 1); 51 | break; 52 | case 'punkbuster': 53 | $this->result->addGeneral('secure', $val == 1); 54 | break; 55 | default: 56 | $this->result->addSetting($key, $val); 57 | } 58 | } 59 | 60 | } -------------------------------------------------------------------------------- /gameq3/protocols/minecraft16.php: -------------------------------------------------------------------------------- 1 | . 17 | * 18 | * 19 | */ 20 | 21 | namespace GameQ3\protocols; 22 | 23 | // http://wiki.vg/Server_List_Ping#1.6 24 | 25 | class Minecraft16 extends \GameQ3\Protocols\Minecraft { 26 | 27 | protected $protocol_version = "\x4a"; 28 | 29 | protected $packets = null; 30 | 31 | protected $protocol = 'minecraft16'; 32 | protected $name = 'minecraft'; 33 | protected $name_long = "Minecraft 1.6"; 34 | 35 | protected function _toShort($i) { 36 | return pack('n*', $i); 37 | } 38 | 39 | protected function _toInt($i) { 40 | return pack('N*', $i); 41 | } 42 | 43 | protected function _networkString($str) { 44 | return iconv('ISO-8859-1//IGNORE', 'UCS-2LE', $str); 45 | } 46 | 47 | protected function _buildStatus($hostname, $port) { 48 | $packet = "\xFE\x01\xFA"; 49 | $packet .= $this->_toShort(11); 50 | $packet .= $this->_networkString("MC|PingHost"); 51 | $packet .= $this->_toShort(7 + 2 * strlen($hostname)); 52 | $packet .= $this->protocol_version; 53 | $packet .= $this->_toShort(strlen($hostname)); 54 | $packet .= $this->_networkString($hostname); 55 | $packet .= $this->_toInt($port); 56 | 57 | return $packet; 58 | } 59 | 60 | public function init() { 61 | $this->queue('status', 'tcp', $this->_buildStatus($this->query_addr, $this->query_port), array('response_count' => 1, 'close' => true)); 62 | } 63 | } -------------------------------------------------------------------------------- /gameq3/protocols/bfv.php: -------------------------------------------------------------------------------- 1 | . 17 | */ 18 | 19 | 20 | namespace GameQ3\protocols; 21 | 22 | class Bfv extends \GameQ3\Protocols\Gamespy2 { 23 | protected $name = "bfv"; 24 | protected $name_long = "Battlefield Vietnam"; 25 | 26 | protected $query_port = 23000; 27 | protected $connect_port = 14567; 28 | protected $ports_type = self::PT_DIFFERENT_NONCOMPUTABLE_VARIABLE; 29 | 30 | protected function _put_var($key, $val) { 31 | switch($key) { 32 | case 'hostname': 33 | $this->result->addGeneral('hostname', iconv("ISO-8859-1//IGNORE", "utf-8", $val)); 34 | break; 35 | case 'mapname': 36 | $this->result->addGeneral('map', $val); 37 | break; 38 | case 'gamever': 39 | $this->result->addGeneral('version', $val); 40 | break; 41 | case 'gametype': 42 | $this->result->addGeneral('mode', $val); 43 | break; 44 | case 'numplayers': 45 | $this->result->addGeneral('num_players', $val); 46 | break; 47 | case 'maxplayers': 48 | $this->result->addGeneral('max_players', $val); 49 | break; 50 | case 'reservedslots': 51 | $this->result->addGeneral('private_players', $val); 52 | $this->result->addSetting($key, $val); 53 | break; 54 | case 'password': 55 | $this->result->addGeneral('password', $val == 1); 56 | break; 57 | case 'sv_punkbuster': 58 | $this->result->addGeneral('secure', $val == 1); 59 | break; 60 | case 'hostport': 61 | $this->setConnectPort($val); 62 | $this->result->addSetting($key, $val); 63 | break; 64 | default: 65 | $this->result->addSetting($key, $val); 66 | } 67 | } 68 | 69 | } -------------------------------------------------------------------------------- /gameq3/protocols/bf2142.php: -------------------------------------------------------------------------------- 1 | . 17 | */ 18 | 19 | 20 | namespace GameQ3\protocols; 21 | 22 | class Bf2142 extends \GameQ3\Protocols\Gamespy3 { 23 | protected $name = "bf2142"; 24 | protected $name_long = "Battlefield 2142"; 25 | 26 | protected $query_port = 29900; 27 | protected $connect_port = 16567; 28 | protected $ports_type = self::PT_DIFFERENT_NONCOMPUTABLE_VARIABLE; 29 | 30 | protected function _put_var($key, $val) { 31 | switch($key) { 32 | case 'hostname': 33 | $this->result->addGeneral('hostname', $val); 34 | break; 35 | case 'mapname': 36 | $this->result->addGeneral('map', $val); 37 | break; 38 | case 'gamever': 39 | $this->result->addGeneral('version', $val); 40 | break; 41 | case 'gametype': 42 | $this->result->addGeneral('mode', $val); 43 | break; 44 | case 'numplayers': 45 | $this->result->addGeneral('num_players', $val); 46 | break; 47 | case 'maxplayers': 48 | $this->result->addGeneral('max_players', $val); 49 | break; 50 | case 'bf2142_reservedslots': 51 | $this->result->addGeneral('private_players', $val); 52 | $this->result->addSetting($key, $val); 53 | break; 54 | case 'password': 55 | $this->result->addGeneral('password', $val == 1); 56 | break; 57 | case 'bf2142_anticheat': 58 | $this->result->addGeneral('secure', $val == 1); 59 | $this->result->addSetting($key, $val); 60 | break; 61 | case 'hostport': 62 | $this->setConnectPort($val); 63 | $this->result->addSetting($key, $val); 64 | break; 65 | default: 66 | $this->result->addSetting($key, $val); 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /gameq3/protocols/bf2.php: -------------------------------------------------------------------------------- 1 | . 17 | */ 18 | 19 | 20 | namespace GameQ3\protocols; 21 | 22 | class Bf2 extends \GameQ3\Protocols\Gamespy3 { 23 | protected $name = "bf2"; 24 | protected $name_long = "Battlefield 2"; 25 | 26 | protected $query_port = 29900; 27 | protected $connect_port = 16567; 28 | protected $ports_type = self::PT_DIFFERENT_NONCOMPUTABLE_VARIABLE; 29 | 30 | protected $packets = array( 31 | 'all' => "\xFE\xFD\x00\x10\x20\x30\x40\xFF\xFF\xFF\x01", 32 | ); 33 | 34 | protected $challenge = false; 35 | 36 | 37 | protected function _put_var($key, $val) { 38 | switch($key) { 39 | case 'hostname': 40 | $this->result->addGeneral('hostname', $val); 41 | break; 42 | case 'mapname': 43 | $this->result->addGeneral('map', $val); 44 | break; 45 | case 'gamever': 46 | $this->result->addGeneral('version', $val); 47 | break; 48 | case 'gametype': 49 | $this->result->addGeneral('mode', $val); 50 | break; 51 | case 'numplayers': 52 | $this->result->addGeneral('num_players', $val); 53 | break; 54 | case 'maxplayers': 55 | $this->result->addGeneral('max_players', $val); 56 | break; 57 | case 'bf2_reservedslots': 58 | $this->result->addGeneral('private_players', $val); 59 | $this->result->addSetting($key, $val); 60 | break; 61 | case 'password': 62 | $this->result->addGeneral('password', $val == 1); 63 | break; 64 | case 'bf2_anticheat': 65 | $this->result->addGeneral('secure', $val == 1); 66 | $this->result->addSetting($key, $val); 67 | break; 68 | case 'hostport': 69 | $this->setConnectPort($val); 70 | $this->result->addSetting($key, $val); 71 | break; 72 | default: 73 | $this->result->addSetting($key, $val); 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /gameq3/protocols/bf1942.php: -------------------------------------------------------------------------------- 1 | . 17 | */ 18 | 19 | 20 | namespace GameQ3\protocols; 21 | 22 | class Bf1942 extends \GameQ3\Protocols\Gamespy { 23 | protected $name = "bf1942"; 24 | protected $name_long = "Battlefield 1942"; 25 | 26 | protected $query_port = 23000; 27 | protected $connect_port = 14567; 28 | protected $ports_type = self::PT_DIFFERENT_NONCOMPUTABLE_VARIABLE; 29 | 30 | 31 | protected function _put_var($key, $val) { 32 | switch($key) { 33 | case 'hostname': 34 | $this->result->addGeneral('hostname', iconv("ISO-8859-1//IGNORE", "utf-8", $val)); 35 | break; 36 | // case 'maptitle': 37 | case 'mapname': 38 | $this->result->addGeneral('map', $val); 39 | break; 40 | case 'gamever': 41 | $this->result->addGeneral('version', $val); 42 | break; 43 | case 'gametype': 44 | $this->result->addGeneral('mode', $val); 45 | break; 46 | case 'numplayers': 47 | $this->result->addGeneral('num_players', $val); 48 | break; 49 | case 'maxplayers': 50 | $this->result->addGeneral('max_players', $val); 51 | break; 52 | case 'reservedslots': 53 | $this->result->addGeneral('private_players', $val); 54 | break; 55 | case 'password': 56 | $this->result->addGeneral('password', $val == 1); 57 | break; 58 | case 'sv_punkbuster': 59 | $this->result->addGeneral('secure', $val == 1); 60 | $this->result->addSetting($key, $val); 61 | break; 62 | case 'tickets1': 63 | $this->teams[1] = array(); 64 | $this->teams[1]['tickets'] = $val; 65 | $this->result->addSetting($key, $val); 66 | break; 67 | case 'tickets2': 68 | $this->teams[2] = array(); 69 | $this->teams[2]['tickets'] = $val; 70 | $this->result->addSetting($key, $val); 71 | break; 72 | case 'hostport': 73 | $this->setConnectPort($val); 74 | $this->result->addSetting($key, $val); 75 | break; 76 | default: 77 | $this->result->addSetting($key, $val); 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /gameq3/protocols/minecraft.php: -------------------------------------------------------------------------------- 1 | . 17 | * 18 | * 19 | */ 20 | 21 | namespace GameQ3\protocols; 22 | 23 | use GameQ3\Buffer; 24 | 25 | class Minecraft extends \GameQ3\Protocols { 26 | 27 | protected $packets = array( 28 | //'status' => "\xFE", 29 | 'status' => "\xFE\x01", 30 | ); 31 | 32 | protected $query_port = 25565; 33 | protected $ports_type = self::PT_SAME; 34 | 35 | protected $protocol = 'minecraft'; 36 | protected $name = 'minecraft'; 37 | protected $name_long = "Minecraft"; 38 | 39 | 40 | public function init() { 41 | $this->queue('status', 'tcp', $this->packets['status'], array('response_count' => 1, 'close' => true)); 42 | } 43 | 44 | protected function processRequests($qid, $requests) { 45 | if ($qid === 'status') { 46 | return $this->_process_status($requests['responses']); 47 | } 48 | } 49 | 50 | protected function _process_status($packets) { 51 | // http://www.wiki.vg/Server_List_Ping 52 | // https://gist.github.com/barneygale/1209061 53 | 54 | $buf = new Buffer($packets[0]); 55 | 56 | if ($buf->read(1) !== "\xFF") { 57 | $this->debug("Wrong header"); 58 | return false; 59 | } 60 | 61 | 62 | // packet length 63 | $buf->skip(2); 64 | 65 | $cbuf = iconv("UTF-16BE//IGNORE", "UTF-8", $buf->getBuffer()); 66 | 67 | // New version 68 | if (substr($cbuf, 0, 2) === "\xC2\xA7") { 69 | $info = explode("\x00", substr($cbuf, 2)); 70 | 71 | // $info[0] = 1 72 | $this->result->addSetting('protocol_version', $info[1]); 73 | 74 | $this->result->addGeneral('version', $info[2]); 75 | $this->result->addGeneral('hostname', $info[3]); 76 | 77 | $this->result->addGeneral('num_players', min(intval($info[4]), intval($info[5]))); 78 | $this->result->addGeneral('max_players', intval($info[5])); 79 | 80 | } else { 81 | $info = explode("\xC2\xA7", $cbuf); 82 | 83 | // Actually it is MotD, but they usually use this as server name 84 | $this->result->addGeneral('hostname', $info[0]); 85 | $this->result->addGeneral('num_players', min(intval($info[1]), intval($info[2]))); 86 | $this->result->addGeneral('max_players', intval($info[2])); 87 | } 88 | } 89 | 90 | } -------------------------------------------------------------------------------- /gameq3/filters/sortplayers.php: -------------------------------------------------------------------------------- 1 | . 17 | * 18 | * 19 | */ 20 | 21 | namespace GameQ3\filters; 22 | 23 | class Sortplayers { 24 | 25 | const DEFAULT_ORDER = 'asc'; 26 | 27 | public static function filter(&$data, $args) { 28 | if (empty($data['players'])) 29 | return; 30 | 31 | $sortkeys = array( 32 | array('key' => 'name', 'order' => 'asc') 33 | ); 34 | if (isset($args['sortkeys'])) { 35 | $sortkeys = $args['sortkeys']; 36 | } else 37 | if (isset($args['sortkey'])) { 38 | $sortkeys = array('key' => $args['sortkey'], 'order' => isset($args['order']) ? $args['order'] : self::DEFAULT_ORDER); 39 | } 40 | 41 | 42 | $s = array(); 43 | foreach($sortkeys as $k) { 44 | $r = new \stdClass(); 45 | 46 | if (!isset($k['key'])) 47 | continue; 48 | 49 | $r->key = $k['key']; 50 | 51 | if (!isset($k['order'])) 52 | $k['order'] = self::DEFAULT_ORDER; 53 | 54 | $k['order'] = ($k['order'] == 'asc') || ($k['order'] == \SORT_ASC); 55 | 56 | $r->order = $k['order']; 57 | 58 | $s []= $r; 59 | } 60 | $sortkeys = $s; 61 | unset($s); 62 | 63 | uasort($data['players'], function($a, $b) use($sortkeys) { 64 | 65 | foreach($sortkeys as $k) { 66 | if (isset($a[$k->key]) && isset($b[$k->key]) && !is_array($a[$k->key]) && !is_array($b[$k->key])) { 67 | $ca = $a[$k->key]; 68 | $cb = $b[$k->key]; 69 | } else 70 | if (isset($a['other'][$k->key]) && isset($b['other'][$k->key]) && !is_array($a['other'][$k->key]) && !is_array($b['other'][$k->key])) { 71 | $ca = $a['other'][$k->key]; 72 | $cb = $b['other'][$k->key]; 73 | } else { 74 | continue; 75 | } 76 | 77 | if (is_string($ca) || is_string($cb)) { 78 | $res = strcasecmp("" . $ca, "" . $cb); 79 | 80 | if ($res == 0) 81 | continue; 82 | 83 | $res = $res < 0; 84 | } else { 85 | if ($ca === $cb) 86 | continue; 87 | 88 | $res = $ca < $cb; 89 | } 90 | 91 | if (!$k->order) 92 | $res = !$res; 93 | 94 | return ($res ? -1 : 1); 95 | } 96 | 97 | return 0; 98 | }); 99 | } 100 | 101 | } -------------------------------------------------------------------------------- /examples/cli.php: -------------------------------------------------------------------------------- 1 | 'CS 1.6', 10 | 'type' => 'cs', 11 | 'connect_host' => 'simhost.org:27015', 12 | ), 13 | array( 14 | 'id' => 'L4D', 15 | 'type' => 'left4dead', 16 | 'connect_host' => 'simhost.org:27009', 17 | ), 18 | ); 19 | 20 | 21 | $gq = new \GameQ3\GameQ3(); 22 | 23 | $gq->setLogLevel(true, true, true, true); 24 | $gq->setFilter('colorize', array( 25 | 'format' => 'strip' 26 | )); 27 | 28 | $gq->setFilter('strip_badchars'); 29 | 30 | $gq->setFilter('sortplayers', array( 31 | 'sortkeys' => array( 32 | array('key' => 'is_bot', 'order' => 'asc'), 33 | array('key' => 'score', 'order' => 'desc'), 34 | array('key' => 'name', 'order' => 'asc'), 35 | ) 36 | )); 37 | 38 | foreach($servers as $server) { 39 | try { 40 | $gq->addServer($server); 41 | } 42 | catch(Exception $e) { 43 | die($e->getMessage()); 44 | } 45 | } 46 | 47 | 48 | 49 | while(true) { 50 | $t = microtime(true); 51 | 52 | $results = $gq->requestAllData(); 53 | 54 | $t = (microtime(true) - $t); 55 | 56 | $onl = 0; 57 | $offl = 0; 58 | $retrys = 0; 59 | $pavg = 0; 60 | 61 | 62 | foreach($results as $id => &$val) { 63 | if ($val['info']['online']) { 64 | $onl++; 65 | $pavg += $val['info']['ping_average']; 66 | 67 | echo "[".str_pad($id."]", 12) 68 | ." ".str_pad($val['info']['short_name'], 10) 69 | ." ".str_pad($val['info']['retry_count'], 2) 70 | ." ".str_pad(sprintf("%.2f", $val['info']['ping_average'])."ms", 10) 71 | ." ".($val['general']['password'] ? 'X' : 'O') 72 | ." ".($val['general']['secure'] ? 'V' : '-') 73 | ." ".str_pad($val['general']['hostname'], 50) 74 | ." ".str_pad(isset($val['general']['map']) ? $val['general']['map'] . (isset($val['general']['mode']) ? " [" . $val['general']['mode'] . "]" : "") : '', 20) 75 | ." ".$val['general']['num_players']." / ".$val['general']['max_players'] 76 | . (is_int($val['general']['private_players']) ? " (" . ( $val['general']['max_players'] + $val['general']['private_players']) . ")" : "") 77 | . (isset($val['general']['version']) ? " [" . $val['general']['version'] ."]": "") 78 | ."\n"; 79 | 80 | $retrys += $val['info']['retry_count']; 81 | 82 | 83 | } else { 84 | $offl++; 85 | echo "[".str_pad($id."]", 12) 86 | ." ".str_pad($val['info']['protocol'], 10) 87 | ." ---------------- offline\n"; 88 | } 89 | } 90 | 91 | echo "Online: " . $onl . ". Offline: " . $offl .". Retrys: " . $retrys . ". Total: " . (count($results)) . ". Ping avg: " . sprintf("%.2f", ($pavg/$onl)) . "ms.\n"; 92 | echo "Time elapsed: " . ($t) . "s.\n"; 93 | 94 | $mu1 = memory_get_usage()/1024; 95 | $results = null; 96 | unset($results); 97 | 98 | 99 | 100 | echo sprintf("Memory result/unset/max (kb) %.0f/%.0f/%.0f\n\n", $mu1, memory_get_usage()/1024, memory_get_peak_usage()/1024); 101 | 102 | sleep(2); 103 | } -------------------------------------------------------------------------------- /gameq3/protocols/mohwf.php: -------------------------------------------------------------------------------- 1 | . 17 | * 18 | * 19 | */ 20 | 21 | namespace GameQ3\protocols; 22 | 23 | class Mohwf extends \GameQ3\Protocols\Bf3 { 24 | protected $name = 'mohwf'; 25 | protected $name_long = "Medal of Honor Warfighter"; 26 | 27 | 28 | protected function _process_status($packets) { 29 | $words = $this->_preparePackets($packets); 30 | 31 | $this->result->addGeneral('hostname', $words[1]); 32 | $this->result->addGeneral('num_players', $this->filterInt($words[2])); 33 | $this->result->addGeneral('max_players', $this->filterInt($words[3])); 34 | $this->result->addGeneral('mode', $words[4]); 35 | $this->result->addGeneral('map', $words[5]); 36 | 37 | $this->result->addSetting('rounds_played', $words[6]); 38 | $this->result->addSetting('rounds_total', $words[7]); 39 | 40 | // Figure out the number of teams 41 | $num_teams = intval($words[8]); 42 | 43 | // Set the current index 44 | $index_current = 9; 45 | 46 | // Loop for the number of teams found, increment along the way 47 | for($id=1; $id<=$num_teams; $id++) { 48 | // We have tickets, but no team name. great... 49 | $this->result->addTeam($id, $id, array('tickets' => $this->filterInt($words[$index_current]))); 50 | 51 | $index_current++; 52 | } 53 | 54 | // Get and set the rest of the data points. 55 | $this->result->addSetting('target_score', $words[$index_current]); 56 | // it seems $words[$index_current + 1] is always empty 57 | $this->result->addSetting('ranked', $words[$index_current + 2] === 'true' ? 1 : 0); 58 | $this->result->addGeneral('secure', $words[$index_current + 3] === 'true'); 59 | $this->result->addGeneral('password', $words[$index_current + 4] === 'true'); 60 | $this->result->addSetting('uptime', $words[$index_current + 5]); 61 | $this->result->addSetting('round_time', $words[$index_current + 6]); 62 | 63 | // The next 3 are empty in MOHWF, kept incase they start to work some day 64 | // ip_port $words[$index_current + 7] 65 | $this->result->addSetting('punkbuster_version', $words[$index_current + 8]); 66 | $this->result->addSetting('join_queue', $words[$index_current + 9] === 'true' ? 1 : 0); 67 | 68 | $this->result->addSetting('region', $words[$index_current + 10]); 69 | $this->result->addSetting('pingsite', $words[$index_current + 11]); 70 | $this->result->addSetting('country', $words[$index_current + 12]); 71 | 72 | 73 | } 74 | } -------------------------------------------------------------------------------- /gameq3/result.php: -------------------------------------------------------------------------------- 1 | . 17 | * 18 | * 19 | */ 20 | 21 | namespace GameQ3; 22 | 23 | class Result { 24 | private $result = array(); 25 | private $ignore = array(); 26 | 27 | 28 | public function __construct($ign) { 29 | 30 | $this->result['info'] = array(); 31 | $this->result['general'] = array(); 32 | 33 | foreach(array('settings', 'players', 'teams') as $t) { 34 | $this->setIgnore($t, isset($ign[$t]) ? $ign[$t] : false); 35 | } 36 | } 37 | 38 | public function setIgnore($t, $value) { 39 | if ($value) { 40 | unset($this->result[$t]); 41 | $this->ignore[$t] = true; 42 | } else { 43 | $this->result[$t] = array(); 44 | $this->ignore[$t] = false; 45 | } 46 | } 47 | 48 | private function isIgnored($t) { 49 | return (isset($this->ignore[$t]) && $this->ignore[$t]); 50 | } 51 | 52 | public function count($key) { 53 | return count($this->result[$key]); 54 | } 55 | 56 | public function addCustom($zone, $name, $value) { 57 | if ($this->isIgnored($zone)) return false; 58 | $this->result[$zone][$name] = $value; 59 | return true; 60 | } 61 | 62 | public function addInfo($name, $value) { 63 | $this->result['info'][$name] = $value; 64 | return true; 65 | } 66 | 67 | public function addGeneral($name, $value) { 68 | $this->result['general'][$name] = $value; 69 | return true; 70 | } 71 | 72 | public function issetGeneral($name) { 73 | return isset($this->result['general'][$name]); 74 | } 75 | 76 | public function getGeneral($name) { 77 | return isset($this->result['general'][$name]) ? $this->result['general'][$name] : null; 78 | } 79 | 80 | public function addSetting($name, $value) { 81 | if ($this->isIgnored('settings')) return false; 82 | $this->result['settings'] []= array($name, $value); 83 | return true; 84 | } 85 | 86 | public function addPlayer($name, $score=null, $teamid=null, $other=array(), $is_bot=null) { 87 | if ($this->isIgnored('players')) return false; 88 | $this->result['players'] []= array( 89 | 'name' => $name, 90 | 'score' => $score, 91 | 'teamid' => $teamid, 92 | 'is_bot' => $is_bot, 93 | 'other' => $other 94 | ); 95 | return true; 96 | } 97 | 98 | public function addTeam($teamid, $name, $other = array()) { 99 | if ($this->isIgnored('teams')) return false; 100 | $this->result['teams'][$teamid]= array( 101 | 'name' => $name, 102 | 'other' => $other 103 | ); 104 | return true; 105 | } 106 | 107 | public function fetch() { 108 | return $this->result; 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /examples/list.php: -------------------------------------------------------------------------------- 1 | getAllProtocolsInfo(); 8 | 9 | $supported_games = count($protocols); 10 | 11 | $content_table = ""; 12 | 13 | function filterNull($v) { 14 | if (is_null($v)) return "not set"; 15 | return $v; 16 | } 17 | 18 | foreach ($protocols as $classname => $dp) { 19 | $cls = empty($cls) ? ' class="uneven"' : ''; 20 | 21 | $l = "\t\t\t\t"; // idents 22 | $l .= ""; 23 | $l .= sprintf("%s%s%s%s", $classname, $dp['name_long'], $dp['name'], $dp['protocol']); 24 | if ($dp['network']) { 25 | if ($dp['ports_type_info']['connect_port'] === false || $dp['ports_type_info']['query_port'] === false) { 26 | if ($dp['ports_type_info']['connect_port'] !== false) { 27 | $v = $dp['connect_port']; 28 | } else { 29 | if ($dp['ports_type_info']['query_port'] !== false) { 30 | $v = $dp['query_port']; 31 | } else { 32 | $v = null; 33 | } 34 | } 35 | 36 | $l .= sprintf("%s", filterNull($v)); 37 | } else { 38 | $l .= sprintf("%s%s", filterNull($dp['query_port']), filterNull($dp['connect_port'])); 39 | } 40 | $l .= sprintf("%s", $dp['ports_type_string']); 41 | } else { 42 | $l .= "Not a network protocol"; 43 | } 44 | $l .= "" . filterNull($dp['connect_string']) . ""; 45 | $l .= "\n"; 46 | 47 | $content_table .= $l; 48 | unset($protocols[$classname]); 49 | } 50 | 51 | ?> 52 | 53 | 54 | 55 | GameQ3 - Supported Games 56 | 57 | 94 | 95 | 96 |

GameQ3 - Supported Games ()

97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 |
GameQ3 identifierGame nameShort game nameProtocolQuery portConnect portPorts typeConnect URL
114 | 115 | -------------------------------------------------------------------------------- /gameq3/protocols/kerbalmultiplayer.php: -------------------------------------------------------------------------------- 1 | . 17 | * 18 | * 19 | */ 20 | 21 | namespace GameQ3\protocols; 22 | 23 | /** 24 | * Query kerbal multi player mod of the Kerbal Space Program game using HTTP 25 | */ 26 | 27 | // https://github.com/TehGimp/KerbalMultiPlayer/blob/c4890648b9919938bb171ede6e83562c4aa47537/KLFServer/Server.cs#L1019 28 | 29 | class kerbalmultiplayer extends \GameQ3\Protocols { 30 | protected $protocol = 'kerbalmultiplayer'; 31 | protected $name = 'kerbalmultiplayer'; 32 | protected $name_long = "Kerbal Space Program - Multiplayer"; 33 | 34 | protected $url = "/"; 35 | 36 | protected $query_port = 8080; 37 | protected $connect_port = 2076; 38 | protected $ports_type = self::PT_DIFFERENT_NONCOMPUTABLE_VARIABLE; 39 | 40 | protected function _put_var($key, $val) { 41 | switch($key) { 42 | case 'Version': 43 | $this->result->addGeneral('version', $val); 44 | break; 45 | 46 | case 'Port': 47 | $this->result->addSetting($key, $val); 48 | $this->setConnectPort($val); 49 | break; 50 | 51 | case 'Num Players': 52 | $pa = explode('/', $val, 2); 53 | if (!isset($pa[1])) { 54 | $this->debug("Bad Num Players row value: " . $val); 55 | break; 56 | } 57 | 58 | $n = intval(trim($pa[0])); 59 | $m = intval(trim($pa[1])); 60 | 61 | $this->result->addGeneral('num_players', $n); 62 | $this->result->addGeneral('max_players', $m); 63 | break; 64 | 65 | case 'Players': 66 | $pa = explode(',', $val); 67 | 68 | foreach ($pa as $pname) { 69 | $pname = trim($pname); 70 | 71 | $this->result->addPlayer($pname); 72 | } 73 | break; 74 | 75 | case 'Information': 76 | $this->result->addGeneral('hostname', $val); 77 | break; 78 | 79 | // the rest is just settings 80 | default: 81 | $this->result->addSetting($key, $val); 82 | } 83 | } 84 | 85 | public function init() { 86 | $this->queue('status', 'http', $this->url); 87 | } 88 | 89 | protected function processRequests($qid, $requests) { 90 | if ($qid === 'status') { 91 | return $this->_process_status($requests['responses']); 92 | } 93 | } 94 | 95 | protected function _process_status($packets) { 96 | $data = $packets[0]; 97 | unset($packets); 98 | 99 | $data = trim($data); 100 | 101 | $data_a = explode("\n", $data); 102 | 103 | foreach($data_a as $row) { 104 | $kv = explode(':', $row, 2); 105 | 106 | if (!isset($kv[1])) { 107 | $this->debug("Skipped row without colon: " . $row); 108 | continue; 109 | } 110 | 111 | $k = trim($kv[0]); 112 | 113 | if (strpos($k, ">") !== false || strpos($k, "<") !== false) { 114 | $this->debug("Key seems to contain HTML tag - skipped"); 115 | continue; 116 | } 117 | $this->_put_var($k, trim($kv[1])); 118 | } 119 | } 120 | 121 | } -------------------------------------------------------------------------------- /gameq3/log.php: -------------------------------------------------------------------------------- 1 | . 17 | * 18 | * 19 | */ 20 | 21 | namespace GameQ3; 22 | 23 | class Log { 24 | const DEBUG = 1; //0b0001; 25 | const WARNING = 2; //0b0010; 26 | const ERROR = 4; //0b0100; 27 | 28 | const TRACE_LIMIT = 4; 29 | const TRACE_IGNORE = 2; 30 | const FORCE_TRACE_LIMIT = 1; 31 | 32 | private $loglevel = 4; //0b0100; 33 | private $trace = false; 34 | private $logger = null; 35 | 36 | public function __construct() { 37 | $this->logger = function($str) { 38 | error_log($str); 39 | }; 40 | } 41 | 42 | public function setLogLevel($error, $warning, $debug, $trace) { 43 | $this->loglevel = 0; 44 | if ($error) $this->loglevel += self::ERROR; 45 | if ($warning) $this->loglevel += self::WARNING; 46 | if ($debug) $this->loglevel += self::DEBUG; 47 | $this->trace = ($trace == true); 48 | } 49 | 50 | public function setLogger($callback) { 51 | if (is_callable($callback)) 52 | $this->logger = $callback; 53 | } 54 | 55 | private function _logger($str) { 56 | call_user_func($this->logger, $str); 57 | } 58 | 59 | private function _log($reason, $str) { 60 | $this->_logger('GameQ3 [' . $reason . '] ' . $str); 61 | } 62 | 63 | private function _backtrace($force, $trace_skip) { 64 | if (!$this->trace && !$force) return; 65 | 66 | $trace_limit = $this->trace ? self::TRACE_LIMIT : self::FORCE_TRACE_LIMIT; 67 | 68 | $trace = debug_backtrace(\DEBUG_BACKTRACE_IGNORE_ARGS, ($trace_limit + $trace_skip + self::TRACE_IGNORE)); 69 | 70 | $i = 0; 71 | $result = "Trace:\n"; 72 | 73 | foreach($trace as $v) { 74 | $i++; 75 | if ($i <= ($trace_skip + self::TRACE_IGNORE)) { 76 | continue; 77 | } 78 | $result .= '#' . str_pad(($i - $trace_skip - self::TRACE_IGNORE - 1), 2) . ' ' . (isset($v['class']) ? $v['class'] . '->' : '') . $v['function'] . '() called at [' . $v['file'] . ':' . $v['line'] . ']' . "\n"; 79 | 80 | if ($i >= ($trace_limit + $trace_skip + self::TRACE_IGNORE)) break; 81 | } 82 | 83 | $this->_logger($result); 84 | } 85 | 86 | private function _eToStr(&$e) { 87 | if (!is_object($e)) return; 88 | if (!($e instanceof \Exception)) return; 89 | $e = "'" . get_class($e) . "' exception with message: " . $e->getMessage(); 90 | } 91 | 92 | private function _message($type, $reason, $str, $force_trace, $trace_skip) { 93 | if (!($this->loglevel & $type)) return; 94 | $this->_eToStr($str); 95 | $this->_log($reason, $str); 96 | $this->_backtrace($force_trace, $trace_skip); 97 | } 98 | 99 | public function debug($str, $force_trace = false, $trace_skip = 0) { 100 | $this->_message(self::DEBUG, 'Debug', $str, $force_trace, $trace_skip); 101 | } 102 | public function warning($str, $force_trace = false, $trace_skip = 0) { 103 | $this->_message(self::WARNING, 'Warning', $str, $force_trace, $trace_skip); 104 | } 105 | public function error($str, $force_trace = false, $trace_skip = 0) { 106 | $this->_message(self::ERROR, 'Error', $str, $force_trace, $trace_skip); 107 | } 108 | } -------------------------------------------------------------------------------- /gameq3/filters/colorize.php: -------------------------------------------------------------------------------- 1 | . 17 | * 18 | * 19 | */ 20 | 21 | namespace GameQ3\filters; 22 | 23 | class Colorize { 24 | 25 | // add html support 26 | public static function filter(&$data, $args) { 27 | 28 | switch($data['info']['protocol']) { 29 | case 'quake2': 30 | case 'quake3': 31 | case 'doom3': 32 | array_walk_recursive($data, "\\GameQ3\\filters\\Colorize::strip", array('t' => 'quake')); 33 | break; 34 | 35 | case 'unreal2': 36 | case 'ut3': 37 | case 'gamespy3': //not sure if gamespy3 supports ut colors but won't hurt 38 | case 'gamespy2': 39 | array_walk_recursive($data, "\\GameQ3\\filters\\Colorize::strip", array('t' => 'ut')); 40 | break; 41 | case 'lfs': 42 | array_walk_recursive($data, "\\GameQ3\\filters\\Colorize::strip", array('t' => 'lfs')); 43 | break; 44 | default: 45 | break; 46 | } 47 | } 48 | 49 | /* 50 | 51 | PORTION OF BAD-FORMATTED SHITCODE 52 | 53 | function lgsl_parse_color($string, $type) 54 | { 55 | switch($type) 56 | { 57 | case "1": 58 | $string = preg_replace("/\^x.../", "", $string); 59 | $string = preg_replace("/\^./", "", $string); 60 | 61 | $string_length = strlen($string); 62 | for ($i=0; $i<$string_length; $i++) 63 | { 64 | $char = ord($string[$i]); 65 | if ($char > 160) { $char = $char - 128; } 66 | if ($char > 126) { $char = 46; } 67 | if ($char == 16) { $char = 91; } 68 | if ($char == 17) { $char = 93; } 69 | if ($char < 32) { $char = 46; } 70 | $string[$i] = chr($char); 71 | } 72 | break; 73 | 74 | case "2": 75 | $string = preg_replace("/\^[\x20-\x7E]/", "", $string); 76 | break; 77 | 78 | case "doomskulltag": 79 | $string = preg_replace("/\\x1c./", "", $string); 80 | break; 81 | 82 | case "farcry": 83 | $string = preg_replace("/\\$\d/", "", $string); 84 | break; 85 | 86 | case "painkiller": 87 | $string = preg_replace("/#./", "", $string); 88 | break; 89 | 90 | case "quakeworld": 91 | $string_length = strlen($string); 92 | for ($i=0; $i<$string_length; $i++) 93 | { 94 | $char = ord($string[$i]); 95 | if ($char > 141) { $char = $char - 128; } 96 | if ($char < 32) { $char = $char + 30; } 97 | $string[$i] = chr($char); 98 | } 99 | break; 100 | 101 | case "savage": 102 | $string = preg_replace("/\^[a-z]/", "", $string); 103 | $string = preg_replace("/\^[0-9]+/", "", $string); 104 | $string = preg_replace("/lan .*\^/U", "", $string); 105 | $string = preg_replace("/con .*\^/U", "", $string); 106 | break; 107 | 108 | case "swat4": 109 | $string = preg_replace("/\[c=......\]/Usi", "", $string); 110 | break; 111 | } 112 | return $string; 113 | } 114 | 115 | */ 116 | 117 | public static function strip(&$string, $key, $opts) { 118 | if (is_string($string)) { 119 | switch($opts['t']) { 120 | case 'quake': 121 | $string = preg_replace('#(\^.)#', '', $string); 122 | break; 123 | case 'ut': 124 | $string = preg_replace('/\x1b.../', '', $string); 125 | break; 126 | case 'lfs': 127 | $string = preg_replace('/<[^>]*>/i', '', $string); 128 | break; 129 | } 130 | } 131 | } 132 | } -------------------------------------------------------------------------------- /gameq3/protocols/tshock.php: -------------------------------------------------------------------------------- 1 | . 17 | * 18 | * 19 | */ 20 | 21 | namespace GameQ3\protocols; 22 | 23 | class Tshock extends \GameQ3\Protocols { 24 | protected $protocol = 'tshock'; 25 | protected $name = 'terraria'; 26 | protected $name_long = "Terraria"; 27 | 28 | protected $query_port = 7878; 29 | protected $connect_port = 7777; 30 | protected $ports_type = self::PT_DIFFERENT_NONCOMPUTABLE_VARIABLE; 31 | 32 | protected $url = "/v2/server/status?players=true&rules=true"; 33 | 34 | public function init() { 35 | if ($this->isRequested('teams')) 36 | $this->result->setIgnore('teams', false); 37 | 38 | $this->queue('status', 'http', $this->url); 39 | } 40 | 41 | protected function processRequests($qid, $requests) { 42 | if ($qid === 'status') { 43 | return $this->_process_status($requests['responses']); 44 | } 45 | } 46 | 47 | protected function _process_status($packets) { 48 | $data = json_decode($packets[0], true); 49 | 50 | if (!isset($data['status'])) return false; 51 | if ($data['status'] != 200) { 52 | $this->debug("Tshock error (" . $data['status'] . ")" . (isset($data['error']) ? " " . $data['error'] : "")); 53 | return false; 54 | } 55 | unset($data['status']); 56 | 57 | if (!isset($data['port'])) return false; 58 | $this->setConnectPort($data['port']); 59 | unset($data['port']); 60 | 61 | $keys_general = array( 62 | "name" => "hostname", 63 | "playercount" => "num_players", 64 | "maxplayers" => "max_players", 65 | "world" => "map", 66 | ); 67 | 68 | foreach($keys_general as $key => $normalized) { 69 | if (!isset($data[$key])) return false; 70 | $this->result->addGeneral($normalized, $data[$key]); 71 | unset($data[$key]); 72 | } 73 | 74 | if (!isset($data["players"]) || !isset($data["rules"]) || !is_array($data["players"]) || !is_array($data["rules"])) return false; 75 | 76 | /* 77 | As I figured out terraria's teams are partys. They haven't got any name or any other information. 78 | */ 79 | $teams = array(); 80 | foreach($data["players"] as $player) { 81 | if (!isset($player["nickname"]) || !isset($player["team"])) { 82 | $this->debug("Bad player skipped"); 83 | continue; 84 | } 85 | 86 | $name = $player["nickname"]; 87 | $teamid = $player["team"]; 88 | $teams[$teamid] = true; 89 | // if ($player["state"] == 10) // player is playing 90 | 91 | unset($player["nickname"], $player["team"]); 92 | 93 | $this->result->addPlayer($name, null, $teamid, $player); 94 | } 95 | 96 | foreach($teams as $teamid => $val) { 97 | $this->result->addTeam($teamid, '' . $teamid); 98 | } 99 | 100 | foreach($data["rules"] as $key => $value) { 101 | // Make value printable 102 | if (is_bool($value)) $value = ($value ? "true" : "false"); 103 | if (is_null($value)) $value = ""; 104 | if (!is_scalar($value)) { 105 | $this->debug("Setting value is not scalar. Skipped. Key: " . $key . ". Value: " . var_export($value, true)); 106 | continue; 107 | } 108 | $this->result->addSetting($key, $value); 109 | } 110 | 111 | unset($data["players"], $data["rules"]); 112 | 113 | foreach($data as $key => $value) { 114 | // Make value printable 115 | if (is_bool($value)) $value = ($value ? "true" : "false"); 116 | if (is_null($value)) $value = ""; 117 | if (!is_scalar($value)) { 118 | continue; 119 | } 120 | $this->result->addSetting($key, $value); 121 | } 122 | 123 | } 124 | } -------------------------------------------------------------------------------- /gameq3/protocols/quake3.php: -------------------------------------------------------------------------------- 1 | . 17 | * 18 | * 19 | */ 20 | 21 | namespace GameQ3\protocols; 22 | 23 | use GameQ3\Buffer; 24 | 25 | class Quake3 extends \GameQ3\Protocols { 26 | 27 | protected $packets = array( 28 | 'status' => "\xFF\xFF\xFF\xFF\x67\x65\x74\x73\x74\x61\x74\x75\x73\x0A", 29 | ); 30 | 31 | protected $query_port = 27960; 32 | protected $ports_type = self::PT_SAME; 33 | 34 | protected $protocol = 'quake3'; 35 | protected $name = 'quake3'; 36 | protected $name_long = "Quake 3"; 37 | 38 | public function init() { 39 | $this->queue('status', 'udp', $this->packets['status'], array('response_count' => 1)); 40 | } 41 | 42 | protected function processRequests($qid, $requests) { 43 | if ($qid === 'status') { 44 | return $this->_process_status($requests['responses']); 45 | } 46 | } 47 | 48 | protected function _process_status($packets) { 49 | $buf = new Buffer($packets[0]); 50 | 51 | // Grab the header 52 | $header = $buf->read(20); 53 | 54 | // Now lets verify the header 55 | if($header != "\xFF\xFF\xFF\xFFstatusResponse\x0A\x5C") { 56 | $this->debug('Unable to match Quake3 challenge response header. Header: '. $header); 57 | return false; 58 | } 59 | 60 | // First section is the server info, the rest is player info 61 | $server_info = $buf->readString("\x0A"); 62 | $players_info = $buf->getBuffer(); 63 | 64 | unset($buf); 65 | 66 | // Make a new buffer for the server info 67 | $buf_server = new Buffer($server_info); 68 | 69 | $private_players = false; 70 | $max_players = false; 71 | 72 | // Key / value pairs 73 | while ($buf_server->getLength()) { 74 | $key = $buf_server->readString('\\'); 75 | $val = $this->filterInt($buf_server->readStringMulti(array('\\', "\x0a"), $delimfound)); 76 | 77 | switch($key) { 78 | case 'g_gametype': $this->result->addGeneral('mode', $val); break; 79 | case 'mapname': $this->result->addGeneral('map', $val); break; 80 | case 'shortversion': $this->result->addGeneral('version', $val); break; 81 | case 'sv_hostname': $this->result->addGeneral('hostname', $val); break; 82 | case 'sv_privateClients': $private_players = $val; break; 83 | case 'ui_maxclients': $max_players = $val; break; 84 | case 'pswrd': $this->result->addGeneral('password', ($val != 0)); break; 85 | case 'sv_punkbuster': $this->result->addGeneral('secure', ($val != 0)); break; 86 | } 87 | $this->result->addSetting($key,$val); 88 | 89 | 90 | if ($delimfound === "\x0a") 91 | break; 92 | } 93 | 94 | if (!is_int($max_players)) { 95 | $this->debug('Max_players is not an integer in quake3: '. var_export($max_players, true)); 96 | return false; 97 | } 98 | 99 | if (is_int($private_players)) { 100 | $this->result->addGeneral('private_players', $private_players); 101 | //$max_players -= $private_players; 102 | } 103 | $this->result->addGeneral('max_players', $max_players); 104 | 105 | // Explode the arrays out 106 | $players = explode("\x0A", $players_info); 107 | 108 | // Remove the last array item as it is junk 109 | array_pop($players); 110 | 111 | // Add total number of players 112 | $this->result->addGeneral('num_players', count($players)); 113 | 114 | // Loop the players 115 | foreach($players AS $player_info) { 116 | $buf = new Buffer($player_info); 117 | 118 | $score = $this->filterInt($buf->readString("\x20")); 119 | $ping = $this->filterInt($buf->readString("\x20")); 120 | 121 | // Skip first " 122 | $buf->skip(1); 123 | $name = trim($buf->readString('"')); 124 | 125 | // Add player info 126 | $this->result->addPlayer($name, $score, null, array('ping' => $ping)); 127 | } 128 | } 129 | } -------------------------------------------------------------------------------- /examples/index.php: -------------------------------------------------------------------------------- 1 | 'TS3', 10 | 'type' => 'teamspeak3', 11 | 'connect_host' => 'simhost.org', 12 | ), 13 | array( 14 | 'id' => 'CS 1.6 server', 15 | 'type' => 'cs', 16 | 'connect_host' => 'simhost.org:27015', 17 | ) 18 | ); 19 | 20 | 21 | $gq = new \GameQ3\GameQ3(); 22 | 23 | //$gq->setLogLevel(true, true, true, true); 24 | $gq->setFilter('colorize', array( 25 | 'format' => 'strip' 26 | )); 27 | 28 | $gq->setFilter('strip_badchars'); 29 | 30 | $gq->setFilter('sortplayers', array( 31 | 'sortkeys' => array( 32 | array('key' => 'is_bot', 'order' => 'asc'), 33 | array('key' => 'score', 'order' => 'desc'), 34 | array('key' => 'name', 'order' => 'asc'), 35 | ) 36 | )); 37 | 38 | foreach($servers as $server) { 39 | try { 40 | $gq->addServer($server); 41 | } 42 | catch(Exception $e) { 43 | die($e->getMessage()); 44 | } 45 | } 46 | 47 | $results = $gq->requestAllData(); 48 | 49 | 50 | // Some functions to print the results 51 | function print_results($results) { 52 | foreach ($results as $id => $data) { 53 | printf("\t\t

%s

\n", $id); 54 | print_table($data); 55 | } 56 | } 57 | 58 | function print_table($data) { 59 | 60 | $info_always = array( 61 | 'query_addr', 62 | 'query_port', 63 | 'connect_addr', 64 | 'connect_port', 65 | 'protocol', 66 | 'short_name', 67 | 'long_name', 68 | 'connect_string', 69 | 'online', 70 | 'ping_average', 71 | 'retry_count', 72 | 'identifier' 73 | ); 74 | 75 | $general_always = array( 76 | 'hostname', 77 | 'version', 78 | 'max_players', 79 | 'num_players', 80 | 'password', 81 | 'private_players', 82 | 'bot_players', 83 | 'map', 84 | 'mode', 85 | 'secure' 86 | ); 87 | 88 | if (!$data['info']['online']) { 89 | echo "

The server did not respond within the specified time.

\n"; 90 | return; 91 | } 92 | 93 | echo "\t\t\n\t\t\n\t\t\t\n\t\t\n\t\t\n"; 94 | 95 | foreach($data as $group => $datac) { 96 | if ($group !== 'info' && $group !== 'general' && $group !== 'settings' && $group !== 'players') { 97 | $cls = empty($cls) ? ' class="uneven"' : ''; 98 | printf("\t\t\t\n", $cls, $group, 'Keys count', count($datac)); 99 | continue; 100 | } 101 | 102 | $grouph = $group; 103 | if ($group === 'info' || $group === 'general') { 104 | $grouph = "" . $group . ""; 105 | } 106 | 107 | foreach ($datac as $key => $val) { 108 | $cls = empty($cls) ? ' class="uneven"' : ''; 109 | 110 | if ($group === 'info') { 111 | if (in_array($key, $info_always)) 112 | $key = "" . $key . ""; 113 | } else 114 | if ($group === 'general') { 115 | if (in_array($key, $general_always)) 116 | $key = "" . $key . ""; 117 | } else 118 | if ($group === 'settings') { 119 | $key = $val[0]; 120 | $val = $val[1]; 121 | } else 122 | if ($group === 'players') { 123 | $key = $val['name']; 124 | $val = $val['score']; 125 | } 126 | 127 | printf("\t\t\t\n", $cls, $grouph, $key, var_export($val, true)); 128 | } 129 | } 130 | 131 | echo "\t\t\n\t\t
GroupVariableValue
%s%s%s
%s%s%s
\n"; 132 | } 133 | 134 | ?> 135 | 136 | 137 | 138 | GameQ3 - Example script 139 | 140 | 183 | 184 | 185 |

GameQ3 - Example script

186 |
187 | This is a simple output example.
188 | Bold, red variables are always set by GameQ3.
189 | Click here for a list of supported games. 190 |
191 | 192 | 193 | -------------------------------------------------------------------------------- /gameq3/protocols/samp.php: -------------------------------------------------------------------------------- 1 | . 17 | * 18 | * 19 | */ 20 | 21 | namespace GameQ3\protocols; 22 | 23 | // http://wiki.sa-mp.com/wiki/Query_Mechanism 24 | 25 | /* 26 | Server not responds to 'players' packet when there are too many players on the server. 27 | 28 | You may check result as follows: 29 | if ($result['info']['online'] == true) { 30 | if ($result['info']['full'] == true) { 31 | // Result has got players 32 | } else { 33 | // Result has not got players 34 | } 35 | } 36 | */ 37 | 38 | use GameQ3\Buffer; 39 | 40 | class Samp extends \GameQ3\Protocols { 41 | 42 | protected $packets = array( 43 | 'status' => "SAMP%si", 44 | 'players' => "SAMP%sd", 45 | 'rules' => "SAMP%sr", 46 | ); 47 | 48 | protected $query_port = 7777; 49 | protected $ports_type = self::PT_SAME; 50 | 51 | protected $protocol = 'samp'; 52 | protected $name = 'samp'; 53 | protected $name_long = "San Andreas Multiplayer"; 54 | 55 | protected $players_received; 56 | 57 | protected function construct() { 58 | $tail = ""; 59 | $tail .= chr(strtok($this->query_addr, '.')); 60 | $tail .= chr(strtok('.')); 61 | $tail .= chr(strtok('.')); 62 | $tail .= chr(strtok('.')); 63 | $tail .= chr($this->query_port & 0xFF); 64 | $tail .= chr($this->query_port >> 8 & 0xFF); 65 | 66 | foreach($this->packets as $packet_type => $packet) { 67 | $this->packets[$packet_type] = sprintf($packet, $tail); 68 | } 69 | } 70 | 71 | 72 | public function init() { 73 | $this->queue('status', 'udp', $this->packets['status']); 74 | if ($this->isRequested('settings')) $this->queue('rules', 'udp', $this->packets['rules']); 75 | if ($this->isRequested('players')) { 76 | $this->players_received = false; 77 | $this->queue('players', 'udp', $this->packets['players']); 78 | $this->unCheck("players"); 79 | } 80 | } 81 | 82 | protected function preFetch() { 83 | $this->result->addInfo('full', (!$this->isRequested('players') || $this->players_received)); 84 | } 85 | 86 | protected function processRequests($qid, $requests) { 87 | if ($qid === 'status') { 88 | return $this->_process_status($requests['responses']); 89 | } else 90 | if ($qid === 'rules') { 91 | return $this->_process_rules($requests['responses']); 92 | } else 93 | if ($qid === 'players') { 94 | return $this->_process_players($requests['responses']); 95 | } 96 | } 97 | 98 | 99 | protected function _preparePackets($packets) { 100 | // Make buffer so we can check this out 101 | $buf = new Buffer(implode('', $packets)); 102 | 103 | // Grab the header 104 | $header = $buf->read(11); 105 | 106 | // Now lets verify the header 107 | if(substr($header, 0, 4) != "SAMP") { 108 | $this->debug('Unable to match SAMP response header. Header: '. $header); 109 | return false; 110 | } 111 | 112 | return $buf; 113 | } 114 | 115 | 116 | protected function _process_status($packets) { 117 | $buf = $this->_preparePackets($packets); 118 | if (!$buf) return false; 119 | 120 | // Pull out the server information 121 | $this->result->addGeneral('password', ($buf->readInt8() == 1)); 122 | $this->result->addGeneral('num_players', $buf->readInt16()); 123 | $this->result->addGeneral('max_players', $buf->readInt16()); 124 | 125 | /// TODO: check other charsets 126 | $this->result->addGeneral('hostname', iconv('windows-1251', 'UTF-8', $buf->read($buf->readInt32()))); 127 | $this->result->addGeneral('mode', $buf->read($buf->readInt32())); 128 | $this->result->addGeneral('map', $buf->read($buf->readInt32())); 129 | } 130 | 131 | protected function _process_rules($packets) { 132 | $buf = $this->_preparePackets($packets); 133 | if (!$buf) return false; 134 | 135 | // Number of rules 136 | $buf->readInt16(); 137 | 138 | while ($buf->getLength()) { 139 | $key = $buf->readPascalString(); 140 | $val = $this->filterInt($buf->readPascalString()); 141 | 142 | if ($key === "version") 143 | $this->result->addGeneral('version', $val); 144 | 145 | $this->result->addSetting($key, $val); 146 | } 147 | } 148 | 149 | protected function _process_players($packets) { 150 | $buf = $this->_preparePackets($packets); 151 | if (!$buf) return false; 152 | 153 | $this->players_received = true; 154 | 155 | $this->result->addGeneral('num_players', $buf->readInt16()); 156 | 157 | while ($buf->getLength()) { 158 | $id = $buf->readInt8(); 159 | $name = $buf->readPascalString(); 160 | $score = $buf->readInt32(); 161 | $ping = $buf->readInt32(); 162 | 163 | $this->result->addPlayer($name, $score, null, array('ping' => $ping)); 164 | } 165 | } 166 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | GameQ3 2 | ====== 3 | A PHP Gameserver Status Query Library. 4 | 5 | ## Introduction 6 | GameQ3 allows you to query multiple game servers at the same time. My goal is to make GameQ3 as fast and reliable as it is possible on PHP (though I think this language is not good for this kind of work). 7 | GameQ3 is influenced by GameQ and GameQ v2. 8 | Design of this library is different from other GameQ versions. 9 | The main differences are queue style of packets and *huge* socket class which uses native sockets for UDP handling instead of stream sockets. 10 | Some protocols are like in GameQ v2, but protocol classes are not compatible. 11 | 12 | ## Requirements 13 | * PHP >= 5.4.0 14 | * sockets extension for UDP handling and AF_INET* constants (compile PHP with --enable-sockets flag) 15 | * cURL extension for HTTP handling (required for some protocols) 16 | * Bzip2 extension for Source protocol (compile PHP with --with-bz2 flag) 17 | 18 | ## Quickstart example 19 | You may want to check [/examples](https://github.com/kostya0shift/GameQ3/tree/master/examples) folder. 20 | 21 | Simple usage example: 22 | 23 | require "gameq3/gameq3.php"; 24 | $gq = new \GameQ3\GameQ3(); 25 | try { 26 | $gq->addServer(array( 27 | 'id' => 'cs1', 28 | 'type' => 'cs', 29 | 'connect_host' => 'simhost.org:27015' 30 | )); 31 | } 32 | catch(\GameQ3\UserException $e) { 33 | die("addServer exception: " . $e->getMessage()); 34 | } 35 | 36 | $results = $gq->requestAllData(); 37 | 38 | var_dump($results); 39 | 40 | 41 | ## What about other PHP game query libraries? 42 | * lgsl (url [www.greycube.com](http://www.greycube.com/site/download.php?view.56), author Richard Perry): poorly coded, currently unsupported, uses linear requests (which are very slow when you are dealing with large amount of servers), but has good admin webinterface and CMS modules. 43 | * GameQ (author Tom Buskens ): currently unsupported, poorly designed, supports many games. 44 | * GameQ v2 (url [Austinb/GameQ](https://github.com/Austinb/GameQ), author Austin Bischoff ): most supported library for today, has many users, supports many games, good for querying small amount of servers. 45 | 46 | ## Why GameQ3? 47 | You should basically compare GameQ3 to GameQ v2. So here is a checklist for you to make a decision: 48 | * For large amount of servers you shoud definitely stick with GameQ3 49 | * GameQ3 is designed to work in forever running daemons (but for single web page loads works also great) 50 | * More complex abilities for protocols. You can implement nearly every protocol you can imagine in GameQ3 51 | * If you need more control for sockets options (useful for ajustment for various environments) to keep requests reliable 52 | 53 | ## How to use 54 | 1. Require gameq3/gameq3.php script 55 | 1. Create instance of \GameQ3\GameQ3() class. 56 | 1. Call *setup* and *info* methods when not in request 57 | 1. Request data using **one** of provided *request* methods. 58 | 59 | ### *Info* methods 60 | getProtocolInfo($protocol) 61 | getAllProtocolsInfo() 62 | 63 | ### *Setup* methods 64 | setLogLevel($error, $warning = true, $debug = false, $trace = false) 65 | setOption($key, $value) 66 | setFilter($name, $args = array()) 67 | unsetFilter($name) 68 | addServer($server_info) 69 | unsetServer($id) 70 | 71 | ### *Request* methods 72 | requestAllData() 73 | requestPartData() 74 | 75 | ## addServer options 76 | **Bold** are mandatory. 77 | * **id** - id of server 78 | * **type** - protocol 79 | * filters - array of (filter_name => args) for this server. This supersedes globally set filters using setFilter. If args is false, then this fillter will not be applied. 80 | * debug - change all debug messages from this protocol to warnings 81 | * unset - array of keys in response which should be ommited (to save memory and for some protocols send less packets) 82 | * All other options are processed by protocols. They might require some other mandatory options. Options for networked protocols: 83 | * connect_addr - host:port 84 | * connect_host 85 | * connect_port 86 | * query_addr (alias addr) - host:port 87 | * query_host (alias host) 88 | * query_port (alias port) 89 | 90 | 91 | ## setOption options 92 | Options you should tweak first are **bold**. 93 | * servers_count (int, 2500) - number of servers to request at the same time 94 | * **connect_timeout** (int, 1) - s. connect timeout for stream sockets 95 | * **send_once_udp** (int, 5) - number of udp packets to send at once 96 | * **send_once_stream** (int, 5) - number of stream packets to send at once 97 | * usleep_udp (int, 100) - ns. pause between udp packet sends 98 | * usleep_stream (int, 100) - ns. pause between stream packet sends 99 | * **read_timeout** (int, 600) - ms. read timeout 100 | * read_got_timeout (int, 30) - ms. how much of time to wait after latest received packet 101 | * **read_retry_timeout** (int, 200) - ms. read_timeout for non-first attempts 102 | * loop_timeout (int, 2) - ms. pause between socket operations 103 | * socket_buffer (int, 8192) - bytes. socket buffer 104 | * **send_retry** (int, 1) - count. number of retry attempts for packets which has timed out 105 | * **curl_connect_timeout** (int, 1000) - ms. connect timeout for http requests 106 | * **curl_total_timeout** (int, 1200) - ms. total page load timeout 107 | * **curl_select_timeout** (int, 1500) - ms. maximum wait time for all curl requests (should be bigger than curl_total_timeout) 108 | * curl_options (array, array()) - array for curl_setopt_array 109 | 110 | ## Filters 111 | * colorize - currently this filter just strips all colors in responses, but it is possible to implement HTML (or whatever) translation 112 | * sortplayers - sorts players list. 113 | Arguments: 114 | * sortkeys - array of sorting keys. They will be tested in the order they are given. This array consists of arrays like this: array('key' => $sortKeyName, 'order' => 'asc' || 'desc') 115 | * strip_badchars - strip non-utf8 characters and trim whitespace in every string of the result. 116 | -------------------------------------------------------------------------------- /gameq3/protocols/squad.php: -------------------------------------------------------------------------------- 1 | . 17 | * 18 | * 19 | */ 20 | 21 | namespace GameQ3\protocols; 22 | 23 | use GameQ3\Buffer; 24 | 25 | class Squad extends \GameQ3\Protocols { 26 | 27 | protected $packets = array( 28 | 'status' => "\xff\xff\xff\xff\x54\x53\x6f\x75\x72\x63\x65\x20\x45\x6e\x67\x69\x6e\x65\x20\x51\x75\x65\x72\x79\x00", 29 | 'challenge' => "\xff\xff\xff\xff\x56\x00\x00\x00\x00", 30 | 'settings' => "\xff\xff\xff\xff\x56%s", 31 | 'players' => "\xff\xff\xff\xff\x55%s", 32 | ); 33 | 34 | protected $ports_type = self::PT_UNKNOWN; 35 | protected $protocol = 'squad'; 36 | protected $name = 'squad'; 37 | protected $name_long = "Squad"; 38 | 39 | 40 | public function init() { 41 | $this->queue('status', 'udp', $this->packets['status']); 42 | if ($this->isRequested('settings') || $this->isRequested('players')) { 43 | $this->queue('challenge', 'udp', $this->packets['challenge']); 44 | } 45 | } 46 | 47 | protected function processRequests($qid, $requests) { 48 | if ($qid === 'status') { 49 | return $this->_process_status($requests['responses']); 50 | } else 51 | if ($qid === 'challenge') { 52 | return $this->_process_challenge($requests['responses']); 53 | } else 54 | if ($qid === 'settings') { 55 | return $this->_process_settings($requests['responses']); 56 | } else 57 | if ($qid === 'players') { 58 | return $this->_process_players($requests['responses']); 59 | } 60 | } 61 | 62 | protected function _preparePackets($packets) { 63 | foreach($packets as $id => $packet) { 64 | $packets[$id] = substr($packet, 5); 65 | } 66 | 67 | return implode('', $packets); 68 | } 69 | 70 | protected function _process_status($packets) { 71 | $buf = new Buffer($this->_preparePackets($packets)); 72 | $buf->jumpto(1); 73 | 74 | $this->result->addGeneral('hostname', $buf->readString()); 75 | $mapstring = $buf->readString(); 76 | $this->result->addGeneral('map', $mapstring); 77 | 78 | $buf->readString();$buf->readString();$buf->readInt8();$buf->readInt8(); 79 | $this->result->addGeneral('num_players', $buf->readInt8()); 80 | $this->result->addGeneral('max_players', $buf->readInt8()); 81 | $this->result->addGeneral('bot_players', $buf->readInt8()); 82 | /*$server_type = $buf->readInt8(); 83 | 84 | $environment = ""; 85 | switch ($buf->readChar()){ 86 | case 'l': 87 | $environment = "Linux"; 88 | break; 89 | 90 | case 'w': 91 | $environment = "Windows"; 92 | break; 93 | 94 | case 'm': 95 | case 'o': 96 | $environment = "Mac OS"; 97 | break; 98 | }*/ 99 | } 100 | 101 | protected function _process_challenge($packets) { 102 | $buf = new Buffer($this->_preparePackets($packets)); 103 | 104 | $challenge = $buf->getData(); 105 | 106 | if ($this->isRequested('settings')) $this->queue('settings', 'udp', sprintf($this->packets['settings'], $challenge)); 107 | if ($this->isRequested('players')) $this->queue('players', 'udp', sprintf($this->packets['players'], $challenge)); 108 | } 109 | 110 | protected function _process_settings($packets) { 111 | $buf = new Buffer($this->_preparePackets($packets)); 112 | 113 | $buf->jumpto(2); 114 | 115 | while ($buf->getLength()>0){ 116 | $buf->lookAhead(1); 117 | $key = $buf->readString(); 118 | $value = $buf->readString(); 119 | $this->result->addSetting($key, $value); 120 | 121 | switch ($key){ 122 | case "GameMode_s": 123 | $this->result->addGeneral('mode', $value); 124 | break; 125 | 126 | case "GameVersion_s": 127 | $this->result->addGeneral("version", $value); 128 | break; 129 | 130 | case "NUMPRIVCONN": 131 | $this->result->addGeneral("private_players", intval($value)); 132 | break; 133 | 134 | case "Password_b": 135 | $this->result->addGeneral("password", ($value=="true") ? 1 : 0); 136 | break; 137 | } 138 | $buf->lookAhead(1); 139 | } 140 | 141 | } 142 | 143 | protected function _process_players($packets) { 144 | $buf = new Buffer($this->_preparePackets($packets)); 145 | 146 | $count = $buf->readInt8(); 147 | 148 | while ($buf->getLength()>0){ 149 | $id = $buf->readInt8(); //= 0 for every player ?? 150 | 151 | $name = $buf->readString(); 152 | 153 | $buf->skip(8); 154 | 155 | $this->result->addPlayer($name, 0, null, null, 0); 156 | } 157 | 158 | } 159 | 160 | } -------------------------------------------------------------------------------- /gameq3/protocols/unreal2.php: -------------------------------------------------------------------------------- 1 | . 17 | * 18 | * 19 | */ 20 | 21 | namespace GameQ3\protocols; 22 | 23 | use GameQ3\Buffer; 24 | 25 | class Unreal2 extends \GameQ3\Protocols { 26 | 27 | protected $packets = array( 28 | 'status' => "\x79\x00\x00\x00\x00", 29 | 'rules' => "\x79\x00\x00\x00\x01", 30 | 'players' => "\x79\x00\x00\x00\x02", 31 | ); 32 | 33 | protected $ports_type = self::PT_UNKNOWN; 34 | protected $protocol = 'unreal2'; 35 | protected $name = 'unreal2'; 36 | protected $name_long = "Unreal 2 Engine"; 37 | 38 | 39 | public function init() { 40 | $this->queue('status', 'udp', $this->packets['status']); 41 | if ($this->isRequested('settings')) $this->queue('rules', 'udp', $this->packets['rules']); 42 | if ($this->isRequested('players')) $this->queue('players', 'udp', $this->packets['players']); 43 | } 44 | 45 | protected function processRequests($qid, $requests) { 46 | if ($qid === 'status') { 47 | return $this->_process_status($requests['responses']); 48 | } else 49 | if ($qid === 'rules') { 50 | return $this->_process_rules($requests['responses']); 51 | } else 52 | if ($qid === 'players') { 53 | return $this->_process_players($requests['responses']); 54 | } 55 | } 56 | 57 | protected function _preparePackets($packets) { 58 | foreach($packets as $id => $packet) { 59 | $packets[$id] = substr($packet, 5); 60 | } 61 | 62 | return implode('', $packets); 63 | } 64 | 65 | 66 | protected function _readBadPascalString(Buffer &$buf) { 67 | $len = $buf->readInt8(); 68 | 69 | $bufpos = $buf->getPosition(); 70 | $buf->jumpto($bufpos + $len - 1); 71 | $charatlen = $buf->read(1); 72 | $buf->jumpto($bufpos); 73 | 74 | if ($charatlen === "\x00") { 75 | // Valid pascal string 76 | return substr($buf->read($len), 0, $len - 1); // cut off null byte 77 | } else { 78 | // Invalid pascal string, assuming end of the string is 0x00 79 | return $buf->readString("\x00"); 80 | } 81 | } 82 | 83 | /* 84 | 85 | This works for hostname, but they count length differently! sometimes they include nullchar and sometimes they dont. 86 | Fuck you unreal2, gonna use dumb readString method provided above. 87 | 88 | protected function _readColoredPascalString(Buffer &$buf) { 89 | $len = $buf->readInt8(); 90 | $str = ""; 91 | 92 | for ($i=0; $i<$len; ) { 93 | $char = $buf->read(1); 94 | $str .= $char; 95 | if ($char === "\x1b") { // it is a color 96 | $char = $buf->read(3); // color has 4 bytes 97 | $str .= $char; 98 | } else { 99 | $i++; 100 | } 101 | } 102 | 103 | if ($buf->read(1) !== "\x00") { 104 | $this->debug(__METHOD__ . ' failed'); 105 | } 106 | return $str; 107 | } 108 | */ 109 | 110 | protected function _findEncoding($str) { 111 | // Shit happens when clients use non-latin names in their game, game mixes ucs-2 encoding with one-byte national encoding 112 | 113 | $encs = array("windows-1251"); 114 | foreach($encs as $enc) { 115 | $s = iconv("utf-8", $enc, iconv("UCS-2//IGNORE", "UTF-8", $str)); 116 | if (@iconv("utf-8", "utf-8", $s) === $s) return $s; // when string has corrupted chars, this thing will fail 117 | } 118 | 119 | return iconv("UCS-2//IGNORE", "UTF-8", $str); 120 | } 121 | 122 | protected function _readUnrealString(Buffer &$buf) { 123 | // Normal pascal string 124 | if (ord($buf->lookAhead(1)) < 129) { 125 | $str = $buf->readPascalString(1); 126 | $cstr = iconv("ISO-8859-1//IGNORE", "utf-8", $str); // some chars like (c) should be converted to utf8 127 | return ($cstr === false ? $str : $cstr); 128 | } 129 | 130 | // UnrealEngine2 color-coded string 131 | $length = ($buf->readInt8() - 128) * 2 - 2; 132 | $encstr = $buf->read($length); 133 | $buf->skip(2); 134 | 135 | // Remove color-code tags 136 | $encstr = preg_replace('~\x5e\\0\x23\\0..~s', '', $encstr); 137 | 138 | $str = $this->_findEncoding($encstr); 139 | 140 | return $str; 141 | } 142 | 143 | protected function _process_status($packets) { 144 | $buf = new Buffer($this->_preparePackets($packets)); 145 | 146 | //$this->p->strReplace("\xa0", "\x20"); 147 | // serverid 148 | $buf->readInt32(); 149 | // serverip 150 | $buf->readPascalString(1); 151 | // gameport 152 | $this->setConnectPort($buf->readInt32()); 153 | // queryport 154 | $buf->readInt32(); 155 | 156 | $this->result->addGeneral('hostname', str_replace("\xa0", "\x20", $this->_readBadPascalString($buf))); 157 | $this->result->addGeneral('map', str_replace("\xa0", "\x20", $buf->readPascalString(1))); 158 | $this->result->addGeneral('mode', str_replace("\xa0", "\x20", $buf->readPascalString(1))); 159 | 160 | $num_players = $buf->readInt32(); 161 | $this->result->addGeneral('num_players', $num_players); 162 | $this->result->addGeneral('max_players', $buf->readInt32()); 163 | 164 | // Ut2 sometimes doesn't send players packet when there are no players on the server 165 | if ($num_players == 0) 166 | $this->unCheck('players'); 167 | 168 | /* 169 | // ping 170 | $buf->readInt32(); 171 | 172 | // UT2004 only 173 | // Check if the buffer contains enough bytes 174 | if ($buf->getLength() > 6) { 175 | // flags 176 | $buf->readInt32(); 177 | // skill 178 | $buf->readInt16(); 179 | }*/ 180 | } 181 | 182 | protected function _process_players($packets) { 183 | $buf = new Buffer($this->_preparePackets($packets)); 184 | 185 | 186 | while ($buf->getLength()) { 187 | $id = $buf->readInt32(); 188 | if ($id === 0) { 189 | break; 190 | } 191 | 192 | $name = $this->_readUnrealString($buf); 193 | $ping = $buf->readInt32(); 194 | $score = $buf->readInt32(); 195 | 196 | $this->result->addPlayer($name, $score, null, array('ping' => $ping)); 197 | 198 | $buf->skip(4); 199 | } 200 | } 201 | 202 | protected function _process_rules($packets) { 203 | $buf = new Buffer($this->_preparePackets($packets)); 204 | 205 | 206 | while ($buf->getLength()) { 207 | $key = $buf->readPascalString(1); 208 | $val = $this->filterInt($buf->readPascalString(1)); 209 | 210 | switch($key) { 211 | case 'IsVacSecured': 212 | $this->result->addGeneral('secure', ($val == 'true')); 213 | break; 214 | case 'ServerVersion': 215 | $this->result->addGeneral('version', $val); 216 | break; 217 | } 218 | 219 | $this->result->addSetting($key, $val); 220 | } 221 | } 222 | 223 | } -------------------------------------------------------------------------------- /gameq3/protocols/gamespy2.php: -------------------------------------------------------------------------------- 1 | . 17 | * 18 | * 19 | */ 20 | 21 | namespace GameQ3\protocols; 22 | 23 | use GameQ3\Buffer; 24 | 25 | class Gamespy2 extends \GameQ3\Protocols { 26 | 27 | protected $packets = array( 28 | 'details' => "\xFE\xFD\x00\x43\x4F\x52\x59\xFF\x00\x00", 29 | 'players' => "\xFE\xFD\x00\x43\x4F\x52\x59\x00\xFF\xFF", 30 | ); 31 | 32 | protected $protocol = 'gamespy2'; 33 | protected $name = 'gamespy2'; 34 | protected $name_long = "Gamespy2"; 35 | 36 | protected $ports_type = self::PT_UNKNOWN; 37 | 38 | 39 | public function init() { 40 | if ($this->isRequested('teams')) 41 | $this->result->setIgnore('teams', false); 42 | 43 | $this->queue('details', 'udp', $this->packets['details'], array('response_count' => 1)); 44 | if ($this->isRequested('players')) 45 | $this->queue('players', 'udp', $this->packets['players'], array('response_count' => 1)); 46 | } 47 | 48 | protected function processRequests($qid, $requests) { 49 | if ($qid === 'details') { 50 | return $this->_process_details($requests['responses']); 51 | } else 52 | if ($qid === 'players') { 53 | return $this->_process_players($requests['responses']); 54 | } 55 | } 56 | 57 | protected function _put_var($key, $val) { 58 | switch($key) { 59 | case 'hostname': 60 | $this->result->addGeneral('hostname', iconv("ISO-8859-1//IGNORE", "utf-8", $val)); 61 | break; 62 | case 'mapname': 63 | $this->result->addGeneral('map', $val); 64 | break; 65 | case 'gamever': 66 | $this->result->addGeneral('version', $val); 67 | break; 68 | case 'gametype': 69 | $this->result->addGeneral('mode', $val); 70 | break; 71 | case 'numplayers': 72 | $this->result->addGeneral('num_players', $val); 73 | break; 74 | case 'maxplayers': 75 | $this->result->addGeneral('max_players', $val); 76 | break; 77 | case 'password': 78 | $this->result->addGeneral('password', $val == 1); 79 | break; 80 | default: 81 | $this->result->addSetting($key, $val); 82 | } 83 | } 84 | 85 | protected function _process_details($packets) { 86 | $buf = new Buffer($packets[0]); 87 | 88 | // Make sure the data is formatted properly 89 | if($buf->lookAhead(5) != "\x00\x43\x4F\x52\x59") { 90 | $this->debug("Data for ".__METHOD__." does not have the proper header. Header: ".$buf->lookAhead(5)); 91 | return false; 92 | } 93 | 94 | // Now verify the end of the data is correct 95 | if($buf->readLast() !== "\x00") { 96 | $this->debug("Data for ".__METHOD__." does not have the proper ending. Ending: ".$buf->readLast()); 97 | return false; 98 | } 99 | 100 | // Skip the header 101 | $buf->skip(5); 102 | 103 | // Loop thru all of the settings and add them 104 | while ($buf->getLength()) { 105 | $key = $buf->readString(); 106 | $val = $buf->readString(); 107 | 108 | // Check to make sure there is a valid pair 109 | if(strlen($key) > 0) { 110 | $this->_put_var($key, $this->filterInt($val)); 111 | } 112 | } 113 | 114 | } 115 | 116 | protected function _process_players($packets) { 117 | $buf = new Buffer($packets[0]); 118 | 119 | // Make sure the data is formatted properly 120 | if($buf->lookAhead(6) != "\x00\x43\x4F\x52\x59\x00") { 121 | $this->debug("Data for ".__METHOD__." does not have the proper header. Header: ".$buf->lookAhead(6)); 122 | return false; 123 | } 124 | 125 | // Now verify the end of the data is correct 126 | if($buf->readLast() !== "\x00") { 127 | $this->debug("Data for ".__METHOD__." does not have the proper ending. Ending: ".$buf->readLast()); 128 | return false; 129 | } 130 | 131 | // Skip the header 132 | $buf->skip(6); 133 | 134 | $res = true; 135 | 136 | // Players are first 137 | $res = $res && $this->_parse_playerteam('players', $buf); 138 | 139 | // Teams are next 140 | $res = $res && $this->_parse_playerteam('teams', $buf); 141 | 142 | return $res; 143 | } 144 | 145 | protected function _parse_playerteam($type, Buffer &$buf) { 146 | $count = $buf->readInt8(); 147 | if ($type === 'players') 148 | $this->result->addGeneral('num_players', $count); 149 | 150 | // Variable names 151 | $varnames = array(); 152 | $team_id = 1; 153 | 154 | // Loop until we run out of length 155 | while ($buf->getLength()) { 156 | $field = $buf->readString(); 157 | if ($type === 'players') { 158 | if (substr($field, -1) !== "_") { 159 | $this->debug("Arrays are not consistent"); 160 | return false; 161 | } 162 | $field = substr($field, 0, -1); 163 | } else 164 | if ($type === 'teams') { 165 | if (substr($field, -2) !== "_t") { 166 | $this->debug("Arrays are not consistent"); 167 | return false; 168 | } 169 | $field = substr($field, 0, -2); 170 | } 171 | $varnames[] = $field; 172 | 173 | if ($buf->lookAhead() === "\x00") { 174 | $buf->skip(); 175 | break; 176 | } 177 | } 178 | 179 | // Check if there are any value entries 180 | if ($buf->lookAhead() == "\x00") { 181 | $buf->skip(); 182 | return; 183 | } 184 | 185 | $ignore = false; 186 | if ($buf->getLength() > 4) { 187 | if ($type === 'players') { 188 | if (!in_array('player', $varnames) || !in_array('score', $varnames)) { 189 | $this->debug("Bad varnames array"); 190 | $ignore = true; 191 | } 192 | } else 193 | if ($type === 'teams') { 194 | if (!in_array('team', $varnames)) { 195 | $this->debug("Bad varnames array"); 196 | $ignore = true; 197 | } 198 | } 199 | } 200 | 201 | // Get the values 202 | while ($buf->getLength() > 4) { 203 | $more = array(); 204 | foreach ($varnames as $varname) { 205 | $more[$varname] = $this->filterInt($buf->readString()); 206 | } 207 | 208 | if (!$ignore) { 209 | if ($type === 'players') { 210 | $name = trim(iconv("ISO-8859-1//IGNORE", "utf-8", $more['player'])); // some chars like (c) should be converted to utf8 211 | $score = $more['score']; 212 | 213 | $teamid = null; 214 | if (isset($more['team']) && $more['team'] !== '') 215 | $teamid = $more['team']; 216 | 217 | unset($more['player'], $more['score'], $more['team']); 218 | 219 | $this->result->addPlayer($name, $score, $teamid, $more); 220 | } else 221 | if ($type === 'teams') { 222 | $name = $more['team']; 223 | unset($more['team']); 224 | 225 | $this->result->addTeam($team_id, $name, $more); 226 | $team_id++; 227 | } 228 | } 229 | 230 | if ($buf->lookAhead() === "\x00") { 231 | $buf->skip(); 232 | break; 233 | } 234 | } 235 | 236 | return true; 237 | } 238 | } -------------------------------------------------------------------------------- /gameq3/protocols/bf3.php: -------------------------------------------------------------------------------- 1 | . 17 | * 18 | * 19 | */ 20 | 21 | namespace GameQ3\protocols; 22 | 23 | use GameQ3\Buffer; 24 | 25 | class Bf3 extends \GameQ3\Protocols { 26 | 27 | protected $packets = array( 28 | // GameTracker sent this: 29 | // "\x00\x00\x00\x00\xa0\x02\x16\xd0\xa0\xe1\x00\x00\x02\x04\x05\xb4\x04\x02\x08\x0a\x91\x31\xaf\xd8\x00\x00\x00\x00\x01\x03\x03\x07" 30 | 'status' => "\x00\x00\x00\x00\x1b\x00\x00\x00\x01\x00\x00\x00\x0a\x00\x00\x00serverInfo\x00", 31 | 'version' => "\x00\x00\x00\x00\x18\x00\x00\x00\x01\x00\x00\x00\x07\x00\x00\x00version\x00", 32 | 'players' => "\x00\x00\x00\x00\x24\x00\x00\x00\x02\x00\x00\x00\x0b\x00\x00\x00listPlayers\x00\x03\x00\x00\x00\x61ll\x00", 33 | ); 34 | 35 | /* 36 | Bf3 servers must be rent, so query port is always different. In case default port exists, it makes no sense to use it, because most servers use different ports. 37 | See this issue: https://github.com/kostya0shift/GameQ3/issues/7 38 | */ 39 | // protected $query_port = 25200; 40 | 41 | protected $ports_type = self::PT_SAME; 42 | 43 | protected $protocol = 'bf3'; 44 | protected $name = 'bf3'; 45 | protected $name_long = "Battlefield 3"; 46 | 47 | 48 | public function init() { 49 | if ($this->isRequested('teams')) 50 | $this->result->setIgnore('teams', false); 51 | 52 | $this->queue('status', 'tcp', $this->packets['status']); 53 | $this->queue('version', 'tcp', $this->packets['version']); 54 | if ($this->isRequested('players')) $this->queue('players', 'tcp', $this->packets['players']); 55 | } 56 | 57 | protected function processRequests($qid, $requests) { 58 | if ($qid === 'status') { 59 | return $this->_process_status($requests['responses']); 60 | } else 61 | if ($qid === 'version') { 62 | return $this->_process_version($requests['responses']); 63 | } else 64 | if ($qid === 'players') { 65 | return $this->_process_players($requests['responses']); 66 | } 67 | } 68 | 69 | protected function _preparePackets($packets) { 70 | $buf = new Buffer(implode('', $packets)); 71 | 72 | $buf->skip(8); /* skip header */ 73 | 74 | 75 | $result = array(); 76 | $num_words = $buf->readInt32(); 77 | 78 | for ($i = 0; $i < $num_words; $i++) { 79 | $len = $buf->readInt32(); 80 | $result[] = $buf->read($len); 81 | $buf->read(1); /* 0x00 string ending */ 82 | } 83 | 84 | if (!isset($result[0]) || $result[0] !== 'OK') { 85 | $this->debug('Packet Response was not OK! Buffer: ' . $buf->getBuffer()); 86 | return false; 87 | } 88 | 89 | return $result; 90 | } 91 | 92 | protected function _process_version($packets) { 93 | $words = $this->_preparePackets($packets); 94 | 95 | if (isset($words[2])) 96 | $this->result->addGeneral('version', $words[2]); 97 | } 98 | 99 | protected function _process_players($packets) { 100 | $words = $this->_preparePackets($packets); 101 | 102 | // Count the number of words and figure out the highest index. 103 | $words_total = count($words)-1; 104 | 105 | // The number of player info points 106 | $num_tags = $words[1]; 107 | 108 | // Pull out the tags, they start at index=3, length of num_tags 109 | $tags = array_slice($words, 2, $num_tags); 110 | 111 | $i_name = false; 112 | $i_score = false; 113 | $i_teamid = false; 114 | 115 | foreach($tags as $tag_i => $tag) { 116 | switch($tag) { 117 | case 'name': 118 | $i_name = $tag_i; 119 | unset($tags[$tag_i]); 120 | break; 121 | case 'teamId': 122 | $i_teamid = $tag_i; 123 | unset($tags[$tag_i]); 124 | break; 125 | case 'score': 126 | $i_score = $tag_i; 127 | unset($tags[$tag_i]); 128 | break; 129 | } 130 | } 131 | if ($i_name === false || $i_score === false || $i_teamid === false) return false; 132 | 133 | // Just in case this changed between calls. 134 | $this->result->addGeneral('num_players', $this->filterInt($words[9])); 135 | 136 | // Loop until we run out of positions 137 | for($pos=(3+$num_tags);$pos<=$words_total;$pos+=$num_tags) { 138 | // Pull out this player 139 | $player = array_slice($words, $pos, $num_tags); 140 | 141 | $m = array(); 142 | 143 | foreach($tags as $tag_i => $tag) { 144 | $m[$tag] = $this->filterInt($player[$tag_i]); 145 | } 146 | 147 | $this->result->addPlayer($player[$i_name], $this->filterInt($player[$i_score]), $this->filterInt($player[$i_teamid]), $m); 148 | 149 | } 150 | 151 | // @todo: Add some team definition stuff 152 | } 153 | 154 | protected function _process_status($packets) { 155 | $words = $this->_preparePackets($packets); 156 | 157 | $this->result->addGeneral('hostname', $words[1]); 158 | $this->result->addGeneral('num_players', $this->filterInt($words[2])); 159 | $this->result->addGeneral('max_players', $this->filterInt($words[3])); 160 | $this->result->addGeneral('mode', $words[4]); 161 | $this->result->addGeneral('map', $words[5]); 162 | 163 | $this->result->addSetting('rounds_played', $words[6]); 164 | $this->result->addSetting('rounds_total', $words[7]); 165 | 166 | // Figure out the number of teams 167 | $num_teams = intval($words[8]); 168 | 169 | // Set the current index 170 | $index_current = 9; 171 | 172 | // Loop for the number of teams found, increment along the way 173 | for($id=1; $id<=$num_teams; $id++) { 174 | // We have tickets, but no team name. great... 175 | $this->result->addTeam($id, $id, array('tickets' => $this->filterInt($words[$index_current]))); 176 | 177 | $index_current++; 178 | } 179 | 180 | // Get and set the rest of the data points. 181 | $this->result->addSetting('target_score', $words[$index_current]); 182 | // it seems $words[$index_current + 1] is always empty 183 | $this->result->addSetting('ranked', $words[$index_current + 2] === 'true' ? 1 : 0); 184 | $this->result->addGeneral('secure', $words[$index_current + 3] === 'true'); 185 | $this->result->addGeneral('password', $words[$index_current + 4] === 'true'); 186 | $this->result->addSetting('uptime', $words[$index_current + 5]); 187 | $this->result->addSetting('round_time', $words[$index_current + 6]); 188 | 189 | // Added in R9 190 | // ip_port $words[$index_current + 7] 191 | $this->result->addSetting('punkbuster_version', $words[$index_current + 8]); 192 | $this->result->addSetting('join_queue', $words[$index_current + 9] === 'true' ? 1 : 0); 193 | $this->result->addSetting('region', $words[$index_current + 10]); 194 | $this->result->addSetting('pingsite', $words[$index_current + 11]); 195 | $this->result->addSetting('country', $words[$index_current + 12]); 196 | 197 | // Added in R29, No docs as of yet 198 | $this->result->addSetting('quickmatch', $words[$index_current + 13] === 'true' ? 1 : 0); // Guessed from research 199 | } 200 | } -------------------------------------------------------------------------------- /gameq3/protocols/lfs.php: -------------------------------------------------------------------------------- 1 | . 17 | * 18 | * 19 | */ 20 | 21 | namespace GameQ3\protocols; 22 | 23 | // This class was created as an example of preFetch() usage, but... It works just fine. 24 | // Don't forget to apply colorize filter to strip spans from hostname. 25 | 26 | use GameQ3\UserException; 27 | 28 | class Lfs extends \GameQ3\Protocols { 29 | protected $protocol = 'lfs'; 30 | protected $name = 'lfs'; 31 | protected $name_long = "Live for Speed"; 32 | 33 | protected $network = false; 34 | 35 | protected $url = "/hoststatus/?h=%s"; 36 | protected $query_addr = "www.lfsworld.net"; 37 | protected $query_port = 80; 38 | 39 | protected $connect_string = 'lfs://join={HOSTNAME}'; 40 | 41 | protected function construct() { 42 | if (!isset($this->server_info['hostname'])) 43 | throw new UserException("Hostname must be set for lfs protocol"); 44 | 45 | $this->url = sprintf($this->url, urlencode($this->server_info['hostname'])); 46 | } 47 | 48 | protected function getIdentifier() { 49 | return $this->server_info['hostname']; 50 | } 51 | 52 | protected function genConnectString() { 53 | return str_replace('{HOSTNAME}', rawurlencode($this->server_info['hostname']), $this->connect_string); 54 | } 55 | 56 | public function init() { 57 | $this->queue('status', 'http', $this->url); 58 | } 59 | 60 | protected function processRequests($qid, $requests) { 61 | if ($qid === 'status') { 62 | return $this->_process_status($requests['responses']); 63 | } 64 | } 65 | 66 | protected function _process_status($packets) { 67 | $data = $packets[0]; 68 | unset($packets); 69 | 70 | preg_match("#
(.*?)
#i", $data, $match); 71 | if (!isset($match[1])) return false; 72 | 73 | $this->result->addGeneral('hostname', $match[1]); 74 | 75 | preg_match_all("#
]*>
([^<]*)
([^<]*)
#i", $data, $match_all, PREG_SET_ORDER); 76 | if (!is_array($match_all)) return; 77 | 78 | foreach($match_all as $match) { 79 | if (!isset($match[3])) continue; 80 | switch($match[1]) { 81 | case 'Mode': 82 | $this->result->addGeneral('mode', $match[3]); 83 | break; 84 | case 'Track': 85 | $this->result->addGeneral('map', $match[3]); 86 | break; 87 | case 'Version': 88 | $this->result->addGeneral('version', $match[3]); 89 | break; 90 | case 'Conns': 91 | $c = explode('/', $match[3]); 92 | if (!isset($c[1])) break; 93 | $n = intval(trim($c[0])); 94 | $m = intval(trim($c[1])); 95 | 96 | $this->result->addGeneral('num_players', $n); 97 | $this->result->addGeneral('max_players', $m); 98 | break; 99 | case 'Settings': 100 | break; 101 | default: 102 | $this->result->addSetting($match[2], $match[3]); 103 | } 104 | } 105 | 106 | preg_match("#
(.*?)
#i", $data, $match); 107 | if (!isset($match[1])) return false; 108 | 109 | preg_match_all("#]*>([^>]*)#i", $match[1], $match_all, PREG_SET_ORDER); 110 | if (!is_array($match_all)) return false; 111 | 112 | foreach($match_all as $match) { 113 | if (!isset($match[2])) continue; 114 | $name = $match[2]; 115 | $url = $match[1]; 116 | 117 | // Sometimes this happens. 118 | // Look at this guy: http://www.lfsworld.net/?win=stats&racer=dzsed%E1j 119 | if (empty($name)) { 120 | $urld = html_entity_decode($url, ENT_HTML5, 'UTF-8'); 121 | list(,$urld) = explode('?', $urld, 2); 122 | $urld = explode('&', $urld); 123 | foreach($urld as $pair) { 124 | $p = explode('=', $pair); 125 | if ($p[0] === "racer") 126 | $name = $p[1]; 127 | } 128 | } 129 | 130 | $this->result->addPlayer($name, null, null, array('url' => $url)); 131 | } 132 | 133 | /* 134 | LFS HostStatus 135 |
[AA] Demo FBM
136 |
Mode
Demo, Race 3 laps
137 |
Track
Blackwood GP
138 |
Cars
FBM
139 |
Settings
RV m
140 |
Version
0.6E
141 |
Conns
14 / 15
142 | 143 | 144 | */ 145 | 146 | } 147 | 148 | } -------------------------------------------------------------------------------- /gameq3/protocols/ut3.php: -------------------------------------------------------------------------------- 1 | . 17 | */ 18 | 19 | 20 | namespace GameQ3\protocols; 21 | /* 22 | This is really strange protocol. 23 | You shouldn't rely on it's results, because UT3 can simply send corrupted and truncated arrays of data (related to players and teams). 24 | Check $result['info']['full'] for results reliability (described in ./gamespy3.php) 25 | Also if you turn on debugging then you will see some information about corruptions in response. 26 | 27 | This is not a bug of query software, it is UT3's implementation of Gamespy3. 28 | Here are some screenshots from serverbrowser. They are really strange: 29 | http://img443.imageshack.us/img443/4982/ut320130310190116282.png 30 | http://img46.imageshack.us/img46/6742/ut320130310191529232.png 31 | */ 32 | 33 | class Ut3 extends \GameQ3\Protocols\Gamespy3 { 34 | protected $name = "ut3"; 35 | protected $name_long = "Unreal Tournament 3"; 36 | 37 | protected $query_port = 6500; 38 | protected $connect_port = 7777; 39 | protected $ports_type = self::PT_DIFFERENT_NONCOMPUTABLE_VARIABLE; 40 | 41 | protected function _parse_arrays_break(&$buf) { 42 | /* 43 | We should break in case: 44 | * We reach new field (next char is any except \x00-\x02) 45 | * We reach new section (next char is \x00, second char is \x01-\x02) 46 | * We reach end of packet (buf length is 1) 47 | */ 48 | 49 | if ($buf->getLength() <= 1) return true; 50 | 51 | $c_1 = $buf->lookAhead(2); 52 | $c_2 = $c_1{1}; 53 | $c_1 = $c_1{0}; 54 | 55 | // New section 56 | if ($c_1 === "\x00" && ($c_2 === "\x01" || $c_2 === "\x02")) return true; 57 | 58 | // New field 59 | if ($c_1 !== "\x00") return true; 60 | 61 | return false; 62 | } 63 | 64 | protected function _parse_settings() { 65 | $s_cnt = count($this->settings); 66 | $i = 0; 67 | $ut3_section = false; 68 | while ($i < $s_cnt) { 69 | $key = $this->settings[$i]; 70 | if (!$ut3_section) { 71 | // Those keys seem to be always set in the beggining of ut3 variables section 72 | if ($key === 's32779' || $key === 's0') 73 | $ut3_section = true; 74 | } 75 | 76 | $val = isset($this->settings[$i+1]) ? $this->settings[$i+1] : ""; 77 | 78 | // Check if next var is ut3 key. Sometimes it's value is skipped, we should process that. 79 | /* Example (look at key p268435968): 80 | 1c 00 70 32 36 38 34 33 35 37 30 36 00 31 32 00 ..p26843 5706.12. 81 | 70 32 36 38 34 33 35 39 36 38 00 70 32 36 38 34 p2684359 68.p2684 82 | 33 35 39 36 39 00 30 00 00 01 70 6c 61 79 65 72 35969.0. ..player 83 | 84 | */ 85 | if ($ut3_section && preg_match('/^(s[0-9]+|p[0-9]+)$/', $val)) { 86 | $i += 1; 87 | $val = ""; 88 | } else { 89 | $i += 2; 90 | } 91 | $this->_put_var($key, $this->filterInt($val)); 92 | 93 | } 94 | unset($this->settings); 95 | return true; 96 | } 97 | 98 | 99 | protected function _put_var($key, $val) { 100 | $normalize = array( 101 | 's6' => 'pure_server', 102 | 's10' => 'force_respawn', 103 | 'p268435704' => 'frag_limit', 104 | 'p268435705' => 'time_limit', 105 | ); 106 | 107 | switch($key) { 108 | // General 109 | 110 | // Hostname is really buggy. The most trustful value is p1073741827, if it is not empty. 111 | case 'hostname': 112 | $this->result->addGeneral('hostname', $val); 113 | break; 114 | // OwningPlayerName is usually set to hostname, but some references say that it is really OwningPlayerName. 115 | case 'OwningPlayerName': 116 | if ($this->result->getGeneral('hostname') !== $val) 117 | $this->result->addSetting('OwningPlayerName', $val); 118 | break; 119 | case 'p1073741827': 120 | if ($val !== "") 121 | $this->result->addGeneral('hostname', $val); 122 | break; 123 | 124 | case 'hostport': 125 | $this->setConnectPort($val); 126 | $this->result->addSetting($key, $val); 127 | break; 128 | case 'p1073741825': 129 | $this->result->addGeneral('map', $val); 130 | break; 131 | case 'EngineVersion': 132 | $this->result->addGeneral('version', $val); 133 | $this->result->addSetting($key, $val); 134 | break; 135 | case 's32779': 136 | switch($val) { 137 | case 1: $m = "dm"; break; 138 | case 2: $m = "war"; break; 139 | case 3: $m = "vctf"; break; 140 | case 4: $m = "tdm"; break; 141 | case 5: $m = "duel"; break; 142 | default: $m = false; break; // don't override p1073741826 143 | } 144 | if (!$m) 145 | $this->result->addSetting($key, $val); 146 | else 147 | $this->result->addGeneral('mode', $m); 148 | break; 149 | case 'p1073741826': 150 | switch($val) { 151 | case 'UTGameContent.UTVehicleCTFGame_Content': $m = "vctf"; break; 152 | case 'UTGameContent.UTCTFGame_Content': $m = "ctf"; break; 153 | case 'UTGame.UTTeamGame': $m = "tdm"; break; 154 | case 'UTGameContent.UTOnslaughtGame_Content': $m = "war"; break; 155 | default: $m = false; break; // don't override s32779 156 | } 157 | if (!$m) 158 | $this->result->addSetting($key, $val); 159 | else 160 | $this->result->addGeneral('mode', $m); 161 | break; 162 | case 'numplayers': 163 | $this->result->addGeneral('num_players', $val); 164 | break; 165 | case 'maxplayers': 166 | $this->result->addGeneral('max_players', $val); 167 | break; 168 | case 'p268435703': 169 | $this->result->addGeneral('bot_players', $val); 170 | break; 171 | case 's7': 172 | $this->result->addGeneral('password', $val == 1); 173 | break; 174 | 175 | // Settings that we need to parse 176 | case 'p268435717': 177 | $m = array(); 178 | if ($val & 1) $m []= "BigHead"; 179 | if ($val & 2) $m []= "FriendlyFire"; 180 | if ($val & 4) $m []= "Handicap"; 181 | if ($val & 8) $m []= "Instagib"; 182 | if ($val & 16) $m []= "LowGrav"; 183 | if ($val & 64) $m []= "NoPowerups"; 184 | if ($val & 128) $m []= "NoTranslocator"; 185 | if ($val & 256) $m []= "Slomo"; 186 | if ($val & 1024) $m []= "SpeedFreak"; 187 | if ($val & 2048) $m []= "SuperBerserk"; 188 | if ($val & 8192) $m []= "WeaponReplacement"; 189 | if ($val & 16384) $m []= "WeaponsRespawn"; 190 | 191 | foreach($m as $mut) 192 | $this->result->addSetting('stock_mutator', $mut); 193 | 194 | //$this->result->addSetting($key, $mut); 195 | break; 196 | case 'p1073741828': 197 | $m = explode("\x1C", $val); 198 | foreach($m as $mut) 199 | if ($mut !== "") $this->result->addSetting('custom_mutator', $mut); 200 | break; 201 | case 'p1073741829': 202 | // Same as p1073741828 list of mutators, but these values are mutator names I think, 203 | // in p1073741828 we have mutator descriptions. 204 | break; 205 | case 's0': 206 | switch($val) { 207 | case 1: $m = "Novice"; break; 208 | case 2: $m = "Average"; break; 209 | case 3: $m = "Experienced"; break; 210 | case 4: $m = "Skilled"; break; 211 | case 5: $m = "Adept"; break; 212 | case 6: $m = "Masterful"; break; 213 | case 7: $m = "Inhuman"; break; 214 | case 8: $m = "Godlike"; break; 215 | default: $m = false; break; 216 | } 217 | if (!$m) 218 | $this->result->addSetting($key, $val); 219 | else 220 | $this->result->addSetting('bot_skill', $m); 221 | break; 222 | case 's8': 223 | switch($val) { 224 | case 0: $m = "false"; break; 225 | case 1: $m = "true"; break; 226 | case 2: $m = "1:1"; break; 227 | case 3: $m = "3:2"; break; 228 | case 4: $m = "2:1"; break; 229 | default: $m = false; break; 230 | } 231 | if (!$m) 232 | $this->result->addSetting($key, $val); 233 | else 234 | $this->result->addSetting('vs_bots', $m); 235 | break; 236 | case 'mapname': 237 | // skip comma-separated list of the same variables 238 | break; 239 | 240 | // All other settings 241 | default: 242 | $this->result->addSetting((isset($normalize[$key]) ? $normalize[$key] : $key), $val); 243 | } 244 | } 245 | } 246 | -------------------------------------------------------------------------------- /gameq3/buffer.php: -------------------------------------------------------------------------------- 1 | . 17 | * 18 | * 19 | */ 20 | 21 | /** 22 | * Provide an interface for easy manipulation of a server response 23 | * 24 | * @author Aidan Lister 25 | * @author Tom Buskens 26 | * @author Kostya Esmukov 27 | */ 28 | 29 | 30 | namespace GameQ3; 31 | 32 | class Buffer { 33 | /** 34 | * The original data 35 | * 36 | * @var string 37 | * @access private 38 | */ 39 | private $data; 40 | 41 | /** 42 | * The original data length 43 | * 44 | * @var int 45 | * @access private 46 | */ 47 | private $length; 48 | 49 | 50 | /** 51 | * Position of pointer 52 | * 53 | * @var string 54 | * @access private 55 | */ 56 | private $index = 0; 57 | 58 | 59 | /** 60 | * Constructor 61 | * 62 | * @param (string|array) $response The data 63 | */ 64 | public function __construct($data) { 65 | $this->data = $data; 66 | $this->length = strlen($data); 67 | } 68 | 69 | /** 70 | * Return all the data 71 | * 72 | * @return string|array The data 73 | */ 74 | public function getData() { 75 | return $this->data; 76 | } 77 | 78 | /** 79 | * Return data currently in the buffer 80 | * 81 | * @return string|array The data currently in the buffer 82 | */ 83 | public function getBuffer() { 84 | return substr($this->data, $this->index); 85 | } 86 | 87 | /** 88 | * Returns the number of bytes in the buffer 89 | * 90 | * @return int Length of the buffer 91 | */ 92 | public function getLength() { 93 | return max($this->length - $this->index, 0); 94 | } 95 | 96 | /** 97 | * Read from the buffer 98 | * 99 | * @param int $length Length of data to read 100 | * @throws \Exception 101 | * @return string The data read 102 | */ 103 | public function read($length=1) { 104 | if (($length + $this->index) > $this->length) { 105 | throw new \Exception('length OOB'); 106 | } 107 | 108 | $string = substr($this->data, $this->index, $length); 109 | $this->index += $length; 110 | 111 | return $string; 112 | } 113 | 114 | /** 115 | * Read the last character from the buffer 116 | * 117 | * Unlike the other read functions, this function actually removes 118 | * the character from the buffer. 119 | * 120 | * @return string The data read 121 | */ 122 | public function readLast() { 123 | $len = strlen($this->data); 124 | $string = $this->data{strlen($this->data) - 1}; 125 | $this->data = substr($this->data, 0, $len - 1); 126 | $this->length -= 1; 127 | 128 | return $string; 129 | } 130 | 131 | /** 132 | * Look at the buffer, but don't remove 133 | * 134 | * @param int $length Length of data to read 135 | * @return string The data read 136 | */ 137 | public function lookAhead($length=1) { 138 | $string = substr($this->data, $this->index, $length); 139 | 140 | return $string; 141 | } 142 | 143 | /** 144 | * Skip forward in the buffer 145 | * 146 | * @param int $length Length of data to skip 147 | * @return void 148 | */ 149 | public function skip($length=1) { 150 | $this->index += $length; 151 | } 152 | 153 | /** 154 | * Jump to a specific position in the buffer, 155 | * will not jump past end of buffer 156 | * 157 | * @param int $index Position to go to 158 | * @return void 159 | */ 160 | public function jumpto($index) { 161 | $this->index = min($index, $this->length - 1); 162 | } 163 | 164 | /** 165 | * Get the current pointer position 166 | * 167 | * @return int The current pointer position 168 | */ 169 | public function getPosition() { 170 | return $this->index; 171 | } 172 | 173 | /** 174 | * Read from buffer until delimiter is reached 175 | * 176 | * If not found, return everything 177 | * 178 | * @param string $delim Read until this character is reached 179 | * @return string The data read 180 | */ 181 | public function readString($delim="\x00") { 182 | // Get position of delimiter 183 | $len = strpos($this->data, $delim, min($this->index, $this->length)); 184 | 185 | // If it is not found then return whole buffer 186 | if ($len === false) { 187 | return $this->read(strlen($this->data) - $this->index); 188 | } 189 | 190 | // Read the string and remove the delimiter 191 | $string = $this->read($len - $this->index); 192 | ++$this->index; 193 | 194 | return $string; 195 | } 196 | 197 | /** 198 | * Reads a pascal string from the buffer 199 | * 200 | * @param int $offset Number of bits to cut off the end 201 | * @param bool $read_offset True if the data after the offset is 202 | * to be read 203 | * @return string The data read 204 | */ 205 | public function readPascalString($offset=0, $read_offset = false) { 206 | // Get the proper offset 207 | $len = $this->readInt8(); 208 | $offset = max($len - $offset, 0); 209 | 210 | // Read the data 211 | if ($read_offset) { 212 | return $this->read($offset); 213 | } 214 | else { 215 | return substr($this->read($len), 0, $offset); 216 | } 217 | } 218 | 219 | /** 220 | * Read from buffer until any of the delimiters is reached 221 | * 222 | * If not found, return everything 223 | * 224 | * @param array $delims Read until these characters are reached 225 | * @param string $delimfound 226 | * @return string The data read 227 | */ 228 | public function readStringMulti($delims, &$delimfound=null) { 229 | // Get position of delimiters 230 | $pos = array(); 231 | foreach ($delims as $delim) { 232 | if ($p = strpos($this->data, $delim, min($this->index, $this->length))) { 233 | $pos[] = $p; 234 | } 235 | } 236 | 237 | // If none are found then return whole buffer 238 | if (empty($pos)) { 239 | return $this->read(strlen($this->data) - $this->index); 240 | } 241 | 242 | // Read the string and remove the delimiter 243 | sort($pos); 244 | $string = $this->read($pos[0] - $this->index); 245 | $delimfound = $this->read(); 246 | 247 | return $string; 248 | } 249 | 250 | /** 251 | * Read a 32-bit unsigned integer 252 | */ 253 | public function readInt32() { 254 | $int = unpack('Lint', $this->read(4)); 255 | return $int['int']; 256 | } 257 | 258 | /** 259 | * Read a 32-bit signed integer 260 | */ 261 | public function readInt32Signed() { 262 | $int = unpack('lint', $this->read(4)); 263 | return $int['int']; 264 | } 265 | 266 | /** 267 | * Read a 16-bit unsigned integer 268 | */ 269 | public function readInt16() { 270 | $int = unpack('Sint', $this->read(2)); 271 | return $int['int']; 272 | } 273 | 274 | /** 275 | * Read a 16-big signed integer 276 | */ 277 | public function readInt16Signed() { 278 | $int = unpack('sint', $this->read(2)); 279 | return $int['int']; 280 | } 281 | 282 | /** 283 | * Read an int8 from the buffer 284 | * 285 | * @return int The data read 286 | */ 287 | public function readInt8() { 288 | return ord($this->read(1)); 289 | } 290 | 291 | /** 292 | * Read an float32 from the buffer 293 | * 294 | * @return int The data read 295 | */ 296 | public function readFloat32() { 297 | $float = unpack('ffloat', $this->read(4)); 298 | return $float['float']; 299 | } 300 | 301 | /** 302 | * Conversion to float 303 | * 304 | * @access public 305 | * @param string $string String to convert 306 | * @return float 32 bit float 307 | */ 308 | public function toFloat($string) { 309 | // Check length 310 | if (strlen($string) !== 4) { 311 | return false; 312 | } 313 | 314 | // Convert 315 | $float = unpack('ffloat', $string); 316 | return $float['float']; 317 | } 318 | 319 | /** 320 | * Conversion to integer 321 | * 322 | * @access public 323 | * @param string $string String to convert 324 | * @param int $bits Number of bits 325 | * @return int Integer according to type 326 | */ 327 | public function toInt($string, $bits = 8) { 328 | // Check length 329 | if (strlen($string) !== ($bits / 8)) { 330 | return false; 331 | } 332 | 333 | // Convert 334 | switch($bits) { 335 | 336 | // 8 bit unsigned 337 | case 8: 338 | $int = ord($string); 339 | break; 340 | 341 | // 16 bit unsigned 342 | case 16: 343 | $int = unpack('Sint', $string); 344 | $int = $int['int']; 345 | break; 346 | 347 | // 32 bit unsigned 348 | case 32: 349 | $int = unpack('Lint', $string); 350 | $int = $int['int']; 351 | break; 352 | 353 | // Invalid type 354 | default: 355 | $int = false; 356 | break; 357 | } 358 | 359 | return $int; 360 | } 361 | } 362 | -------------------------------------------------------------------------------- /gameq3/protocols/teamspeak3.php: -------------------------------------------------------------------------------- 1 | . 17 | * 18 | * 19 | */ 20 | 21 | namespace GameQ3\protocols; 22 | 23 | // TS3 lets us send 30 queries in a minute and then bans as for 600 seconds. 24 | // Great reference: http://media.teamspeak.com/ts3_literature/TeamSpeak%203%20Server%20Query%20Manual.pdf 25 | 26 | use GameQ3\Buffer; 27 | use GameQ3\UserException; 28 | 29 | class Teamspeak3 extends \GameQ3\Protocols { 30 | 31 | protected $packets = array( 32 | 'login' => "login client_login_name=%s client_login_password=%s\x0A", 33 | 'clientlogin' => "clientupdate client_nickname=%s\x0A", 34 | 35 | 'usesid' => "use sid=%s\x0A", 36 | 'useport' => "use port=%d\x0A", 37 | 38 | 'serverinfo' => "serverinfo\x0A", 39 | 'channellist' => "channellist -topic -flags -voice -limits\x0A", 40 | 'clientlist' => "clientlist -uid -away -voice -groups\x0A", 41 | //'servergroup' => "servergrouplist\x0A", 42 | //'channelgroup' => "channelgrouplist\x0A", 43 | 44 | //'quit' => "quit\x0A", 45 | ); 46 | 47 | protected $connect_port = 9987; 48 | protected $query_port = 10011; 49 | protected $ports_type = self::PT_DIFFERENT_NONCOMPUTABLE_FIXED; 50 | protected $connect_string = 'ts3server://{CONNECT_ADDR}?port={CONNECT_PORT}'; // &nickname= . Nickname defaults to Player. 51 | 52 | protected $protocol = 'teamspeak3'; 53 | protected $name = 'teamspeak3'; 54 | protected $name_long = "Teamspeak 3"; 55 | 56 | protected $string_find = array( 57 | "\\\\", 58 | "\\/", 59 | "\\s", 60 | "\\p", 61 | "\\;", 62 | "\\a", 63 | "\\b", 64 | "\\f", 65 | "\\n", 66 | "\\r", 67 | "\\t", 68 | "\\v", 69 | ); 70 | 71 | protected $string_replace = array( 72 | "\\", 73 | "/", 74 | " ", 75 | "|", 76 | ";", 77 | "\a", 78 | "\b", 79 | "\f", 80 | "\n", 81 | "\r", 82 | "\t", 83 | "\v", 84 | ); 85 | 86 | protected $packet; 87 | protected $reply_format; 88 | 89 | protected function construct() { 90 | // Make packet that we will send every time 91 | 92 | $formed_packet = ""; 93 | $reply_format = array(); 94 | 95 | if (isset($this->server_info['login_name']) && isset($this->server_info['login_password'])) { 96 | $formed_packet .= $this->_formCommand('login', $this->server_info['login_name'], $this->server_info['login_password']); 97 | $reply_format []= 'cmd'; 98 | } 99 | 100 | if (isset($this->server_info['sid'])) { 101 | $formed_packet .= $this->_formCommand('usesid', $this->server_info['sid']); 102 | } else { 103 | if (!is_int($this->connect_port)) 104 | throw new UserException("Both connect_port and sid are missed in TS3"); 105 | $formed_packet .= $this->_formCommand('useport', $this->connect_port); 106 | } 107 | $reply_format []= 'cmd'; 108 | 109 | if (isset($this->server_info['nickname'])) { 110 | $formed_packet .= $this->_formCommand('clientlogin', $this->server_info['nickname']); 111 | $reply_format []= 'cmd'; 112 | } 113 | 114 | $formed_packet .= $this->_formCommand('serverinfo'); 115 | $reply_format []= 'serverinfo'; 116 | 117 | if ($this->isRequested('players')) { 118 | $formed_packet .= $this->_formCommand('clientlist'); 119 | $reply_format []= 'clientlist'; 120 | //$formed_packet .= $this->packets['servergroup']; 121 | //$reply_format []= 'servergroup'; 122 | } 123 | 124 | if ($this->isRequested('channels')) { 125 | $formed_packet .= $this->_formCommand('channellist'); 126 | $reply_format []= 'channellist'; 127 | //$formed_packet .= $this->packets['channelgroup']; 128 | //$reply_format []= 'channelgroup'; 129 | } 130 | 131 | //$formed_packet .= $this->packets['quit']; 132 | //$reply_format []= 'cmd'; 133 | 134 | $this->packet = $formed_packet; 135 | $this->reply_format = $reply_format; 136 | } 137 | 138 | public function init() { 139 | $this->queue('all', 'tcp', $this->packet, array('close' => true)); 140 | } 141 | 142 | protected function processRequests($qid, $requests) { 143 | if ($qid === 'all') { 144 | return $this->_process_r($requests['responses']); 145 | } 146 | } 147 | 148 | protected function _process_r($packets) { 149 | $packet_data = implode("", $packets); 150 | $buf = new Buffer($packet_data); 151 | 152 | unset($packets); 153 | 154 | $result = array(); 155 | 156 | // remove header if present 157 | if ($buf->lookAhead(3) === 'TS3') { 158 | // TS3 159 | $buf->readString("\n"); 160 | // Welcome to the serverquery blah-blah-blah 161 | $buf->readString("\n"); 162 | } 163 | 164 | foreach($this->reply_format as $reply) { 165 | $data = trim($buf->readString("\n")); 166 | 167 | if ($reply !== "cmd" && substr($data, 0, 6) !== "error ") { 168 | $result[$reply] = array(); 169 | 170 | $data = explode ('|', $data); 171 | 172 | 173 | foreach ($data as $part) { 174 | $variables = explode (' ', $part); 175 | 176 | $info = array(); 177 | 178 | foreach ($variables as $variable) { 179 | $ar = explode('=', $variable, 2); 180 | 181 | $info[$ar[0]] = (isset($ar[1]) ? $this->_unescape($ar[1]) : ''); 182 | } 183 | 184 | 185 | $result[$reply][] = $info; 186 | } 187 | 188 | $data = trim($buf->readString("\n")); 189 | } 190 | 191 | $res = $this->_verify_response($data); 192 | // Response is incorrect (this occures when some packets are not received due to timeout) 193 | if ($res !== true) { 194 | $this->debug("TS3 Error occured." . (is_string($res) ? $res : "\nBuffer:\n" . $packet_data ) ); 195 | return false; 196 | } 197 | } 198 | 199 | $bots_count = 0; 200 | foreach($result as $type => $reply) { 201 | if ($type === "serverinfo") { 202 | foreach($reply[0] as $key => $val) { 203 | $val = $this->filterInt($val); 204 | 205 | switch($key) { 206 | case 'virtualserver_name': 207 | $this->result->addGeneral('hostname', $val); 208 | break; 209 | case 'virtualserver_flag_password': 210 | $this->result->addGeneral('password', ($val == 1)); 211 | break; 212 | case 'virtualserver_clientsonline': 213 | $this->result->addGeneral('num_players', $val); 214 | break; 215 | case 'virtualserver_maxclients': 216 | $this->result->addGeneral('max_players', $val); 217 | break; 218 | case 'virtualserver_version': 219 | $this->result->addGeneral('version', $val); 220 | break; 221 | case 'virtualserver_port': 222 | $this->setConnectPort($val); 223 | break; 224 | } 225 | $this->result->addSetting($key, $val); 226 | } 227 | } else 228 | if ($type === "clientlist") { 229 | foreach($reply as $player) { 230 | $name = $player['client_nickname']; 231 | unset($player['client_nickname']); 232 | 233 | foreach($player as $key => &$val) { 234 | $val = $this->filterInt($val); 235 | } 236 | 237 | $is_bot = (isset($player['client_type']) && $player['client_type'] == 1) || (isset($player['client_unique_identifier']) && $player['client_unique_identifier'] == "ServerQuery"); 238 | 239 | if ($is_bot) 240 | $bots_count++; 241 | 242 | // cid - channel id. But most probably we will not use that value as we don't use teams, so we don't pass it to teamid 243 | $this->result->addPlayer($name, null, null, $player, $is_bot); 244 | } 245 | } else 246 | if ($type === "channellist") { 247 | foreach($reply as $channel) { 248 | foreach($channel as $key => &$val) { 249 | $val = $this->filterInt($val); 250 | } 251 | $cid = $channel['cid']; 252 | unset($channel['cid']); 253 | $this->result->addCustom('channels', $cid, $channel); 254 | } 255 | } 256 | } 257 | 258 | $this->result->addGeneral('bot_players', $bots_count); 259 | 260 | } 261 | 262 | protected function _unescape($str) { 263 | return str_replace($this->string_find, $this->string_replace, $str); 264 | } 265 | 266 | protected function _escape($str) { 267 | return str_replace($this->string_replace, $this->string_find, $str); 268 | } 269 | 270 | // command_name, arg1, arg2, ... 271 | protected function _formCommand($command) { 272 | $args = func_get_args(); 273 | array_shift($args); 274 | 275 | $that = $this; 276 | array_walk($args, function (&$v) use ($that) { 277 | $v = $that->_escape($v); 278 | }); 279 | 280 | array_unshift($args, $this->packets[$command]); 281 | 282 | return call_user_func_array('sprintf', $args); 283 | } 284 | 285 | protected function _verify_response($response) { 286 | // Check the response 287 | if($response === 'error id=0 msg=ok') return true; 288 | 289 | if (substr($response, 0, 6) === "error ") { 290 | $errstr = ""; 291 | 292 | $vars = explode(" ", substr($response, 6)); 293 | foreach($vars as $pair) { 294 | $ar = explode('=', $pair, 2); 295 | 296 | $key = $ar[0]; 297 | $val = (isset($ar[1]) ? $this->_unescape($ar[1]) : ''); 298 | 299 | $errstr .= " " . ucfirst($key) .": ".$val . "."; 300 | } 301 | 302 | 303 | return $errstr; 304 | } 305 | 306 | return false; 307 | } 308 | } -------------------------------------------------------------------------------- /gameq3/protocols/gamespy.php: -------------------------------------------------------------------------------- 1 | . 17 | * 18 | * 19 | */ 20 | 21 | namespace GameQ3\protocols; 22 | 23 | /* 24 | Notice: sometimes players list is truncated (even with "\players\" request. 25 | We receive something like this: 26 | ... player_31\MEXAHIK.\score_31\0\ping_31\106\team_31\2\player_32\Gre\final\\queryid\375.3" 27 | ------------- 28 | There is nothing really bad (I think) if there will not be some players. 29 | But the point is that in this case teams are not received. 30 | This is very bad. I can't do anything with it. 31 | The only solution is to remember teams' names and use them when teams are empty. 32 | 33 | You may check result as follows: 34 | if ($result['info']['online'] == true) { 35 | if ($result['info']['full'] == true) { 36 | // Result has full list of players and teams 37 | } else { 38 | // Result is not full 39 | } 40 | } 41 | */ 42 | 43 | class Gamespy extends \GameQ3\Protocols { 44 | 45 | protected $packets = array( 46 | 'all' => "\\status\\", 47 | 48 | /* 49 | 'players' => "\x5C\x70\x6C\x61\x79\x65\x72\x73\x5C", 50 | 'details' => "\x5C\x69\x6E\x66\x6F\x5C", 51 | 'basic' => "\x5C\x62\x61\x73\x69\x63\x5C", 52 | 'rules' => "\x5C\x72\x75\x6C\x65\x73\x5C", 53 | */ 54 | ); 55 | 56 | protected $protocol = 'gamespy'; 57 | protected $name = 'gamespy'; 58 | protected $name_long = "Gamespy"; 59 | 60 | protected $ports_type = self::PT_UNKNOWN; 61 | 62 | protected $teams; 63 | 64 | public function init() { 65 | if ($this->isRequested('teams')) 66 | $this->result->setIgnore('teams', false); 67 | 68 | $this->queue('all', 'udp', $this->packets['all']); 69 | 70 | /* 71 | $this->queue('players', 'udp', $this->packets['players']); 72 | $this->queue('details', 'udp', $this->packets['details']); 73 | $this->queue('basic', 'udp', $this->packets['basic']); 74 | $this->queue('rules', 'udp', $this->packets['rules']); 75 | */ 76 | } 77 | 78 | protected function processRequests($qid, $requests) { 79 | if ($qid === 'all') { 80 | return $this->_process_all($requests['responses']); 81 | } 82 | } 83 | 84 | protected function _put_var($key, $val) { 85 | switch($key) { 86 | case 'hostname': 87 | $this->result->addGeneral('hostname', iconv("ISO-8859-1//IGNORE", "utf-8", $val)); 88 | break; 89 | // case 'maptitle': 90 | case 'mapname': 91 | $this->result->addGeneral('map', $val); 92 | break; 93 | case 'gamever': 94 | $this->result->addGeneral('version', $val); 95 | break; 96 | case 'gametype': 97 | $this->result->addGeneral('mode', $val); 98 | break; 99 | case 'numplayers': 100 | $this->result->addGeneral('num_players', $val); 101 | break; 102 | case 'maxplayers': 103 | $this->result->addGeneral('max_players', $val); 104 | break; 105 | case 'password': 106 | $this->result->addGeneral('password', $val == 1); 107 | break; 108 | case 'hostport': 109 | $this->setConnectPort($val); 110 | $this->result->addSetting($key, $val); 111 | break; 112 | default: 113 | $this->result->addSetting($key, $val); 114 | } 115 | } 116 | 117 | protected function _preparePackets($packets) { 118 | // Holds the new list of packets, which will be stripped of queryid and ordered properly. 119 | $packets_ordered = array(); 120 | $final = false; 121 | 122 | // Single packets may not contain queryid 123 | $single = (count($packets) == 1); 124 | 125 | // Loop thru the packets 126 | foreach ($packets as $packet) { 127 | if(preg_match("/^(.*)\\\\queryid\\\\([^\\\\]+)(.*)$/", $packet, $matches) === FALSE) { 128 | if (!$single) { 129 | $this->debug('An error occured while parsing the status packets'); 130 | return false; 131 | } else { 132 | $packets_ordered[0] = $packet; 133 | continue; 134 | } 135 | } 136 | 137 | // Lets make the key proper incase of decimal points 138 | if(strstr($matches[2], '.')) { 139 | list($req_id, $req_num) = explode('.', $matches[2]); 140 | 141 | $key = $req_num; 142 | } else { 143 | $key = $matches[2]; 144 | } 145 | 146 | if (isset($matches[3]) && $matches[3] == "\\final\\") 147 | $final = true; 148 | 149 | // Add this stripped queryid to the new array with the id as the key 150 | $packets_ordered[$key] = $matches[1]; 151 | } 152 | 153 | // Sort the new array to make sure the keys (query ids) are in the proper order 154 | ksort($packets_ordered, SORT_NUMERIC); 155 | $result = implode('', $packets_ordered); 156 | 157 | if ($result{0} !== "\\") { 158 | $this->debug("Wrong response format"); 159 | return false; 160 | } 161 | 162 | // IL-2 has final before queryid 163 | if (!$final) { 164 | if (substr($result, -7) !== "\\final\\") { 165 | $this->debug("Wrong response format"); 166 | return false; 167 | } else { 168 | $result = substr($result, 1, -7); 169 | } 170 | } else { 171 | $result = substr($result, 1); 172 | } 173 | 174 | return $result; 175 | } 176 | 177 | 178 | protected function _process_all($packets) { 179 | $packet = $this->_preparePackets($packets); 180 | if (!$packet) return false; 181 | 182 | $data = explode("\\", $packet); 183 | 184 | unset($packets, $packet); 185 | 186 | $full = true; 187 | 188 | // BF1942: Teams' tickets defined in rules. We should save these values. 189 | $this->teams = array(); 190 | 191 | $data_cnt = count($data); 192 | if ($data_cnt % 2 !== 0) { 193 | $this->debug("Not even count of rules"); 194 | $full = false; 195 | $data_cnt--; 196 | unset($data[$data_cnt]); 197 | } 198 | 199 | $players = array(); 200 | $teams = array(); 201 | 202 | for($i=0; $i < $data_cnt; $i+=2) { 203 | $key = $data[$i]; 204 | $val = $this->filterInt($data[$i+1]); 205 | 206 | $dt_pos = strpos($key, "_t"); 207 | $dp_pos = strpos($key, "_"); 208 | if ($dt_pos !== false && is_numeric(substr($key, $dt_pos + 2))) { 209 | $index = $this->filterInt(substr($key, $dt_pos + 2)); 210 | $item = substr($key, 0, $dt_pos); 211 | if (!isset($teams[$index])) $teams[$index] = array(); 212 | $teams[$index][$item] = $val; 213 | } else 214 | if ($dp_pos !== false && is_numeric(substr($key, $dp_pos + 1))) { 215 | $index = $this->filterInt(substr($key, $dp_pos + 1)); 216 | $item = substr($key, 0, $dp_pos); 217 | 218 | // BF1942 219 | if ($item === "teamname") { 220 | if (!isset($teams[$index+1])) $teams[$index+1] = array(); 221 | $teams[$index+1]['team'] = $val; 222 | } else { 223 | if (!isset($players[$index])) $players[$index] = array(); 224 | $players[$index][$item] = $val; 225 | } 226 | } else { 227 | $this->_put_var($key, $val); 228 | } 229 | } 230 | 231 | if (!empty($players)) { 232 | // Remember fields of the first player 233 | $fields = array_keys(reset($players)); 234 | $fields_cnt = count($fields); 235 | 236 | $player_key = false; 237 | 238 | foreach(array('player', 'playername') as $key) { 239 | if (in_array($key, $fields)) { 240 | $player_key = $key; 241 | break; 242 | } 243 | } 244 | 245 | if ($player_key === false) { 246 | $this->debug("Arrays are not consistent"); 247 | return false; 248 | } 249 | 250 | foreach($players as $more) { 251 | if (count($more) !== $fields_cnt) { 252 | $this->debug("Invalid player, skipped"); 253 | $full = false; 254 | continue; 255 | } 256 | $cntn = false; 257 | foreach($fields as $field) { 258 | if (!isset($more[$field])) { 259 | $this->debug("Invalid player, skipped"); 260 | $cntn = true; 261 | $full = false; 262 | break; 263 | } 264 | } 265 | if ($cntn) continue; 266 | 267 | $name = iconv("ISO-8859-1//IGNORE", "utf-8", $more[$player_key]); // some chars like (c) should be converted to utf8 268 | $score = isset($more['score']) ? $more['score'] : (isset($more['frags']) ? $more['frags'] : null); 269 | $teamid = isset($more['team']) ? $more['team'] : null; 270 | 271 | unset($more[$player_key], $more['score'], $more['frags'], $more['team']); 272 | 273 | $this->result->addPlayer($name, $score, $teamid, $more); 274 | } 275 | } 276 | 277 | if ($full) { 278 | $players_cnt = count($players); 279 | 280 | // BF1942 281 | if ($players_cnt > $this->result->getGeneral('num_players')) { 282 | $this->result->addGeneral('num_players', $players_cnt); 283 | } else 284 | // Il-2 285 | if ($players_cnt < $this->result->getGeneral('num_players')) 286 | $full = false; 287 | } 288 | 289 | // We know that teams can be empty. Assume we don't fail when teams are wrong. 290 | if (!empty($teams)) { 291 | // Remember fields of the first player 292 | $fields = array_keys(reset($teams)); 293 | $fields_cnt = count($fields); 294 | 295 | if (!in_array('team', $fields)) { 296 | $this->debug("Arrays are not consistent"); 297 | return; 298 | } 299 | 300 | $teams_r = array(); 301 | // Ensure we have full teams array 302 | foreach($teams as $index => $more) { 303 | if (count($more) !== $fields_cnt) { 304 | $this->debug("Invalid team, broken"); 305 | $teams_r = array(); 306 | $full = false; 307 | break; 308 | } 309 | $brk = false; 310 | foreach($fields as $field) { 311 | if (!isset($more[$field])) { 312 | $this->debug("Invalid team, broken"); 313 | $teams_r = array(); 314 | $brk = true; 315 | $full = false; 316 | break; 317 | } 318 | } 319 | if ($brk) { 320 | break; 321 | } 322 | 323 | $team = $more['team']; 324 | unset($more['team']); 325 | $teams_r[$index] = array('team' => $team, 'more' => $more); 326 | } 327 | 328 | foreach($teams_r as $index => $val) { 329 | 330 | if (!empty($this->teams[$index])) { 331 | foreach($this->teams[$index] as $k => $v) { 332 | $val['more'][$k] = $v; 333 | } 334 | } 335 | 336 | $this->result->addTeam($index, $val['team'], $val['more']); 337 | } 338 | } 339 | 340 | $this->result->addInfo('full', $full); 341 | 342 | return true; 343 | } 344 | 345 | } -------------------------------------------------------------------------------- /gameq3/protocols/source.php: -------------------------------------------------------------------------------- 1 | . 17 | * 18 | * 19 | */ 20 | 21 | namespace GameQ3\protocols; 22 | 23 | use GameQ3\Buffer; 24 | 25 | class Source extends \GameQ3\Protocols { 26 | 27 | protected $packets = array( 28 | 'challenge' => "\xFF\xFF\xFF\xFF\x56\xFF\xFF\xFF\xFF", 29 | 'details' => "\xFF\xFF\xFF\xFFTSource Engine Query\x00", 30 | 'players' => "\xFF\xFF\xFF\xFF\x55%s", 31 | 'rules' => "\xFF\xFF\xFF\xFF\x56%s", 32 | ); 33 | 34 | protected $query_port = 27015; 35 | protected $ports_type = self::PT_SAME; 36 | 37 | protected $protocol = 'source'; 38 | protected $name = 'source'; 39 | protected $name_long = "Source Server"; 40 | 41 | protected $connect_string = 'steam://connect/{IDENTIFIER}'; 42 | 43 | protected $source_engine = true; 44 | 45 | protected $appid = null; 46 | 47 | 48 | public function init() { 49 | $this->queue('details', 'udp', $this->packets['details']); 50 | if ($this->isRequested('settings') || $this->isRequested('players')) 51 | $this->queue('challenge', 'udp', $this->packets['challenge'], array('response_count' => 1)); 52 | } 53 | 54 | protected function processRequests($qid, $requests) { 55 | if ($qid === 'challenge') { 56 | return $this->_process_challenge($requests['responses']); 57 | } else 58 | if ($qid === 'details') { 59 | return $this->_process_details($requests['responses']); 60 | } else 61 | if ($qid === 'rules') { 62 | return $this->_process_rules($requests['responses']); 63 | } else 64 | if ($qid === 'players') { 65 | return $this->_process_players($requests['responses']); 66 | } 67 | } 68 | 69 | 70 | 71 | protected function _process_challenge($packets) { 72 | $buf = new Buffer($packets[0]); 73 | $head = $buf->read(4); 74 | 75 | if ($head !== "\xFF\xFF\xFF\xFF") { 76 | $this->debug("Wrong challenge"); 77 | return false; 78 | } 79 | 80 | // 0x41 (?) 81 | $buf->read(); 82 | 83 | $chal = $buf->read(4); 84 | 85 | if ($this->isRequested('settings')) $this->queue('rules', 'udp', sprintf($this->packets['rules'], $chal)); 86 | if ($this->isRequested('players')) $this->queue('players', 'udp', sprintf($this->packets['players'], $chal)); 87 | } 88 | 89 | 90 | 91 | protected function _preparePackets($packets) { 92 | $buffer = new Buffer($packets[0]); 93 | 94 | // First we need to see if the packet is split 95 | // -2 = split packets 96 | // -1 = single packet 97 | $packet_type = $buffer->readInt32Signed(); 98 | 99 | // This is one packet so just return the rest of the buffer 100 | if($packet_type == -1) { 101 | // We always return the packet as expected, with null included 102 | return $packets[0]; 103 | } 104 | 105 | unset($buffer); 106 | 107 | 108 | $packs = array(); 109 | 110 | // We have multiple packets so we need to get them and order them 111 | foreach($packets as $packet) { 112 | // Make a buffer so we can read this info 113 | $buffer = new Buffer($packet); 114 | 115 | // Pull some info 116 | $packet_type = $buffer->readInt32Signed(); 117 | $request_id = $buffer->readInt32Signed(); 118 | 119 | // Check to see if this is compressed 120 | if($request_id & 0x80000000) { 121 | 122 | // Check to see if we have Bzip2 installed 123 | if(!function_exists('bzdecompress')) { 124 | $this->error('Bzip2 is not installed. See http://www.php.net/manual/en/book.bzip2.php for more info.'); 125 | return false; 126 | } 127 | 128 | // Get some info 129 | $num_packets = $buffer->readInt8(); 130 | $cur_packet = $buffer->readInt8(); 131 | $packet_length = $buffer->readInt32(); 132 | $packet_checksum = $buffer->readInt32(); 133 | 134 | // Try to decompress 135 | $result = bzdecompress($buffer->getBuffer()); 136 | 137 | // Now verify the length 138 | if(strlen($result) != $packet_length) { 139 | $this->debug("Checksum for compressed packet failed! Length expected {$packet_length}, length returned".strlen($result)); 140 | } 141 | 142 | // Set the new packs 143 | $packs[$cur_packet] = $result; 144 | } else { 145 | 146 | // Gold source does things a bit different 147 | if(!$this->source_engine) { 148 | $packet_number = $buffer->readInt8(); 149 | } else { 150 | $packet_number = $buffer->readInt16Signed(); 151 | $split_length = $buffer->readInt16Signed(); 152 | } 153 | 154 | // Now add the rest of the packet to the new array with the packet_number as the id so we can order it 155 | $packs[$packet_number] = $buffer->getBuffer(); 156 | } 157 | 158 | unset($buffer); 159 | } 160 | 161 | unset($packets, $packet); 162 | 163 | // Sort the packets by packet number 164 | ksort($packs); 165 | 166 | // Now combine the packs into one and return 167 | return implode("", $packs); 168 | } 169 | 170 | 171 | 172 | 173 | protected function _process_players($packets) { 174 | 175 | $packet = $this->_preparePackets($packets); 176 | 177 | if (!$packet) return false; 178 | 179 | $buf = new Buffer($packet); 180 | 181 | $header = $buf->read(5); 182 | if($header !== "\xFF\xFF\xFF\xFF\x44") { 183 | $this->debug("Data for ".__METHOD__." does not have the proper header (should be 0xFF0xFF0xFF0xFF0x44). Header: ".bin2hex($header)); 184 | return false; 185 | } 186 | 187 | // Pull out the number of players 188 | $num_players = $buf->readInt8(); 189 | 190 | $this->result->addGeneral('num_players', $num_players); 191 | 192 | // No players so no need to look any further 193 | if($num_players === 0) { 194 | return; 195 | } 196 | 197 | $players = array(); 198 | $players_times = array(); 199 | 200 | /* 201 | Detecting bots by name is a bad idea. 202 | But bots usually have the same 'time', which is max. 203 | So we stupidly mark players with the maximum time as bots. 204 | */ 205 | 206 | // Players list 207 | while ($buf->getLength()) { 208 | $id = $buf->readInt8(); 209 | $name = $buf->readString(); 210 | $score = $buf->readInt32Signed(); 211 | $time = $buf->readFloat32(); 212 | 213 | $players []= array( 214 | 'name' => $name, 215 | 'score' => $score, 216 | 'bot' => false, 217 | 'time' => $time, 218 | ); 219 | 220 | $players_times []= array( 221 | 'pos' => count($players) - 1, 222 | 'time' => $time, 223 | ); 224 | 225 | } 226 | 227 | $bots_count = $this->result->getGeneral('bot_players'); 228 | 229 | if ($bots_count) { 230 | usort($players_times, function ($a, $b) { 231 | if ($a['time'] == $b['time']) 232 | return 0; 233 | 234 | return ($b['time'] < $a['time']) ? -1 : 1; 235 | }); 236 | 237 | 238 | $i = 0; 239 | foreach($players_times as $r) { 240 | if ($i >= $bots_count) 241 | break; 242 | 243 | $players[$r['pos']]['bot'] = true; 244 | 245 | $i++; 246 | } 247 | } 248 | 249 | foreach($players as $player) { 250 | $this->result->addPlayer($player['name'], $player['score'], null, array('time' => gmdate("H:i:s", $player['time'])), $player['bot']); 251 | } 252 | } 253 | 254 | protected function _put_var($key, $val) { 255 | $this->result->addSetting($key, $val); 256 | } 257 | 258 | protected function _parse_rules(&$packet) { 259 | $buf = new Buffer($packet); 260 | 261 | $header = $buf->read(5); 262 | if($header !== "\xFF\xFF\xFF\xFF\x45") { 263 | $this->debug("Data for ".__METHOD__." does not have the proper header (should be 0xFF0xFF0xFF0xFF0x45). Header: ".bin2hex($header)); 264 | return false; 265 | } 266 | 267 | // number of rules 268 | $buf->readInt16Signed(); 269 | 270 | 271 | // We can tell it is dm (it's 90%), but lets try to be honest and report only trustful info. 272 | $m = false; 273 | 274 | while ($buf->getLength()) { 275 | $key = $buf->readString(); 276 | $val = $buf->readString(); 277 | 278 | $val = $this->filterInt($val); 279 | 280 | // I found only one game that reports its gamemode - tf2. l4d`s are stupid. 281 | switch($key) { 282 | case 'tf_gamemode_arena': if ($val == 1) $m = 'arena'; break; 283 | case 'tf_gamemode_cp': if ($val == 1) $m = 'cp'; break; 284 | case 'tf_gamemode_ctf': if ($val == 1) $m = 'ctf'; break; 285 | case 'tf_gamemode_mvm': if ($val == 1) $m = 'mvm'; break; 286 | case 'tf_gamemode_payload': if ($val == 1) $m = 'payload'; break; 287 | case 'tf_gamemode_sd': if ($val == 1) $m = 'sd'; break; 288 | } 289 | 290 | $this->_put_var($key, $val); 291 | } 292 | 293 | if ($m !== false) 294 | $this->result->addGeneral('mode', $m); 295 | } 296 | 297 | 298 | protected function _process_rules($packets) { 299 | 300 | $packet = $this->_preparePackets($packets); 301 | 302 | if (!$packet) return false; 303 | 304 | $this->_parse_rules($packet); 305 | 306 | } 307 | 308 | protected function _detectMode($game_description, $appid) { 309 | 310 | } 311 | 312 | protected function _parseDetailsExtension(&$buf, $appid) { 313 | 314 | } 315 | 316 | 317 | protected function _process_details($packets) { 318 | // A2S_INFO is not splitted 319 | 320 | // All info is here: https://developer.valvesoftware.com/wiki/Server_Queries 321 | 322 | // Goldsource sends two packets - oldstyle and newstyle. We don't care what type to use 323 | $data = $packets[0]; 324 | 325 | $buf = new Buffer($data); 326 | 327 | $head = $buf->read(4); 328 | 329 | if ($head !== "\xFF\xFF\xFF\xFF") { 330 | $this->debug("Wrong header"); 331 | return false; 332 | } 333 | 334 | // Get the type 335 | $type = $buf->read(1); 336 | 337 | // Goldsource type 338 | if ($type === "\x6d") { 339 | $this->source_engine = false; 340 | } else 341 | if ($type === "\x49" || $type === "\x44") { // 0x44? wtf? 342 | $this->source_engine = true; 343 | } else { 344 | $this->debug("Data for ".__METHOD__." does not have the proper header type (should be 0x49|0x44|0x6d). Header type: 0x".bin2hex($type)); 345 | return false; 346 | } 347 | 348 | 349 | // Check engine type 350 | if (!$this->source_engine) { 351 | // address 352 | $buf->readString(); 353 | } else { 354 | // protocol 355 | $buf->readInt8(); 356 | } 357 | 358 | $this->result->addGeneral('hostname', $buf->readString()); 359 | 360 | $this->result->addGeneral('map', $buf->readString()); 361 | 362 | // Sometimes those names are changeg. Aware them. 363 | $game_directory = $buf->readString(); 364 | $game_description = $buf->readString(); 365 | 366 | $this->result->addSetting('game_directory', $game_directory); 367 | $this->result->addSetting('game_description', $game_description); 368 | 369 | 370 | // Check engine type 371 | if ($this->source_engine) { 372 | $this->appid = $buf->readInt16(); 373 | $this->result->addInfo('app_id', $this->appid); 374 | 375 | $this->_detectMode($game_description, $this->appid); 376 | } 377 | 378 | $this->result->addGeneral('num_players', $buf->readInt8()); 379 | $this->result->addGeneral('max_players', $buf->readInt8()); 380 | 381 | 382 | if (!$this->source_engine) { 383 | $this->result->addGeneral('version', $buf->readInt8()); 384 | } else { 385 | $this->result->addGeneral('bot_players', $buf->readInt8()); 386 | } 387 | 388 | //$this->result->addSettings('dedicated', $buf->read()); 389 | //$this->result->addSettings('os', $buf->read()); 390 | 391 | // dedicated 392 | $d = strtolower($buf->read()); 393 | switch($d) { 394 | case 'l': $ds = "Listen"; break; 395 | case 'p': $ds = "HLTV"; break; 396 | default: $ds = "Dedicated"; 397 | } 398 | $this->result->addSetting('server_type', $ds); 399 | 400 | // os 401 | $d = strtolower($buf->read()); 402 | switch($d) { 403 | case 'w': $ds = "Windows"; break; 404 | default: $ds = "Linux"; 405 | } 406 | $this->result->addSetting('os', $ds); 407 | 408 | $this->result->addGeneral('password', ($buf->readInt8() == 1)); 409 | 410 | 411 | if (!$this->source_engine) { 412 | // is HL mod 413 | $is_mod = ($buf->readInt8() == 1); 414 | 415 | if ($is_mod) { 416 | $this->result->addSetting('hlmod_link', $buf->readString()); 417 | $this->result->addSetting('hlmod_url', $buf->readString()); 418 | 419 | // null byte 420 | $buf->read(); 421 | 422 | $this->result->addSetting('hlmod_version', $buf->readInt32Signed()); 423 | $this->result->addSetting('hlmod_size', $buf->readInt32Signed()); 424 | 425 | $this->result->addSetting('hlmod_mp_only', $buf->readInt8()); 426 | $this->result->addSetting('hlmod_own_dll', $buf->readInt8()); 427 | } 428 | } 429 | 430 | $this->result->addGeneral('secure', ($buf->readInt8() == 1)); 431 | 432 | if (!$this->source_engine) { 433 | $this->result->addGeneral('bot_players', $buf->readInt8()); 434 | } else { 435 | $this->_parseDetailsExtension($buf, $this->appid); 436 | 437 | $this->result->addGeneral('version', $buf->readString()); 438 | 439 | 440 | // EDF 441 | } 442 | 443 | unset($buf); 444 | 445 | } 446 | 447 | } -------------------------------------------------------------------------------- /gameq3/gameq3.php: -------------------------------------------------------------------------------- 1 | . 17 | * 18 | * 19 | */ 20 | 21 | /** 22 | * GameQ3 23 | * 24 | * This is a library to query gameservers and return their answer in universal well-formatted array. 25 | * 26 | * This library influenced by GameQv1 (author Tom Buskens ) 27 | * and GameQv2 (url https://github.com/Austinb/GameQ, author Austin Bischoff ) 28 | * 29 | * @author Kostya Esmukov 30 | */ 31 | 32 | namespace GameQ3; 33 | 34 | // Autoload classes 35 | spl_autoload_extensions(".php"); 36 | spl_autoload_register(); 37 | 38 | set_include_path(get_include_path() . PATH_SEPARATOR . realpath(dirname(__FILE__). '/../')); 39 | 40 | 41 | class GameQ3 { 42 | 43 | // Config 44 | private $servers_count = 2500; 45 | 46 | // Working vars 47 | private $sock = null; 48 | private $log = null; 49 | private $filters = array(); 50 | private $servers_filters = array(); 51 | private $servers = array(); 52 | 53 | private $started = false; 54 | private $request_servers = array(); 55 | 56 | public function __construct() { 57 | $this->log = new Log(); 58 | $this->sock = new Sockets($this->log); 59 | } 60 | 61 | /** 62 | * Set logging rules. 63 | * Logger uses error_log() by default. This can be changed using GameQ3::setLogger() 64 | * @param bool $error Log errors or not 65 | * @param bool $warning Log warnings or not 66 | * @param bool $debug Log debug messages or not 67 | * @param bool $trace Log backtrace or not 68 | */ 69 | public function setLogLevel($error, $warning = true, $debug = false, $trace = false) { 70 | $this->log->setLogLevel($error, $warning, $debug, $trace); 71 | } 72 | 73 | /** 74 | * Set logger function. 75 | * @param callable $callback function($msg) 76 | * @throws UserException 77 | */ 78 | public function setLogger($callback) { 79 | if (is_callable($callback)) 80 | $this->log->setLogger($callback); 81 | else 82 | throw new UserException("Argument for setLogger must be callable"); 83 | } 84 | 85 | private function _getsetOption($set, $key, $value = null) { 86 | $error = false; 87 | 88 | switch($key) { 89 | case 'servers_count': 90 | if ($set) { 91 | if (is_int($value)) 92 | $this->$key = $value; 93 | else 94 | $error = 'int'; 95 | } else { 96 | return $this->$key; 97 | } 98 | break; 99 | 100 | default: 101 | return $this->sock->getsetOption($set, $key, $value); 102 | } 103 | 104 | if ($error !== false) 105 | throw new UserException("Value for setOption must be " . $error . ". Got value: " . var_export($value, true)); 106 | 107 | return true; 108 | } 109 | 110 | /** 111 | * Set option. See readme for a list of options. 112 | * @param string $key 113 | * @param mixed $value 114 | * @throws UserException 115 | */ 116 | public function setOption($key, $value) { 117 | if ($this->started) 118 | throw new UserException("You cannot set options while in request"); 119 | return $this->_getsetOption(true, $key, $value); 120 | } 121 | 122 | public function __set($key, $value) { 123 | return $this->setOption($key, $value); 124 | } 125 | 126 | public function __get($key) { 127 | return $this->_getsetOption(false, $key); 128 | } 129 | 130 | /** 131 | * Set filter. See readme for a list of filters 132 | * @param string $name Filter name 133 | * @param array $args Filter options 134 | * @throws UserException 135 | */ 136 | public function setFilter($name, $args = array()) { 137 | if ($this->started) 138 | throw new UserException("You cannot set filter while in request"); 139 | 140 | if (!is_array($args)) 141 | throw new UserException("Args must be an array in setFilter (name '" . $name . "')"); 142 | 143 | $this->filters[$name] = $args; 144 | } 145 | 146 | /** 147 | * Unset filter. 148 | * @param string $name Filter name 149 | * @throws UserException 150 | */ 151 | public function unsetFilter($name) { 152 | if ($this->started) 153 | throw new UserException("You cannot unset filter while in request"); 154 | 155 | unset($this->filters[$name]); 156 | } 157 | 158 | /** 159 | * Returns information about protocol. 160 | * @param string $protocol 161 | * @throws UserException 162 | * @return array 163 | */ 164 | public function getProtocolInfo($protocol) { 165 | if (!is_string($protocol)) 166 | throw new UserException("Protocol must be a string"); 167 | 168 | $className = "\\GameQ3\\Protocols\\". ucfirst(strtolower($protocol)); 169 | 170 | $reflection = new \ReflectionClass($className); 171 | 172 | if(!$reflection->IsInstantiable()) { 173 | return false; 174 | } 175 | 176 | $dp = $reflection->getDefaultProperties(); 177 | $dc = $reflection->getConstants(); 178 | $pt_string = 'UNKNOWN'; 179 | 180 | foreach($dc as $name => $val) { 181 | // filter out non-PT constants 182 | if (substr($name, 0, 3) !== "PT_") continue; 183 | 184 | if ($val === $dp['ports_type']) { 185 | $pt_string = substr($name, 3); 186 | break; 187 | } 188 | } 189 | 190 | $res = array( 191 | 'protocol' => $dp['protocol'], 192 | 'name' => $dp['name'], 193 | 'name_long' => $dp['name_long'], 194 | 'query_port' => (is_int($dp['query_port']) ? $dp['query_port'] : null), 195 | 'connect_port' => (is_int($dp['connect_port']) ? $dp['connect_port'] : ($pt_string === 'SAME' ? $dp['query_port'] : null)), // connect_port shouldn't be set in PT_SAME 196 | 'ports_type' => $dp['ports_type'], 197 | 'ports_type_string' => $pt_string, 198 | 'ports_type_info' => array( 199 | 'connect_port' => true, 200 | 'query_port' => ($pt_string !== 'SAME'), // Only PT_SAME ignores query port 201 | ), 202 | 'network' => $dp['network'], 203 | 'connect_string' => (is_string($dp['connect_string']) ? $dp['connect_string'] : null), 204 | ); 205 | 206 | unset($reflection); 207 | 208 | return $res; 209 | } 210 | 211 | /** 212 | * Returns array of info for each protocol 213 | * @see GameQ3::getProtocolInfo() 214 | * @return array 215 | */ 216 | public function getAllProtocolsInfo() { 217 | $protocols_path = dirname(__FILE__) . "/protocols/"; 218 | 219 | $dir = dir($protocols_path); 220 | $protocols = array(); 221 | 222 | while (true) { 223 | $entry = $dir->read(); 224 | if ($entry === false) break; 225 | 226 | if(!is_file($protocols_path.$entry)) { 227 | continue; 228 | } 229 | 230 | $protocol = pathinfo($entry, PATHINFO_FILENAME); 231 | 232 | $res = $this->getProtocolInfo($protocol); 233 | 234 | if (!empty($res)) 235 | $protocols[strtolower($protocol)] = $res; 236 | } 237 | 238 | unset($dir); 239 | 240 | ksort($protocols); 241 | 242 | return $protocols; 243 | } 244 | 245 | /** 246 | * Add a server to be queried 247 | * @param array $server_info 248 | * @throws UserException 249 | */ 250 | public function addServer($server_info) { 251 | if ($this->started) 252 | throw new UserException("You cannot add servers while in request"); 253 | 254 | if (!is_array($server_info)) 255 | throw new UserException("Server_info must be an array"); 256 | 257 | if (!isset($server_info['type']) || !is_string($server_info['type'])) { 258 | throw new UserException("Missing server info key 'type'"); 259 | } 260 | 261 | if (!isset($server_info['id']) || (!is_string($server_info['id']) && !is_numeric($server_info['id']))) { 262 | throw new UserException("Missing server info key 'id'"); 263 | } 264 | 265 | // already added 266 | if (isset($this->servers[ $server_info['id'] ])) 267 | return; 268 | 269 | if (!empty($server_info['filters'])) { 270 | if (!is_array($server_info['filters'])) 271 | throw new UserException("Server info key 'filters' must be an array"); 272 | 273 | $this->servers_filters[ $server_info['id'] ] = array(); 274 | // check filters array 275 | foreach($server_info['filters'] as $filter => &$args) { 276 | if ($args !== false && !is_array($args)) 277 | throw new UserException("Filter arguments must be an array or boolean false"); 278 | $this->servers_filters[ $server_info['id'] ][ $filter ] = $args; 279 | } 280 | 281 | unset($server_info['filters']); 282 | } 283 | 284 | $protocol_class = "\\GameQ3\\Protocols\\".ucfirst($server_info['type']); 285 | 286 | try { 287 | if (!class_exists($protocol_class, true)) // PHP 5.3 288 | throw new UserException("Class " . $protocol_class . " could not be loaded"); 289 | $this->servers[ $server_info['id'] ] = new $protocol_class($server_info, $this->log); 290 | } 291 | catch(\LogicException $e) { // Class not found PHP 5.4 292 | throw new UserException($e->getMessage()); 293 | } 294 | } 295 | 296 | /** 297 | * Unset server from list of serers to query 298 | * @param string $id Server id 299 | * @throws UserException 300 | */ 301 | public function unsetServer($id) { 302 | if ($this->started) 303 | throw new UserException("You cannot unset servers while in request"); 304 | 305 | unset($this->servers[$id]); 306 | } 307 | 308 | // addServers removed because you have to decide what to do when exception occurs. This function does not handle them. 309 | 310 | private function _clear() { 311 | //$this->filters = array(); 312 | //$this->servers_filters = array(); 313 | //$this->servers = array(); 314 | $this->started = false; 315 | $this->request_servers = array(); 316 | } 317 | 318 | /** 319 | * Request added servers and return all responses 320 | * @return array 321 | */ 322 | public function requestAllData() { 323 | $result = array(); 324 | while (true) { 325 | $res = $this->_request(); 326 | if ($res === false || !is_array($res)) 327 | break; 328 | 329 | // I hate array_merge. It's like a blackbox. 330 | foreach($res as $key => $val) { 331 | $result[$key] = $val; 332 | unset($res[$key]); 333 | } 334 | } 335 | return $result; 336 | } 337 | 338 | // returns array until we have servers to reqest. otherwise returns false 339 | 340 | /** 341 | * Request added servers and return responses by parts. Returns false when there are no responses left. 342 | * @return mixed array or false 343 | */ 344 | public function requestPartData() { 345 | return $this->_request(); 346 | } 347 | 348 | private function _applyFilters($key, &$result) { 349 | $sf = (isset($this->servers_filters[$key]) ? $this->servers_filters[$key] : array()); 350 | foreach($this->filters as $name => $args) { 351 | if (isset($sf[$name])) { 352 | $args = $sf[$name]; 353 | unset($sf[$name]); 354 | if ($args === false) continue; 355 | } 356 | $filt = "\\GameQ3\\Filters\\".ucfirst($name); 357 | 358 | try { 359 | class_exists($filt, true); // try to load class 360 | call_user_func_array($filt . "::filter", array( &$result, $args )); 361 | } 362 | catch(\Exception $e) { 363 | $this->log->warning($e); 364 | } 365 | } 366 | 367 | foreach($sf as $name => $args) { 368 | if ($args === false) continue; 369 | 370 | $filt = "\\GameQ3\\Filters\\".ucfirst($name); 371 | 372 | try { 373 | class_exists($filt, true); // try to load class 374 | call_user_func_array($filt . "::filter", array( &$result, $args )); 375 | } 376 | catch(\Exception $e) { 377 | $this->log->warning($e); 378 | } 379 | } 380 | } 381 | 382 | private function _request() { 383 | if (!$this->started) { 384 | $this->started = true; 385 | // \/ 386 | foreach($this->servers as &$instance) { 387 | try { 388 | $instance->protocolInit(); 389 | } 390 | catch (\Exception $e) { 391 | $this->log->warning($e); 392 | } 393 | } 394 | // /\ memory allocated 14649/5000=3 kb, 152/50=3 kb 395 | 396 | $this->request_servers = $this->servers; 397 | } 398 | 399 | if (empty($this->request_servers)) { 400 | $this->started = false; 401 | $this->_clear(); 402 | return false; 403 | } 404 | 405 | $servers_left = array(); 406 | $servers_queried = array(); 407 | 408 | $s_cnt = 1; 409 | foreach($this->request_servers as $server_id => &$instance) { 410 | $servers_left[$server_id] = $instance; 411 | $servers_queried[$server_id] = $instance; 412 | unset($this->request_servers[$server_id]); 413 | 414 | $s_cnt++; 415 | if ($s_cnt > $this->servers_count) 416 | break; 417 | } 418 | 419 | $process = array(); 420 | 421 | while (true) { 422 | if (empty($servers_left)) break; 423 | 424 | $final_process = true; 425 | 426 | foreach($servers_left as $server_id => &$instance) { 427 | try { 428 | $instance_queue = $instance->popRequests(); 429 | 430 | if (empty($instance_queue)) { 431 | unset($servers_left[$server_id]); 432 | continue; 433 | } 434 | 435 | $final_process = false; 436 | 437 | foreach($instance_queue as $queue_id => &$queue_qopts) { 438 | $sid = $this->sock->allocateSocket($server_id, $queue_id, $queue_qopts); 439 | $process[$sid] = array( 440 | 'id' => $queue_id, 441 | 'i' => $instance 442 | ); 443 | } 444 | } 445 | catch (SocketsException $e) { // not resolvable hostname, etc 446 | $this->log->debug($e); 447 | } 448 | catch (\Exception $e) { // wrong input data 449 | $this->log->warning($e); 450 | } 451 | } 452 | 453 | if ($final_process) { 454 | $response = $this->sock->finalProcess(); 455 | if (empty($response)) break; 456 | } else { 457 | $response = $this->sock->process(); 458 | } 459 | 460 | foreach($response as $sid => $ra) { 461 | if (empty($ra['p']) || !isset($process[$sid])) continue; 462 | 463 | try { // Protocols should handle exceptions by themselves 464 | $process[$sid]['i']->startRequestProcessing( 465 | $process[$sid]['id'], 466 | array( 467 | 'ping' => $ra['pg'], 468 | 'retry_cnt' => ($ra['t']-1), 469 | 'responses' => $ra['p'], 470 | 'socket_recreated' => $ra['sr'], 471 | 'info' => $ra['i'], 472 | ) 473 | ); 474 | } 475 | catch(\Exception $e) { 476 | $this->log->debug($e); 477 | } 478 | unset($response[$sid]); 479 | unset($process[$sid]); 480 | } 481 | } 482 | 483 | $this->sock->cleanUp(); 484 | 485 | $result = array(); 486 | foreach($servers_queried as $key => &$instance) { 487 | try { 488 | $instance->startPreFetch(); 489 | } 490 | catch(\Exception $e) { 491 | $this->log->debug($e); 492 | } 493 | $result[$key] = $instance->resultFetch(); 494 | $this->_applyFilters($key, $result[$key]); 495 | } 496 | 497 | return $result; 498 | } 499 | } 500 | 501 | class UserException extends \Exception {} --------------------------------------------------------------------------------