1
0
Fork 0
mirror of https://codeberg.org/demostf/api.git synced 2026-06-03 18:04:08 +02:00

split parser and add tests

This commit is contained in:
Robin Appelman 2017-04-08 23:25:33 +02:00
commit ae39548ddb
11 changed files with 1801 additions and 102 deletions

View file

@ -27,6 +27,7 @@ A seperate PostgreSQL database is required to run the image, the database detail
- DB_USERNAME=$database_user - DB_USERNAME=$database_user
- DB_PASSWORD=$database_password - DB_PASSWORD=$database_password
- BASE_HOST=$host - BASE_HOST=$host
- PARSER_URL=$parser_host // the full url for the demo parser's upload endpoint
## Installing ## Installing

View file

@ -1,4 +1,6 @@
<?php namespace Demostf\API\Demo; <?php declare(strict_types=1);
namespace Demostf\API\Demo;
/** /**
* HL2 demo metadata * HL2 demo metadata
@ -76,80 +78,47 @@ class Header {
$this->sigon = $info['sigon']; $this->sigon = $info['sigon'];
} }
/** public function getDuration(): float {
* @return float
*/
public function getDuration() {
return $this->duration; return $this->duration;
} }
/** public function getFrames(): int {
* @return int
*/
public function getFrames() {
return $this->frames; return $this->frames;
} }
/** public function getGame(): string {
* @return string
*/
public function getGame() {
return $this->game; return $this->game;
} }
/** public function getMap(): string {
* @return string
*/
public function getMap() {
return $this->map; return $this->map;
} }
/** public function getNick(): string {
* @return string
*/
public function getNick() {
return $this->nick; return $this->nick;
} }
/** public function getProtocol(): int {
* @return int
*/
public function getProtocol() {
return $this->protocol; return $this->protocol;
} }
/** public function getServer(): string {
* @return string
*/
public function getServer() {
return $this->server; return $this->server;
} }
/** public function getSigon(): int {
* @return int
*/
public function getSigon() {
return $this->sigon; return $this->sigon;
} }
/** public function getTicks(): int {
* @return int
*/
public function getTicks() {
return $this->ticks; return $this->ticks;
} }
/** public function getType(): string {
* @return string
*/
public function getType() {
return $this->type; return $this->type;
} }
/** public function getVersion(): int {
* @return int
*/
public function getVersion() {
return $this->version; return $this->version;
} }

46
src/Demo/HeaderParser.php Normal file
View file

@ -0,0 +1,46 @@
<?php declare(strict_types=1);
namespace Demostf\API\Demo;
class HeaderParser {
/**
* @param string $head string containing the demo header binary data
* @return Header
* @throws \InvalidArgumentException
*/
public function parseString(string $head): Header {
$info = @unpack('A8type/Iversion/Iprotocol/A260server/A260nick/A260map/A260game/fduration/Vticks/Vframes/Vsigon',
$head);
if (!isset($info['type']) || $info['type'] !== 'HL2DEMO') {
throw new \InvalidArgumentException('Not an HL2 demo');
}
return new Header($info);
}
/**
* Parse demo info from a stream
*
* @param resource $stream
* @return Header
* @throws \InvalidArgumentException
*/
public function parseStream($stream): Header {
$head = fread($stream, 2048);
return $this->parseString($head);
}
/**
* Parse demo info from a local file
*
* @param string $path
* @return Header
* @throws \InvalidArgumentException
*/
public function parseHeader(string $path): Header {
if (!is_readable($path)) {
throw new \InvalidArgumentException('Unable to open demo: ' . $path);
}
$fh = fopen($path, 'rb');
return $this->parseStream($fh);
}
}

View file

@ -1,67 +1,31 @@
<?php namespace Demostf\API\Demo; <?php declare(strict_types=1);
namespace Demostf\API\Demo;
use GuzzleHttp\Client; use GuzzleHttp\Client;
/**
* Higher level parser
*
* Processes the raw demo.js output to something more suitable for our purpose
*/
class Parser { class Parser {
const ANALYSER_BASEURL = 'http://demoserver.azurewebsites.net'; /** @var RawParser */
private $rawParser;
/** public function __construct(RawParser $rawParser) {
* @param string $head string containing the demo header binary data $this->rawParser = $rawParser;
* @return Header }
* @throws \Exception
*/ public function analyse(string $path): array {
public function parseString($head) { $data = $this->rawParser->parse($path);
set_error_handler(array($this, 'errorHandler')); if (!is_array($data)) {
$info = unpack("A8type/Iversion/Iprotocol/A260server/A260nick/A260map/A260game/fduration/Iticks/Iframes/Isigon", throw new \InvalidArgumentException('Error parsing demo');
$head);
restore_error_handler();
if ($info['type'] !== 'HL2DEMO') {
throw new \Exception('Not an HL2 demo');
} }
return new Header($info);
}
/**
* Parse demo info from a stream
*
* @param resource $stream
* @return Header
* @throws \Exception
*/
public function parseStream($stream) {
$head = fread($stream, 2048);
return $this->parseString($head);
}
/**
* Parse demo info from a local file
*
* @param string $path
* @return Header
* @throws \Exception
*/
public function parseHeader($path) {
if (!is_readable($path)) {
throw new \Exception('Unable to open demo: ' . $path);
}
$fh = fopen($path, 'rb');
return $this->parseStream($fh);
}
public function analyse(StoredDemo $storedDemo) {
$endPoint = self::ANALYSER_BASEURL . '/url';
$client = new Client();
$response = $client->post($endPoint, [
'body' => $storedDemo->getUrl()
]);
$data = $response->getBody();
return $this->handleData($data); return $this->handleData($data);
} }
private function handleData($data) { private function handleData(array $data) {
if (!is_array($data)) {
throw new \Exception('Error parsing demo');
}
$intervalPerTick = $data['intervalPerTick']; $intervalPerTick = $data['intervalPerTick'];
$red = 0; $red = 0;
$blue = 0; $blue = 0;
@ -94,7 +58,7 @@ class Parser {
$class = $classId; $class = $classId;
} }
} }
if ($class and $player['steamId']) {//skip spectators if ($class && $player['steamId']) {//skip spectators
$players[] = [ $players[] = [
'name' => $player['name'], 'name' => $player['name'],
'demo_user_id' => $player['userId'], 'demo_user_id' => $player['userId'],
@ -116,7 +80,7 @@ class Parser {
]; ];
} }
private function getClassName($classId) { private function getClassName(int $classId): string {
$classes = [ $classes = [
1 => 'scout', 1 => 'scout',
2 => 'sniper', 2 => 'sniper',
@ -128,6 +92,6 @@ class Parser {
8 => 'spy', 8 => 'spy',
9 => 'engineer' 9 => 'engineer'
]; ];
return isset($classes[$classId]) ? $classes[$classId] : 'Unknown'; return $classes[$classId] ?? 'Unknown';
} }
} }

