Skip to content

Fix button extraction for delta-encoded usercmds - #348

Open
WangChuDi wants to merge 2 commits into
LaihoE:mainfrom
WangChuDi:feat/complete-usercmd-reconstruction
Open

Fix button extraction for delta-encoded usercmds#348
WangChuDi wants to merge 2 commits into
LaihoE:mainfrom
WangChuDi:feat/complete-usercmd-reconstruction

Conversation

@WangChuDi

Copy link
Copy Markdown
Contributor

Description

This PR ports the missing codegen_delta_encoder behavior identified while investigating #343 and keeps the existing parse_ticks usercmd path as the public interface. It covers the command-number baseline ring, baseline validation, wire type 7 resets, nested message merges, and indexed repeated delta operations required by post-July demos.

The decoder behavior missing from #343

#343 could decode the ordinary protobuf shape of delta_data, but it did not model the client-side command state closely enough. The game does not apply every delta to one unverified last command per player: it selects a baseline from a 150-slot command-number ring, verifies the stored command number, and refuses a missing or mismatched baseline without mutating the previous state.

The delta stream is also not an ordinary protobuf replacement message. Valve's generated encoder uses wire type 7 to clear protobuf presence and restore generated defaults, recursively merges nested messages, and uses indexed operations for repeated input_history and subtick_moves entries. Treating these operations as ordinary field replacement can produce a syntactically valid command with fields inherited from the wrong baseline or repeated entries lost.

Changes in this PR

  • Add a 150-slot command-numbered ring for each player and require an exact command-number match before applying a delta.
  • Track baseline missing, ring-slot mismatch, command-order mismatch, and delta failure categories instead of silently falling back to an unrelated command.
  • Exclude full-packet checkpoint usercmds from the normal transport baseline sequence.
  • Implement wire type 7 presence resets, including generated non-zero defaults and nested clear paths.
  • Recursively merge nested messages and apply indexed repeated operations with patch, truncation, growth, preservation, malformed-input, and bounds handling.
  • Preserve and project the reconstructed usercmd fields used by the existing parse_ticks path, including buttons, mouse movement, weapon selection, input history, subtick moves, execution notes, prediction fields, and attack-history indexes.
  • Keep the existing parse_ticks(["buttons"]) routing fix so aggregate button requests activate the stateful decoder and remain on the ordered parser path.

No parseusercmds API is added. The DLL comparison hooks and counters are test/diagnostic support only; production parsing does not load client.dll or require HLAE.

Public usercmd properties

Relative to the current fork/main baseline, this PR additionally exposes the following usercmd properties through the existing property mapping: usercmd_command_number, usercmd_server_tick_executed, usercmd_legacy_command_number, usercmd_base_client_tick, usercmd_prediction_offset_ticks_x256, usercmd_up_move, usercmd_random_seed, usercmd_move_crc, usercmd_cmd_flags, usercmd_execution_notes, usercmd_attack1_start_history_index, usercmd_attack2_start_history_index, usercmd_is_predicting_body_shot_fx, usercmd_is_predicting_head_shot_fx, usercmd_is_predicting_kill_ragdolls, usercmd_transport_client_tick, usercmd_pawn_entity_handle, usercmd_player_slot, usercmd_subtick_move_analog_forward_delta, usercmd_subtick_move_analog_left_delta, usercmd_subtick_move_button, usercmd_subtick_move_when, usercmd_subtick_move_pitch_delta, and usercmd_subtick_move_yaw_delta.

The baseline already exposed usercmd_viewangle_x/y/z, usercmd_forward_move, usercmd_left_move, usercmd_impulse, usercmd_mouse_dx/dy, usercmd_buttonstate_1/2/3, usercmd_weapon_select, usercmd_left_hand_desired, usercmd_consumed_server_angle_changes, usercmd_input_history, and usercmd_subtick_moves; this PR fixes their reconstruction when the transport uses delta encoding. buttons remains the aggregate convenience property, while usercmd_buttonstate_1/2/3 expose the raw button masks.

All of these public property names can be requested directly through the existing parse_ticks([...]) API; no parseusercmds API is required. For example: parser.parse_ticks(["usercmd_command_number", "usercmd_viewangle_x", "usercmd_mouse_dx", "usercmd_weapon_select", "usercmd_input_history", "usercmd_subtick_moves"]).

usercmd_input_history and usercmd_subtick_moves are repeated nested structures, so their output contains structured lists and child values. usercmd_move_crc is exposed as a hexadecimal string. Command number, server tick, player slot, pawn handle, and transport client tick are envelope metadata projected alongside the reconstructed CSGOUserCmdPB, while usercmd_execution_notes is diagnostic information rather than a protobuf payload field.

The internal decoder reconstructs the complete known usercmd payload and the DLL differential test reports zero field differences, but parse_ticks intentionally exposes only the mapped properties listed above; arbitrary future protobuf fields are not automatically exported.

Game DLL reference

The reference implementation was observed from an offline -insecure CS2 process using the public CS2 usercmd DLL oracle. IDA was used to identify the pre-decode transport path, the post-decode point after ring/cache updates, and the generated CSGOUserCmdPB serializer. The probe records the game's result; it does not replace the game decoder or run inside demoparser.

