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:
parent
1c37d3513e
commit
ae39548ddb
11 changed files with 1801 additions and 102 deletions
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
46
src/Demo/HeaderParser.php
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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 parseString($head) {
|
|
||||||
set_error_handler(array($this, 'errorHandler'));
|
|
||||||
$info = unpack("A8type/Iversion/Iprotocol/A260server/A260nick/A260map/A260game/fduration/Iticks/Iframes/Isigon",
|
|
||||||
$head);
|
|
||||||
restore_error_handler();
|
|
||||||
if ($info['type'] !== 'HL2DEMO') {
|
|
||||||
throw new \Exception('Not an HL2 demo');
|
|
||||||
}
|
|
||||||
return new Header($info);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
public function analyse(string $path): array {
|
||||||
* Parse demo info from a stream
|
$data = $this->rawParser->parse($path);
|
||||||
*
|
if (!is_array($data)) {
|
||||||
* @param resource $stream
|
throw new \InvalidArgumentException('Error parsing demo');
|
||||||
* @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
27
src/Demo/RawParser.php
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
|
||||||
|
|
|
||||||
58
tests/Demo/HeaderParserTest.php
Normal file
58
tests/Demo/HeaderParserTest.php
Normal 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
37
tests/Demo/ParserTest.php
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
1597
tests/data/product-analyse.json
Normal file
1597
tests/data/product-analyse.json
Normal file
File diff suppressed because it is too large
Load diff
1
tests/data/product-raw.json
Normal file
1
tests/data/product-raw.json
Normal file
File diff suppressed because one or more lines are too long
BIN
tests/data/product.dem
Normal file
BIN
tests/data/product.dem
Normal file
Binary file not shown.
Loading…
Add table
Add a link
Reference in a new issue