27
src/Demo/RawParser.php Normal file
View file

@ -0,0 +1,27 @@
<?php declare(strict_types=1);
namespace Demostf\API\Demo;
use GuzzleHttp\Client;
/**
* Wrapper around demo.js parser
*
* Doesn't do any post-processing on the result
*/
class RawParser {
/** @var string */
private $parserUrl;
public function __construct(string $parserUrl) {
$this->parserUrl = $parserUrl;
}
public function parse(string $path): ?array {
$client = new Client();
$response = $client->post($this->parserUrl, [
'body' => fopen($path, 'r')
]);
return json_decode($response->getBody(), true);
}
}

View file

@ -3,7 +3,6 @@
namespace Demostf\API\Providers; namespace Demostf\API\Providers;
use Demostf\API\Data\User; use Demostf\API\Data\User;
use Demostf\API\Exception\NotFoundException;
use Doctrine\DBAL\Connection; use Doctrine\DBAL\Connection;
use RandomLib\Generator; use RandomLib\Generator;

View file

@ -0,0 +1,58 @@
<?php declare(strict_types=1);
namespace Demostf\API\Test\Providers;
use Demostf\API\Demo\Header;
use Demostf\API\Demo\HeaderParser;
use Demostf\API\Test\TestCase;
class HeaderParserTest extends TestCase {
public function testParseFile() {
$parser = new HeaderParser();
$expected = new Header([
'type' => 'HL2DEMO',
'version' => 3,
'protocol' => 24,
'server' => 'UGC Highlander Match',
'nick' => 'SourceTV Demo',
'map' => 'koth_product_rc8',
'game' => 'tf',
'duration' => 778.4849853515625,
'ticks' => 51899,
'frames' => 25703,
'sigon' => 818263
]);
$parsed = $parser->parseHeader(__DIR__ . '/../data/product.dem');
$this->assertEquals($expected->getServer(), $parsed->getServer());
$this->assertEquals($expected->getDuration(), $parsed->getDuration());
$this->assertEquals($expected->getTicks(), $parsed->getTicks());
$this->assertEquals($expected->getFrames(), $parsed->getFrames());
$this->assertEquals($expected->getGame(), $parsed->getGame());
$this->assertEquals($expected->getMap(), $parsed->getMap());
$this->assertEquals($expected->getNick(), $parsed->getNick());
$this->assertEquals($expected->getProtocol(), $parsed->getProtocol());
$this->assertEquals($expected->getSigon(), $parsed->getSigon());
$this->assertEquals($expected->getType(), $parsed->getType());
$this->assertEquals($expected->getVersion(), $parsed->getVersion());
}
/**
* @expectedException \InvalidArgumentException
* @expectedExceptionMessage Not an HL2 demo
*/
public function testNonDemoShort() {
$parser = new HeaderParser();
$parser->parseString("short");
}
/**
* @expectedException \InvalidArgumentException
* @expectedExceptionMessage Not an HL2 demo
*/
public function testNonDemoLong() {
$parser = new HeaderParser();
$parser->parseHeader(__FILE__);
}
}

37
tests/Demo/ParserTest.php Normal file
View file

@ -0,0 +1,37 @@
<?php declare(strict_types=1);
namespace Demostf\API\Test\Providers;
use Demostf\API\Demo\Parser;
use Demostf\API\Demo\RawParser;
use Demostf\API\Test\TestCase;
class ParserTest extends TestCase {
/** @var RawParser */
private $rawParser;
public function setUp() {
parent::setUp();
$this->rawParser = $this->getMockBuilder(RawParser::class)
->disableOriginalConstructor()
->getMock();
$this->rawParser->expects($this->any())
->method('parse')
->will($this->returnCallback(function ($path) {
$jsonPath = str_replace('.dem', '-raw.json', $path);
return json_decode(file_get_contents($jsonPath), true);
}));
}
public function testAnalyse() {
$parser = new Parser($this->rawParser);
$result = $parser->analyse(__DIR__ . '/../data/product.dem');
$expected = json_decode(file_get_contents(__DIR__ . '/../data/product-analyse.json'), true);
$this->assertEquals($expected, $result);
}
}

File diff suppressed because it is too large Load diff

File diff suppressed because one or more lines are too long

BIN
tests/data/product.dem Normal file

Binary file not shown.