Rejected DLL transports are recorded as rejected because the game never produces a complete post-decode payload for them. The parser therefore rejects the same transport rather than inventing a command from an unavailable baseline.

Validation demo and baseline

The validation demo is the demo attached to the issue #340 comment, stored locally as issue340_pr343_sample.dem. The comparison baseline is LaihoE/demoparser origin/main at commit 266a831f08b0264dd722b017a5c05d765206a7ed.

Decoder result comparison

The table below uses the same demo for the DLL oracle and the current parser. The DLL column counts every transport observation, the origin/main column is the captured baseline at 266a831, and the current parser column shows raw/aligned transport counts where public parse_ticks output intentionally deduplicates by (player_slot, command_number).

Item Game DLL oracle LaihoE/demoparser origin/main Current parser 57414d8 Diff (parser 57414d8 vs DLL) Reason
Transport records 1,323,304 1,323,664* 1,323,314 raw; 1,323,304 aligned +10 raw; 0 aligned The raw parser scan includes 10 delta-only bootstrap records before the first DLL-aligned anchor; the aligned transport stream is identical.
Full data records 2,221 2,571* 2,221 raw; 10 public unique 0 raw; -2,211 public unique The public result deduplicates repeated transport/checkpoint keys, while the DLL count keeps every transport observation.
delta_data records 1,321,083 1,321,093* 1,321,093 raw; 1,300,726 applied +10 raw; 0 aligned The 10 raw excess records are the same pre-anchor bootstrap records; 1,300,726 is successful delta application count, not a DLL transport count.
Successfully reconstructed payloads 1,302,947 1,075,850* 1,302,947 aligned; 1,300,736 public unique 0 aligned; -2,211 public unique The aligned decoded payload stream matches; public output collapses repeated (player_slot, command_number) keys.
Rejected or unavailable payloads 20,357 10 baseline-missing + 247,814 generic delta failures* 20,357 aligned 0 aligned
Baseline diagnostics Not exposed Not tracked* 20,340 missing; 0 mismatch; 0 fallback N/A The DLL oracle does not expose these internal counters; the parser's raw missing count includes the 10 bootstrap records, while the aligned missing count is 20,330.
Player slots 1-10 1-10 1-10 0
Records with non-zero buttons 841,739 739,104* 841,584 public -155 public The DLL counts every decoded transport; the parser metric counts unique public commands after deduplication.
Records with mouse movement 313,180 417,711* 313,159 public -21 public The DLL counts every decoded transport; the parser metric counts unique public commands after deduplication.
Weapon-selection records 13,304 99,548* 13,304 public 0
Records containing subtick moves 59,713 130,090* 59,713 public 0
Maximum subtick moves in one command 7 5* 7 0
Maximum input-history entries 5 4* 5 0
cannot_move 261,665 249,697* 260,888 public -777 public cannot_move comes from usercmd execution notes; the DLL counts transport records, while the parser metric is recorded only for unique public commands.
dead Not present in DLL oracle 102,270* 22,168 public N/A dead is derived from demo entity life_state through the pawn handle; it is not a field in the DLL usercmd payload.

The current parser statistics were generated after the ring and decoder changes: raw full_data=2,221, raw delta_data=1,321,093, delta_applied=1,300,726, baseline_missing=20,340, and baseline_mismatch=0. Public totals are lower because repeated checkpoint records and repeated command keys are not emitted as new public command rows.

The full streaming differential test compared all 1,302,947 decoded payloads and 20,357 rejected records against the game oracle. It compared scalar fields, nested messages, repeated input-history entries, repeated subtick entries, command metadata, and ring/cache command numbers and reported zero protobuf field differences.

Values marked with * are not game truth: origin/main used one unchecked baseline per player and did not model command-number mismatch behavior. The differences in aggregate public counts are event-versus-public-key scope differences, not a failure of the aligned field comparison.

Tests

  • cargo test --manifest-path src/parser/Cargo.toml: 346 passed, 0 failed, 2 ignored.
  • matches_full_instrumented_dll_usercmd_stream_field_for_field with the complete binary oracle: 1 passed, 0 failed; 1,302,947 decoded payloads and 20,357 rejected records aligned.
  • git diff --check: passed.

Completion and limitations

For the tested client.dll build and issue demo, the standalone decoder now matches the game's post-decode usercmd payload field for field, including the command-number baseline behavior and rejection boundaries. Future CS2 client updates may change generated fields, defaults, delta schemas, ring behavior, or hook signatures and require a new oracle capture and review.

GPT assistance disclosure

As with #343, GPT models were used to assist with the reverse-engineering analysis and demoparser code changes. The implementation was checked against IDA observations, the real issue demo, the origin/main baseline, and the injected client.dll oracle.

Copilot AI lite review requested due to automatic review settings August 18, 2026 08:59

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@ItsCEED

ItsCEED commented Aug 28, 2026

Copy link
Copy Markdown

Are we getting this baked any time soon?

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.

3 participants