1
0
Fork 0
mirror of https://github.com/demostf/demo.js synced 2026-06-03 16:44:12 +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
lint: node_modules
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();
var Demo = require('../index');
var fs = require('fs');
var argv = require('minimist')(process.argv.slice(2), {boolean: true});
const Demo = require('../index');
const fs = require('fs');
const argv = require('minimist')(process.argv.slice(2), {boolean: true});
if (argv._.length !== 1) {
console.log('Usage: "node analyse [--strings] [--dump] [--head] FILE"');
console.log('Usage: "node analyse [--strings] [--dump] [--head] [--event-list] [--create-event-definitions] FILE"');
process.exit(1);
}
var echo = function (data) {
var string = JSON.stringify(data, null, 2);
const echo = function (data) {
const string = JSON.stringify(data, null, 2);
console.log(string);
};
fs.readFile(argv._[0], function (err, data) {
if (err) throw err;
var demo = Demo.fromNodeBuffer(data);
var parser = demo.getParser(true);
var head = parser.readHeader();
const demo = Demo.fromNodeBuffer(data);
const parser = demo.getParser(true);
const head = parser.readHeader();
if (argv.head) {
echo(head);
return;
}
var match = parser.parseBody();
if (argv.dump) {
const match = parser.parseBody();
if (argv['create-event-definitions']) {
const definitions = Array.from(parser.match.eventDefinitions.values());
const definition = definitions
.map(createEventDefinition)
.join('\n\n')
+ '\n\n' + createEventDefinitionUnion(definitions) + '\n\n'
+ 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);
} else if (argv.strings) {
echo(parser.match.strings);
@ -32,3 +42,118 @@ fs.readFile(argv._[0], function (err, data) {
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 {
id: number;
name: string;
entries: GameEventEntry[];
}
import {GameEvent} from './GameEventTypes';
export interface GameEvent {
name: string;
values: GameEventValues;
export interface GameEventDefinition<T extends GameEvent['name']> {
id: number;
name: T;
entries: GameEventEntry[];
}
export interface GameEventEntry {
name: string;
type: GameEventType;
type: GameEventValueType;
}
export enum GameEventType {
export enum GameEventValueType {
STRING = 1,
FLOAT = 2,
LONG = 3,
@ -24,42 +21,8 @@ export enum GameEventType {
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 interface GameEventValueMap {
export interface GameEventValues {
[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 {Round} from './Round';
import {Chat} from './Chat';
import {GameEvent} from './GameEventTypes';
export class Match {
public tick: number = 0;
@ -30,7 +31,7 @@ export class Match {
public startTick: number = 0;
public intervalPerTick: number = 0;
public staticBaseLines: BitStream[] = [];
public eventDefinitions: Map<number, GameEventDefinition> = new Map();
public eventDefinitions: Map<number, GameEventDefinition<GameEvent['name']>> = new Map();
public world: World = {
boundaryMin: {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 {GameEvent, GameEventDefinition} from './GameEvent';
import {GameEventDefinition} from './GameEvent';
import {PacketEntity} from './PacketEntity';
import {SendTable} from './SendTable';
import {ServerClass} from './ServerClass';
import {StringTable, StringTableEntry} from './StringTable';
import {Vector} from './Vector';
import {GameEvent, GameEventType} from './GameEventTypes';
export interface BasePacket {
}
@ -70,7 +71,7 @@ export interface GameEventPacket extends BasePacket {
export interface GameEventListPacket extends BasePacket {
packetType: 'gameEventList';
eventList: Map<number, GameEventDefinition>;
eventList: Map<number, GameEventDefinition<GameEvent['name']>>;
}
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 {GameEventPacket} from '../Data/Packet';
import {
ObjectDestroyedEvent, PlayerDeathEvent, PlayerSpawnEvent, TeamPlayRoundStartEvent, TeamPlayRoundWinEvent
} from '../Data/GameEventTypes';
export function handleGameEvent(packet: GameEventPacket, match: Match) {
switch (packet.event.name) {
case 'player_death':
handlePlayerDeath(packet, match);
handlePlayerDeath(packet.event, match);
break;
case 'teamplay_round_win':
handleRoundWin(packet, match);
handleRoundWin(packet.event, match);
break;
case 'player_spawn':
handlePlayerSpawn(packet, match);
handlePlayerSpawn(packet.event, match);
break;
case 'object_destroyed':
handleObjectDestroyed(packet, match);
handleObjectDestroyed(packet.event, match);
break;
case 'teamplay_round_start':
handleRoundStart(packet, match);
handleRoundStart(packet.event, match);
break;
}
}
function handlePlayerDeath(packet: GameEventPacket, match: Match) {
const values = packet.event.values as DeathEventValues;
function handlePlayerDeath(event: PlayerDeathEvent, match: Match) {
const values = event.values;
while (values.assister > 256 && values.assister < (1024 * 16)) {
values.assister -= 256;
}
@ -44,8 +46,8 @@ function handlePlayerDeath(packet: GameEventPacket, match: Match) {
});
}
function handleRoundWin(packet: GameEventPacket, match: Match) {
const values = packet.event.values as RoundWinEventValues;
function handleRoundWin(event: TeamPlayRoundWinEvent, match: Match) {
const values = event.values;
if (values.winreason !== 6) {// 6 = timelimit
match.rounds.push({
winner: values.team === 2 ? 'red' : 'blue',
@ -55,8 +57,8 @@ function handleRoundWin(packet: GameEventPacket, match: Match) {
}
}
function handlePlayerSpawn(packet: GameEventPacket, match: Match) {
const values = packet.event.values as PlayerSpawnEventValues;
function handlePlayerSpawn(event: PlayerSpawnEvent, match: Match) {
const values = event.values;
const userId = values.userid;
const userState = match.getUserInfo(userId);
const player = match.playerEntityMap.get(userState.entityId);
@ -72,11 +74,11 @@ function handlePlayerSpawn(packet: GameEventPacket, match: Match) {
userState.classes[classId]++;
}
function handleObjectDestroyed(packet: GameEventPacket, match: Match) {
const values = packet.event.values as ObjectDestroyedValues;
function handleObjectDestroyed(event: ObjectDestroyedEvent, match: Match) {
const values = event.values;
match.buildings.delete(values.index);
}
function handleRoundStart(packet: GameEventPacket, match: Match) {
function handleRoundStart(event: TeamPlayRoundStartEvent, match: Match) {
match.buildings.clear();
}

View file

@ -1,47 +1,48 @@
import {BitStream} from 'bit-buffer';
import {
GameEvent as IGameEvent, GameEventDefinition, GameEventEntry, GameEventType,
GameEventValue, GameEventValueMap,
GameEventDefinition, GameEventEntry,
GameEventValue, GameEventValueType,
} from '../../Data/GameEvent';
import {GameEvent, GameEventType} from '../../Data/GameEventTypes';
import {Match} from '../../Data/Match';
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);
if (!eventDescription) {
throw new Error('unknown event type');
}
const values: GameEventValueMap = {};
const values: GameEvent['values'] = {};
for (const entry of eventDescription.entries) {
const value = getGameEventValue(stream, entry);
if (value) {
values[entry.name] = value;
}
}
const name = eventDescription.name;
return {
name: eventDescription.name,
name,
values,
};
}
function getGameEventValue(stream: BitStream, entry: GameEventEntry): GameEventValue | null {
switch (entry.type) {
case GameEventType.STRING:
case GameEventValueType.STRING:
return stream.readUTF8String();
case GameEventType.FLOAT:
case GameEventValueType.FLOAT:
return stream.readFloat32();
case GameEventType.LONG:
case GameEventValueType.LONG:
return stream.readUint32();
case GameEventType.SHORT:
case GameEventValueType.SHORT:
return stream.readUint16();
case GameEventType.BYTE:
case GameEventValueType.BYTE:
return stream.readUint8();
case GameEventType.BOOLEAN:
case GameEventValueType.BOOLEAN:
return stream.readBoolean();
case GameEventType.LOCAL:
case GameEventValueType.LOCAL:
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;
return {
packetType: 'gameEvent',
event,
event: event as GameEvent,
};
}

View file

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

View file

@ -4,7 +4,7 @@ export {StreamParser} from './StreamParser';
export {Match} from './Data/Match';
export {Player} from './Data/Player';
export {PlayerCondition} from './Data/PlayerCondition';
export {GameEvent} from './Data/GameEvent';
export {GameEvent} from './Data/GameEventTypes';
export {PacketEntity} from './Data/PacketEntity';
export {SendPropDefinition, SendPropFlag, SendPropType} from './Data/SendPropDefinition';
export {SendProp} from './Data/SendProp';

View file

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