# demoparser reads Counter-Strike 2 demos by querying them

> LaihoE/demoparser is a Rust library for Counter-Strike 2 replay files with Python, Node and WebAssembly bindings. Instead of streaming game events, it exposes a demo as something you query like a table, and the field names map onto CS2 netvar names.

**LaihoE/demoparser** — Counter-Strike 2 replay parser for Python and JavaScript

- Repository: https://github.com/LaihoE/demoparser
- Stars: 735 · Forks: 105
- Language: Rust
- License: MIT
- Published: 2026-10-21 · Updated: 2026-10-21 · Language: en
- Canonical page: https://hysenlabs.com/projects/laihoe-demoparser

## A demo is queried like a table, not consumed as an event stream

Most replay tooling hands you a callback per event and lets you build your own state machine. demoparser takes the opposite route. As the project puts it, the parser lets you "query" the demo similarly to how you would interact with a database, and the calling code asks for a table of values rather than listening to a firehose. That choice matters in practice. A round-by-round kill chart does not require you to reconstruct the whole match in memory, and the same demo file can be queried several times for different fields without re-deriving anything.

The second half of that design is that the heavy work happens in Rust while the caller stays in Python or JavaScript. The parsing cost is paid in native code, and the results come back as data structures the calling language already knows: data frames in Python, plain objects in Node. For a pipeline that reads a folder of matches and writes CSV files, that combination removes the usual reason to write a Rust extension by hand.

The project is aimed squarely at Counter-Strike 2 analysis, with the older CSGO lineage visible in the topic tags. A demo file is the only input, and there is no server connection, no game client and no live feed anywhere in the interface.

## One Rust core behind three package surfaces

The repository is a Rust project with three published surfaces built on top of it. Python users install `demoparser2`, Node users install `@laihoe/demoparser2`, and browser or other WebAssembly users install the unscoped `demoparser2` package. The repository tree shows a `src/` directory holding the Rust implementation, an `examples/` directory with runnable examples, a `documentation/` directory and a `benchmark/` directory, so the same engine is exercised from several languages.

The WebAssembly build matters for a specific reason: it means a browser page can parse a demo without a Python runtime on the server. The repository carries a `examples/WASM/` folder, which suggests the authors expected that use case rather than treating the WASM package as an afterthought.

The licence is MIT, so the terms are simple for commercial use, for embedding the parser in an internal analytics tool, or for vendoring the Rust source into a larger pipeline. No legal advice follows from that, but the practical point is that the licence does not restrict redistribution of a derived tool in the way some other licences would. There is no homepage set for the repository, and there are no GitHub releases, so version pinning is done against the package registries and the main branch rather than against tagged artifacts.

## Installing for Python, Node and the browser

Installation is one package name per runtime, and the Python build requires Python 3.8 or newer. The Python entry point is a class that takes the demo path at construction time, then exposes two query methods:

```python
from demoparser2 import DemoParser

parser = DemoParser("path_to_demo.dem")
event_df = parser.parse_event("player_death", player=["X", "Y"], other=["total_rounds_played"])
ticks_df = parser.parse_ticks(["X", "Y"])
```

The first argument to `parse_event` is the event name, `player_death` in this example, and the `player` list names the players whose rows you want. The `other` list pulls in extra fields such as `total_rounds_played` so the same result set carries context about how long the match ran. The return value is a data frame, which means the next step in a pipeline is usually a groupby rather than a loop.

The Node binding exposes the same two operations as functions, with the demo path passed on every call:

```javascript
var {parseEvent, parseTicks} = require('@laihoe/demoparser2');

let event_json = parseEvent("path_to_demo.dem", "player_death", ["X", "Y"], ["total_rounds_played"])
let ticks_json = parseTicks("path_to_demo.dem", ["X", "Y"])
```

If you are starting from scratch, replace the placeholder names with real Steam names from the demo you want to analyse, and expect the two calls to behave differently: `parse_event` returns rows keyed to discrete events, while `parse_ticks` returns state sampled at tick resolution, which is the heavier of the two.

## parse_event and parse_ticks are the entire public surface

Two methods carry the weight of the library, and the difference between them is the main design decision you face as a user. `parse_event` filters a named event, such as `player_death`, down to a set of players and a set of fields. `parse_ticks` takes the same player list and returns the player state across the demo, sampled per tick. Round-level questions, positioning questions and economy questions all fall out of the second call, but with far more rows than the first.

The examples directory makes that division visible. There are folders for `kills_per_round`, `kda_per_zone`, `find_bomb_site`, `equipment_value_freeze_end`, `chicken_kills`, `crosshair_codes`, `buttons_bitfield`, `voice_to_wav` and `analyse_aim`, among others. A single example named `efficiently_parse_multi_events_and_ticks` hints that the author treats repeated parsing as a cost worth thinking about.

There is a top-level file named `PARSE_SPEEDUP_BASELINE.md`, which is a strong sign that parse speed is treated as a first class concern rather than an afterthought. The two call shapes also explain why the library can answer questions that a pure event listener struggles with, such as which zone a player died in or what a player had in inventory when a freeze ended: those answers require state, and state is what the query model gives you directly.

## Field names are aliases over CS2 netvar names

