Skip to content

v5 backport: all fixes from master without behavioural changes, API breakages or Go version changes - #687

Draft
markus-wa wants to merge 138 commits into
v5from
v5-backport
Draft

markus-wa wants to merge 138 commits into
v5from
v5-backport

Conversation

@markus-wa

Copy link
Copy Markdown
Owner

What

Backports everything merged to master since v5.2.0 to the v5 maintenance line — except behavioral changes, API breakages and Go version changes.

Branched from master @ b793dd5 (the last /v5-module commit, before the v6 module rename).

What was reverted (relative to master)

Revert Reason
#681 — require go 1.27 Go version change; v5 stays on go 1.24.0
#658 — x/image 0.35 → 0.41 requires go ≥ 1.25, incompatible with v5's go 1.24
#663: b6ac932 + 65a13e4 InfernoFireOut event (new event dispatched = behavioral change)
#663: 4126b7b PlayerFlashed victims paired by detonation instead of FIFO
#663: e1e45f5 + 9285813 fatal-hit HealthDamageTaken capped at remaining HP
#675 PlayerHurt dispatched before entity updates (Player.Health() reporting pre-event HP)

These are exactly the behavioral changes called out in the v6.0.0-alpha.0 release notes Breaking Changes section, plus the Go version bump.

What's included (kept from master)

Verification

  • go build ./..., go vet ./..., full go test ./pkg/... green
  • test/default.golden regenerated: exactly 41 lines removed, 0 added — all InfernoFireOut (Site/Position/event-value fixes kept, so no other golden drift)
  • scripts/check-interfaces-generated.sh clean
  • go.mod: module github.com/markus-wa/demoinfocs-golang/v5, go 1.24.0, x/image v0.35.0

Notes

  • Merging this fast-forwards v5 past b1bc851 (v5.2.0) — no force needed.
  • The fix: dispatch empty-weapon PlayerHurt before entity updates #675 revert intentionally reintroduces the known PlayerHurt behavior from v5.2.0 (bomb-explosion hurts observe post-update entity state), per the no-behavioral-changes rule for this line. The fix remains available on master/v6.

Copilot AI and others added 30 commits April 21, 2026 13:19
broadcasts: fix player user-id mappings when starting parsing mid-match
fix: always update player user ID in getOrCreatePlayer
feat: update proto messages
Net-messages are handled on a queued goroutine that can lag thousands of
messages behind the frame loop. When a message handler panics, the
dispatcher's PanicHandler records the error immediately and the frame
loop stops on its next iteration - but the already-queued backlog is
still fully processed, both by the consumer and by shutdown's
SyncAllQueues().

For a panic inside CSVCMsg_PacketEntities handling this is destructive:
the message is half-applied, the entity table no longer matches the
stream, and every backlog message decodes against wrong classes. In
production we watched ~3,100 queued messages get processed after the
first recorded panic, generating ~3,000 follow-on panics and multi-GB
allocations from desynced field paths (~121 GB live heap, OOM kill).

Skip handler work once a fatal error has been recorded: aborted() checks
the recorded error, and every handler that decodes data or invokes user
callbacks returns early. The backlog then drains as no-ops and
ParseToEnd returns the recorded first error promptly. Cancel() gets the
same fast-stop behaviour since it uses the same error mechanism.

Validated on the production demo that motivated this: with a panicking
user handler, the parse now returns the recorded error in ~2.1s at
~139MB peak instead of being OOM-killed at ~121GB; healthy demos parse
with byte-identical results.
fieldState.set trusts decoded field-path components completely; get()
bounds-checks but set() does not. On a desynced bitstream a garbage
component such as 2142459506 (observed in production on a CS2 demo after
a handler panic corrupted entity state) makes set() allocate
(z+1) * 16 bytes in a single call - about 32 GB. Live heap reached
~121 GB before the OOM killer intervened; because the memory is
reachable, GOMEMLIMIT cannot defend against it. The variable-size
collection path in Entity.readFields has the same exposure through
make([]any, newLen, newLen*2).

