CS2OpenDev.Sdk.Entities.Abstractions 1.0.3

dotnet add package CS2OpenDev.Sdk.Entities.Abstractions --version 1.0.3
                    
NuGet\Install-Package CS2OpenDev.Sdk.Entities.Abstractions -Version 1.0.3
                    
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.Entities.Abstractions" Version="1.0.3" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="CS2OpenDev.Sdk.Entities.Abstractions" Version="1.0.3" />
                    
Directory.Packages.props
<PackageReference Include="CS2OpenDev.Sdk.Entities.Abstractions" />
                    
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.Entities.Abstractions --version 1.0.3
                    
#r "nuget: CS2OpenDev.Sdk.Entities.Abstractions, 1.0.3"
                    
#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.Entities.Abstractions@1.0.3
                    
#: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.Entities.Abstractions&version=1.0.3
                    
Install as a Cake Addin
#tool nuget:?package=CS2OpenDev.Sdk.Entities.Abstractions&version=1.0.3
                    
Install as a Cake Tool

CS2OpenDev.Sdk.Entities.Abstractions

The read contract that generated Counter-Strike 2 entity wrappers are emitted against.

A demo parser implements it. Generated wrapper code consumes it. It carries field identity and value semantics, and deliberately nothing about storage, decode or lifetime. Those are private engineering decisions every parser makes differently, and a seam that encoded one parser's answers would only ever fit that parser.

Zero dependencies outside the BCL. Trimmable and AOT-compatible.

The shape

Four types carry the contract proper. IEntityFieldReader reads one entity's current field values, by ordinal. IEntityWorld turns a raw packed handle into a wrapper; it has one member. EntityWrapper is the base class holding a reader and a world, and EntityClassBinding is the pure-data manifest: canonical paths, aliases, handle ordinals.

Around them sit QAngle, for Euler angles, since Vector3's X/Y/Z names mislead on an angle triple; SchemaFieldVersionAttribute, recording which CS2 builds a field existed in; and the conformance kit, DictionaryEntityReader (the reference implementation) plus BindingConformance (the structural checks a well-formed binding passes).

Two things worth knowing before implementing it

Absence first. Every TryRead* returns false when a field has never been received on the wire, which is different from receiving a default. m_lifeState's 0 means LIFE_ALIVE, so a reader that cannot distinguish the two reports corpses as healthy. If your storage does not already track per-field presence, this is the member that will tell you.

Handles second, and they cross the seam undecoded. TryReadEntityHandle hands back a raw uint. The packing is (serial << index_bits) | index and how many bits the index gets is not documented authoritatively upstream; two implementations in this ecosystem already disagree. Mask, sentinel encodings and serial validation are yours. Resolution goes through IEntityWorld.Resolve<T>, which returns null for every way a handle can fail to name a live entity of the requested type.

Implementing it

Bind once per class, read many times. The binding gives you CanonicalPaths; build your own ordinal → wherever-you-keep-that-field map from it at bind time, falling back through Aliases when the canonical path is absent from the demo's serializer. That fallback is what lets a wrapper generated today read a recording made before Valve renamed the field.

Bind against the array, never against hard-coded ordinals. Ordinals are an implementation detail shared between a generated wrapper and the manifest emitted beside it, and they are not stable across releases.

Testing it

DictionaryEntityReader is the reference implementation. Running your reader against the same assertions is how you find out whether you agree with it about what the contract means:

var binding = new EntityClassBinding(
    EngineClass: "CCSPlayerPawn",
    NetName: "CSPlayerPawn",
    CanonicalPaths: ["m_ArmorValue", "m_CBodyComponent.m_pSceneNode.m_vecOrigin"],
    Aliases: new Dictionary<string, string>
    {
        ["m_vecOrigin"] = "m_CBodyComponent.m_pSceneNode.m_vecOrigin",
    },
    HandleOrdinals: []);

var reader = new DictionaryEntityReader(binding, new Dictionary<string, object?>
{
    ["m_ArmorValue"] = 100,
});

reader.TryReadInt32(0, out int armor);   // true, 100
reader.TryReadInt32(1, out _);           // false — never received, not zero
reader.TryReadByEnginePath("m_vecOrigin", out _); // alias resolves to the canonical path

BindingConformance.ThrowIfInvalid checks a manifest's structural invariants (dense ordinals, aliases that resolve, handle ordinals in range) with nothing constructed. Worth running at startup over whatever binding set you load.

Versioning

1.0 claims exactly one thing: the shape survived contact with a second implementation. Not that the author is happy with it. The criterion was written down before it was met, and it is met.

DemoViewer.NET implemented IEntityFieldReader and IEntityWorld over their own runtime against 0.1.1 and reported no contract change required, no runtime hooks added, and 43 conformance tests passing. That met the criterion on 0.1.1. 1.0 was withheld anyway, because their findings had already changed the reference reader underneath it: TryReadEntityHandle now folds integral widths rather than converting them, so a handle written as int -1 reads as the 0xFFFFFFFF sentinel instead of absent, and they had validated the reader before that change. Shipping 1.0 on evidence that predates it would have asserted the criterion without meeting it.

They re-ran against 0.2.1 and confirmed it narrowly: the conformance port executed inside a full run of their parser suite, 268/268 passed, 0 failed, 0 skipped — so nothing passed by being skipped rather than updated. Their handle-sentinel test predates the reference fix and already asserted present-0xFFFFFFFF, which is why the reference catching up moved nothing on their side.

The published release was 0.3.0, not the 0.2.1 they ran. That gap closes by inspecting the diff: 0.3.0 was the versionHeightOffset cut, and its diff against 0.2.1 over this directory is version.json alone. No reader, no interface, no conformance kit moved between the code they validated and the code 1.0.0 carries. Had that diff shown one line of contract, this would be another 0.x.

What 1.0 costs from here: a breaking change to this contract is a MAJOR. SDK#30 has candidate resolutions that would move the read seam off ordinal addressing; if one of those is chosen, it ships as 2.0 rather than sliding in. Pricing that correctly is the point.

What it does not claim: that the contract is finished, or that a third implementation would find nothing. Only that the shape held when someone other than its author built against it.

One friction is deferred rather than resolved: a binding set has no contract-visible place to record which Schema Lens state it was derived from. That belongs on the generated wrapper registry, which does not exist yet. It is recorded here so it lands in the registry's design rather than being bolted into a NetName string later.

This package does not share the major version of CS2OpenDev.Sdk / .GameEvents / .Protos. Those regenerate together from the schema; this is a contract with its own life, and its version.json is scoped to its own directory so a schema regen cannot move it.

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.
  • net8.0

    • No dependencies.

NuGet packages (2)

Showing the top 2 NuGet packages that depend on CS2OpenDev.Sdk.Entities.Abstractions:

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.

CS2OpenDev.Sdk.Entities

Typed entity wrappers for Counter-Strike 2, generated from the curated Schema Lens state. Reads through CS2OpenDev.Sdk.Entities.Abstractions, so the wrappers work over any demo parser implementing that contract rather than over one particular runtime.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
1.0.3 1,146 8/18/2026