CS2DemoKit.Parser
0.9.2-beta0003
See the version list below for details.
dotnet add package CS2DemoKit.Parser --version 0.9.2-beta0003
NuGet\Install-Package CS2DemoKit.Parser -Version 0.9.2-beta0003
<PackageReference Include="CS2DemoKit.Parser" Version="0.9.2-beta0003" />
<PackageVersion Include="CS2DemoKit.Parser" Version="0.9.2-beta0003" />
<PackageReference Include="CS2DemoKit.Parser" />
paket add CS2DemoKit.Parser --version 0.9.2-beta0003
#r "nuget: CS2DemoKit.Parser, 0.9.2-beta0003"
#:package CS2DemoKit.Parser@0.9.2-beta0003
#addin nuget:?package=CS2DemoKit.Parser&version=0.9.2-beta0003&prerelease
#tool nuget:?package=CS2DemoKit.Parser&version=0.9.2-beta0003&prerelease
CS2DemoKit.Parser
A zero-copy parser for CS2 (Counter-Strike 2) .dem files. Parses the frame stream directly off
the input buffer with no intermediate copies, decodes 272 typed game events, and includes
EntityTracker for stateful entity replay (player positions, health, weapons, …). Typed entity
wrappers (CSPlayerPawn, CSGameRules, WeaponAWP, …) come from the companion
CS2OpenDev.Sdk.Entities package and bind over this runtime through the
Entities/SdkAbstractions seam (LensBoundReader / TrackerEntityWorld). Targets net10.0.
This package has no knowledge of rules, stats, or highlights — see CS2DemoKit.Analysis for that.
Dependencies: Google.Protobuf, Snappier, and CS2OpenDev.Sdk.Entities.Abstractions (the
entity read contract the seam implements).
Quickstart
using CS2DemoKit.Parser;
using CS2DemoKit.Parser.GameEvents;
// A stable file already fully written to disk — the memory-mapped path avoids putting the whole
// file on the managed heap.
ParsedDemo demo = MemoryMappedDemoSource.ParseFile(path);
foreach (GameEvent evt in demo.AllGameEvents)
{
if (evt is PlayerDeathEvent death)
{
PlayerInfo? victim = demo.Players.GetValueOrDefault(death.VictimSlot);
PlayerInfo? killer = demo.Players.GetValueOrDefault(death.KillerSlot);
Console.WriteLine($"tick {death.GameTick}: {killer?.SteamId64} killed {victim?.SteamId64} with {death.Weapon}");
}
}
ParsedDemo.Players is keyed by player slot (int), the same key every typed game event uses
for its slot-shaped fields (VictimSlot, KillerSlot, …) — join through it to get PlayerInfo
(SteamId64, Name, Team, …).
Tick clocks — read this before touching ticks
| Property | Clock | Notes |
|---|---|---|
DemoFrame.ServerTick (int) |
frame clock | Despite the name, this holds the game tick in CS2 — pre-game frames carry a large negative sentinel, gameplay frames run 1, 2, 3, … There is no DemoFrame.Tick. |
DemoFrame.GameTick (int?) |
frame clock | An alias of ServerTick, set by the parser after the header decodes. |
GameEvent.GameTick (int) |
frame clock | Same clock as the two above. |
GameEvent.ServerTick (int) |
absolute engine tick | Convert to frame clock with GameEvent.ServerTick - ParsedDemo.ServerStartTick. |
Rule: never subtract ParsedDemo.ServerStartTick from a value that is already frame clock
(DemoFrame.ServerTick/GameTick, GameEvent.GameTick). Only the absolute GameEvent.ServerTick
needs that conversion.
Input: buffer vs. file path
DemoParser.Parse(ReadOnlyMemory<byte> data, DemoProfile? profileOverride = null) is the parser's
one entry point. MemoryMappedDemoSource wraps a local file as a ReadOnlyMemory<byte> without
materializing it as a managed array; MemoryMappedDemoSource.ParseFile(path) is the one-line
convenience over Open + Parse.
- Uploaded / in-flight demos →
byte[].DemoParser.Parse(bytes.AsMemory())snapshots the bytes up front, so it's safe even if the source is still being written or copied concurrently. - Stable files already fully on disk →
MemoryMappedDemoSource. Cheaper (~166 MB avoided on the large-object heap for a 180 MB demo), but only for files that will not be written to while mapped: a concurrent truncation while a page is mapped raises an uncatchableAccessViolationExceptionthat kills the process — notry/catcharound the read fires. Don't map a file mid-download or mid-copy. - The memory-mapped path rejects files over
int.MaxValue(~2 GB) — a singleReadOnlyMemory<byte>can't address more. Thebyte[]path has no explicit guard, but is bounded by the sameint-typed frame offsets internally, so plan for the same ceiling either way. - A parse retains roughly 2.5× the input file's size in the returned
ParsedDemo(frames, decoded proto messages, event/player indexes). Size worker pools and per-process demo-concurrency limits against that multiplier, not the raw file size. - Per-parse control (0.8+):
DemoParser.Parse(data, new ParseOptions { ... }, profileOverride)—CancellationToken(checked at pass boundaries and per frame in the parallel pass),MaxDegreeOfParallelismfor the parallel decode pass (null/≤0 = unbounded), throttledIProgress<double>, andOnUnknownMessage, a per-parse callback that doesn't cross-talk between concurrent parses the way the staticOnUnknownMessageTypeevent does. Still gate the number of concurrent demos with your ownSemaphoreSlimsized to the ~2.5× memory multiplier. - Scoring an untrusted upload: set
ParseOptions.CountDropSites = true— silently-dropped net-messages then surface onParsedDemo.WarningsasParseWarningCodes.NetMessageDroppedentries (top offenders + remainder, each with aCount), emitted after the parse's own structural warnings so they never displace them.
Parse warnings
ParsedDemo.Warnings (IReadOnlyList<ParseWarning>) carries non-fatal, structured diagnostics —
a damaged demo still yields a usable partial parse, but the damage is no longer silent. Each
ParseWarning has a stable Code from ParseWarningCodes (StringTableCreateFailed,
StringTableUpdateFailed, StringTableTruncated, PlayerInfoUnreadable), a human Message, and
an optional Tick.
The list is capped at 256 entries per parse; once the cap is hit, a final entry with code
ParseWarningCodes.WarningsTruncated reports how many further warnings were suppressed — it is
always the last entry when present.
Pool-consumer caveat: the accumulator behind Warnings is thread-affine ([ThreadStatic]),
drained into the ParsedDemo you get back and reset on that same thread. If a parse throws
before constructing its ParsedDemo, its warnings are left on that thread; the next successful
parse on the same thread drains them along with its own, so a dead parse's warnings can end up
misattributed to whatever parse runs next on that thread. This only bites if you run parses on a
reused thread pool (e.g. Task.Run over a shared pool) and something upstream swallows the
exception from a failed parse — treat Warnings on the following result on that thread as suspect
in that case.
Entity tracking
Build a lens-bound tracker in one call: EntityTrackerFactory.CreateCurated() binds the
generated Schema Lens (omitting it does not throw — lane-routed reads silently degrade to the
fallback dict, so prefer the factory). To read through typed wrappers, register the
CS2OpenDev.Sdk.Entities factories via TrackerEntityWorld.RegisterWrapper (or bind a wrapper
per entity with new TrackerEntityWorld(tracker).CreateReader(binding, state)).
AdvanceTo/AdvanceToIndex replay from frame 0 on every call; for forward walks use
EntityStateLayer in CS2DemoKit.Analysis instead. Decode diagnostics go to
EntityTracker.DecodeDiagnosticSink (Action<string>, defaults to Console.WriteLine) — redirect
or silence it per tracker in batch services. PositionUtil.CellToWorld /
CellToWorldVector (namespace CS2DemoKit.Parser.EntityTracking) is the oracle-pinned pawn
cell→world reconstruction; TickMapper and TickBoundaries.FrameIndices cover demo-tick mapping
and tick-boundary frame indexing.
Working with raw net messages
Generated Valve proto types (CDemoPacket, CSVCMsg_PacketEntities, CCSUsrMsg_*, …) live in
CS2OpenSchema.Protos, not the global namespace — add the using to pattern-match
NetMessage.Payload. They ship in the CS2OpenDev.Protos package, which comes along as a
dependency of this one:
using CS2OpenSchema.Protos;
foreach (NetMessage msg in frame.InnerMessages)
{
if (msg.Payload is CSVCMsg_PacketEntities entities)
{
// ...
}
}
Opt-in diagnostics
Two environment variables gate diagnostics that are off by default and cost nothing when unset:
CS2DEMOKIT_PROFILE=1 enables the parse-profiling accumulator, CS2DEMOKIT_TRACE_DECODE=1
enables verbose entity-decode tracing. Both are settable in code as well, via Profiling.Enabled
and Tracing.Enabled — set them before the run they govern begins.
The DEMOVIEWER_* spellings these switches shipped under are still honoured, second in
precedence, so existing scripts keep working.
License
MIT. Contains code adapted from demofile-net (MIT) —
see THIRD-PARTY-NOTICES.md in the repo for the full attribution and file list.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net10.0
- CS2OpenDev.Protos (>= 0.9.0)
- CS2OpenDev.Sdk (>= 0.9.0)
- CS2OpenDev.Sdk.Entities.Abstractions (>= 1.0.3)
- CS2OpenDev.Sdk.GameEvents (>= 0.9.0)
- Google.Protobuf (>= 3.29.5)
- Snappier (>= 1.3.1)
NuGet packages (1)
Showing the top 1 NuGet packages that depend on CS2DemoKit.Parser:
| Package | Downloads |
|---|---|
|
CS2DemoKit.Analysis
Rule-driven analysis engine for parsed Counter-Strike 2 demos: a state-graph evaluator, the shipped rulesets embedded in the assembly, per-player stats, rich highlights, and a 3D line-of-sight engine. |
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 0.12.0 | 83 | 9/17/2026 |
| 0.11.0 | 204 | 9/14/2026 |
| 0.10.0 | 395 | 8/24/2026 |
| 0.10.0-beta0002 | 107 | 8/24/2026 |
| 0.10.0-beta0001 | 99 | 8/24/2026 |
| 0.9.2 | 119 | 8/24/2026 |
| 0.9.2-beta0003 | 113 | 8/24/2026 |
| 0.9.2-beta0002 | 112 | 8/23/2026 |
| 0.9.2-beta0001 | 102 | 8/22/2026 |
| 0.9.1 | 123 | 8/18/2026 |