1
0
Fork 0
mirror of https://github.com/demostf/demo.js synced 2026-06-03 16:44:12 +02:00

more splitting of analyser

This commit is contained in:
Robin Appelman 2017-09-23 18:39:00 +02:00
commit b11cd7eec0
9 changed files with 240 additions and 211 deletions

View file

@ -5,96 +5,96 @@ const fs = require('fs');
const argv = require('minimist')(process.argv.slice(2), {boolean: true});
if (argv._.length !== 1) {
console.log('Usage: "node analyse [--strings] [--dump] [--head] [--event-list] [--create-event-definitions] FILE"');
process.exit(1);
console.log('Usage: "node analyse [--strings] [--dump] [--head] [--event-list] [--create-event-definitions] FILE"');
process.exit(1);
}
const echo = function (data) {
const string = JSON.stringify(data, null, 2);
console.log(string);
const string = JSON.stringify(data, null, 2);
console.log(string);
};
fs.readFile(argv._[0], function (err, data) {
if (err) throw err;
const demo = Demo.fromNodeBuffer(data);
const parser = demo.getParser(true);
const head = parser.readHeader();
if (argv.head) {
echo(head);
return;
}
const match = parser.parseBody();
if (argv['create-event-definitions']) {
const definitions = Array.from(parser.match.eventDefinitions.values());
const definition = definitions
.map(createEventDefinition)
.join('\n\n')
+ '\n\n' + createEventDefinitionUnion(definitions) + '\n\n'
+ 'export type GameEventType = GameEvent[\'name\'];\n\n'
+ createEventTypeMap(definitions) + '\n\n'
+ createEventTypeIdMap(parser.match.eventDefinitions) + '\n';
console.log(definition);
} else if (argv['event-list']) {
echo(Array.from(parser.match.eventDefinitions.values()));
} else if (argv.dump) {
echo(parser.match.packets);
} else if (argv.strings) {
echo(parser.match.strings);
} else {
echo(match.getState());
}
if (err) throw err;
const demo = Demo.fromNodeBuffer(data);
const analyser = demo.getAnalyser(true);
const head = analyser.getHeader();
if (argv.head) {
echo(head);
return;
}
const match = analyser.getBody();
if (argv['create-event-definitions']) {
const definitions = Array.from(match.eventDefinitions.values());
const definition = definitions
.map(createEventDefinition)
.join('\n\n')
+ '\n\n' + createEventDefinitionUnion(definitions) + '\n\n'
+ 'export type GameEventType = GameEvent[\'name\'];\n\n'
+ createEventTypeMap(definitions) + '\n\n'
+ createEventTypeIdMap(match.eventDefinitions) + '\n';
console.log(definition);
} else if (argv['event-list']) {
echo(Array.from(match.eventDefinitions.values()));
} else if (argv.dump) {
echo(match.packets);
} else if (argv.strings) {
echo(match.strings);
} else {
echo(match.getState());
}
});
function getEventTypeName(s) {
const name = s.replace(/(\_\w)/g, function (m) {
return m[1].toUpperCase();
}).replace(/\b[a-z]/g, function (letter) {
return letter.toUpperCase();
});
if (EventNameReplace.has(name)) {
return EventNameReplace.get(name);
} else {
return name
.replace('Teamplay', 'TeamPlay')
.replace('death', 'Death')
.replace('panel', 'Panel')
.replace('object', 'Object')
.replace('update', 'Update')
.replace('ready', 'Ready')
.replace('Gameui', 'GameUI')
.replace('onhit', 'OnHit')
.replace('bymedic', 'ByMedic')
.replace('Controlpoint', 'ControlPoint')
.replace('Pipebomb', 'PipeBomb')
.replace('Scorestats', 'ScoreStats')
.replace('Creditbonus', 'CreditBonus')
.replace('Sentrybuster', 'SentryBuster')
.replace('Questlog', 'QuestLog')
.replace('Localplayer', 'LocalPlayer')
.replace('Minigame', 'MiniGame')
.replace('Winlimit', 'WinLimit')
.replace('Hltv', 'HLTV');
}
const name = s.replace(/(\_\w)/g, function (m) {
return m[1].toUpperCase();
}).replace(/\b[a-z]/g, function (letter) {
return letter.toUpperCase();
});
if (EventNameReplace.has(name)) {
return EventNameReplace.get(name);
} else {
return name
.replace('Teamplay', 'TeamPlay')
.replace('death', 'Death')
.replace('panel', 'Panel')
.replace('object', 'Object')
.replace('update', 'Update')
.replace('ready', 'Ready')
.replace('Gameui', 'GameUI')
.replace('onhit', 'OnHit')
.replace('bymedic', 'ByMedic')
.replace('Controlpoint', 'ControlPoint')
.replace('Pipebomb', 'PipeBomb')
.replace('Scorestats', 'ScoreStats')
.replace('Creditbonus', 'CreditBonus')
.replace('Sentrybuster', 'SentryBuster')
.replace('Questlog', 'QuestLog')
.replace('Localplayer', 'LocalPlayer')
.replace('Minigame', 'MiniGame')
.replace('Winlimit', 'WinLimit')
.replace('Hltv', 'HLTV');
}
}
function getEntryTypeDefinition(typeId) {
switch (typeId) {
case 1:
return 'string';
case 2:
case 3:
case 4:
case 5:
return 'number';
case 6:
return 'boolean';
case 7:
return 'null';
}
switch (typeId) {
case 1:
return 'string';
case 2:
case 3:
case 4:
case 5:
return 'number';
case 6:
return 'boolean';
case 7:
return 'null';
}
}
function createEventDefinition(definition) {
return `
return `
export interface ${getEventTypeName(definition.name)}Event {
name: '${definition.name}';
values: {
@ -104,21 +104,21 @@ ${definition.entries.map(entry => ` ${entry.name}: ${getEntryTypeDefinition(ent
}
function createEventDefinitionUnion(definitions) {
return `export type GameEvent = ` +
definitions.map(definition => '\t' + getEventTypeName(definition.name) + 'Event')
.join(' |\n').trim()
+ ';';
return `export type GameEvent = ` +
definitions.map(definition => '\t' + getEventTypeName(definition.name) + 'Event')
.join(' |\n').trim()
+ ';';
}
function createEventTypeMap(definitions) {
return `export type GameEventTypeMap = {
return `export type GameEventTypeMap = {
${definitions.map(definition => ` ${definition.name}: ${getEventTypeName(definition.name)}Event;`).join('\n')}
};`;
}
function createEventTypeIdMap(definitionMap) {
const definitionEntries = Array.from(definitionMap.entries());
return `export type GameEventTypeId = number;
const definitionEntries = Array.from(definitionMap.entries());
return `export type GameEventTypeId = number;
export const GameEventTypeIdMap: Map<GameEventType, GameEventTypeId> = new Map<GameEventType, GameEventTypeId>([
${definitionEntries.map(([typeId, definition]) => ` ['${definition.name}', ${typeId}],`).join('\n')}
@ -126,45 +126,45 @@ ${definitionEntries.map(([typeId, definition]) => ` ['${definition.name}', ${typ
}
const EventNameReplace = new Map([
['ReplayReplaysavailable', 'ReplayReplaysAvailable'],
['ServerAddban', 'ServerAddBan'],
['ServerRemoveban', 'ServerRemoveBan'],
['ClientBeginconnect', 'ClientBeginConnect'],
['ClientFullconnect', 'ClientFullConnect'],
['PlayerChangename', 'PlayerChangeName'],
['PlayerHintmessage', 'PlayerHintMessage'],
['GameNewmap', 'GameNewMap'],
['IntroNextcamera', 'IntroNextCamera'],
['PlayerChangeclass', 'PlayerChangeClass'],
['ControlpointInitialized', 'ControlPointInitialized'],
['ControlpointUpdateimages', 'ControlPointUpdateImages'],
['ControlpointUpdatelayout', 'ControlPointUpdateLayout'],
['ControlpointUpdatecapping', 'ControlPointUpdateCapping'],
['ControlpointUpdateowner', 'ControlPointUpdateOwner'],
['ControlpointStarttouch', 'ControlPointStartTouch'],
['ControlpointEndtouch', 'ControlPointEndTouch'],
['ControlpointPulseElement', 'ControlPointPulseElement'],
['ControlpointFakeCapture', 'ControlPointFakeCapture'],
['ControlpointFakeCaptureMult', 'ControlPointFakeCaptureMult'],
['TeamplayWaitingAbouttoend', 'TeamPlayWaitingAboutToEnd'],
['TeamplayPointStartcapture', 'TeamPlayPointStartCapture'],
['FreezecamStarted', 'FreezeCamStarted'],
['LocalplayerChangeteam', 'LocalPlayerChangeTeam'],
['LocalplayerChangeclass', 'LocalPlayerChangeClass'],
['LocalplayerChangedisguise', 'LocalPlayerChangeDisguise'],
['FlagstatusUpdate', 'FlagStatusUpdate'],
['TournamentEnablecountdown', 'TournamentEnableCountdown'],
['PlayerCalledformedic', 'PlayerCalledForMedic'],
['PlayerAskedforball', 'PlayerAskedForBall'],
['LocalplayerBecameobserver', 'LocalPlayerBecameObserver'],
['PlayerHealedmediccall', 'PlayerHealedMedicCall'],
['ArenaMatchMaxstreak', 'ArenaMatchMaxStreak'],
['StatsResetround', 'StatsResetRound'],
['FishNotice_arm', 'FishNoticeArm'],
['PlayerBonuspoints', 'PlayerBonusPoints'],
['PlayerUsedPowerupBottle', 'PlayerUsedPowerUpBottle'],
['ReplayStartrecord', 'ReplayStartRecord'],
['ReplaySessioninfo', 'ReplaySessionInfo'],
['ReplayEndrecord', 'ReplayEndRecord'],
['ReplayServererror', 'ReplayServerError']
['ReplayReplaysavailable', 'ReplayReplaysAvailable'],
['ServerAddban', 'ServerAddBan'],
['ServerRemoveban', 'ServerRemoveBan'],
['ClientBeginconnect', 'ClientBeginConnect'],
['ClientFullconnect', 'ClientFullConnect'],
['PlayerChangename', 'PlayerChangeName'],
['PlayerHintmessage', 'PlayerHintMessage'],
['GameNewmap', 'GameNewMap'],
['IntroNextcamera', 'IntroNextCamera'],
['PlayerChangeclass', 'PlayerChangeClass'],
['ControlpointInitialized', 'ControlPointInitialized'],
['ControlpointUpdateimages', 'ControlPointUpdateImages'],
['ControlpointUpdatelayout', 'ControlPointUpdateLayout'],
['ControlpointUpdatecapping', 'ControlPointUpdateCapping'],
['ControlpointUpdateowner', 'ControlPointUpdateOwner'],
['ControlpointStarttouch', 'ControlPointStartTouch'],
['ControlpointEndtouch', 'ControlPointEndTouch'],
['ControlpointPulseElement', 'ControlPointPulseElement'],
['ControlpointFakeCapture', 'ControlPointFakeCapture'],
['ControlpointFakeCaptureMult', 'ControlPointFakeCaptureMult'],
['TeamplayWaitingAbouttoend', 'TeamPlayWaitingAboutToEnd'],
['TeamplayPointStartcapture', 'TeamPlayPointStartCapture'],
['FreezecamStarted', 'FreezeCamStarted'],
['LocalplayerChangeteam', 'LocalPlayerChangeTeam'],
['LocalplayerChangeclass', 'LocalPlayerChangeClass'],
['LocalplayerChangedisguise', 'LocalPlayerChangeDisguise'],
['FlagstatusUpdate', 'FlagStatusUpdate'],
['TournamentEnablecountdown', 'TournamentEnableCountdown'],
['PlayerCalledformedic', 'PlayerCalledForMedic'],
['PlayerAskedforball', 'PlayerAskedForBall'],
['LocalplayerBecameobserver', 'LocalPlayerBecameObserver'],
['PlayerHealedmediccall', 'PlayerHealedMedicCall'],
['ArenaMatchMaxstreak', 'ArenaMatchMaxStreak'],
['StatsResetround', 'StatsResetRound'],
['FishNotice_arm', 'FishNoticeArm'],
['PlayerBonuspoints', 'PlayerBonusPoints'],
['PlayerUsedPowerupBottle', 'PlayerUsedPowerUpBottle'],
['ReplayStartrecord', 'ReplayStartRecord'],
['ReplaySessioninfo', 'ReplaySessionInfo'],
['ReplayEndrecord', 'ReplayEndRecord'],
['ReplayServererror', 'ReplayServerError']
]);

30
src/Analyser.ts Normal file
View file

@ -0,0 +1,30 @@
import {Parser} from './Parser';
import {Match} from './Data/Match';
import {EventEmitter} from 'events';
import {Header} from './Data/Header';
export class Analyser extends EventEmitter {
private parser: Parser;
private match: Match;
constructor(parser: Parser) {
super();
this.parser = parser;
}
public getHeader(): Header {
return this.parser.getHeader();
}
public getBody(): Match {
if (!this.match) {
this.match = new Match(this.parser.parserState);
for (const packet of this.parser.getPackets()) {
this.match.handlePacket(packet);
this.emit('packet', packet);
}
this.emit('done');
}
return this.match;
}
}

View file

@ -36,7 +36,11 @@ export class Match {
public teamEntityMap: Map<EntityId, Team> = new Map();
public buildings: Map<EntityId, Building> = new Map();
public playerResources: PlayerResource[] = [];
public readonly parserState: ParserState = new ParserState();
public readonly parserState: ParserState;
constructor(parserState: ParserState) {
this.parserState = parserState;
}
public getState() {
const users = {};

View file

@ -13,6 +13,7 @@ import {
} from '../PacketHandler/StringTable';
import {handleGameEventList} from '../PacketHandler/GameEventList';
import {DataTablesMessage, Message, MessageType, StringTablesMessage} from './Message';
import {handlePacketEntitiesForState} from '../PacketHandler/PacketEntities';
export class ParserState {
public version: number = 0;
@ -44,6 +45,9 @@ export class ParserState {
case 'gameEventList':
handleGameEventList(packet, this);
break;
case 'packetEntities':
handlePacketEntitiesForState(packet, this);
break;
}
}

View file

@ -1,6 +1,7 @@
import {BitStream} from 'bit-buffer';
import {Parser} from './Parser';
import {PacketTypeId} from './Data/Packet';
import {Analyser} from './Analyser';
export class Demo {
public static fromNodeBuffer(nodeBuffer) {
@ -31,4 +32,8 @@ export class Demo {
}
return this.parser;
}
public getAnalyser(fastMode: boolean = false) {
return new Analyser(this.getParser(fastMode));
}
}

View file

@ -7,24 +7,30 @@ import {SendProp} from '../Data/SendProp';
import {Vector} from '../Data/Vector';
import {CWeaponMedigun, Weapon} from '../Data/Weapon';
import {TeamNumber} from '../Data/Team';
import {ParserState} from '../Data/ParserState';
export function handlePacketEntities(packet: PacketEntitiesPacket, match: Match) {
for (const removedEntityId of packet.removedEntities) {
match.parserState.entityClasses.delete(removedEntityId);
}
for (const entity of packet.entities) {
saveEntity(entity, match);
handleEntity(entity, match);
}
}
function saveEntity(packetEntity: PacketEntity, match: Match) {
if (packetEntity.pvs === PVS.DELETE) {
match.parserState.entityClasses.delete(packetEntity.entityIndex);
export function handlePacketEntitiesForState(packet: PacketEntitiesPacket, state: ParserState) {
for (const removedEntityId of packet.removedEntities) {
state.entityClasses.delete(removedEntityId);
}
match.parserState.entityClasses.set(packetEntity.entityIndex, packetEntity.serverClass);
for (const entity of packet.entities) {
saveEntity(entity, state);
}
}
function saveEntity(packetEntity: PacketEntity, state: ParserState) {
if (packetEntity.pvs === PVS.DELETE) {
state.entityClasses.delete(packetEntity.entityIndex);
}
state.entityClasses.set(packetEntity.entityIndex, packetEntity.serverClass);
}
function handleEntity(entity: PacketEntity, match: Match) {

View file

@ -1,14 +1,13 @@
import {BitStream} from 'bit-buffer';
import {EventEmitter} from 'events';
import {Header} from './Data/Header';
import {Match} from './Data/Match';
import {ConsoleCmdHandler} from './Parser/Message/ConsoleCmd';
import {DataTableHandler} from './Parser/Message/DataTable';
import {PacketMessageHandler} from './Parser/Message/Packet';
import {StringTableHandler} from './Parser/Message/StringTable';
import {UserCmdHandler} from './Parser/Message/UserCmd';
import {PacketTypeId} from './Data/Packet';
import {Packet, PacketTypeId} from './Data/Packet';
import {Message, MessageHandler, MessageType, PacketMessage} from './Data/Message';
import {ParserState} from './Data/ParserState';
const messageHandlers: Map<MessageType, MessageHandler<Message>> = new Map<MessageType, MessageHandler<Message>>([
[MessageType.Sigon, PacketMessageHandler],
@ -19,39 +18,41 @@ const messageHandlers: Map<MessageType, MessageHandler<Message>> = new Map<Messa
[MessageType.StringTables, StringTableHandler],
]);
export class Parser extends EventEmitter {
public stream: BitStream;
public match: Match;
protected skipPackets: PacketTypeId[];
export class Parser {
public readonly stream: BitStream;
public readonly parserState: ParserState;
private header: Header | null = null;
protected readonly skipPackets: PacketTypeId[];
public viewOrigin: number[][] = [[], []];
public viewAngles: number[][] = [[], []];
constructor(stream: BitStream, skipPackets: PacketTypeId[] = []) {
super();
this.stream = stream;
this.match = new Match();
this.on('packet', this.match.handlePacket.bind(this.match));
this.parserState = new ParserState();
this.skipPackets = skipPackets;
}
public readHeader() {
return this.parseHeader(this.stream);
public getHeader() {
if (!this.header) {
this.header = this.parseHeader(this.stream);
}
return this.header;
}
public parseBody() {
public * getPackets(): Iterable<Packet> {
// ensure that we are past the header
this.getHeader();
const messages = this.getMessages();
for (const message of messages) {
this.handleMessage(message);
yield* this.handleMessage(message);
}
this.emit('done', this.match);
return this.match;
}
private * getMessages(): Iterable<Message> {
let hasNext: boolean = true;
while (hasNext) {
const message = this.readMessage(this.stream, this.match);
const message = this.readMessage(this.stream, this.parserState);
if (!message) {
hasNext = false;
} else {
@ -60,20 +61,12 @@ export class Parser extends EventEmitter {
}
}
public tick() {
const message = this.readMessage(this.stream, this.match);
if (message) {
this.handleMessage(message);
}
return !!message;
}
protected parseMessage(data: BitStream, type: MessageType, tick: number, match: Match): Message {
protected parseMessage(data: BitStream, type: MessageType, tick: number, state: ParserState): Message {
const handler = messageHandlers.get(type);
if (!handler) {
throw new Error(`No handler for message of type ${MessageType[type]}`);
}
return handler.parseMessage(data, tick, match.parserState);
return handler.parseMessage(data, tick, state);
}
protected parseHeader(stream): Header {
@ -92,17 +85,17 @@ export class Parser extends EventEmitter {
};
}
protected handleMessage(message: Message) {
this.match.parserState.handleMessage(message);
protected * handleMessage(message: Message): Iterable<Packet> {
this.parserState.handleMessage(message);
if (message.type === MessageType.Packet) {
for (const packet of (message as PacketMessage).packets) {
this.match.parserState.handlePacket(packet);
this.emit('packet', packet);
this.parserState.handlePacket(packet);
yield packet;
}
}
}
protected readMessage(stream: BitStream, match: Match): Message | false {
protected readMessage(stream: BitStream, state: ParserState): Message | false {
if (stream.bitsLeft < 8) {
return false;
}
@ -143,6 +136,6 @@ export class Parser extends EventEmitter {
const length = stream.readInt32();
const buffer = stream.readBitStream(length * 8);
return this.parseMessage(buffer, type, tick, match);
return this.parseMessage(buffer, type, tick, state);
}
}

View file

@ -6,37 +6,57 @@ import {BitStream} from 'bit-buffer';
import * as split2 from 'split2';
import {createUnzip, createGunzip} from 'zlib';
import {PassThrough} from 'stream';
import {EntityId, PVS} from '../../Data/PacketEntity';
import {SendPropValue} from '../../Data/SendProp';
interface ResultData {
tick: number,
serverClass: string,
id: EntityId,
props: {[propName: string]: SendPropValue},
pvs: PVS
}
function writeEntities(name: string) {
const targetFile = `${__dirname}/../data/${name}_entities.json`;
const source = readFileSync(`${__dirname}/../data/${name}.dem`);
const demo = Demo.fromNodeBuffer(source);
const parser = demo.getParser(false);
parser.readHeader();
const match = parser.match;
const resultData = getResultData(parser.getPackets());
const writeStream = createWriteStream(targetFile, 'utf8');
parser.on('packet', (packet: Packet) => {
for (const result of resultData) {
writeStream.write(JSON.stringify(result) + '\n');
}
writeStream.end();
}
function* getResultData(packets: Iterable<Packet>): IterableIterator<ResultData> {
let tick = 0;
for (const packet of packets) {
if (packet.packetType === 'netTick') {
tick = packet.tick;
}
if (packet.packetType === 'packetEntities') {
for (const entity of packet.entities) {
const entityProps = {};
for (const prop of entity.props) {
entityProps[`${prop.definition.name}`] = prop.value;
}
writeStream.write(JSON.stringify({
tick: match.tick,
yield {
tick: tick,
serverClass: entity.serverClass.name,
id: entity.entityIndex,
props: entityProps,
pvs: entity.pvs
}) + '\n');
};
}
}
});
parser.parseBody();
writeStream.end();
}
}
function testEntities(name: string, entityCount: number) {
@ -45,34 +65,8 @@ function testEntities(name: string, entityCount: number) {
const source = readFileSync(`${__dirname}/../data/${name}.dem`);
const demo = Demo.fromNodeBuffer(source);
const parser = demo.getParser(false);
parser.readHeader();
const match = parser.match;
const resultData: any[] = [];
parser.on('packet', (packet: Packet) => {
if (packet.packetType === 'packetEntities') {
for (const entity of packet.entities) {
const entityProps = {};
for (const prop of entity.props) {
entityProps[`${prop.definition.name}`] = prop.value;
}
resultData.push({
tick: match.tick,
serverClass: entity.serverClass.name,
id: entity.entityIndex,
props: entityProps,
pvs: entity.pvs
});
}
}
});
function parseEntities() {
const message = parser.tick();
if (message && resultData.length === 0) {
parseEntities();
}
}
const resultData = getResultData(parser.getPackets());
const readStream = createReadStream(targetFile);
@ -81,14 +75,10 @@ function testEntities(name: string, entityCount: number) {
readStream
.pipe(createUnzip())
.pipe(split2(JSON.parse)).on('data', (data) => {
if (resultData.length < 1) {
parseEntities();
}
const result = resultData.shift();
assert.deepEqual(data, result, `Failed asserting that packet ${parsed} is the same`);
const result = resultData.next();
assert.deepEqual(data, result.value, `Failed asserting that packet ${parsed} is the same`);
parsed++;
}).on('end', () => {
assert.equal(resultData.length, 0, 'Entities left to be checked');
assert.equal(parsed, entityCount, 'unexpected number of entities');
resolve();

View file

@ -1,17 +1,14 @@
import * as assert from 'assert';
import {readFileSync} from 'fs';
import {Demo} from '../../Demo';
import {Packet} from '../../Data/Packet';
import {BitStream} from 'bit-buffer';
function testDemo(name: string, fastMode: boolean = false) {
const target = JSON.parse(readFileSync(`${__dirname}/../data/${name}.json`, 'utf8'));
const source = readFileSync(`${__dirname}/../data/${name}.dem`);
const demo = Demo.fromNodeBuffer(source);
const parser = demo.getParser(fastMode);
parser.readHeader();
parser.parseBody();
const parsed = parser.match.getState();
const analyser = demo.getAnalyser(fastMode);
const parsed = analyser.getBody().getState();
assert.deepEqual(JSON.parse(JSON.stringify(parsed)), target);
}