github markus-wa/demoinfocs-golang v6.0.0-alpha.2

pre-release4 hours ago

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 NewParser constructor with functional options & context.Context, Cancel() removed by @markus-wa in #724
    • NewParser(r io.Reader, opts ...option.Option) is the only constructor. ParserConfig, DefaultParserConfig and all *WithConfig variants are replaced by the new option package (option.NetMessageDecryptionKey(key), option.UserCmdParsing(option.UserCmdParsingFull), …), with the same defaults as before
    • ParseToEnd(ctx) & ParseFile(ctx, path, cb, opts...) take a context: cancel it instead of calling Cancel(), check errors.Is(err, context.Canceled) instead of ErrCancelled. 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)), then NewParser(r). NewCSTVBroadcastParser/ParseCSTVBroadcast are gone. A non-200 response to the initial requests now returns cstv.ErrUnexpectedStatus instead 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 the CCSGameRulesProxy entity exists no longer keeps a nil Entity() forever)
    • sendtablescs2 is merged into sendtables; Entity, Property, ServerClass and ServerClasses are concrete types (*sendtables.Entity etc., Property() returns nil if not found)
    • The ifacemaker-generated interfaces and both fake packages are removed (no more testify in your build); examples/mocking shows how to define your own small interface + fake
    • Entity.Destroy, Entity.Get, Exists, the Get* getters and Map are unexported or removed; Entity.String(), ServerClasses.String() and Entity.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: ErrUnexpectedEndOfDemo for truncated demos, *DecodeError / ErrDecodeFailed (with Frame, Cause & Stack) for corrupt data, unsupported schemas and missing properties (incl. PropertyValueMust calls inside your handlers), and reading demo: … for errors from your io.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 returns ErrParserStopped
    • 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 returns ErrDecodeFailed instead of allocating gigabytes or hanging forever. Broken DEM_FullPacket frames are skipped with a ParserWarn (WarnTypeBrokenFullPacket), as the game does (#409); a corrupt frame of any other type is now ErrDecodeFailed instead of a warning + empty message. Fuzz targets and a corrupt-demo corpus are part of the test suite now
    • option.IgnorePacketEntitiesPanic now actually dispatches ParserWarn{Type: WarnTypePacketEntitiesPanic, Err} (it used to print to stderr) and no longer swallows user panics; events.ParserWarn has a new Err field
    • NewParser no longer reads from the stream, read errors surface from ParseToEnd/ParseNextFrame; Close() closes the stream even before parsing. PropertyValueMust panics with *sendtables.PropertyNotFoundError instead of a string
  • Player.Name & Player.SteamID64 now 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 on broken_1.dem), brings back player_changename renames 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 gets PlayerDisconnected, IsConnected = false & Entity = nil. An existing player's SteamID64 is never changed
    • PlayerInfo & StringTablePlayerUpdateApplied only fire when a slot's info actually changes; PlayerNameChange comes from controller updates and is not dispatched for bots, unknown → name, or placeholder names (empty / SteamID64)
    • Player.IsBot is m_steamID == 0, Player.Team is set on creation, raw userinfo is kept after disconnect
    • New sendtables.Entity.OnUpdateFinished(func()): the parser applies controller changes there, so inside your own CCSPlayerController prop handlers the bound Player isn't updated yet, read it from an OnUpdateFinished delegate 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 in Playing() 0 → 0, kills identical
  • common.UniqueID replaces Equipment.UniqueID2() and the int64 IDs of GrenadeProjectile / Inferno by @markus-wa in #712 (with @AdamMagued, #711) — one uint64 counter 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/ulid is 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 in GameState().Weapons() with its Owner until it is destroyed or comes back
    • A same-serial re-create (PVS re-entry) keeps the same *Entity: entity handlers see EntityOpUpdated|EntityOpEntered instead of Created|Entered, ServerClass.OnEntityCreated doesn't fire again, and property handlers fire only for values that changed
    • POV demos no longer emit duplicate GrenadeProjectileThrow, grenade WeaponFire & InfernoStart events for projectiles / infernos that re-enter the PVS (pov.dem: 904 → 315 throws, 253 → 86 inferno starts; GOTV output is byte-identical)
  • BombDropped.EntityID is 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 at BombDropped.Player.EntityID by @markus-wa in #737
  • Ambiguous bare sendtable field names no longer resolve by @markus-wa in #706 — e.g. m_vecX on 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_iHealth etc.) are unaffected
  • Equipment.AmmoInMagazine() returns one more than before by @markus-wa in #718 — the m_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 +1 you added downstream. Also fixed on v5 (#719)
  • Deterministic ordering by @markus-wa in #739 — Player.Weapons(), GameState.Hostages() and Participants.SpottersOf() / SpottedBy() are sorted by entity ID, and leftover projectiles / infernos at round_officially_ended are 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 your go.sum; cd examples before go run ./print-events -demo …
  • The v5 line continues to be maintained on the v5 branch 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.Projectile by @markus-wa in #708 (#361, #601)
    • HeExplode, FlashExplode, SmokeStart / SmokeExpired, DecoyStart / DecoyExpired, FireGrenadeStart / FireGrenadeExpired and InfernoStart / InfernoExpired carry the *GrenadeProjectile, resolved from the event's entity ID, so Projectile.UniqueID() identifies one throw
    • Grenade is the projectile's WeaponInstance instead 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), Thrower is filled from the projectile for CS2's thrower-less inferno_* events
    • Infernos are linked to their molotov / incendiary projectile (all inferno events in the corpus, GOTV and POV). InfernoStart is now dispatched at the end of the frame the inferno was created in
  • Player.Velocity() works on demos recorded before AnimGraph 2 (protocol < 14089, where m_vecVelocity isn'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 (one WarnTypeVelocityTrackingStartedLazily per 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
  • New sendtables.Entity API: OnUpdateFinished, InPVS(), OnLeavePVS, OnEnterPVS, Replaced() (destroyed because its index was reused without a delete, e.g. on round restarts / backup restores) and String(), see above
  • events.BombsiteUnknown (the misspelled BomsiteUnknown stays 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, so bindWeapon reused the stale *Equipment (and its ID) for the new weapon: 2,239 weapon entities sharing an *Equipment across 23 of 49 demos → 0, 158 spurious ItemRefunds across 14 demos gone, and a USP-S no longer shows up as a P2000. Stale projectiles / infernos that lingered in GameState() 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.WeaponInstance is 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
  • Fix GameRules.RoundTime() always returning ErrFailedToRetrieveGameRule on 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 & BombPickup for 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 no BombDropped
    • no duplicate BombPickup on repeated owner updates (36 across 49 demos), no BombDropped for a pass to an owner that hasn't resolved yet
    • with option.DisableMimicSource1Events(true), BombDropped.EntityID / Player and BombPickup.Player are read from CS2's entindex / userid_pawn keys (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 WarnTypeCantReadEncryptedNetMessage warning 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.dem peak 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, ErrInvalidFileType message) fixed in #717; new README "Error Handling" section in #730

Changes

  • Dependencies: pkg/errors & x/exp replaced with the standard library in #716 (%w, errors.Is, cmp.Ordered), oklog/ulid dropped in #712, testify/objx dropped in #725, example-only deps (draw2d, go-heatmap, freetype, x/image, vdf) moved to the examples module in #717, godispatch v1.5.1 (fixes handler order & dropped handlers when registering after the first Dispatch)
  • Performance: within noise of alpha.1 on BenchmarkDemoInfoCs across the API cleanup PRs (benchstat, 10 interleaved rounds each); Participants().All() goes from 3 to 1 allocs/op in #739. User OnUpdate property 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 → main in #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-csda re-enabled against the v6 fork of CS Demo Analyzer in #703, push / PR runs deduped in #720, coverage uploads via codecov-action in #723, lint / race / coverage in parallel with cached test demos in #744

Full Changelog: v6.0.0-alpha.1...v6.0.0-alpha.2

Discussions / Chat / Announcements

Discord Chat

Don't miss a new demoinfocs-golang release

NewReleases is sending notifications on new releases.