Skip to content

Commit

Permalink
Added quake 2 base protocol with tests. Fixes #343.
Browse files Browse the repository at this point in the history
  • Loading branch information
Austinb committed Nov 26, 2016
1 parent 6cc2125 commit 03f2e5c
Show file tree
Hide file tree
Showing 9 changed files with 371 additions and 2 deletions.
219 changes: 219 additions & 0 deletions src/GameQ/Protocols/Quake2.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,219 @@
<?php


namespace GameQ\Protocols;

use GameQ\Protocol;
use GameQ\Buffer;
use GameQ\Result;
use GameQ\Exception\Protocol as Exception;

/**
* Quake2 Protocol Class
*
* Handles processing Quake 3 servers
*
* @package GameQ\Protocols
*/
class Quake2 extends Protocol
{
/**
* Array of packets we want to look up.
* Each key should correspond to a defined method in this or a parent class
*
* @type array
*/
protected $packets = [
self::PACKET_STATUS => "\xFF\xFF\xFF\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();
}
}
13 changes: 13 additions & 0 deletions tests/Protocols/Providers/Quake2/1_response.txt
Original file line number Diff line number Diff line change
@@ -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"
1 change: 1 addition & 0 deletions tests/Protocols/Providers/Quake2/1_result.json
Original file line number Diff line number Diff line change
@@ -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"}}
12 changes: 12 additions & 0 deletions tests/Protocols/Providers/Quake2/2_response.txt
Original file line number Diff line number Diff line change
@@ -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"
1 change: 1 addition & 0 deletions tests/Protocols/Providers/Quake2/2_result.json
Original file line number Diff line number Diff line change
@@ -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"}}
2 changes: 2 additions & 0 deletions tests/Protocols/Providers/Quake2/3_response.txt
Original file line number Diff line number Diff line change
@@ -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
1 change: 1 addition & 0 deletions tests/Protocols/Providers/Quake2/3_result.json
Original file line number Diff line number Diff line change
@@ -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"}}
120 changes: 120 additions & 0 deletions tests/Protocols/Quake2.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
<?php
/**
* This file is part of GameQ.
*
* GameQ is free software; you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* GameQ is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

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);
}
}
4 changes: 2 additions & 2 deletions tests/Protocols/Quake3.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down

0 comments on commit 03f2e5c

Please sign in to comment.