CS2OpenDev.Sdk.GameEvents 0.9.5

This package has a SemVer 2.0.0 package version: 0.9.5+cs.25218825.dump.2026-09-09.
dotnet add package CS2OpenDev.Sdk.GameEvents --version 0.9.5
                    
NuGet\Install-Package CS2OpenDev.Sdk.GameEvents -Version 0.9.5
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="CS2OpenDev.Sdk.GameEvents" Version="0.9.5" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="CS2OpenDev.Sdk.GameEvents" Version="0.9.5" />
                    
Directory.Packages.props
<PackageReference Include="CS2OpenDev.Sdk.GameEvents" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add CS2OpenDev.Sdk.GameEvents --version 0.9.5
                    
#r "nuget: CS2OpenDev.Sdk.GameEvents, 0.9.5"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package CS2OpenDev.Sdk.GameEvents@0.9.5
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=CS2OpenDev.Sdk.GameEvents&version=0.9.5
                    
Install as a Cake Addin
#tool nuget:?package=CS2OpenDev.Sdk.GameEvents&version=0.9.5
                    
Install as a Cake Tool

CS2OpenDev.Sdk.GameEvents

Decodes Counter-Strike 2's legacy game-event wire messages into the strongly-typed event records shipped by CS2OpenDev.Sdk.

