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:
parent
d8f01428c5
commit
198fe0b1ba
18 changed files with 459 additions and 421 deletions
|
|
@ -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
74
src/Data/Message.ts
Normal 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;
|
||||||
|
}
|
||||||
|
|
@ -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;
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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;
|
|
||||||
}
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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');
|
||||||
}
|
}
|
||||||
|
|
|
||||||
106
src/Parser.ts
106
src/Parser.ts
|
|
@ -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,
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
|
||||||
|
|
@ -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');
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
@ -80,16 +81,17 @@ export class Packet extends Parser {
|
||||||
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,
|
||||||
get bitsLeft() {
|
rawData: stream,
|
||||||
return (this.length * 8) - this.stream.index;
|
packets
|
||||||
}
|
};
|
||||||
|
},
|
||||||
|
encodeMessage: (message, stream) => {
|
||||||
|
throw new Error('Not implemented');
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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');
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
|
||||||
|
|
@ -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');
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
|
||||||
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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);
|
|
||||||
}
|
|
||||||
|
|
@ -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';
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue