Go Get
go get -u github.com/markus-wa/demoinfocs-golang/v6/pkg/demoinfocs@v6.0.0-alpha.2
To upgrade from v5, this may be useful to update import paths (use at your own risk):
sed -i 's/demoinfocs-golang\/v5/demoinfocs-golang\/v6/' **/*.go
See the v6.0.0-alpha.0 and v6.0.0-alpha.1 release notes for the changes since v5 (import path /v5 → /v6, Go 1.27 requirement, etc.).
This release contains the bulk of the v6 API cleanup (#715). Most breaking changes are mechanical, see the migration table at the end of the section.
Breaking Changes
- Single
NewParserconstructor with functional options &context.Context,Cancel()removed by @markus-wa in #724NewParser(r io.Reader, opts ...option.Option)is the only constructor.ParserConfig,DefaultParserConfigand all*WithConfigvariants are replaced by the newoptionpackage (option.NetMessageDecryptionKey(key),option.UserCmdParsing(option.UserCmdParsingFull), …), with the same defaults as beforeParseToEnd(ctx)&ParseFile(ctx, path, cb, opts...)take a context: cancel it instead of callingCancel(), checkerrors.Is(err, context.Canceled)instead ofErrCancelled. A cancellation that happens while the message queue drains is now returned too, instead of silently giving incomplete data- CSTV broadcasts are just an
io.Reader:r, err := cstv.NewReader(ctx, url, cstv.WithTimeout(d), cstv.WithHTTPClient(c)), thenNewParser(r).NewCSTVBroadcastParser/ParseCSTVBroadcastare gone. A non-200 response to the initial requests now returnscstv.ErrUnexpectedStatusinstead of a confusing JSON error or an error page in the demo stream
- Concrete types instead of interfaces ("accept interfaces, return structs") by @markus-wa in #725
*demoinfocs.Parser,*GameState,Participants(value) and a live*GameRules(a value fetched before theCCSGameRulesProxyentity exists no longer keeps a nilEntity()forever)sendtablescs2is merged intosendtables;Entity,Property,ServerClassandServerClassesare concrete types (*sendtables.Entityetc.,Property()returns nil if not found)- The ifacemaker-generated interfaces and both
fakepackages are removed (no more testify in your build);examples/mockingshows how to define your own small interface + fake Entity.Destroy,Entity.Get,Exists, theGet*getters andMapare unexported or removed;Entity.String(),ServerClasses.String()andEntity.Replaced()are newly exported
- Errors instead of panics (#715 PR 4) by @markus-wa in #729, #730 and #732
- The library's own failures are returned from
ParseToEnd/ParseNextFrame:ErrUnexpectedEndOfDemofor truncated demos,*DecodeError/ErrDecodeFailed(withFrame,Cause&Stack) for corrupt data, unsupported schemas and missing properties (incl.PropertyValueMustcalls inside your handlers), andreading demo: …for errors from yourio.Reader - A panic in one of your handlers is re-raised on the caller's goroutine as
*PanicError(original stack kept, recoverable) instead of being returned as an error string. To stop parsing from a handler, cancel the context (context.WithCancelCause) instead of panicking. Calling a parse method again afterwards returnsErrParserStopped - Hardened against corrupt input: frames are capped at 16 MiB (64 MiB for
DEM_AnimationData), field indices & collection sizes at 65536, plus per-parse budgets for entity state. Exceeding a limit returnsErrDecodeFailedinstead of allocating gigabytes or hanging forever. BrokenDEM_FullPacketframes are skipped with aParserWarn(WarnTypeBrokenFullPacket), as the game does (#409); a corrupt frame of any other type is nowErrDecodeFailedinstead of a warning + empty message. Fuzz targets and a corrupt-demo corpus are part of the test suite now option.IgnorePacketEntitiesPanicnow actually dispatchesParserWarn{Type: WarnTypePacketEntitiesPanic, Err}(it used to print to stderr) and no longer swallows user panics;events.ParserWarnhas a newErrfieldNewParserno longer reads from the stream, read errors surface fromParseToEnd/ParseNextFrame;Close()closes the stream even before parsing.PropertyValueMustpanics with*sendtables.PropertyNotFoundErrorinstead of a string
- The library's own failures are returned from
Player.Name&Player.SteamID64now come from the CS2 player controller (CCSPlayerController.m_iszPlayerName/m_steamID, i.e. the server-enforced scoreboard name) by @markus-wa in #710 — fixes names being reverted after a reconnect for good (#692, FACEIT names onbroken_1.dem), brings backplayer_changenamerenames that alpha.1 dropped, and keeps plugin prefixes like"COACH E"- The Steam / connect name stays available via
PlayerInfo&PlayerConnect - A slot taken over by a different person gets a new
*Player; the previous occupant getsPlayerDisconnected,IsConnected = false&Entity = nil. An existing player'sSteamID64is never changed PlayerInfo&StringTablePlayerUpdateAppliedonly fire when a slot's info actually changes;PlayerNameChangecomes from controller updates and is not dispatched for bots,unknown→ name, or placeholder names (empty / SteamID64)Player.IsBotism_steamID == 0,Player.Teamis set on creation, raw userinfo is kept after disconnect- New
sendtables.Entity.OnUpdateFinished(func()): the parser applies controller changes there, so inside your ownCCSPlayerControllerprop handlers the boundPlayerisn't updated yet, read it from anOnUpdateFinisheddelegate instead - Also fixes string-table entry index decoding (indices are relative, not absolute): 18 of 72 demos had userinfo entries attributed to the wrong slot (up to ~25k per demo)
- Verified on 60 demos: name mismatches vs. the controller 1.67M → 0, duplicate
*Players inPlaying()0 → 0, kills identical
- The Steam / connect name stays available via
common.UniqueIDreplacesEquipment.UniqueID2()and theint64IDs ofGrenadeProjectile/Infernoby @markus-wa in #712 (with @AdamMagued, #711) — oneuint64counter type for all three, comparable with<and sorted in creation order.String()/ JSON is the decimal number instead of a 26-character ULID, so IDs stored from earlier versions won't match.oklog/ulidis no longer a dependency- Entities leaving & re-entering the PVS by @markus-wa in #740 and #746 (#728, #741, #742)
- New
Entity.InPVS(),Entity.OnLeavePVS(func())&Entity.OnEnterPVS(func()). A weapon that left the PVS stays inGameState().Weapons()with itsOwneruntil it is destroyed or comes back - A same-serial re-create (PVS re-entry) keeps the same
*Entity: entity handlers seeEntityOpUpdated|EntityOpEnteredinstead ofCreated|Entered,ServerClass.OnEntityCreateddoesn't fire again, and property handlers fire only for values that changed - POV demos no longer emit duplicate
GrenadeProjectileThrow, grenadeWeaponFire&InfernoStartevents for projectiles / infernos that re-enter the PVS (pov.dem: 904 → 315 throws, 253 → 86 inferno starts; GOTV output is byte-identical)
- New
BombDropped.EntityIDis now the C4's entity ID on both the synthesized and the game-event path (it was the dropping player's entity ID, or 0 with mimicking disabled); the player's ID is still atBombDropped.Player.EntityIDby @markus-wa in #737- Ambiguous bare sendtable field names no longer resolve by @markus-wa in #706 — e.g.
m_vecXon a player pawn silently returned the view offset, not the velocity. Use the sendNode-prefixed name (m_vecVelocity.m_vecX,m_vecViewOffset.m_vecX,m_AttributeManager.m_Item.m_NetworkedDynamicAttributes.m_Attributes, …). Names that are unique within their class (m_iHealthetc.) are unaffected Equipment.AmmoInMagazine()returns one more than before by @markus-wa in #718 — them_iClip1+1 offset was subtracted twice on CS2 demos, so every magazine read one round low and an empty one underflowed to 4294967295. Drop any+1you added downstream. Also fixed on v5 (#719)- Deterministic ordering by @markus-wa in #739 —
Player.Weapons(),GameState.Hostages()andParticipants.SpottersOf()/SpottedBy()are sorted by entity ID, and leftover projectiles / infernos atround_officially_endedare destroyed in ascending entity-ID order (not throw order). 12 of 49 demos produced a different event order from run to run before - Examples are a separate Go module (
examples/go.mod) by @markus-wa in #717 — the heatmap / draw2d / vdf dependencies no longer end up in yourgo.sum;cd examplesbeforego run ./print-events -demo … - The
v5line continues to be maintained on thev5branch for users not ready to upgrade
Migration from alpha.1
| alpha.1 | alpha.2 |
|---|---|
NewParserWithConfig(r, cfg)
| NewParser(r, option.X(...), ...)
|
ParseFile(path, cb) / ParseFileWithConfig(path, cfg, cb)
| ParseFile(ctx, path, cb, option.X(...))
|
NewCSTVBroadcastParser(url) / cfg.CSTVTimeout = d
| r, err := cstv.NewReader(ctx, url, cstv.WithTimeout(d)) + NewParser(r)
|
p.ParseToEnd() / p.Cancel() / err == ErrCancelled
| p.ParseToEnd(ctx) / cancel ctx / errors.Is(err, context.Canceled)
|
demoinfocs.UserCmdParsingFull, demoinfocs.NetMessageCreator
| option.UserCmdParsingFull, option.NetMessageCreator
|
demoinfocs.Parser / GameState (interfaces)
| *demoinfocs.Parser / *demoinfocs.GameState
|
sendtables.Entity / Property / ServerClass (interfaces)
| *sendtables.Entity / *sendtables.Property / *sendtables.ServerClass
|
sendtablescs2.X
| sendtables.X
|
fake.NewParser(), sendtables/fake
| your own interface + fake, see examples/mocking
|
Equipment.UniqueID2() (ULID), GrenadeProjectile.UniqueID() (int64)
| UniqueID() (common.UniqueID), .Uint64() for the number
|
panic(...) in a handler to stop parsing
| cancel the ctx (context.WithCancelCause)
|
entity.Property("m_vecX") on a pawn
| entity.Property("m_vecVelocity.m_vecX")
|
New Features
- Track individual grenades:
GrenadeEvent.Projectileby @markus-wa in #708 (#361, #601)HeExplode,FlashExplode,SmokeStart/SmokeExpired,DecoyStart/DecoyExpired,FireGrenadeStart/FireGrenadeExpiredandInfernoStart/InfernoExpiredcarry the*GrenadeProjectile, resolved from the event's entity ID, soProjectile.UniqueID()identifies one throwGrenadeis the projectile'sWeaponInstanceinstead of a "last grenade of that type the player threw" guess (was wrong for 578 of 8,313 flashes and 512 of 4,107 smoke expiries in a 50-demo corpus),Throweris filled from the projectile for CS2's thrower-lessinferno_*events- Infernos are linked to their molotov / incendiary projectile (all inferno events in the corpus, GOTV and POV).
InfernoStartis now dispatched at the end of the frame the inferno was created in
Player.Velocity()works on demos recorded before AnimGraph 2 (protocol < 14089, wherem_vecVelocityisn't networked on GOTV and was returning zero) by @markus-wa in #709 — derived from position deltas in snapshot server-ticks (v4 had a similar fallback, but under-reported by ~3× on demos with 3 ticks per snapshot and spiked on round restarts)- Tracking starts lazily on the first
Velocity()call per player (oneWarnTypeVelocityTrackingStartedLazilyper parse);option.EagerVelocityTracking(true)tracks everyone from creation. No cost on demos where the velocity is networked - The POV recording player's networked value is used as-is; during an entity snapshot the velocity between the last two snapshots is returned so moving shooters don't read 0 at
WeaponFire
- Tracking starts lazily on the first
- New
sendtables.EntityAPI:OnUpdateFinished,InPVS(),OnLeavePVS,OnEnterPVS,Replaced()(destroyed because its index was reused without a delete, e.g. on round restarts / backup restores) andString(), see above events.BombsiteUnknown(the misspelledBomsiteUnknownstays as a deprecated alias) in #717
Fixes
- Fix entity index reuse without a delete (#601) by @markus-wa in #712 — the server sometimes creates a new entity (new serial) at the index of one it never deleted, e.g. on round restarts or backup restores. The old entity was overwritten without
OnDestroy, sobindWeaponreused the stale*Equipment(and its ID) for the new weapon: 2,239 weapon entities sharing an*Equipmentacross 23 of 49 demos → 0, 158 spuriousItemRefunds across 14 demos gone, and a USP-S no longer shows up as a P2000. Stale projectiles / infernos that lingered inGameState()for 10k+ ticks are destroyed too - Fix grenade attribution by @markus-wa in #712 and #738
- the thrown-grenade bookkeeping removes the grenade that actually went off instead of the last one of its type (6
PlayerHurt.Weapons corrected over 74 demos, tracked-but-never-detonated grenades at demo end 183 → 74) GrenadeProjectile.WeaponInstanceis taken from the weapon entity's own throw state (m_bPinPulled/m_fThrowTime), which fixes the wrong instance when a player throws a grenade on the same tick they pick up a floor grenade of the same type
- the thrown-grenade bookkeeping removes the grenade that actually went off instead of the last one of its type (6
- Fix
GameRules.RoundTime()always returningErrFailedToRetrieveGameRuleon CS2 demos (it still looked up the CS:GO property name) by @markus-wa in #726. Also fixed on v5 (#727) - Bomb event fixes by @markus-wa in #737
Bomb().Carrier&BombPickupfor a C4 that is created before its owner's pawn (demos starting in freeze time):Bomb().Position()returned (0,0,0) for up to 6s and a drop by the first holder dispatched noBombDropped- no duplicate
BombPickupon repeated owner updates (36 across 49 demos), noBombDroppedfor a pass to an owner that hasn't resolved yet - with
option.DisableMimicSource1Events(true),BombDropped.EntityID/PlayerandBombPickup.Playerare read from CS2'sentindex/userid_pawnkeys (they were always 0 / the GOTV bot)
- Never abort parsing on undecodable encrypted net-messages by @markus-wa in #705 — empty, truncated or corrupt payloads now give a
WarnTypeCantReadEncryptedNetMessagewarning instead of a panic or a fatal error, so the alpha.1 caveat no longer applies. The path is covered by unit tests with ICE-encrypted fixtures now; there is still no real encrypted demo in the test suite, so reports are welcome - Fix ~30% higher peak memory on POV demos introduced by the hardening in #732: frame buffers above 4 MiB are reused again (#743) by @markus-wa in #745 (
pov.dempeak RSS 211–220 → 171–178 MB) - Docs:
HealthDamageTaken's +1 depends on the server build, not on the Oct 2025 update, in #707; stale API docs (ParseHeader(),NewCustomParser(), CS:GO references,ErrInvalidFileTypemessage) fixed in #717; new README "Error Handling" section in #730
Changes
- Dependencies:
pkg/errors&x/expreplaced with the standard library in #716 (%w,errors.Is,cmp.Ordered),oklog/uliddropped in #712,testify/objxdropped in #725, example-only deps (draw2d,go-heatmap,freetype,x/image,vdf) moved to the examples module in #717,godispatchv1.5.1 (fixes handler order & dropped handlers when registering after the firstDispatch) - Performance: within noise of alpha.1 on
BenchmarkDemoInfoCsacross the API cleanup PRs (benchstat, 10 interleaved rounds each);Participants().All()goes from 3 to 1 allocs/op in #739. UserOnUpdateproperty handlers cost ~9 ns more per call for the panic guard (BenchmarkUserPropertyHandlers+3.6%, worst case with a handler on every pawn property) - Repo: the default branch was renamed
master→mainin #721 (git branch -m master main && git fetch origin && git branch -u origin/main main && git remote set-head origin -a), the GitLab mirror is gone - CI:
test-csdare-enabled against the v6 fork of CS Demo Analyzer in #703, push / PR runs deduped in #720, coverage uploads viacodecov-actionin #723, lint / race / coverage in parallel with cached test demos in #744
Full Changelog: v6.0.0-alpha.1...v6.0.0-alpha.2