UNPKG

deadem

Version:

JavaScript (Node.js & Browsers) parser for Deadlock (Valve Source 2 Engine) demo/replay files

396 lines (280 loc) 20.2 kB
<h1 align="center"> <a href="https://deadem.com" alt=""> <img alt="deadem" src="https://deadem.com/logo80.svg" height="80" /> </a> <br/> deadem <br/> </h1> <a href="https://github.com/Igor-Losev/deadem/actions/workflows/ci.yml" alt=""><img src="https://github.com/Igor-Losev/deadem/actions/workflows/ci.yml/badge.svg" /></a> <a href="https://www.npmjs.com/package/deadem" alt=""><img src="https://img.shields.io/npm/v/deadem" /></a> <a href="https://github.com/Igor-Losev/deadem" alt=""><img src="https://img.shields.io/badge/Deadlock%20Game%20Build-5888-darkGreen" /></a> **Deadem** is a JavaScript parser for Deadlock (Valve Source 2 Engine) demo/replay files, compatible with Node.js and modern browsers. <p align="center"> ┌ Node.js &nbsp;&nbsp;&nbsp;&nbsp;&nbsp; Browser ┐ </p> <p align="center"> <img src="https://deadem.s3.us-east-1.amazonaws.com/deadlock/example-node.png" alt="node" width="45%" height="376px"> <img src="https://deadem.s3.us-east-1.amazonaws.com/deadlock/example-browser.png" alt="browser" width="45%" height="376px"> </p> ## Contents * [Installation](#installation)<br/> Installing and including the library in your project. * [Examples](#examples)<br/> Running example scripts and working with demo files. * [Overview](#overview)<br/> Core concepts and architecture of the parser. * [Understanding Demo](#understanding-demo)<br/> Structure and content of demo files. * [Understanding Parser](#understanding-parser)<br/> Parser internals and state management. * [Understanding Interceptors](#understanding-interceptors)<br/> Extracting data during parsing. * [Configuration](#configuration)<br/> Customizing parser options and behavior. * [Usage](#usage)<br/> Basic usage example with real game data. * [Demo File](#demo-file)<br/> Parsing demo using `.dem` file. * [HTTP Broadcast](#http-broadcast)<br/> Parsing demo using `HTTP Broadcast`. * [Data Extraction](#data-extraction)<br/> Extracting data during parsing. * [Compatibility](#compatibility)<br/> Supported environments and versions. * [Performance](#performance)<br/> Benchmark results across platforms. * [Building](#building)<br/> Setup and build instructions. * [License](#license)<br/> Project licensing information. * [Acknowledgements](#acknowledgements)<br/> Credits to upstream and inspiring projects. ## Installation ### Node.js ```shell npm install deadem --save ``` ```js import { Parser } from 'deadem'; ``` ### Browser ```js <script src="//cdn.jsdelivr.net/npm/deadem@1.X.X/dist/deadem.min.js"></script> ``` ```js const { Parser } = window.deadem; ``` ## Examples ### Node.js The example scripts will, by default, look for demo files in the `/demos` folder. There are two types of files used: - `DemoSource.REPLAY` files with the `.dem` extension - `DemoSource.HTTP_BROADCAST` files with the `.bin` extension If no local demo file is found, the scripts will automatically download the required file from a public S3 bucket: ```text https://deadem.s3.us-east-1.amazonaws.com/deadlock/demos/${matchId}-{gameBuild?}.dem (for REPLAY files) https://deadem.s3.us-east-1.amazonaws.com/deadlock/demos/${matchId}-{gameBuild?}.bin (for HTTP_BROADCAST files) ``` A list of all available demo files can be found in the [DemoFile](./packages/examples-common/data/DemoFile.js) class. | № | Description | Commands | | ------------------------------------------------------------------------------------------------------------ | ----------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- | | [01](./packages/examples-node/scripts/01_parse.js) | Parse a single replay file | `node ./packages/examples-node/scripts/01_parse.js` | | [02](./packages/examples-node/scripts/02_parse_multiple.js) | Parse multiple replay files | `node ./packages/examples-node/scripts/02_parse_multiple.js --matches="36126255,36127043"`<br/>`node ./packages/examples-node/scripts/02_parse_multiple --matches=all` | | [03](./packages/examples-node/scripts/03_parse_http_broadcast.js) | Parse HTTP Broadcast data from web server | `node ./packages/examples-node/scripts/03_parse_http_broadcast.js` | | [04](./packages/examples-node/scripts/04_parse_http_broadcast_file.js) | Parse HTTP Broadcast data from file | `node ./packages/examples-node/scripts/04_parse_http_broadcast_file.js` | | [05](./packages/examples-node/scripts/05_http_broadcast_save_to_file.js) | Save HTTP Broadcast data to a file | `node ./packages/examples-node/scripts/05_http_broadcast_save_to_file.js` | | [10](./packages/examples-node/scripts/10_parse_game_time.js) | Parse game duration from replay | `node ./packages/examples-node/scripts/10_parse_game_time.js` | | [11](./packages/examples-node/scripts/11_parse_top_damage_dealer.js) | Identify top damage dealer | `node ./packages/examples-node/scripts/11_parse_top_damage_dealer.js` | | [12](./packages/examples-node/scripts/12_parse_chat.js) | Extract chat messages | `node ./packages/examples-node/scripts/12_parse_chat.js` | | [13](./packages/examples-node/scripts/13_parse_kill_feed.js) | Extract kill feed events | `node ./packages/examples-node/scripts/13_parse_kill_feed.js` | | [14](./packages/examples-node/scripts/14_parse_ability_feed.js) | Extract ability usage events | `node ./packages/examples-node/scripts/14_parse_ability_feed.js` | | [15](./packages/examples-node/scripts/15_parse_mid_boss_deaths.js) | Parse mid boss death events | `node ./packages/examples-node/scripts/15_parse_mid_boss_deaths.js` | | [16](./packages/examples-node/scripts/16_parse_tower_deaths.js) | Parse tower destruction events | `node ./packages/examples-node/scripts/16_parse_tower_deaths.js` | ### Browser | № | Description | Commands | | ----------------------------------------------------------------------------------------- | -------------------------- | ----------- | | [01](https://deadem.com) | Deadem Explorer | `npm start` | ## Overview ### Understanding Demo The demo file consists of a sequential stream of outer packets, referred to in this project as [DemoPacket](./packages/lib/src/data/DemoPacket.js). Each packet represents a type defined in [DemoPacketType](./packages/lib/src/data/enums/DemoPacketType.js). Most [DemoPacket](./packages/lib/src/data/DemoPacket.js) types, once parsed, become plain JavaScript objects containing structured data. However, some packet types — such as `DemoPacketType.DEM_PACKET`, `DemoPacketType.DEM_SIGNON_PACKET`, and `DemoPacketType.DEM_FULL_PACKET` — encapsulate an array of inner packets, referred to in this project as [MessagePacket](./packages/lib/src/data/MessagePacket.js). These inner packets correspond to a message types defined in [MessagePacketType](./packages/lib/src/data/enums/MessagePacketType.js). Similarly, most [MessagePacket](./packages/lib/src/data/MessagePacket.js) types also parse into regular data objects. There are two notable exceptions that require additional parsing: 1. **Entities** ([Developier Wiki](https://developer.valvesoftware.com/wiki/Networking_Entities)) - `MessagePacketType.SVC_PACKET_ENTITIES`: contains granular (or full) updates to existing entities (i.e. game world objects). 2. **String Tables** ([Developer Wiki](https://developer.valvesoftware.com/wiki/String_Table_Dictionary)) - `MessagePacketType.SVC_CREATE_STRING_TABLE`, `MessagePacketType.SVC_UPDATE_STRING_TABLE`, `MessagePacketType.SVC_CLEAR_ALL_STRING_TABLES`: granular (or full) updates to existing string tables (see [StringTableType](./packages/lib/src/data/enums/StringTableType.js)). > ⚠️ **Warning** > > Demo files contain only the minimal data required for visual playback — not all game state information is preserved or available. Additionally, the parser may skip packets it cannot decode. > > You can retrieve detailed statistics about parsed and skipped packets by calling `parser.getStats()`. ### Understanding Parser The parser accepts a readable stream and incrementally parses individual packets from it. It maintains an internal, **mutable** instance of [Demo](./packages/lib/src/data/Demo.js), which represents the current state of the game. You can access it by calling: ```js const demo = parser.getDemo(); ``` > Note: The parser overwrites the existing state with each tick and **does not** store past states. ### Understanding Interceptors Interceptors are user-defined functions that hook into the parsing process **before** or **after** specific stages (called [InterceptorStage](./packages/lib/src/data/enums/InterceptorStage.js)). They allow to inspect and extract desired data during parsing. Currently, there are three supported stages: - `InterceptorStage.DEMO_PACKET` - `InterceptorStage.MESSAGE_PACKET` - `InterceptorStage.ENTITY_PACKET` Use the following methods to register hooks: - **Before** the [Demo](./packages/lib/src/data/Demo.js) state is affected: `parser.registerPreInterceptor(InterceptorStage.DEMO_PACKET, hookFn);` - **After** the [Demo](./packages/lib/src/data/Demo.js) state is affected: `parser.registerPostInterceptor(InterceptorStage.DEMO_PACKET, hookFn);` The diagram below provides an example of the parsing timeline, showing when **pre** and **post** interceptors are invoked at each stage: ```text ... PRE DEMO_PACKET └─ DEM_FILE_HEADER POST DEMO_PACKET ... PRE DEMO_PACKET └─ DEM_SEND_TABLES POST DEMO_PACKET ... PRE DEMO_PACKET └─ DEM_PACKET ├─ PRE MESSAGE_PACKET │ └─ NET_TICK └─ POST MESSAGE_PACKET ├─ PRE MESSAGE_PACKET │ └─ SVC_ENTITIES │ ├─ PRE ENTITY_PACKET │ │ └─ ENTITY_1 │ └─ POST ENTITY_PACKET │ ├─ PRE ENTITY_PACKET │ │ └─ ENTITY_2 │ └─ POST ENTITY_PACKET └─ POST MESSAGE_PACKET POST DEMO_PACKET ... ``` Each interceptor receives different arguments depending on the `InterceptorStage`: | Interceptor Stage | Hook Type | Hook Signature | |-------------------|----------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | `DEMO_PACKET` | `pre` / `post` | (demoPacket: [DemoPacket](./packages/lib/src/data/DemoPacket.js)) => void | | `MESSAGE_PACKET` | `pre` / `post` | (demoPacket: [DemoPacket](./packages/lib/src/data/DemoPacket.js), messagePacket: [MessagePacket](./packages/lib/src/data/MessagePacket.js)) => void | | `ENTITY_PACKET` | `pre` / `post` | (demoPacket: [DemoPacket](./packages/lib/src/data/DemoPacket.js), messagePacket: [MessagePacket](./packages/lib/src/data/MessagePacket.js), events: Array<[EntityMutationEvent](./packages/lib/src/data/entity/EntityMutationEvent.js)>) => void | > ❗ **Important** > > Interceptors hooks are **blocking** — the internal packet analyzer waits for hooks to complete before moving forward. ## Configuration ### Parsing Below is a list of available options that can be passed to the `ParserConfiguration`: | Option | Description | Type | Default | | --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------ | ------- | | `breakInterval` | How often (in packets) to yield to the event loop to avoid blocking. The smaller the value, the more responsive the interface will be (may slow down parser performance). | number | `1000` | | `parserThreads` | Number of **additional** threads used by the parser. | number | `0` | ### Logging The library provides a [Logger](./packages/lib/src/core/Logger.js) class with several pre-defined logging strategies. For example: - `Logger.CONSOLE_WARN` — only logs warnings and errors. - `Logger.NOOP` - disables all logging. ```js import { Logger, Parser, ParserConfiguration } from 'deadem'; const configuration = new ParserConfiguration({ parserThreads: 2 }, Logger.CONSOLE_WARN); const parser = new Parser(configuration); ``` ## Usage ### Demo File ```js import { createReadStream } from 'node:fs'; import { Parser, Printer } from 'deadem'; const parser = new Parser(); const printer = new Printer(parser); const readable = createReadStream(PATH_TO_DEM_FILE); await parser.parse(readable); printer.printStats(); ``` ### HTTP Broadcast ```js import { BroadcastAgent, BroadcastGateway, DemoSource, Parser, Printer } from 'deadem'; const FROM_BEGINNING = false; const MATCH_ID = 38624662; const broadcastGateway = new BroadcastGateway('dist1-ord1.steamcontent.com/tv'); const broadcastAgent = new BroadcastAgent(broadcastGateway, MATCH_ID); const parser = new Parser(); const printer = new Printer(parser); const readable = broadcastAgent.stream(FROM_BEGINNING); await parser.parse(readable, DemoSource.HTTP_BROADCAST); printer.printStats(); ``` ### Data Extraction ```js ... ... // #1: Extraction of chat messages parser.registerPostInterceptor(InterceptorStage.MESSAGE_PACKET, (demoPacket, messagePacket) => { if (messagePacket.type === MessagePacketType.CITADEL_USER_MESSAGE_CHAT_MESSAGE) { console.log(`CHAT_MESSAGE: player slot [ ${messagePacket.data.playerSlot} ], message [ ${messagePacket.data.text} ]`); } }); const topDamageDealer = { player: null, damage: 0 }; // #2: Getting top hero-damage dealer parser.registerPostInterceptor(InterceptorStage.ENTITY_PACKET, async (demoPacket, messagePacket, events) => { events.forEach((event) => { const entity = event.entity; if (entity.class.name === 'CCitadelPlayerController') { const data = entity.unpackFlattened(); if (data.m_iHeroDamage > topDamageDealer.damage) { topDamageDealer.player = data.m_iszPlayerName; topDamageDealer.damage = data.m_iHeroDamage; } } }); }); await parser.parse(readable); console.log(`Top damage dealer is [ ${topDamageDealer.player} ] with [ ${topDamageDealer.damage} ] damage`); ``` ## Compatibility Tested with Deadlock demo files from game build `5888` and below. * **Node.js:** v16.17.0 and above. * **Browsers:** All modern browsers, including the latest versions of Chrome, Firefox, Safari, Edge. ## Performance By default, entities are **parsed but not unpacked**. Parser performance may vary depending on the number of `entity.unpackFlattened()` calls. The table below shows performance results **without calling `entity.unpackFlattened()`** for MacBook Pro with M3 chip: ### 1. `configuration.parserThreads = 0`: | # | Runtime | Speed, ticks per second | Speed, game seconds per second (tick rate — 64) | Time to parse a 30-minute game, seconds | Max Memory Usage, mb | | --- | --------------------- | ----------------------- | ----------------------------------------------- | --------------------------------------- | -------------------- | | 1 | Node.js v22.14.0 | 8 542 ± 1.30% | 133.47 ± 1.30% | ~13.53 | 329 ± 6.21% | | 2 | Browser Chrome v133.0 | 7 650 ± 0.59% | 119.53 ± 0.59 | ~15.06 | - | | 3 | Node.js v16.20.2 | 5 405 ± 0.61% | 84.45 ± 0.26% | ~21.31 | 270 ± 6.98% | | 4 | Browser Safari v18.3 | 5 295 ± 1.27% | 82.73 ± 1.27% | ~21.76 | - | ### 2. `configuration.parserThreads = 3`: | # | Runtime | Speed, ticks per second | Speed, game seconds per second (tick rate — 64) | Time to parse a 30-minute game, seconds | Max Memory Usage, mb | Performance Gain (vs 0 p. threads), % | | --- | --------------------- | ----------------------- | ----------------------------------------------- | --------------------------------------- | -------------------- | ------------------------------------- | | 1 | Node.js v22.14.0 | 11 292 ± 0.26% | 176.44 ± 0.26% | ~10.20 | 639.16 ± 4.94% | 32.19 | | 2 | Browser Chrome v133.0 | 9 560 ± 0.43% | 149.38 ± 0.43% | ~12.05 | - | 24.97 | | 3 | Node.js v16.20.2 | 8 696 ± 0.26% | 135.86 ± 0.26% | ~13.25 | 497.86 ± 6.57% | 60.89 | | 4 | Browser Safari v18.3 | 7 073 ± 0.44% | 110.52 ± 0.44% | ~16.29 | - | 33.58 | ## Building ### 1. Installing dependencies ```shell npm install ``` ### 2. Compiling .proto ```shell npm run proto:json ``` ### 3. Building a bundle ```shell npm run build ``` ## License This project is licensed under the [MIT](./LICENSE) License. ## Acknowledgements This project was inspired by and built upon the work of the following repositories: - [dotabuff/manta](https://github.com/dotabuff/manta) - Dotabuff's Dota 2 replay parser in Go. - [saul/demofile-net](https://github.com/saul/demofile-net) - CS2 / Deadlock replay parser in C#. Huge thanks to their authors and contributors!