diff --git a/src/GameQ/Protocols/Quake2.php b/src/GameQ/Protocols/Quake2.php new file mode 100644 index 00000000..f0366c2c --- /dev/null +++ b/src/GameQ/Protocols/Quake2.php @@ -0,0 +1,219 @@ + "\xFF\xFF\xFF\xFFstatus\x00", + ]; + + /** + * Use the response flag to figure out what method to run + * + * @type array + */ + protected $responses = [ + "\xFF\xFF\xFF\xFF\x70\x72\x69\x6e\x74" => 'processStatus', + ]; + + /** + * The query protocol used to make the call + * + * @type string + */ + protected $protocol = 'quake2'; + + /** + * String name of this protocol class + * + * @type string + */ + protected $name = 'quake2'; + + /** + * Longer string name of this protocol class + * + * @type string + */ + protected $name_long = "Quake 2 Server"; + + /** + * The client join link + * + * @type string + */ + protected $join_link = null; + + /** + * Normalize settings for this protocol + * + * @type array + */ + protected $normalize = [ + // General + 'general' => [ + // target => source + 'gametype' => 'gamename', + 'hostname' => 'hostname', + 'mapname' => 'mapname', + 'maxplayers' => 'maxclients', + 'mod' => 'g_gametype', + 'numplayers' => 'clients', + 'password' => 'password', + ], + // Individual + 'player' => [ + 'name' => 'name', + 'ping' => 'ping', + 'score' => 'frags', + ], + ]; + + /** + * Handle response from the server + * + * @return mixed + * @throws Exception + */ + public function processResponse() + { + // Make a buffer + $buffer = new Buffer(implode('', $this->packets_response)); + + // Grab the header + $header = $buffer->readString("\x0A"); + + // Figure out which packet response this is + if (empty($header) || !array_key_exists($header, $this->responses)) { + throw new Exception(__METHOD__ . " response type '" . bin2hex($header) . "' is not valid"); + } + + return call_user_func_array([$this, $this->responses[$header]], [$buffer]); + } + + /** + * Process the status response + * + * @param Buffer $buffer + * + * @return array + */ + protected function processStatus(Buffer $buffer) + { + // We need to split the data and offload + $results = $this->processServerInfo(new Buffer($buffer->readString("\x0A"))); + + $results = array_merge_recursive( + $results, + $this->processPlayers(new Buffer($buffer->getBuffer())) + ); + + unset($buffer); + + // Return results + return $results; + } + + /** + * Handle processing the server information + * + * @param Buffer $buffer + * + * @return array + */ + protected function processServerInfo(Buffer $buffer) + { + // Set the result to a new result instance + $result = new Result(); + + // Burn leading \ if one exists + $buffer->readString('\\'); + + // Key / value pairs + while ($buffer->getLength()) { + // Add result + $result->add( + trim($buffer->readString('\\')), + utf8_encode(trim($buffer->readStringMulti(['\\', "\x0a"]))) + ); + } + + $result->add('password', 0); + $result->add('mod', 0); + + unset($buffer); + + return $result->fetch(); + } + + /** + * Handle processing of player data + * + * @param Buffer $buffer + * + * @return array + */ + protected function processPlayers(Buffer $buffer) + { + // Some games do not have a number of current players + $playerCount = 0; + + // Set the result to a new result instance + $result = new Result(); + + // Loop until we are out of data + while ($buffer->getLength()) { + // Make a new buffer with this block + $playerInfo = new Buffer($buffer->readString("\x0A")); + + // Add player info + $result->addPlayer('frags', $playerInfo->readString("\x20")); + $result->addPlayer('ping', $playerInfo->readString("\x20")); + + // Skip first " + $playerInfo->skip(1); + + // Add player name, encoded + $result->addPlayer('name', utf8_encode(trim(($playerInfo->readString('"'))))); + + // Skip first " + $playerInfo->skip(2); + + // Add address + $result->addPlayer('address', trim($playerInfo->readString('"'))); + + // Increment + $playerCount++; + + // Clear + unset($playerInfo); + } + + $result->add('clients', $playerCount); + + // Clear + unset($buffer, $playerCount); + + return $result->fetch(); + } +} diff --git a/tests/Protocols/Providers/Quake2/1_response.txt b/tests/Protocols/Providers/Quake2/1_response.txt new file mode 100644 index 00000000..2bdb3e22 --- /dev/null +++ b/tests/Protocols/Providers/Quake2/1_response.txt @@ -0,0 +1,13 @@ +˙˙˙˙print +\Q2Admin\1.17.44-tsmod-2\mapname\q2dm6\anticheat\1\sv_noobquad\0\gamedate\Aug 2 2009\gamename\baseq2\sv_handicap\1\maxspectators\8\INFO2\NO BOTS, HACKS, CHEATS PLEASE\INFO1\All Skill Levels Welcome\cheats\0\timelimit\15\fraglimit\30\dmflags\16404\deathmatch\1\version\R1Q2 b7864 i386 Oct 1 2008 Linux\hostname\tastyspleen.net::dm\maxclients\17 +0 2 "WallFly[BZZZ]" +2 162 "ninja" +3 196 "Richo de Lula" +22 52 "Fatal[KiD]" +15 25 "Anus Paste" +12 22 "hindmost" +29 59 "[A2W]H3mr0yd" +4 159 "nx0" +26 40 "0:0" +18 41 "mag" +9 71 "mind" diff --git a/tests/Protocols/Providers/Quake2/1_result.json b/tests/Protocols/Providers/Quake2/1_result.json new file mode 100644 index 00000000..09813a08 --- /dev/null +++ b/tests/Protocols/Providers/Quake2/1_result.json @@ -0,0 +1 @@ +{"67.228.69.114:27916":{"INFO1":"All Skill Levels Welcome","INFO2":"NO BOTS, HACKS, CHEATS PLEASE","Q2Admin":"1.17.44-tsmod-2","anticheat":"1","cheats":"0","clients":11,"deathmatch":"1","dmflags":"16404","fraglimit":"30","gamedate":"Aug 2 2009","gamename":"baseq2","gq_address":"67.228.69.114","gq_joinlink":"","gq_name":"Quake 2 Server","gq_online":true,"gq_port_client":27916,"gq_port_query":27916,"gq_protocol":"quake2","gq_transport":"udp","gq_type":"quake2","hostname":"tastyspleen.net::dm","mapname":"q2dm6","maxclients":"17","maxspectators":"8","mod":0,"password":0,"players":[{"frags":"0","ping":"2","name":"WallFly[BZZZ]","address":""},{"frags":"2","ping":"162","name":"ninja","address":""},{"frags":"3","ping":"196","name":"Richo de Lula","address":""},{"frags":"22","ping":"52","name":"Fatal[KiD]","address":""},{"frags":"15","ping":"25","name":"Anus Paste","address":""},{"frags":"12","ping":"22","name":"hindmost","address":""},{"frags":"29","ping":"59","name":"[A2W]H3mr0yd","address":""},{"frags":"4","ping":"159","name":"nx0","address":""},{"frags":"26","ping":"40","name":"0:0","address":""},{"frags":"18","ping":"41","name":"mag","address":""},{"frags":"9","ping":"71","name":"mind","address":""}],"sv_handicap":"1","sv_noobquad":"0","timelimit":"15","version":"R1Q2 b7864 i386 Oct 1 2008 Linux"}} \ No newline at end of file diff --git a/tests/Protocols/Providers/Quake2/2_response.txt b/tests/Protocols/Providers/Quake2/2_response.txt new file mode 100644 index 00000000..f460deb8 --- /dev/null +++ b/tests/Protocols/Providers/Quake2/2_response.txt @@ -0,0 +1,12 @@ +˙˙˙˙print +\mapname\q2dm1\ngWorldStats_Status\<<< Disabled >>>\anticheat\1\match_info\Runes, Hook, Protection\match_type\RegularDM\time_remaining\17:28\gamedate\Jul 1 1999\gamename\baseq2 \cheats\0\timelimit\20\fraglimit\0\dmflags\17940\version\R1Q2 b7864 i386 Oct 1 2008 Linux\hostname\Clanworld's Lithium-DM #1\deathmatch\1\gamedir\ospl2\port\27910\game\ospl2\maxclients\32 +0 15 "nikon" +0 16 "Ulrik" +0 16 "bubbleuniverse" +0 18 "Centriot" +0 17 "AL1AS" +0 17 "Oilver" +0 17 "snappy" +16 55 "4833" +0 17 "rackor" +0 66 "Player" diff --git a/tests/Protocols/Providers/Quake2/2_result.json b/tests/Protocols/Providers/Quake2/2_result.json new file mode 100644 index 00000000..9db8cff3 --- /dev/null +++ b/tests/Protocols/Providers/Quake2/2_result.json @@ -0,0 +1 @@ +{"77.66.46.219:27910":{"anticheat":"1","cheats":"0","clients":10,"deathmatch":"1","dmflags":"17940","fraglimit":"0","game":"ospl2","gamedate":"Jul 1 1999","gamedir":"ospl2","gamename":"baseq2","gq_address":"77.66.46.219","gq_joinlink":"","gq_name":"Quake 2 Server","gq_online":true,"gq_port_client":27910,"gq_port_query":27910,"gq_protocol":"quake2","gq_transport":"udp","gq_type":"quake2","hostname":"Clanworld's Lithium-DM #1","mapname":"q2dm1","match_info":"Runes, Hook, Protection","match_type":"RegularDM","maxclients":"32","mod":0,"ngWorldStats_Status":"<<< Disabled >>>","password":0,"players":[{"frags":"0","ping":"15","name":"nikon","address":""},{"frags":"0","ping":"16","name":"Ulrik","address":""},{"frags":"0","ping":"16","name":"bubbleuniverse","address":""},{"frags":"0","ping":"18","name":"Centriot","address":""},{"frags":"0","ping":"17","name":"AL1AS","address":""},{"frags":"0","ping":"17","name":"Oilver","address":""},{"frags":"0","ping":"17","name":"snappy","address":""},{"frags":"16","ping":"55","name":"4833","address":""},{"frags":"0","ping":"17","name":"rackor","address":""},{"frags":"0","ping":"66","name":"Player","address":""}],"port":"27910","time_remaining":"17:28","timelimit":"20","version":"R1Q2 b7864 i386 Oct 1 2008 Linux"}} \ No newline at end of file diff --git a/tests/Protocols/Providers/Quake2/3_response.txt b/tests/Protocols/Providers/Quake2/3_response.txt new file mode 100644 index 00000000..0d3971cf --- /dev/null +++ b/tests/Protocols/Providers/Quake2/3_response.txt @@ -0,0 +1,2 @@ +˙˙˙˙print +\Score_A\WARMUP\Score_B\WARMUP\anticheat\1\cheats\0\deathmatch\1\dmflags\1040\fraglimit\0\game\opentdm\gamedate\Feb 9 2013\gamedir\opentdm\gamename\OpenTDM\hostname\PlayGround.ru - Teamplay\mapname\q2dm1\match_type\TDM\maxclients\12\port\27911\protocol\34\revision\22\time_remaining\WARMUP\timelimit\0\version\q2proded r1504~924ff39 Dec 3 2014 Linux i386 diff --git a/tests/Protocols/Providers/Quake2/3_result.json b/tests/Protocols/Providers/Quake2/3_result.json new file mode 100644 index 00000000..d5134f60 --- /dev/null +++ b/tests/Protocols/Providers/Quake2/3_result.json @@ -0,0 +1 @@ +{"212.42.38.88:27911":{"Score_A":"WARMUP","Score_B":"WARMUP","anticheat":"1","cheats":"0","clients":0,"deathmatch":"1","dmflags":"1040","fraglimit":"0","game":"opentdm","gamedate":"Feb 9 2013","gamedir":"opentdm","gamename":"OpenTDM","gq_address":"212.42.38.88","gq_joinlink":"","gq_name":"Quake 2 Server","gq_online":true,"gq_port_client":27911,"gq_port_query":27911,"gq_protocol":"quake2","gq_transport":"udp","gq_type":"quake2","hostname":"PlayGround.ru - Teamplay","mapname":"q2dm1","match_type":"TDM","maxclients":"12","mod":0,"password":0,"port":"27911","protocol":"34","revision":"22","time_remaining":"WARMUP","timelimit":"0","version":"q2proded r1504~924ff39 Dec 3 2014 Linux i386"}} \ No newline at end of file diff --git a/tests/Protocols/Quake2.php b/tests/Protocols/Quake2.php new file mode 100644 index 00000000..65cbda38 --- /dev/null +++ b/tests/Protocols/Quake2.php @@ -0,0 +1,120 @@ +. + */ + +namespace GameQ\Tests\Protocols; + +class Quake2 extends Base +{ + + /** + * Holds stub on setup + * + * @type \GameQ\Protocols\Quake2 + */ + protected $stub; + + /** + * Holds the expected packets for this protocol class + * + * @type array + */ + protected $packets = [ + \GameQ\Protocol::PACKET_STATUS => "\xFF\xFF\xFF\xFFstatus\x00", + ]; + + /** + * Setup + */ + public function setUp() + { + + // Create the stub class + $this->stub = $this->getMock('\GameQ\Protocols\Quake2', null, [[]]); + } + + /** + * Test the packets to make sure they are correct for source + */ + public function testPackets() + { + + // Test to make sure packets are defined properly + $this->assertEquals($this->packets, \PHPUnit_Framework_Assert::readAttribute($this->stub, 'packets')); + } + + /** + * Test invalid packet type without debug + */ + public function testInvalidPacketType() + { + + // Read in a quake 2 source file + $source = file_get_contents(sprintf('%s/Providers/Quake2/1_response.txt', __DIR__)); + + // Change the first packet to some unknown header + $source = str_replace("\xFF\xFF\xFF\xFFprint", "\xFF\xFF\xFF\xFFprints", $source); + + // Should show up as offline + $testResult = $this->queryTest('127.0.0.1:27910', 'quake2', explode(PHP_EOL . '||' . PHP_EOL, $source), false); + + $this->assertFalse($testResult['gq_online']); + } + + /** + * Test for invalid packet type in response + * + * @expectedException Exception + * @expectedExceptionMessage GameQ\Protocols\Quake2::processResponse response type + * 'ffffffff7072696e7473' is not valid + */ + public function testInvalidPacketTypeDebug() + { + + // Read in a quake 2 source file + $source = file_get_contents(sprintf('%s/Providers/Quake2/1_response.txt', __DIR__)); + + // Change the first packet to some unknown header + $source = str_replace("\xFF\xFF\xFF\xFFprint", "\xFF\xFF\xFF\xFFprints", $source); + + // Should show up as offline + $this->queryTest('127.0.0.1:27910', 'quake2', explode(PHP_EOL . '||' . PHP_EOL, $source), true); + } + + /** + * Test responses for Quake3 + * + * @dataProvider loadData + * + * @param $responses + * @param $result + */ + public function testResponses($responses, $result) + { + + // Pull the first key off the array this is the server ip:port + $server = key($result); + + $testResult = $this->queryTest( + $server, + 'quake2', + $responses + ); + + $this->assertEquals($result[$server], $testResult); + } +} diff --git a/tests/Protocols/Quake3.php b/tests/Protocols/Quake3.php index b7777500..7082ee0a 100644 --- a/tests/Protocols/Quake3.php +++ b/tests/Protocols/Quake3.php @@ -63,7 +63,7 @@ public function testPackets() public function testInvalidPacketType() { - // Read in a ut2004 source file + // Read in a Quake 3 source file $source = file_get_contents(sprintf('%s/Providers/Quake3/1_response.txt', __DIR__)); // Change the first packet to some unknown header @@ -85,7 +85,7 @@ public function testInvalidPacketType() public function testInvalidPacketTypeDebug() { - // Read in a ut2004 source file + // Read in a Quake 3 source file $source = file_get_contents(sprintf('%s/Providers/Quake3/1_response.txt', __DIR__)); // Change the first packet to some unknown header