├── 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 | | GameQ3 identifier |
101 | Game name |
102 | Short game name |
103 | Protocol |
104 | Query port |
105 | Connect port |
106 | Ports type |
107 | Connect URL |
108 |
109 |
110 |
111 |
112 |
113 |
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| Group | Variable | Value |
\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| %s | %s | %s |
\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| %s | %s | %s |
\n", $cls, $grouph, $key, var_export($val, true));
128 | }
129 | }
130 |
131 | echo "\t\t\n\t\t
\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 |
137 |
138 |
139 |
140 |
141 |
142 | snowman781,
,
JakBot,
-Jarek-,
eduardo579,
Devils lil Helper,
jirkahrb,
teme10,
HighDriver,
kmalos,
ldlian987,
CsK,
Moslow,
delix_plus710
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 {}
--------------------------------------------------------------------------------