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
<PackageReference Include="CS2OpenDev.Sdk.Entities.Abstractions" Version="1.0.3" />
<PackageVersion Include="CS2OpenDev.Sdk.Entities.Abstractions" Version="1.0.3" />
<PackageReference Include="CS2OpenDev.Sdk.Entities.Abstractions" />
paket add CS2OpenDev.Sdk.Entities.Abstractions --version 1.0.3
#r "nuget: CS2OpenDev.Sdk.Entities.Abstractions, 1.0.3"
#:package CS2OpenDev.Sdk.Entities.Abstractions@1.0.3
#addin nuget:?package=CS2OpenDev.Sdk.Entities.Abstractions&version=1.0.3
#tool nuget:?package=CS2OpenDev.Sdk.Entities.Abstractions&version=1.0.3
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 | Versions 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. |
-
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 |