Bound both with a shared maxFieldIndex (1<<20). Real components top out
in the double digits and the largest networked arrays hold a few
thousand entries, so the bound only fires on corrupt or desynced input,
converting an OOM kill into a recoverable parse error.

Validated against the production demo that motivated this: parse now
fails in ~2.3s at ~159MB peak instead of OOM at ~121GB, and healthy
demos parse with byte-identical results.
Bumps [github.com/stretchr/testify](https://github.com/stretchr/testify) from 1.11.1 to 1.12.1.
- [Release notes](https://github.com/stretchr/testify/releases)
- [Commits](stretchr/testify@v1.11.1...v1.12.1)

---
updated-dependencies:
- dependency-name: github.com/stretchr/testify
  dependency-version: 1.12.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Fixes an entity-stream desync (panic: "unable to find existing entity")
on demos containing plugin-spawned entities such as CBeam.

The quantized-float decoder truncated in quantize(), but the engine's
encoder rounds to nearest. For fields like CBeam m_fWidth (bits=10,
low=0, high=102.3, round-up flag), quantize(high) yields 1022.99994:
truncation reconstructs a value != high, so the round-up flag was
wrongly kept and the decoder consumed a phantom 1-bit special flag
where the stream held a plain 10-bit value, desyncing the rest of the
message.
fix: round to nearest when quantizing floats
Regression noticed while backporting 74a7360 into the v4 branch. See the CS demo analyzer tests https://github.com/markus-wa/demoinfocs-golang/actions/runs/33257364117/job/99116464310 failure.

In PlayerHurt events, event.Player.Health() == event.Health, which should not.
event.Health is the new health and event.Player.Health() should be the HP before the event.

It happens because entity props are updated after game events and the event was delayed.
It's unnecessary, the bomb_exploded game event always precedes the
same-frame player_hurt events caused by the explosion.
markus-wa and others added 29 commits August 31, 2026 00:42
Add a permanent regression test using a CS2 deathmatch demo, where the
polymorphic m_pGameRules.m_pGameModeRules pointer resolves to
CCSGameModeRules_Deathmatch - a type with its own sub-fields (unlike the
Noop type used by every other demo in the s2 set).

The demo is added to the cs-demos submodule as a new small deathmatch.7z
LFS archive and extracted into the s2 set, so it also joins the
TestDemoSetS2 no-unexpected-warnings sweep.

The new test asserts:
- the active deathmatch type's sub-fields resolve by name and enumerate
  via Properties() (regression for the getFieldPaths nested-table fix)
- values: m_flDMBonusStartTime, m_flDMBonusTimeLength, m_sDMBonusWeapon
- a competitive-only field (m_nHalvesPlayed) does not resolve under the
  deathmatch type

CI: coverage.sh and race-tests.sh now pull deathmatch.7z, and the race
run covers TestPolymorphicGameModeRules alongside TestDemoInfoCs.
…nd creation

Add an append-only handlerLog to Entity as the single source of truth for
update-handler dispatch ordering. handlersByFP rebuilds (on polymorphic type
change) and the entity-creation initial dispatch now iterate the log in
registration order instead of name-sorted map keys, so handlers sharing an
fp key fire in their original registration order, stable across rebuilds.

Resolves the pre-existing TODO in OnPacketEntities (introduced by
6a6eb08 'fix random ordered create handlers') which noted the creation
dispatch should be ordered by handler registration order.

Regenerates test/default.golden: the only change is IsWarmupPeriodChanged
now firing in registration order relative to GamePhaseChanged.
…tries

- maxPolyID: only cache fully computed results. A back-edge into an
  in-progress traversal (a serializer cycle, which flattened-serializer
  data cannot produce) no longer persists an understated value in the
  memo map, so a later top-level call on such a node recomputes it and a
  class backed by it gets a correctly sized polySerializers slice.
- readFields: panic on a sub-field update under a polymorphic pointer
  with no active serializer instead of silently skipping the value bits
  (which would desync the rest of the packet).
- PropertyEntries/collectFieldsEntries: enumerate sub-fields of every
  polymorphic alternative, not just the default serializer.
- Tests: maxPolyID cycle no-stale-cache regression, poly PropertyEntries.
…ymorphic-fields

Fix polymorphic field support in CS2 sendtables parser
Various fixes: correctness, new events/getters, and doc corrections
Bumps [golang.org/x/image](https://github.com/golang/image) from 0.35.0 to 0.41.0.
- [Commits](golang/image@v0.35.0...v0.41.0)

---
updated-dependencies:
- dependency-name: golang.org/x/image
  dependency-version: 0.41.0
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
…rg/x/image-0.41.0

chore(deps): bump golang.org/x/image from 0.35.0 to 0.41.0
…te churn

noscaleDecoder re-boxed the cached float32 into a fresh any on every decode,
defeating reader.f32Cache and costing ~786k extra heap allocations per demo
parse (+37% allocs, +8.5% wall time vs v5.2.0, confirmed via pprof alloc_objects).
Return the pre-boxed cache value directly again, as in v5.2.0.

Polymorphic pointer base decoders allocated a polyUpdate per decode even when
the active type did not change; return a pointer to a scratch slot on the reader
instead. readFields consumes the value synchronously before the next decode on
the same reader, so reuse is safe.

Benchmarks (s2.dem, benchstat):
  allocs/op  5.792M -> 5.630M (-2.8% vs v5.2.0)
  B/op       350.0Mi -> 324.8Mi (-7.2%)
  CPU        parity (pprof -diff_base: -0.03s net)
IsWalking() is backed by m_bIsWalking, which indicates that the walk
(sneak) key is held - it is true even while standing still.

Document this and point users to Velocity() (or position deltas between
ticks, since Velocity() returns a zero vector on GOTV demos) for actual
movement detection.

Refs #318
docs(common): clarify IsWalking() reflects walk key, not movement
Second optimization pass; allocs another -9% and bytes another -25% below
the previous commit, both now well below v5.2.0 levels.

- f32Cache 512 -> 4096 entries: fewer conflict misses; working-set misses
  dominate, so larger sizes don't pay off
- quantized float32 decodes route through reader.f32Cache, reusing
  pre-boxed values (~145k allocs/parse less; stationary players repeat
  positions across ticks)
- QAngle decoders return pre-boxed [3]float32 from a new per-reader
  direct-mapped v3Cache keyed by the three bit patterns
- frame and embedded-message payload buffers are recycled via sync.Pool;
  proto.Unmarshal always copies bytes fields, so the input buffers are
  dead after unmarshaling. CSTV broadcasts keep the unpooled path since
  they retain buffers in messages (m.Data = buf)
- compressed frames decode snappy into a per-parser scratch buffer
  instead of a fresh allocation per frame

Benchmarks (s2.dem, benchstat vs v5.2.0):
  allocs/op  5.792M -> 5.107M (-11.8%)
  B/op       350.0Mi -> 243.9Mi (-30.2%)
  CPU        differential pprof: ~3% below v5.2.0
perf: fix alloc regressions, pool frame buffers, end below v5.2.0 in allocs & memory
…golang.org/x/image-0.41.0"

This reverts commit c1d131b, reversing
changes made to c8ed29f.
…h-timing"

This reverts commit a441e78, reversing
changes made to ca6decb.
Brings in encrypted net-message support (#696, #697) and the v5 lint
base fix (#698). Conflict resolution:
- protos & generated .pb.go: take v5's (identical to master's), keep usercmd.proto in generate.sh
- s2_commands.go: master's resolution from a2dd17a (msgCreatorForType + usercmd handling)
- events.go: WarnType order matches master so values are identical on v5 and v6
- ci.yml: keep v5's base_rev for lint-changes

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@markus-wa
markus-wa marked this pull request as draft October 11, 2026 11:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants