1
0
Fork 0
mirror of https://github.com/demostf/demo.js synced 2026-06-04 00:54:14 +02:00

start splitting parserstate from analysing

This commit is contained in:
Robin Appelman 2017-09-23 17:41:52 +02:00
commit 198fe0b1ba
18 changed files with 459 additions and 421 deletions

View file

@ -1,19 +1,12 @@
import {BitStream} from 'bit-buffer'; import {BitStream} from 'bit-buffer';
import {handleDataTable} from '../PacketHandler/DataTable';
import {handleGameEvent} from '../PacketHandler/GameEvent'; import {handleGameEvent} from '../PacketHandler/GameEvent';
import {handleGameEventList} from '../PacketHandler/GameEventList';
import {handlePacketEntities} from '../PacketHandler/PacketEntities'; import {handlePacketEntities} from '../PacketHandler/PacketEntities';
import {handleSayText2} from '../PacketHandler/SayText2'; import {handleSayText2} from '../PacketHandler/SayText2';
import {handleStringTable, handleStringTables, handleStringTableUpdate} from '../PacketHandler/StringTable';
import {Building} from './Building'; import {Building} from './Building';
import {Death} from './Death'; import {Death} from './Death';
import {GameEventDefinition} from './GameEvent';
import {EntityId, PacketEntity} from './PacketEntity'; import {EntityId, PacketEntity} from './PacketEntity';
import {Player} from './Player'; import {Player} from './Player';
import {PlayerResource} from './PlayerResource'; import {PlayerResource} from './PlayerResource';
import {SendTable, SendTableName} from './SendTable';
import {ServerClass, ServerClassId} from './ServerClass';
import {StringTable} from './StringTable';
import {Team, TeamNumber} from './Team'; import {Team, TeamNumber} from './Team';
import {UserInfo} from './UserInfo'; import {UserInfo} from './UserInfo';
import {Weapon} from './Weapon'; import {Weapon} from './Weapon';
@ -21,11 +14,10 @@ import {World} from './World';
import {Round} from './Round'; import {Round} from './Round';
import {Chat} from './Chat'; import {Chat} from './Chat';
import {Packet} from './Packet'; import {Packet} from './Packet';
import {GameEventType} from './GameEventTypes';
import {ParserState} from './ParserState'; import {ParserState} from './ParserState';
import {SendProp} from './SendProp'; import {StringTableEntry} from './StringTable';
export class Match implements ParserState { export class Match {
public tick: number = 0; public tick: number = 0;
public chat: Chat[] = []; public chat: Chat[] = [];
public users: Map<number, UserInfo> = new Map(); public users: Map<number, UserInfo> = new Map();
@ -33,26 +25,18 @@ export class Match implements ParserState {
public rounds: Round[] = []; public rounds: Round[] = [];
public startTick: number = 0; public startTick: number = 0;
public intervalPerTick: number = 0; public intervalPerTick: number = 0;
public staticBaseLines: Map<ServerClassId, BitStream> = new Map();
public staticBaselineCache: Map<ServerClassId, SendProp[]> = new Map();
public eventDefinitions: Map<number, GameEventDefinition<GameEventType>> = new Map();
public world: World = { public world: World = {
boundaryMin: {x: 0, y: 0, z: 0}, boundaryMin: {x: 0, y: 0, z: 0},
boundaryMax: {x: 0, y: 0, z: 0}, boundaryMax: {x: 0, y: 0, z: 0},
}; };
public playerEntityMap: Map<EntityId, Player> = new Map(); public playerEntityMap: Map<EntityId, Player> = new Map();
public entityClasses: Map<EntityId, ServerClass> = new Map();
public sendTables: Map<SendTableName, SendTable> = new Map();
public instanceBaselines: [Map<EntityId, SendProp[]>, Map<EntityId, SendProp[]>] = [new Map(), new Map()];
public weaponMap: Map<EntityId, Weapon> = new Map(); public weaponMap: Map<EntityId, Weapon> = new Map();
public outerMap: Map<number, EntityId> = new Map(); public outerMap: Map<number, EntityId> = new Map();
public teams: Map<TeamNumber, Team> = new Map(); public teams: Map<TeamNumber, Team> = new Map();
public teamEntityMap: Map<EntityId, Team> = new Map(); public teamEntityMap: Map<EntityId, Team> = new Map();
public version: number = 0;
public buildings: Map<EntityId, Building> = new Map(); public buildings: Map<EntityId, Building> = new Map();
public playerResources: PlayerResource[] = []; public playerResources: PlayerResource[] = [];
public stringTables: StringTable[] = []; public readonly parserState: ParserState = new ParserState();
public serverClasses: ServerClass[] = [];
public getState() { public getState() {
const users = {}; const users = {};
@ -91,32 +75,55 @@ export class Match implements ParserState {
break; break;
case 'serverInfo': case 'serverInfo':
this.intervalPerTick = packet.intervalPerTick; this.intervalPerTick = packet.intervalPerTick;
this.version = packet.version; break;
case 'createStringTable':
if (packet.table.name === 'userinfo') {
this.calculateUserInfo();
}
break; break;
case 'sayText2': case 'sayText2':
handleSayText2(packet, this); handleSayText2(packet, this);
break; break;
case 'dataTable':
handleDataTable(packet, this);
break;
case 'stringTable':
handleStringTables(packet, this);
break;
case 'createStringTable':
handleStringTable(packet, this);
break;
case 'updateStringTable':
handleStringTableUpdate(packet, this);
break;
case 'gameEventList':
handleGameEventList(packet, this);
break;
case 'gameEvent': case 'gameEvent':
handleGameEvent(packet, this); handleGameEvent(packet, this);
break; break;
} }
} }
private calculateUserInfo() {
for (const [text, extraData] of this.parserState.userInfoEntries.entries()) {
this.calculateUserInfoFromEntry(text, extraData);
}
}
private calculateUserInfoByEntityId(entityId: number) {
const text = `${entityId - 1}`;
const extraData = this.parserState.userInfoEntries.get(text);
if (!extraData) {
throw new Error(`No user info in stringable for entity id ${entityId}`);
}
return this.calculateUserInfoFromEntry(text, extraData);
}
private calculateUserInfoFromEntry(text: string, extraData: BitStream): UserInfo {
if (extraData.bitsLeft > (32 * 8)) {
const name = extraData.readUTF8String(32);
const userId = extraData.readUint32();
const steamId = extraData.readUTF8String();
if (steamId) {
const userState = this.getUserInfo(userId);
userState.name = name;
userState.steamId = steamId;
userState.entityId = parseInt(text, 10) + 1;
return userState;
} else {
throw new Error(`No steamid for user info ${text}`);
}
} else {
throw new Error();
}
}
public getUserInfo(userId: number): UserInfo { public getUserInfo(userId: number): UserInfo {
// no clue why it does this // no clue why it does this
// only seems to be the case with per user ready // only seems to be the case with per user ready
@ -125,6 +132,7 @@ export class Match implements ParserState {
} }
const user = this.users.get(userId); const user = this.users.get(userId);
if (!user) { if (!user) {
const newUser = { const newUser = {
name: '', name: '',
userId, userId,
@ -139,12 +147,12 @@ export class Match implements ParserState {
return user; return user;
} }
public getUserInfoForEntity(entity: PacketEntity): UserInfo { public getUserInfoForEntity(entity: PacketEntity): UserInfo | null {
for (const user of this.users.values()) { for (const user of this.users.values()) {
if (user && user.entityId === entity.entityIndex) { if (user && user.entityId === entity.entityIndex) {
return user; return user;
} }
} }
throw new Error('User not found for entity ' + entity.entityIndex); return this.calculateUserInfoByEntityId(entity.entityIndex);
} }
} }

74
src/Data/Message.ts Normal file
View file

@ -0,0 +1,74 @@
import {Packet} from './Packet';
import {BitStream} from 'bit-buffer';
import {ServerClass} from './ServerClass';
import {SendTable} from './SendTable';
import {StringTable} from './StringTable';
import {ParserState} from './ParserState';
export enum MessageType {
Sigon = 1,
Packet = 2,
SyncTick = 3,
ConsoleCmd = 4,
UserCmd = 5,
DataTables = 6,
Stop = 7,
StringTables = 8,
}
export interface BaseMessage {
tick: number;
rawData: BitStream;
}
export interface SigonMessage extends BaseMessage {
type: MessageType.Sigon;
packets: Packet[];
}
export interface PacketMessage extends BaseMessage {
type: MessageType.Packet;
packets: Packet[];
}
export interface SyncTickMessage extends BaseMessage {
type: MessageType.SyncTick;
}
export interface ConsoleCmdMessage extends BaseMessage {
type: MessageType.ConsoleCmd;
command: string;
}
export interface UserCmdMessage extends BaseMessage {
type: MessageType.UserCmd;
}
export interface DataTablesMessage extends BaseMessage {
type: MessageType.DataTables;
tables: SendTable[];
serverClasses: ServerClass[]
}
export interface StopMessage extends BaseMessage {
type: MessageType.Stop;
}
export interface StringTablesMessage extends BaseMessage {
type: MessageType.StringTables;
tables: StringTable[]
}
export type Message = SigonMessage |
PacketMessage |
SyncTickMessage |
ConsoleCmdMessage |
UserCmdMessage |
DataTablesMessage |
StopMessage |
StringTablesMessage;
export interface MessageHandler<M extends Message> {
parseMessage: (stream: BitStream, tick: number, state: ParserState) => M;
encodeMessage: (message: M, stream: BitStream, state: ParserState) => void;
}

View file

@ -1,22 +1,83 @@
import {BitStream} from 'bit-buffer'; import {BitStream} from 'bit-buffer';
import {GameEventDefinition} from './GameEvent'; import {GameEventDefinition} from './GameEvent';
import {EntityId, PacketEntity} from './PacketEntity'; import {EntityId} from './PacketEntity';
import {SendTable, SendTableName} from './SendTable'; import {SendTable, SendTableName} from './SendTable';
import {ServerClass, ServerClassId} from './ServerClass'; import {ServerClass, ServerClassId} from './ServerClass';
import {StringTable} from './StringTable'; import {StringTable} from './StringTable';
import {GameEventType} from './GameEventTypes'; import {GameEventType} from './GameEventTypes';
import {SendProp} from './SendProp'; import {SendProp} from './SendProp';
import {Packet, PacketTypeId} from './Packet';
import {
handleStringTable, handleStringTables, handleStringTableUpdate,
handleTable
} from '../PacketHandler/StringTable';
import {handleGameEventList} from '../PacketHandler/GameEventList';
import {DataTablesMessage, Message, MessageType, StringTablesMessage} from './Message';
export interface ParserState { export class ParserState {
staticBaseLines: Map<ServerClassId, BitStream>; public version: number = 0;
staticBaselineCache: Map<ServerClassId, SendProp[]>; public staticBaseLines: Map<ServerClassId, BitStream> = new Map();
eventDefinitions: Map<number, GameEventDefinition<GameEventType>>; public staticBaselineCache: Map<ServerClassId, SendProp[]> = new Map();
entityClasses: Map<EntityId, ServerClass>; public eventDefinitions: Map<number, GameEventDefinition<GameEventType>> = new Map();
sendTables: Map<SendTableName, SendTable>; public entityClasses: Map<EntityId, ServerClass> = new Map();
version: number; public sendTables: Map<SendTableName, SendTable> = new Map();
stringTables: StringTable[]; public stringTables: StringTable[] = [];
serverClasses: ServerClass[]; public serverClasses: ServerClass[] = [];
instanceBaselines: [Map<EntityId, SendProp[]>, Map<EntityId, SendProp[]>]; public instanceBaselines: [Map<EntityId, SendProp[]>, Map<EntityId, SendProp[]>] = [new Map(), new Map()];
public skippedPackets: PacketTypeId[] = [];
public userInfoEntries: Map<string, BitStream> = new Map();
public handlePacket(packet: Packet) {
switch (packet.packetType) {
case 'serverInfo':
this.version = packet.version;
break;
case 'stringTable':
handleStringTables(packet, this);
break;
case 'createStringTable':
handleStringTable(packet, this);
break;
case 'updateStringTable':
handleStringTableUpdate(packet, this);
break;
case 'gameEventList':
handleGameEventList(packet, this);
break;
}
}
public handleMessage(message: Message) {
switch (message.type) {
case MessageType.DataTables:
this.handleDataTableMessage(message);
break;
case MessageType.StringTables:
this.handleStringTableMessage(message);
break;
}
}
private handleDataTableMessage(message: DataTablesMessage) {
for (const table of message.tables) {
this.sendTables.set(table.name, table);
}
this.serverClasses = message.serverClasses;
}
private handleStringTableMessage(message: StringTablesMessage) {
for (const table of message.tables) {
handleTable(table, this);
}
}
public getStringTable(name: string): StringTable | null {
const table = this.stringTables.find(table => table.name === name);
if (!table) {
return null;
}
return table;
}
} }
export function getClassBits(state: ParserState) { export function getClassBits(state: ParserState) {
@ -32,24 +93,5 @@ export function getSendTable(state: ParserState, dataTable: string): SendTable {
} }
export function createParserState(): ParserState { export function createParserState(): ParserState {
return { return new ParserState();
staticBaseLines: new Map(),
staticBaselineCache: new Map(),
eventDefinitions: new Map(),
entityClasses: new Map(),
sendTables: new Map(),
version: 0,
stringTables: [],
serverClasses: [],
instanceBaselines: [new Map(), new Map()]
};
}
export function getStringTable(state: ParserState, name: string) {
for (const table of state.stringTables) {
if (table.name === name) {
return table;
}
}
return null;
} }

View file

@ -1,10 +1,7 @@
import {BitStream} from 'bit-buffer'; import {BitStream} from 'bit-buffer';
import {Parser} from './Parser'; import {Parser} from './Parser';
import {StreamDemo} from './StreamDemo';
import {PacketTypeId} from './Data/Packet'; import {PacketTypeId} from './Data/Packet';
export {StreamDemo} from './StreamDemo';
export class Demo { export class Demo {
public static fromNodeBuffer(nodeBuffer) { public static fromNodeBuffer(nodeBuffer) {
const arrayBuffer = new ArrayBuffer(nodeBuffer.length); const arrayBuffer = new ArrayBuffer(nodeBuffer.length);
@ -15,10 +12,6 @@ export class Demo {
return new Demo(arrayBuffer); return new Demo(arrayBuffer);
} }
public static fromNodeStream(nodeStream) {
return new StreamDemo(nodeStream);
}
public stream: BitStream; public stream: BitStream;
public parser: Parser | null; public parser: Parser | null;

View file

@ -1,9 +0,0 @@
import {Match} from '../Data/Match';
import {DataTablePacket} from '../Data/Packet';
export function handleDataTable(packet: DataTablePacket, match: Match) {
for (const table of packet.tables) {
match.sendTables.set(table.name, table);
}
match.serverClasses = packet.serverClasses;
}

View file

@ -1,6 +1,6 @@
import {Match} from '../Data/Match';
import {GameEventListPacket} from '../Data/Packet'; import {GameEventListPacket} from '../Data/Packet';
import {ParserState} from '../Data/ParserState';
export function handleGameEventList(packet: GameEventListPacket, match: Match) { export function handleGameEventList(packet: GameEventListPacket, state: ParserState) {
match.eventDefinitions = packet.eventList; state.eventDefinitions = packet.eventList;
} }

View file

@ -10,7 +10,7 @@ import {TeamNumber} from '../Data/Team';
export function handlePacketEntities(packet: PacketEntitiesPacket, match: Match) { export function handlePacketEntities(packet: PacketEntitiesPacket, match: Match) {
for (const removedEntityId of packet.removedEntities) { for (const removedEntityId of packet.removedEntities) {
match.entityClasses.delete(removedEntityId); match.parserState.entityClasses.delete(removedEntityId);
} }
for (const entity of packet.entities) { for (const entity of packet.entities) {
@ -21,10 +21,10 @@ export function handlePacketEntities(packet: PacketEntitiesPacket, match: Match)
function saveEntity(packetEntity: PacketEntity, match: Match) { function saveEntity(packetEntity: PacketEntity, match: Match) {
if (packetEntity.pvs === PVS.DELETE) { if (packetEntity.pvs === PVS.DELETE) {
match.entityClasses.delete(packetEntity.entityIndex); match.parserState.entityClasses.delete(packetEntity.entityIndex);
} }
match.entityClasses.set(packetEntity.entityIndex, packetEntity.serverClass); match.parserState.entityClasses.set(packetEntity.entityIndex, packetEntity.serverClass);
} }
function handleEntity(entity: PacketEntity, match: Match) { function handleEntity(entity: PacketEntity, match: Match) {
@ -80,9 +80,13 @@ function handleEntity(entity: PacketEntity, match: Match) {
* "DT_TFPlayerShared.m_flCloakMeter": 100, * "DT_TFPlayerShared.m_flCloakMeter": 100,
*/ */
const userInfo = match.getUserInfoForEntity(entity);
if (!userInfo) {
throw new Error(`No user info for entity ${entity.entityIndex}`);
}
const player: Player = (match.playerEntityMap.has(entity.entityIndex)) ? const player: Player = (match.playerEntityMap.has(entity.entityIndex)) ?
match.playerEntityMap.get(entity.entityIndex) as Player : match.playerEntityMap.get(entity.entityIndex) as Player :
new Player(match, match.getUserInfoForEntity(entity)); new Player(match, userInfo);
if (!match.playerEntityMap.has(entity.entityIndex)) { if (!match.playerEntityMap.has(entity.entityIndex)) {
match.playerEntityMap.set(entity.entityIndex, player); match.playerEntityMap.set(entity.entityIndex, player);
} }

View file

@ -1,68 +1,53 @@
import {Match} from '../Data/Match';
import {CreateStringTablePacket, StringTablePacket, UpdateStringTablePacket} from '../Data/Packet'; import {CreateStringTablePacket, StringTablePacket, UpdateStringTablePacket} from '../Data/Packet';
import {StringTable, StringTableEntry} from '../Data/StringTable'; import {StringTable, StringTableEntry} from '../Data/StringTable';
import {getStringTable} from '../Data/ParserState'; import {ParserState} from '../Data/ParserState';
export function handleStringTable(packet: CreateStringTablePacket, match: Match) { export function handleStringTable(packet: CreateStringTablePacket, state: ParserState) {
handleTable(packet.table, match); handleTable(packet.table, state);
} }
export function handleStringTables(packet: StringTablePacket, match: Match) { export function handleStringTables(packet: StringTablePacket, state: ParserState) {
for (const table of packet.tables) { for (const table of packet.tables) {
handleTable(table, match); handleTable(table, state);
} }
} }
export function handleStringTableUpdate(packet: UpdateStringTablePacket, match: Match) { export function handleStringTableUpdate(packet: UpdateStringTablePacket, state: ParserState) {
const updatedTable = match.stringTables[packet.tableId]; const updatedTable = state.stringTables[packet.tableId];
handleStringTableEntries(updatedTable.name, packet.entries, match); handleStringTableEntries(updatedTable.name, packet.entries, state);
} }
function handleTable(table: StringTable, match: Match) { export function handleTable(table: StringTable, state: ParserState) {
if (!getStringTable(match, table.name)) { if (!state.getStringTable(table.name)) {
match.stringTables.push(table); state.stringTables.push(table);
} }
handleStringTableEntries(table.name, table.entries, match); handleStringTableEntries(table.name, table.entries, state);
} }
function handleStringTableEntries(tableName: string, entries: StringTableEntry[], match: Match) { function handleStringTableEntries(tableName: string, entries: StringTableEntry[], state: ParserState) {
if (tableName === 'userinfo') { if (tableName === 'userinfo') {
for (const userData of entries) { for (const entry of entries) {
saveUserData(userData, match); if (entry && entry.extraData) {
state.userInfoEntries.set(entry.text, entry.extraData);
}
} }
} }
if (tableName === 'instancebaseline') { if (tableName === 'instancebaseline') {
for (const instanceBaseLine of entries) { for (const instanceBaseLine of entries) {
if (instanceBaseLine) { if (instanceBaseLine) {
saveInstanceBaseLine(instanceBaseLine, match); saveInstanceBaseLine(instanceBaseLine, state);
} }
} }
} }
} }
function saveUserData(userData: StringTableEntry, match: Match) { function saveInstanceBaseLine(entry: StringTableEntry, state: ParserState) {
if (userData && userData.extraData) {
if (userData.extraData.bitsLeft > (32 * 8)) {
const name = userData.extraData.readUTF8String(32);
const userId = userData.extraData.readUint32();
const steamId = userData.extraData.readUTF8String();
if (steamId) {
const userState = match.getUserInfo(userId);
userState.name = name;
userState.steamId = steamId;
userState.entityId = parseInt(userData.text, 10) + 1;
}
}
}
}
function saveInstanceBaseLine(entry: StringTableEntry, match: Match) {
if (entry.extraData) { if (entry.extraData) {
const serverClassId = parseInt(entry.text, 10); const serverClassId = parseInt(entry.text, 10);
match.staticBaselineCache.delete(serverClassId); state.staticBaselineCache.delete(serverClassId);
match.staticBaseLines.set(serverClassId, entry.extraData); state.staticBaseLines.set(serverClassId, entry.extraData);
} else { } else {
throw new Error('Missing baseline'); throw new Error('Missing baseline');
} }

View file

@ -2,19 +2,31 @@ import {BitStream} from 'bit-buffer';
import {EventEmitter} from 'events'; import {EventEmitter} from 'events';
import {Header} from './Data/Header'; import {Header} from './Data/Header';
import {Match} from './Data/Match'; import {Match} from './Data/Match';
import {ConsoleCmd} from './Parser/Message/ConsoleCmd'; import {ConsoleCmdHandler} from './Parser/Message/ConsoleCmd';
import {DataTable} from './Parser/Message/DataTable'; import {DataTableHandler} from './Parser/Message/DataTable';
import {Packet} from './Parser/Message/Packet'; import {PacketMessageHandler} from './Parser/Message/Packet';
import {Parser as MessageParser} from './Parser/Message/Parser'; import {StringTableHandler} from './Parser/Message/StringTable';
import {StringTable} from './Parser/Message/StringTable'; import {UserCmdHandler} from './Parser/Message/UserCmd';
import {UserCmd} from './Parser/Message/UserCmd';
import {PacketTypeId} from './Data/Packet'; import {PacketTypeId} from './Data/Packet';
import {Message, MessageHandler, MessageType, PacketMessage} from './Data/Message';
const messageHandlers: Map<MessageType, MessageHandler<Message>> = new Map<MessageType, MessageHandler<Message>>([
[MessageType.Sigon, PacketMessageHandler],
[MessageType.Packet, PacketMessageHandler],
[MessageType.ConsoleCmd, ConsoleCmdHandler],
[MessageType.UserCmd, UserCmdHandler],
[MessageType.DataTables, DataTableHandler],
[MessageType.StringTables, StringTableHandler],
]);
export class Parser extends EventEmitter { export class Parser extends EventEmitter {
public stream: BitStream; public stream: BitStream;
public match: Match; public match: Match;
protected skipPackets: PacketTypeId[]; protected skipPackets: PacketTypeId[];
public viewOrigin: number[][] = [[], []];
public viewAngles: number[][] = [[], []];
constructor(stream: BitStream, skipPackets: PacketTypeId[] = []) { constructor(stream: BitStream, skipPackets: PacketTypeId[] = []) {
super(); super();
this.stream = stream; this.stream = stream;
@ -28,39 +40,40 @@ export class Parser extends EventEmitter {
} }
public parseBody() { public parseBody() {
let hasNext = true; const messages = this.getMessages();
while (hasNext) { for (const message of messages) {
hasNext = this.tick(); this.handleMessage(message);
} }
this.emit('done', this.match); this.emit('done', this.match);
return this.match; return this.match;
} }
private * getMessages(): Iterable<Message> {
let hasNext: boolean = true;
while (hasNext) {
const message = this.readMessage(this.stream, this.match);
if (!message) {
hasNext = false;
} else {
yield message;
}
}
}
public tick() { public tick() {
const message = this.readMessage(this.stream, this.match); const message = this.readMessage(this.stream, this.match);
if (message instanceof MessageParser) { if (message) {
this.handleMessage(message); this.handleMessage(message);
} }
return !!message; return !!message;
} }
protected parseMessage(data: BitStream, type: MessageType, tick: number, length: number, match: Match): MessageParser { protected parseMessage(data: BitStream, type: MessageType, tick: number, match: Match): Message {
const handler = messageHandlers.get(type);
switch (type) { if (!handler) {
case MessageType.Sigon: throw new Error(`No handler for message of type ${MessageType[type]}`);
case MessageType.Packet:
return new Packet(type, tick, data, length, match, this.skipPackets);
case MessageType.ConsoleCmd:
return new ConsoleCmd(type, tick, data, length, match, this.skipPackets);
case MessageType.UserCmd:
return new UserCmd(type, tick, data, length, match, this.skipPackets);
case MessageType.DataTables:
return new DataTable(type, tick, data, length, match, this.skipPackets);
case MessageType.StringTables:
return new StringTable(type, tick, data, length, match, this.skipPackets);
default:
throw new Error('unknown message type');
} }
return handler.parseMessage(data, tick, match.parserState);
} }
protected parseHeader(stream): Header { protected parseHeader(stream): Header {
@ -79,18 +92,17 @@ export class Parser extends EventEmitter {
}; };
} }
protected handleMessage(message: MessageParser) { protected handleMessage(message: Message) {
if (message.parse) { this.match.parserState.handleMessage(message);
const packets = message.parse(); if (message.type === MessageType.Packet) {
for (const packet of packets) { for (const packet of (message as PacketMessage).packets) {
if (packet) { this.match.parserState.handlePacket(packet);
this.emit('packet', packet); this.emit('packet', packet);
} }
} }
} }
}
protected readMessage(stream: BitStream, match: Match): MessageParser | boolean { protected readMessage(stream: BitStream, match: Match): Message | false {
if (stream.bitsLeft < 8) { if (stream.bitsLeft < 8) {
return false; return false;
} }
@ -100,21 +112,16 @@ export class Parser extends EventEmitter {
} }
const tick = stream.readInt32(); const tick = stream.readInt32();
const viewOrigin: number[][] = [];
const viewAngles: number[][] = [];
switch (type) { switch (type) {
case MessageType.Sigon: case MessageType.Sigon:
case MessageType.Packet: case MessageType.Packet:
this.stream.readInt32(); // flags this.stream.readInt32(); // flags
for (let j = 0; j < 2; j++) { for (let j = 0; j < 2; j++) {
viewOrigin[j] = [];
viewAngles[j] = [];
for (let i = 0; i < 3; i++) { for (let i = 0; i < 3; i++) {
viewOrigin[j][i] = this.stream.readInt32(); this.viewOrigin[j][i] = this.stream.readFloat32();
} }
for (let i = 0; i < 3; i++) { for (let i = 0; i < 3; i++) {
viewAngles[j][i] = this.stream.readInt32(); this.viewAngles[j][i] = this.stream.readFloat32();
} }
for (let i = 0; i < 3; i++) { for (let i = 0; i < 3; i++) {
this.stream.readInt32(); // local viewAngles this.stream.readInt32(); // local viewAngles
@ -127,22 +134,15 @@ export class Parser extends EventEmitter {
stream.byteIndex += 0x04; // unknown / outgoing sequence stream.byteIndex += 0x04; // unknown / outgoing sequence
break; break;
case MessageType.SyncTick: case MessageType.SyncTick:
return true; return {
type: MessageType.SyncTick,
tick,
rawData: stream.readBitStream(0)
};
} }
const length = stream.readInt32(); const length = stream.readInt32();
const buffer = stream.readBitStream(length * 8); const buffer = stream.readBitStream(length * 8);
return this.parseMessage(buffer, type, tick, length, match); return this.parseMessage(buffer, type, tick, match);
} }
} }
export enum MessageType {
Sigon = 1,
Packet = 2,
SyncTick = 3,
ConsoleCmd = 4,
UserCmd = 5,
DataTables = 6,
Stop = 7,
StringTables = 8,
}

View file

@ -1,5 +1,7 @@
import {ConsoleCmdPacket} from '../../Data/Packet'; import {ConsoleCmdPacket} from '../../Data/Packet';
import {Parser} from './Parser'; import {Parser} from './Parser';
import {BitStream} from 'bit-buffer';
import {ConsoleCmdMessage, MessageHandler, MessageType} from '../../Data/Message';
export class ConsoleCmd extends Parser { export class ConsoleCmd extends Parser {
public parse(): ConsoleCmdPacket[] { public parse(): ConsoleCmdPacket[] {
@ -9,3 +11,17 @@ export class ConsoleCmd extends Parser {
}]; }];
} }
} }
export const ConsoleCmdHandler: MessageHandler<ConsoleCmdMessage> = {
parseMessage: (stream: BitStream, tick: number) => {
return {
type: MessageType.ConsoleCmd,
tick,
rawData: stream,
command: stream.readUTF8String()
};
},
encodeMessage: (message: ConsoleCmdMessage, stream: BitStream) => {
stream.writeUTF8String(message.command);
}
};

View file

@ -1,41 +1,41 @@
import {DataTablePacket} from '../../Data/Packet';
import {SendPropDefinition, SendPropFlag, SendPropType} from '../../Data/SendPropDefinition'; import {SendPropDefinition, SendPropFlag, SendPropType} from '../../Data/SendPropDefinition';
import {SendTable} from '../../Data/SendTable'; import {SendTable} from '../../Data/SendTable';
import {ServerClass} from '../../Data/ServerClass'; import {ServerClass} from '../../Data/ServerClass';
import {Parser} from './Parser'; import {DataTablesMessage, MessageHandler, MessageType} from '../../Data/Message';
import {BitStream} from 'bit-buffer';
export class DataTable extends Parser { export const DataTableHandler: MessageHandler<DataTablesMessage> = {
public parse(): DataTablePacket[] { parseMessage: (stream: BitStream, tick: number) => {
// https://github.com/LestaD/SourceEngine2007/blob/43a5c90a5ada1e69ca044595383be67f40b33c61/src_main/engine/dt_common_eng.cpp#L356 // https://github.com/LestaD/SourceEngine2007/blob/43a5c90a5ada1e69ca044595383be67f40b33c61/src_main/engine/dt_common_eng.cpp#L356
// https://github.com/LestaD/SourceEngine2007/blob/43a5c90a5ada1e69ca044595383be67f40b33c61/src_main/engine/dt_recv_eng.cpp#L310 // https://github.com/LestaD/SourceEngine2007/blob/43a5c90a5ada1e69ca044595383be67f40b33c61/src_main/engine/dt_recv_eng.cpp#L310
// https://github.com/PazerOP/DemoLib/blob/master/DemoLib/Commands/DemoDataTablesCommand.cs // https://github.com/PazerOP/DemoLib/blob/master/DemoLib/Commands/DemoDataTablesCommand.cs
const tables: SendTable[] = []; const tables: SendTable[] = [];
const tableMap: {[key: string]: SendTable} = {}; const tableMap: {[key: string]: SendTable} = {};
while (this.stream.readBoolean()) { while (stream.readBoolean()) {
const needsDecoder = this.stream.readBoolean(); const needsDecoder = stream.readBoolean();
const tableName = this.stream.readASCIIString(); const tableName = stream.readASCIIString();
const numProps = this.stream.readBits(10); const numProps = stream.readBits(10);
const table = new SendTable(tableName); const table = new SendTable(tableName);
// get props metadata // get props metadata
let arrayElementProp; let arrayElementProp;
for (let i = 0; i < numProps; i++) { for (let i = 0; i < numProps; i++) {
const propType = this.stream.readBits(5); const propType = stream.readBits(5);
const propName = this.stream.readASCIIString(); const propName = stream.readASCIIString();
const nFlagsBits = 16; // might be 11 (old?), 13 (new?), 16(networked) or 17(??) const nFlagsBits = 16; // might be 11 (old?), 13 (new?), 16(networked) or 17(??)
const flags = this.stream.readBits(nFlagsBits); const flags = stream.readBits(nFlagsBits);
const prop = new SendPropDefinition(propType, propName, flags, tableName); const prop = new SendPropDefinition(propType, propName, flags, tableName);
if (propType === SendPropType.DPT_DataTable) { if (propType === SendPropType.DPT_DataTable) {
prop.excludeDTName = this.stream.readASCIIString(); prop.excludeDTName = stream.readASCIIString();
} else { } else {
if (prop.isExcludeProp()) { if (prop.isExcludeProp()) {
prop.excludeDTName = this.stream.readASCIIString(); prop.excludeDTName = stream.readASCIIString();
} else if (prop.type === SendPropType.DPT_Array) { } else if (prop.type === SendPropType.DPT_Array) {
prop.numElements = this.stream.readBits(10); prop.numElements = stream.readBits(10);
} else { } else {
prop.lowValue = this.stream.readFloat32(); prop.lowValue = stream.readFloat32();
prop.highValue = this.stream.readFloat32(); prop.highValue = stream.readFloat32();
prop.bitCount = this.stream.readBits(7); prop.bitCount = stream.readBits(7);
} }
} }
@ -85,31 +85,36 @@ export class DataTable extends Parser {
} }
} }
const numServerClasses = this.stream.readUint16(); // short const numServerClasses = stream.readUint16(); // short
const serverClasses: ServerClass[] = []; const serverClasses: ServerClass[] = [];
if (numServerClasses <= 0) { if (numServerClasses <= 0) {
throw new Error('expected one or more serverclasses'); throw new Error('expected one or more serverclasses');
} }
for (let i = 0; i < numServerClasses; i++) { for (let i = 0; i < numServerClasses; i++) {
const classId = this.stream.readUint16(); const classId = stream.readUint16();
if (classId > numServerClasses) { if (classId > numServerClasses) {
throw new Error('invalid class id'); throw new Error('invalid class id');
} }
const className = this.stream.readASCIIString(); const className = stream.readASCIIString();
const dataTable = this.stream.readASCIIString(); const dataTable = stream.readASCIIString();
serverClasses.push(new ServerClass(classId, className, dataTable)); serverClasses.push(new ServerClass(classId, className, dataTable));
} }
const bitsLeft = (this.length * 8) - this.stream.index; const bitsLeft = (this.length * 8) - stream.index;
if (bitsLeft > 7 || bitsLeft < 0) { if (bitsLeft > 7 || bitsLeft < 0) {
throw new Error('unexpected remaining data in datatable (' + bitsLeft + ' bits)'); throw new Error('unexpected remaining data in datatable (' + bitsLeft + ' bits)');
} }
return [{ return {
packetType: 'dataTable', type: MessageType.DataTables,
tick,
rawData: stream,
tables, tables,
serverClasses, serverClasses,
}]; };
},
encodeMessage: (message, stream) => {
throw new Error('Not implemented');
} }
} };

View file

@ -13,14 +13,15 @@ import {EncodeUpdateStringTable, ParseUpdateStringTable} from '../Packet/UpdateS
import {EncodeUserMessage, ParseUserMessage} from '../Packet/UserMessage'; import {EncodeUserMessage, ParseUserMessage} from '../Packet/UserMessage';
import {EncodeVoiceData, ParseVoiceData} from '../Packet/VoiceData'; import {EncodeVoiceData, ParseVoiceData} from '../Packet/VoiceData';
import {EncodeVoiceInit, ParseVoiceInit} from '../Packet/VoiceInit'; import {EncodeVoiceInit, ParseVoiceInit} from '../Packet/VoiceInit';
import {Parser} from './Parser';
import {Packet as IPacket, PacketTypeId} from '../../Data/Packet'; import {Packet as IPacket, PacketTypeId} from '../../Data/Packet';
import {MessageHandler, MessageType, PacketMessage} from '../../Data/Message';
import {BitStream} from 'bit-buffer';
import {ParserState} from '../../Data/ParserState';
type PacketHandlerMap = Map<PacketTypeId, PacketHandler<IPacket>>; type PacketHandlerMap = Map<PacketTypeId, PacketHandler<IPacket>>;
export class Packet extends Parser { const handlers: PacketHandlerMap = new Map<PacketTypeId, PacketHandler<IPacket>>([
private static handlers: PacketHandlerMap = new Map<PacketTypeId, PacketHandler<IPacket>>([
[PacketTypeId.file, [PacketTypeId.file,
make('file', 'transferId{32}fileName{s}requested{b}')], make('file', 'transferId{32}fileName{s}requested{b}')],
[PacketTypeId.netTick, [PacketTypeId.netTick,
@ -78,18 +79,19 @@ export class Packet extends Parser {
make('getCvarValue', 'cookie{32}value{s}')], make('getCvarValue', 'cookie{32}value{s}')],
[PacketTypeId.cmdKeyValues, [PacketTypeId.cmdKeyValues,
make('cmdKeyValues', 'length{32}data{$length}')], make('cmdKeyValues', 'length{32}data{$length}')],
]); ]);
public parse() { export const PacketMessageHandler: MessageHandler<PacketMessage> = {
parseMessage: (stream: BitStream, tick: number, state: ParserState) => {
const packets: IPacket[] = []; const packets: IPacket[] = [];
let lastPacketType = 0; let lastPacketType = 0;
while (this.bitsLeft > 6) { // last 6 bits for NOOP while (stream.bitsLeft > 6) { // last 6 bits for NOOP
const type = this.stream.readBits(6) as PacketTypeId; const type = stream.readBits(6) as PacketTypeId;
if (type !== 0) { if (type !== 0) {
const parser = Packet.handlers.get(type); const parser = handlers.get(type);
if (parser) { if (parser) {
const skip = this.skippedPackets.indexOf(type) !== -1; const skip = state.skippedPackets.indexOf(type) !== -1;
const packet = parser.parser(this.stream, this.state, skip); const packet = parser.parser(stream, state, skip);
packets.push(packet); packets.push(packet);
} else { } else {
throw new Error(`Unknown packet type ${type} just parsed a ${PacketTypeId[lastPacketType]}`); throw new Error(`Unknown packet type ${type} just parsed a ${PacketTypeId[lastPacketType]}`);
@ -97,10 +99,14 @@ export class Packet extends Parser {
lastPacketType = type; lastPacketType = type;
} }
} }
return packets; return {
type: MessageType.Packet,
tick,
rawData: stream,
packets
};
},
encodeMessage: (message, stream) => {
throw new Error('Not implemented');
} }
};
get bitsLeft() {
return (this.length * 8) - this.stream.index;
}
}

View file

@ -1,6 +1,6 @@
import {BitStream} from 'bit-buffer'; import {BitStream} from 'bit-buffer';
import {Packet, PacketTypeId} from '../../Data/Packet'; import {Packet, PacketTypeId} from '../../Data/Packet';
import {MessageType} from '../../Parser'; import {MessageType} from '../../Data/Message';
import {ParserState} from '../../Data/ParserState'; import {ParserState} from '../../Data/ParserState';
export abstract class Parser { export abstract class Parser {

View file

@ -1,45 +1,49 @@
import {StringTablePacket} from '../../Data/Packet';
import {StringTable as StringTableObject, StringTableEntry} from '../../Data/StringTable'; import {StringTable as StringTableObject, StringTableEntry} from '../../Data/StringTable';
import {Parser} from './Parser'; import {MessageHandler, MessageType, StringTablesMessage} from '../../Data/Message';
import {BitStream} from 'bit-buffer';
export class StringTable extends Parser { export const StringTableHandler: MessageHandler<StringTablesMessage> = {
public parse(): StringTablePacket[] { parseMessage: (stream: BitStream, tick: number) => {
// we get the tables from the packets // we get the tables from the packets
// return [{ // return [{
// packetType: 'stringTable', // packetType: 'stringTable',
// tables: [] // tables: []
// }]; // }];
// https://github.com/StatsHelix/demoinfo/blob/3d28ea917c3d44d987b98bb8f976f1a3fcc19821/DemoInfo/ST/StringTableParser.cs // https://github.com/StatsHelix/demoinfo/blob/3d28ea917c3d44d987b98bb8f976f1a3fcc19821/DemoInfo/ST/StringTableParser.cs
const tableCount = this.stream.readUint8(); const tableCount = stream.readUint8();
const tables: StringTableObject[] = []; const tables: StringTableObject[] = [];
let extraDataLength; let extraDataLength;
for (let i = 0; i < tableCount; i++) { for (let i = 0; i < tableCount; i++) {
const entries: StringTableEntry[] = []; const entries: StringTableEntry[] = [];
const tableName = this.stream.readASCIIString(); const tableName = stream.readASCIIString();
const entryCount = this.stream.readUint16(); const entryCount = stream.readUint16();
for (let j = 0; j < entryCount; j++) { for (let j = 0; j < entryCount; j++) {
let entry: StringTableEntry; let entry: StringTableEntry;
try { try {
entry = { entry = {
text: this.stream.readUTF8String(), text: stream.readUTF8String(),
}; };
} catch (e) { } catch (e) {
return [{ return {
packetType: 'stringTable', type: MessageType.StringTables,
tick,
rawData: stream,
tables, tables,
}]; };
} }
if (this.stream.readBoolean()) { if (stream.readBoolean()) {
extraDataLength = this.stream.readUint16(); extraDataLength = stream.readUint16();
if ((extraDataLength * 8) > this.stream.bitsLeft) { if ((extraDataLength * 8) > stream.bitsLeft) {
// extradata to long, can't continue parsing the tables // extradata to long, can't continue parsing the tables
// seems to happen in POV demos after the MyM update // seems to happen in POV demos after the MyM update
return [{ return {
packetType: 'stringTable', type: MessageType.StringTables,
tick,
rawData: stream,
tables, tables,
}]; };
} }
entry.extraData = this.stream.readBitStream(extraDataLength * 8); entry.extraData = stream.readBitStream(extraDataLength * 8);
} }
entries.push(entry); entries.push(entry);
} }
@ -49,18 +53,23 @@ export class StringTable extends Parser {
maxEntries: entryCount, maxEntries: entryCount,
}; };
tables.push(table); tables.push(table);
if (this.stream.readBits(1)) { if (stream.readBits(1)) {
this.stream.readASCIIString(); stream.readASCIIString();
if (this.stream.readBits(1)) { if (stream.readBits(1)) {
// throw 'more extra data not implemented'; // throw 'more extra data not implemented';
extraDataLength = this.stream.readBits(16); extraDataLength = stream.readBits(16);
this.stream.readBits(extraDataLength); stream.readBits(extraDataLength);
} }
} }
} }
return [{ return {
packetType: 'stringTable', type: MessageType.StringTables,
tick,
rawData: stream,
tables, tables,
}]; };
},
encodeMessage: (message, stream) => {
throw new Error('Not implemented');
} }
} };

View file

@ -1,7 +1,15 @@
import {Parser} from './Parser'; import {MessageHandler, MessageType, UserCmdMessage} from '../../Data/Message';
import {BitStream} from 'bit-buffer';
export class UserCmd extends Parser { export const UserCmdHandler: MessageHandler<UserCmdMessage> = {
public parse() { parseMessage: (stream: BitStream, tick: number) => {
return []; return {
type: MessageType.UserCmd,
tick,
rawData: stream
};
},
encodeMessage: (message, stream) => {
throw new Error('not implemented');
} }
} };

View file

@ -1,14 +0,0 @@
import {Stream} from 'stream';
import {StreamParser} from './StreamParser';
export class StreamDemo {
public stream: Stream;
constructor(nodeStream: Stream) {
this.stream = nodeStream;
}
public getParser() {
return new StreamParser(this.stream);
}
}

View file

@ -1,88 +0,0 @@
import {BitStream} from 'bit-buffer';
import {Buffer} from 'buffer';
import {Stream} from 'stream';
import {MessageType, Parser} from './Parser';
export class StreamParser extends Parser {
public header: any;
private buffer: Buffer;
private sourceStream: Stream;
constructor(stream: Stream) {
super(new BitStream(new ArrayBuffer(0)));
this.sourceStream = stream;
this.on('packet', this.match.handlePacket.bind(this.match));
this.header = null;
this.buffer = new Buffer(0);
}
public start() {
this.sourceStream.on('data', this.handleData.bind(this));
this.sourceStream.on('end', function() {
this.emit('done', this.match);
}.bind(this));
}
private eatBuffer(length) {
this.buffer = shrinkBuffer(this.buffer, length);
}
private handleData(data) {
this.buffer = Buffer.concat([this.buffer, data]);
if (this.header === null) {
if (this.buffer.length > 1072) {
this.header = this.parseHeader(new BitStream(this.buffer));
this.eatBuffer(1072);
}
} else {
this.readStreamMessage();
}
}
private readStreamMessage() {
if (this.buffer.length < 9) { // 9 byte minimum message header (type, tick, length)
return;
}
const stream = new BitStream(this.buffer);
const type = stream.readBits(8);
if (type === MessageType.Stop) {
return;
}
const tick = stream.readInt32();
let headerSize = 5;
let extraHeader = 0;
switch (type) {
case MessageType.Sigon:
case MessageType.Packet:
extraHeader += 0x54; // command/sequence info
break;
case MessageType.UserCmd:
extraHeader += 0x04; // unknown / outgoing sequence
break;
case MessageType.Stop:
case MessageType.SyncTick:
this.eatBuffer(headerSize);
return;
}
stream.byteIndex += extraHeader;
const length = stream.readInt32();
headerSize += extraHeader + 4;
if (this.buffer.length < (headerSize + length)) {
return;
}
const messageStream = stream.readBitStream(length * 8);
const message = this.parseMessage(messageStream, type, tick, length, this.match);
this.handleMessage(message);
}
}
function shrinkBuffer(buffer, length) {
if (length < 0) {
throw new Error('cant shrink by negative length ' + length);
}
return buffer.slice(length, buffer.length);
}

View file

@ -1,6 +1,5 @@
export {Demo} from './Demo'; export {Demo} from './Demo';
export {Parser} from './Parser'; export {Parser} from './Parser';
export {StreamParser} from './StreamParser';
export {Match} from './Data/Match'; export {Match} from './Data/Match';
export {Player} from './Data/Player'; export {Player} from './Data/Player';
export {PlayerCondition} from './Data/PlayerCondition'; export {PlayerCondition} from './Data/PlayerCondition';