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

strict game event types

This commit is contained in:
Robin Appelman 2017-09-02 22:44:10 +02:00
commit a7911f2d3d
11 changed files with 3925 additions and 96 deletions

View file

@ -25,3 +25,7 @@ unit: node_modules
.PHONY: lint .PHONY: lint
lint: node_modules lint: node_modules
node_modules/.bin/tslint -p tsconfig.json node_modules/.bin/tslint -p tsconfig.json
.PHONY: src/Data/GameEventTypes.ts
src/Data/GameEventTypes.ts:
node bin/analyse.js --create-event-definitions src/tests/data/celt.dem > src/Data/GameEventTypes.ts

View file

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

View file

@ -1,20 +1,17 @@
export interface GameEventDefinition { import {GameEvent} from './GameEventTypes';
id: number;
name: string;
entries: GameEventEntry[];
}
export interface GameEvent { export interface GameEventDefinition<T extends GameEvent['name']> {
name: string; id: number;
values: GameEventValues; name: T;
entries: GameEventEntry[];
} }
export interface GameEventEntry { export interface GameEventEntry {
name: string; name: string;
type: GameEventType; type: GameEventValueType;
} }
export enum GameEventType { export enum GameEventValueType {
STRING = 1, STRING = 1,
FLOAT = 2, FLOAT = 2,
LONG = 3, LONG = 3,
@ -24,42 +21,8 @@ export enum GameEventType {
LOCAL = 7, LOCAL = 7,
} }
export interface DeathEventValues {
attacker: number;
userid: number;
assister: number;
weapon: string;
}
export interface RoundWinEventValues {
winreason: number;
team: number;
round_time: number;
}
export interface PlayerSpawnEventValues {
userid: number;
team: number;
'class': number;
}
export interface ObjectDestroyedValues {
userid: number;
attacker: number;
weapon: string;
weapinid: number;
objecttype: number;
index: number;
}
export type GameEventValue = string | number | boolean; export type GameEventValue = string | number | boolean;
export interface GameEventValueMap { export interface GameEventValues {
[name: string]: GameEventValue; [name: string]: GameEventValue;
} }
export type GameEventValues = GameEventValueMap |
DeathEventValues |
RoundWinEventValues |
PlayerSpawnEventValues |
ObjectDestroyedValues;

3730
src/Data/GameEventTypes.ts Normal file

File diff suppressed because it is too large Load diff

View file

@ -20,6 +20,7 @@ import {Weapon} from './Weapon';
import {World} from './World'; import {World} from './World';
import {Round} from './Round'; import {Round} from './Round';
import {Chat} from './Chat'; import {Chat} from './Chat';
import {GameEvent} from './GameEventTypes';
export class Match { export class Match {
public tick: number = 0; public tick: number = 0;
@ -30,7 +31,7 @@ export class Match {
public startTick: number = 0; public startTick: number = 0;
public intervalPerTick: number = 0; public intervalPerTick: number = 0;
public staticBaseLines: BitStream[] = []; public staticBaseLines: BitStream[] = [];
public eventDefinitions: Map<number, GameEventDefinition> = new Map(); public eventDefinitions: Map<number, GameEventDefinition<GameEvent['name']>> = 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},

View file

@ -1,10 +1,11 @@
import {BitStream} from 'bit-buffer'; import {BitStream} from 'bit-buffer';
import {GameEvent, GameEventDefinition} from './GameEvent'; import {GameEventDefinition} from './GameEvent';
import {PacketEntity} from './PacketEntity'; import {PacketEntity} from './PacketEntity';
import {SendTable} from './SendTable'; import {SendTable} from './SendTable';
import {ServerClass} from './ServerClass'; import {ServerClass} from './ServerClass';
import {StringTable, StringTableEntry} from './StringTable'; import {StringTable, StringTableEntry} from './StringTable';
import {Vector} from './Vector'; import {Vector} from './Vector';
import {GameEvent, GameEventType} from './GameEventTypes';
export interface BasePacket { export interface BasePacket {
} }
@ -70,7 +71,7 @@ export interface GameEventPacket extends BasePacket {
export interface GameEventListPacket extends BasePacket { export interface GameEventListPacket extends BasePacket {
packetType: 'gameEventList'; packetType: 'gameEventList';
eventList: Map<number, GameEventDefinition>; eventList: Map<number, GameEventDefinition<GameEvent['name']>>;
} }
export interface PacketEntitiesPacket extends BasePacket { export interface PacketEntitiesPacket extends BasePacket {

View file

@ -1,29 +1,31 @@
import {DeathEventValues, ObjectDestroyedValues, PlayerSpawnEventValues, RoundWinEventValues} from '../Data/GameEvent';
import {Match} from '../Data/Match'; import {Match} from '../Data/Match';
import {GameEventPacket} from '../Data/Packet'; import {GameEventPacket} from '../Data/Packet';
import {
ObjectDestroyedEvent, PlayerDeathEvent, PlayerSpawnEvent, TeamPlayRoundStartEvent, TeamPlayRoundWinEvent
} from '../Data/GameEventTypes';
export function handleGameEvent(packet: GameEventPacket, match: Match) { export function handleGameEvent(packet: GameEventPacket, match: Match) {
switch (packet.event.name) { switch (packet.event.name) {
case 'player_death': case 'player_death':
handlePlayerDeath(packet, match); handlePlayerDeath(packet.event, match);
break; break;
case 'teamplay_round_win': case 'teamplay_round_win':
handleRoundWin(packet, match); handleRoundWin(packet.event, match);
break; break;
case 'player_spawn': case 'player_spawn':
handlePlayerSpawn(packet, match); handlePlayerSpawn(packet.event, match);
break; break;
case 'object_destroyed': case 'object_destroyed':
handleObjectDestroyed(packet, match); handleObjectDestroyed(packet.event, match);
break; break;
case 'teamplay_round_start': case 'teamplay_round_start':
handleRoundStart(packet, match); handleRoundStart(packet.event, match);
break; break;
} }
} }
function handlePlayerDeath(packet: GameEventPacket, match: Match) { function handlePlayerDeath(event: PlayerDeathEvent, match: Match) {
const values = packet.event.values as DeathEventValues; const values = event.values;
while (values.assister > 256 && values.assister < (1024 * 16)) { while (values.assister > 256 && values.assister < (1024 * 16)) {
values.assister -= 256; values.assister -= 256;
} }
@ -44,8 +46,8 @@ function handlePlayerDeath(packet: GameEventPacket, match: Match) {
}); });
} }
function handleRoundWin(packet: GameEventPacket, match: Match) { function handleRoundWin(event: TeamPlayRoundWinEvent, match: Match) {
const values = packet.event.values as RoundWinEventValues; const values = event.values;
if (values.winreason !== 6) {// 6 = timelimit if (values.winreason !== 6) {// 6 = timelimit
match.rounds.push({ match.rounds.push({
winner: values.team === 2 ? 'red' : 'blue', winner: values.team === 2 ? 'red' : 'blue',
@ -55,8 +57,8 @@ function handleRoundWin(packet: GameEventPacket, match: Match) {
} }
} }
function handlePlayerSpawn(packet: GameEventPacket, match: Match) { function handlePlayerSpawn(event: PlayerSpawnEvent, match: Match) {
const values = packet.event.values as PlayerSpawnEventValues; const values = event.values;
const userId = values.userid; const userId = values.userid;
const userState = match.getUserInfo(userId); const userState = match.getUserInfo(userId);
const player = match.playerEntityMap.get(userState.entityId); const player = match.playerEntityMap.get(userState.entityId);
@ -72,11 +74,11 @@ function handlePlayerSpawn(packet: GameEventPacket, match: Match) {
userState.classes[classId]++; userState.classes[classId]++;
} }
function handleObjectDestroyed(packet: GameEventPacket, match: Match) { function handleObjectDestroyed(event: ObjectDestroyedEvent, match: Match) {
const values = packet.event.values as ObjectDestroyedValues; const values = event.values;
match.buildings.delete(values.index); match.buildings.delete(values.index);
} }
function handleRoundStart(packet: GameEventPacket, match: Match) { function handleRoundStart(event: TeamPlayRoundStartEvent, match: Match) {
match.buildings.clear(); match.buildings.clear();
} }

View file

@ -1,47 +1,48 @@
import {BitStream} from 'bit-buffer'; import {BitStream} from 'bit-buffer';
import { import {
GameEvent as IGameEvent, GameEventDefinition, GameEventEntry, GameEventType, GameEventDefinition, GameEventEntry,
GameEventValue, GameEventValueMap, GameEventValue, GameEventValueType,
} from '../../Data/GameEvent'; } from '../../Data/GameEvent';
import {GameEvent, GameEventType} from '../../Data/GameEventTypes';
import {Match} from '../../Data/Match'; import {Match} from '../../Data/Match';
import {GameEventPacket} from '../../Data/Packet'; import {GameEventPacket} from '../../Data/Packet';
function parseGameEvent(eventId: number, stream: BitStream, events: Map<number, GameEventDefinition>): IGameEvent { function parseGameEvent(eventId: number, stream: BitStream, events: Map<number, GameEventDefinition<GameEventType>>) {
const eventDescription = events.get(eventId); const eventDescription = events.get(eventId);
if (!eventDescription) { if (!eventDescription) {
throw new Error('unknown event type'); throw new Error('unknown event type');
} }
const values: GameEventValueMap = {}; const values: GameEvent['values'] = {};
for (const entry of eventDescription.entries) { for (const entry of eventDescription.entries) {
const value = getGameEventValue(stream, entry); const value = getGameEventValue(stream, entry);
if (value) { if (value) {
values[entry.name] = value; values[entry.name] = value;
} }
} }
const name = eventDescription.name;
return { return {
name: eventDescription.name, name,
values, values,
}; };
} }
function getGameEventValue(stream: BitStream, entry: GameEventEntry): GameEventValue | null { function getGameEventValue(stream: BitStream, entry: GameEventEntry): GameEventValue | null {
switch (entry.type) { switch (entry.type) {
case GameEventType.STRING: case GameEventValueType.STRING:
return stream.readUTF8String(); return stream.readUTF8String();
case GameEventType.FLOAT: case GameEventValueType.FLOAT:
return stream.readFloat32(); return stream.readFloat32();
case GameEventType.LONG: case GameEventValueType.LONG:
return stream.readUint32(); return stream.readUint32();
case GameEventType.SHORT: case GameEventValueType.SHORT:
return stream.readUint16(); return stream.readUint16();
case GameEventType.BYTE: case GameEventValueType.BYTE:
return stream.readUint8(); return stream.readUint8();
case GameEventType.BOOLEAN: case GameEventValueType.BOOLEAN:
return stream.readBoolean(); return stream.readBoolean();
case GameEventType.LOCAL: case GameEventValueType.LOCAL:
return null; return null;
default:
throw new Error('invalid game event type');
} }
} }
@ -53,6 +54,6 @@ export function ParseGameEvent(stream: BitStream, match: Match): GameEventPacket
stream.index = end; stream.index = end;
return { return {
packetType: 'gameEvent', packetType: 'gameEvent',
event, event: event as GameEvent,
}; };
} }

View file

@ -1,6 +1,7 @@
import {BitStream} from 'bit-buffer'; import {BitStream} from 'bit-buffer';
import {GameEventDefinition, GameEventEntry} from '../../Data/GameEvent'; import {GameEventDefinition, GameEventEntry} from '../../Data/GameEvent';
import {GameEventListPacket} from '../../Data/Packet'; import {GameEventListPacket} from '../../Data/Packet';
import {GameEvent} from '../../Data/GameEventTypes';
export function ParseGameEventList(stream: BitStream): GameEventListPacket { // 30: gameEventList export function ParseGameEventList(stream: BitStream): GameEventListPacket { // 30: gameEventList
const s = stream.index; const s = stream.index;
@ -8,10 +9,10 @@ export function ParseGameEventList(stream: BitStream): GameEventListPacket { //
// list of game events and parameters // list of game events and parameters
const numEvents = stream.readBits(9); const numEvents = stream.readBits(9);
const length = stream.readBits(20); const length = stream.readBits(20);
const eventList: Map<number, GameEventDefinition> = new Map(); const eventList: Map<number, GameEventDefinition<GameEvent['name']>> = new Map();
for (let i = 0; i < numEvents; i++) { for (let i = 0; i < numEvents; i++) {
const id = stream.readBits(9); const id = stream.readBits(9);
const name = stream.readASCIIString(); const name = stream.readASCIIString() as GameEvent['name'];
let type = stream.readBits(3); let type = stream.readBits(3);
const entries: GameEventEntry[] = []; const entries: GameEventEntry[] = [];
while (type !== 0) { while (type !== 0) {
@ -57,8 +58,8 @@ export function EncodeGameEventList(packet: GameEventListPacket, stream: BitStre
stream.writeBitStream(eventListStream); stream.writeBitStream(eventListStream);
} }
function getEventListLength(eventList: GameEventDefinition[]) { function getEventListLength(eventList: GameEventDefinition<GameEvent['name']>[]) {
return eventList.reduce((length: number, entry: GameEventDefinition) => { return eventList.reduce((length: number, entry: GameEventDefinition<GameEvent['name']>) => {
return length + return length +
9 + 9 +
(entry.name.length + 1) * 8 + (entry.name.length + 1) * 8 +

View file

@ -4,7 +4,7 @@ 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';
export {GameEvent} from './Data/GameEvent'; export {GameEvent} from './Data/GameEventTypes';
export {PacketEntity} from './Data/PacketEntity'; export {PacketEntity} from './Data/PacketEntity';
export {SendPropDefinition, SendPropFlag, SendPropType} from './Data/SendPropDefinition'; export {SendPropDefinition, SendPropFlag, SendPropType} from './Data/SendPropDefinition';
export {SendProp} from './Data/SendProp'; export {SendProp} from './Data/SendProp';

View file

@ -3,6 +3,7 @@ import {assertEncoder, assertParser, getStream} from './PacketTest';
import {readFileSync} from 'fs'; import {readFileSync} from 'fs';
import {EncodeGameEventList, ParseGameEventList} from '../../../../Parser/Packet/GameEventList'; import {EncodeGameEventList, ParseGameEventList} from '../../../../Parser/Packet/GameEventList';
import {GameEventListPacket} from '../../../../Data/Packet'; import {GameEventListPacket} from '../../../../Data/Packet';
import {GameEvent} from '../../../../Data/GameEventTypes';
const data = JSON.parse(readFileSync(__dirname + '/../../../data/gameEventListData.json', 'utf8')); const data = JSON.parse(readFileSync(__dirname + '/../../../data/gameEventListData.json', 'utf8'));
const expectedSource = JSON.parse(readFileSync(__dirname + '/../../../data/gameEventList.json', 'utf8')); const expectedSource = JSON.parse(readFileSync(__dirname + '/../../../data/gameEventList.json', 'utf8'));
@ -17,7 +18,7 @@ const eventList: GameEventListPacket = {
'eventList': new Map([ 'eventList': new Map([
[0, { [0, {
'id': 0, 'id': 0,
'name': 'server_spawn', 'name': 'server_spawn' as GameEvent['name'],
'entries': [ 'entries': [
{ {
'type': 1, 'type': 1,
@ -63,7 +64,7 @@ const eventList: GameEventListPacket = {
}], }],
[1, { [1, {
'id': 1, 'id': 1,
'name': 'server_changelevel_failed', 'name': 'server_changelevel_failed' as GameEvent['name'],
'entries': [ 'entries': [
{ {
'type': 1, 'type': 1,
@ -73,7 +74,7 @@ const eventList: GameEventListPacket = {
}], }],
[2, { [2, {
'id': 2, 'id': 2,
'name': 'server_shutdown', 'name': 'server_shutdown' as GameEvent['name'],
'entries': [ 'entries': [
{ {
'type': 1, 'type': 1,