Published to NuGet.org, and mirrored to GitHub Packages (https://nuget.pkg.github.com/CS2OpenDev/index.json); the .nupkg is also attached to each release. Ignore the unlisted CS2OpenDev.Sdk 1.0.1 from May 2026 — it is stale, carries a licence the project disowned, and never resolves unless pinned explicitly. See the root README.

Every CS2 demo parser ends up writing the same code: key-by-name dispatch, the val_long / val_short / val_byte fallback chain, entity-handle and controller-slot handling. This is that code, generated from the same schema the records come from.

using CS2OpenDev.Sdk.GameEvents;
using CS2OpenSchema.Events;

var decoder = new GameEventDecoder();
decoder.LoadDescriptors(eventList);        // once, when the demo stream yields it

if (decoder.TryDecode(msg, out object? ev) && ev is PlayerDeathEvent death)
{
    Console.WriteLine($"{death.Attacker} killed {death.Userid} with {death.Weapon}");
}

Why you need the descriptor table

The wire format splits an event across two messages. CMsgSource1LegacyGameEventList arrives once and declares, per event id, the event's name and its key names in order. CMsgSource1LegacyGameEvent carries an event id and a positional value list. No names.

Neither is decodable alone, and the table never repeats. So GameEventDecoder is stateful for the lifetime of a demo: feed it the list when you see it, then feed it events. If you decode without loading descriptors, TryDecode returns false rather than guessing.

The integer fallback chain

key_t has four integer widths plus a bool, and the server is not obliged to use the one the schema declares — it writes the narrowest slot the value fits. A decoder that reads only the declared slot silently returns 0 for values that are plainly on the wire. This is the single most common way a hand-rolled decoder goes wrong.

GameEventReader collapses whichever slot was actually used, widest-first, so a value that needed the wider slot is never truncated. Narrow properties saturate rather than wrap: an out-of-range value becomes short.MaxValue, not a plausible-looking negative.

Duplicate event names

Native event names are not unique. Across 292 declarations there are 276 distinct names: 15 carry more than one, because the same event is declared in several .gameevents files with different field sets. player_death has two declarations (core: 2 fields, mod: 22); round_end has three.

// Resolves the declaration CS2 actually fires — mod overrides game overrides core.
GameEventRegistry.TryGetFactory("player_death", out var factory);

// Every declaration, if you specifically want an older shape.
foreach (var d in GameEventRegistry.GetAllFactories("player_death"))
{
    Console.WriteLine($"{d.Source} -> {d.RecordType.Name}");
}

A registry keyed on name alone cannot round-trip. If your dispatcher assumes one record per name, that assumption holds for 261 of 276 and silently truncates on the rest.

Curated events the schema doesn't declare

Three of those 276 names — item_drop, halftime, game_restart — are not in the extracted schema. They appear in the CMsgSource1LegacyGameEventList descriptor real GOTV demos carry, and they fire, but nothing upstream of this repo declares them: not gameevents_schema.json, not the SchemaTracker artifact it derives from.

The gap is worth naming because of how it fails. A missing field is loud: the record is there, the property isn't, your code doesn't compile. A missing record is silent at every layer that compiles. Your rule bound to item_drop just never fires, and nothing logs. That's how this was found (issue #3), and finding it took a demo.

So they ship as records, generated from a game-event-supplement.json in the repo root and marked in their own XML docs:

[NativeName("item_drop")]
[GameEventSource("sdk.supplement")]      // not a .gameevents file — nothing extracted this
public sealed partial record ItemDropEvent { … }

GameEventRegistry and GameEventFactories treat them like any other declaration, so TryGetFactory("item_drop", …) works and the reader calls are the same ones item_pickup gets. The difference is provenance, and provenance is the thing to act on: the field lists were observed on a demo, never declared upstream. item_drop carries userid and item because that is what the descriptor table shows, with the KV1 tags copied from item_pickup. halftime and game_restart are empty records; no keys were observed on them. Treat all three as a floor. An extracted record is a promise about shape, and these are not.

They are also temporary, and enforced to be. The supplement is additive only: it can introduce a native name, never replace one. The moment upstream declares any of these, generation fails with CS2_GEN_008 naming the event, and the entry has to be deleted before the build goes green again. That's deliberate. The alternative is a curated guess quietly outliving the real declaration and shipping forever under a slightly different type name.

If you build the SDK from source and hit an event of your own with the same problem, add it there, in the same shape as gameevents_schema.json's events array, minus source, which the generator stamps:

{
  "events": [{
    "name": "some_undeclared_event",
    "fields": [{ "name": "userid", "type": "player_controller" }],
    "annotations": { "description": "What you observed." }
  }]
}

Resolved next to the schema first, then the working directory, the same search as game-event-overrides.json below. Absent, it changes nothing.

Transport context

Records model exactly what the schema declares and nothing else. When an event happened is a property of the fire, not of the event, and it comes from the demo container rather than the event message. So it travels in an envelope:

if (decoder.TryDecode<PlayerDeathEvent>(msg, gameTick, frameNumber, out var envelope))
{
    // envelope.Payload, .EventId, .ServerTick, .GameTick, .FrameNumber
}

The generated records are partial and property-based, so if you would rather carry this on the record itself, add it in a sibling file. The envelope is the recommended shape, but not the only one.

Performance

Factories are generated code, not reflection. The records carry [NativeName] and [GameEventFieldType] for consumers who want to introspect, but the decode path never reads them. A demo can fire hundreds of thousands of events; an attribute lookup per field is not something to pay on that path. GameEventReader is a struct over two references, so decoding allocates only the record itself.

Key lookup is a linear scan over the descriptor's key names. Events carry a handful of keys (the widest in the schema has 18), and building a per-event dictionary costs more than the scan.

Customising the projection

By default all three player-reference tags (player_controller, player_pawn, player_controller_and_pawn) project to int, the raw userid the engine emits, with the original tag preserved on the property. If you build the SDK from source and would rather the records carried your own type, drop a game-event-overrides.json next to the schema:

{
  "usings": ["MyGame.Model"],
  "fieldTypes": {
    "player_controller_and_pawn": {
      "csharpType": "PlayerRef",
      "readAs": "Int32",
      "wrap": "new PlayerRef({0})"
    }
  }
}

The same entry drives the record property and the factory that fills it, so the two cannot disagree. readAs must name a GameEventReader accessor; a typo fails at generation time with a message naming the valid set, rather than emitting code that will not compile.

Dependencies

CS2OpenDev.Protos, because the decoder's input type is a protobuf message, and CS2OpenDev.Sdk for the event records. Both resolve from NuGet.org, same as this package.

This package exists so that split can hold: CS2OpenDev.Sdk ships zero package dependencies, and a consumer who only wants schema types is never made to take Google.Protobuf. CI asserts that property on every build.

MIT.

Product Compatible and additional computed target framework versions.
.NET net8.0 is compatible.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 was computed.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 was computed.  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. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages (1)

Showing the top 1 NuGet packages that depend on CS2OpenDev.Sdk.GameEvents:

Package Downloads
CS2DemoKit.Parser

Zero-copy parser for Counter-Strike 2 (.dem) demo files: the frame and net-message stream, 272 typed game events, and stateful entity tracking with typed entity wrappers.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
0.9.5 28 9/14/2026
0.9.1 48 8/18/2026
0.9.0 751 8/18/2026