The supported field list is long and, for the most part, mechanical. Each friendly name maps to the underlying CS2 property: `health` is `m_iHealth`, `score` is `m_iScore`, `is_alive` is `m_bPawnIsAlive`, `balance` is `m_iAccount`, `pitch` and `yaw` come from `m_angEyeAngles`, `fov` is `m_iDesiredFOV`, `ping` is `m_iPing`, `stamina` is `m_flStamina` and `is_defusing` is `m_bIsDefusing`.

That mapping is the single most useful thing the project publishes. When you know CS2's netvar names, you can guess the alias, and when you do not, the table tells you which underlying property backs a given field. A handful of entries have no netvar equivalent and are marked with a dash: `velocity`, `velocity_X`, `velocity_Y`, `velocity_Z`, `agent_skin`, `user_id`, `entity_id`, `glove_paint_id`, `glove_paint_seed` and `glove_paint_float` are computed by the parser rather than read straight out of the demo.

There is a separate group of button fields that report whether a player is pressing a key, such as `FORWARD` and `LEFT`, both backed by the button down mask. And there are predictive rank fields, `rank_if_win`, `rank_if_loss` and `rank_if_tie`, which come from the competitive ranking properties. The last table in the list is truncated in the README, so the full button set is larger than what is shown there.

## The benchmark covers 50 demos and 4.6GB of files

The performance section describes one specific workload: finding the coordinates of all player deaths. The benchmark uses 50 demos containing a mix of MM, Faceit and HLTV demos, with a total size of 4.6GB. Two machines are reported. A gaming PC with 12 CPU cores, a Ryzen 5900x and a Samsung 980 Pro NVMe drive processed the set in 6.14 seconds, an average of 749 MB/s. A ThinkPad T14 gen 2 with 4 cores, an i5-1335g7 and a Toshiba XG6 NVMe took 14.00 seconds, an average of 328 MB/s.

Both runs were on Ubuntu 20.04, and the README states that Python and JavaScript are roughly as fast, which follows from the work happening in the shared Rust core.

Two caveats are worth keeping in mind. The figures describe a specific operation, coordinate extraction for deaths, not every possible query, and a `parse_ticks` call over the same files returns a very different volume of data. The results also come from Linux machines with fast NVMe storage, so the throughput number says as much about storage speed as about parsing speed. The benchmark code is kept in the repository's `benchmark/` folder, so the workload is reproducible rather than a claim without a script behind it.

## Where a replay parser is the wrong tool

The scope is narrow in a useful way and narrow in an unhelpful way. Demoparser reads Counter-Strike 2 demo files and nothing else. If your question is about a live match, about a player who is currently connected, or about data that only exists in the game's memory and never reaches the replay, then a parser reading .dem files is the wrong instrument, and the README does not document any live or in-process mode.

The second limitation is that the parser works from a fixed list of fields. There is no documented way to request an arbitrary netvar that is not on that list, so a question about a property nobody thought to expose cannot be answered by tweaking a query. The `documentation/` directory is even linked under the heading "Scuffed Documentation", which is the author's own acknowledgement that the prose there is thin.

There is also a versioning question. The repository has no GitHub releases, so there is no published changelog and no version tag to pin against. If your pipeline needs reproducibility, you should pin the package version you test with rather than tracking the main branch, because the main branch is where the last push landed, on 2026-10-05.

## Conclusion

demoparser fits analysts who already have .dem files and need per-player fields such as health, position or equipment value, and it is a poor fit for live telemetry or for questions that need round-level state the parser does not expose. Check first that the fields you need exist in the supported list, since the parser offers no way to query arbitrary netvars, and note that the repository publishes no GitHub releases, so upgrades track the main branch where the last push was on 2026-10-05.

## FAQ

### How do I install demoparser for Python?

Install the `demoparser2` package with pip; the project states that Python 3.8 or newer is required. Import `DemoParser` from `demoparser2` and pass the path to your .dem file when constructing it.

### What is the difference between parse_event and parse_ticks?

parse_event filters a named event such as `player_death` for a list of players and returns rows for those events, while parse_ticks returns the state of the listed players sampled across the demo at tick resolution. The tick call produces far more data.

### Does demoparser work in Node.js and in the browser?

There is a Node package named `@laihoe/demoparser2` exposing `parseEvent` and `parseTicks`, and a separate WebAssembly build published as the unscoped `demoparser2` package. The repository includes an `examples/WASM/` folder for the browser-oriented build.

### Can I query a player property that is not on the supported field list?

The documented field list maps friendly names onto CS2 netvar names such as `m_iHealth` or `m_iPing`, and the parser does not document a way to request arbitrary properties outside that list. Check the list first to confirm the field you need is exposed.

### How fast is demoparser on a large set of demos?

The project benchmarks finding death coordinates across 50 demos totalling 4.6GB: 6.14 seconds at 749 MB/s on a 12 core Ryzen 5900x machine and 14.00 seconds at 328 MB/s on a 4 core ThinkPad T14 gen 2, both running Ubuntu 20.04.

## Sources

- [Issues](https://github.com/LaihoE/demoparser/issues)
- [LaihoE/demoparser on GitHub](https://github.com/LaihoE/demoparser)
- [License: MIT](https://github.com/LaihoE/demoparser/blob/main/LICENSE)
- [README](https://github.com/LaihoE/demoparser/blob/main/README.md)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/laihoe-demoparser
