Skip to content

Instantly share code, notes, and snippets.

@zer0k-z
Last active September 18, 2023 17:18
Show Gist options
  • Save zer0k-z/a55e469c4f670e3c98636a67df5250cc to your computer and use it in GitHub Desktop.
Save zer0k-z/a55e469c4f670e3c98636a67df5250cc to your computer and use it in GitHub Desktop.
Weird subtick jump inputs behavior

As of September 18, jump inputs using scroll are treated very weirdly compared to using a normal key. By enabling cl_showusercmd 1 (hidden cvar) and with some extra hackery, I managed to figure out a bit about how the jump input cooldown works.

Basically, the cooldown of 0.015625s (or whatever sv_jump_spam_penalty_time's value is) starts when the player stop pressing the jump button. Whether you can jump again or not depends on when you release the next jump input (or until the end of that tick).

There are two common ways player can jump: Either with a keypress (condition 1 in the examples) or with a scroll step (2). This cooldown functionality works fine with method 1, but gets really confused if players use the wheel to jump.

Example 1: On low timescale, I very quickly pressed spacebar to jump, twice.

cl: 54894 ===========================
cl: 54894: CSGOUserCmdPB
cl: 54894: {
cl: 54894: base {
cl: 54894:   command_number: 54894
cl: 54894:   tick_count: 55024
cl: 54894:   buttons_pb {
cl: 54894:     buttonstate1: 0
cl: 54894:     buttonstate2: 0
cl: 54894:     buttonstate3: 2
cl: 54894:   }
cl: 54894:   viewangles {
cl: 54894:     x: 16.6319809
cl: 54894:     y: 28.4767494
cl: 54894:     z: 0
cl: 54894:   }
cl: 54894:   forwardmove: 0
cl: 54894:   leftmove: 0
cl: 54894:   upmove: 0
cl: 54894:   random_seed: 489248490
cl: 54894:   mousedx: 0
cl: 54894:   mousedy: 0
cl: 54894:   pawn_entity_handle: 11173965
cl: 54894:   subtick_moves {
cl: 54894:     button: 2
cl: 54894:     pressed: true
cl: 54894:     when: 0.611063421
cl: 54894:   }
cl: 54894:   subtick_moves {
cl: 54894:     button: 2
cl: 54894:     pressed: false
cl: 54894:     when: 0.691629827
cl: 54894:   }
cl: 54894:   subtick_moves {
cl: 54894:     button: 2
cl: 54894:     pressed: true
cl: 54894:     when: 0.761942327
cl: 54894:   }
cl: 54894:   subtick_moves {
cl: 54894:     button: 2
cl: 54894:     pressed: false
cl: 54894:     when: 0.834696233
cl: 54894:   }
cl: 54894:   move_crc: "\032\006\010\000\020\000\030\002\"\017\rL\016\205A\025b\320\343A\035\000\000\000\000"
cl: 54894: }
cl: 54894: }
Jump attempt made via condition 1
Jump attempt! Current time = 859.760803 (Frametime = 0.001259), new "last jump input" time = 859.760803 (+0.124451)
Jump attempt made via condition 1
Jump attempt! Current time = 859.763062 (Frametime = 0.001137), new "last jump input" time = 859.763062 (+0.002258)

The jump attempts are made at the end of each jump input. 859.760803 is 69.1% into the tick, which correspond to 0.691 in the subtick move. Same for 859.763062 and 0.834696233.


Things get weird when you use scroll to jump. Due to the fact that scrolling is essentially pressing and releasing the same key at the exact same time, the game gets confused and believes you never released jump until the end of the tick.

Example 2: One scroll step near at the end of the tick. Doesn't matter at which point you scrolled, the "current time" value will always be at the end of the tick.

cl: 55584 ===========================
cl: 55584: CSGOUserCmdPB
cl: 55584: {
cl: 55584: base {
cl: 55584:   command_number: 55584
cl: 55584:   tick_count: 55714
cl: 55584:   buttons_pb {
cl: 55584:     buttonstate1: 0
cl: 55584:     buttonstate2: 0
cl: 55584:     buttonstate3: 2
cl: 55584:   }
cl: 55584:   viewangles {
cl: 55584:     x: 16.6319809
cl: 55584:     y: 28.4767494
cl: 55584:     z: 0
cl: 55584:   }
cl: 55584:   forwardmove: 0
cl: 55584:   leftmove: 0
cl: 55584:   upmove: 0
cl: 55584:   random_seed: 568888718
cl: 55584:   mousedx: 0
cl: 55584:   mousedy: 0
cl: 55584:   pawn_entity_handle: 11173965
cl: 55584:   subtick_moves {
cl: 55584:     button: 2
cl: 55584:     pressed: true
cl: 55584:     when: 0.98557514
cl: 55584:   }
cl: 55584:   subtick_moves {
cl: 55584:     button: 2
cl: 55584:     pressed: false
cl: 55584:     when: 0.98557514
cl: 55584:   }
cl: 55584:   move_crc: "\032\006\010\000\020\000\030\002\"\017\rL\016\205A\025b\320\343A\035\000\000\000\000"
cl: 55584: }
cl: 55584: }
Jump attempt made via condition 2
Jump attempt! Current time = 870.546875 (Frametime = 0.000225), new "last jump input" time = 870.546875 (+0.062500)

In this example 3 below, the game only thinks you released the jump key once there's another jump input in the tick:

cl: 55085 ===========================
cl: 55085: CSGOUserCmdPB
cl: 55085: {
cl: 55085: base {
cl: 55085:   command_number: 55085
cl: 55085:   tick_count: 55215
cl: 55085:   buttons_pb {
cl: 55085:     buttonstate1: 0
cl: 55085:     buttonstate2: 0
cl: 55085:     buttonstate3: 2
cl: 55085:   }
cl: 55085:   viewangles {
cl: 55085:     x: 16.6319809
cl: 55085:     y: 28.4767494
cl: 55085:     z: 0
cl: 55085:   }
cl: 55085:   forwardmove: 0
cl: 55085:   leftmove: 0
cl: 55085:   upmove: 0
cl: 55085:   random_seed: 1738159218
cl: 55085:   mousedx: 0
cl: 55085:   mousedy: 0
cl: 55085:   pawn_entity_handle: 11173965
cl: 55085:   subtick_moves {
cl: 55085:     button: 2
cl: 55085:     pressed: true
cl: 55085:     when: 0.164774343
cl: 55085:   }
cl: 55085:   subtick_moves {
cl: 55085:     button: 2
cl: 55085:     pressed: false
cl: 55085:     when: 0.164774343
cl: 55085:   }
cl: 55085:   subtick_moves {
cl: 55085:     button: 2
cl: 55085:     pressed: true
cl: 55085:     when: 0.851786077
cl: 55085:   }
cl: 55085:   subtick_moves {
cl: 55085:     button: 2
cl: 55085:     pressed: false
cl: 55085:     when: 0.851786077
cl: 55085:   }
cl: 55085:   move_crc: "\032\006\010\000\020\000\030\002\"\017\rL\016\205A\025b\320\343A\035\000\000\000\000"
cl: 55085: }
cl: 55085: }
Jump attempt made via condition 2
Jump attempt! Current time = 862.747681 (Frametime = 0.010735), new "last jump input" time = 862.747681 (+0.028931)
Jump attempt made via condition 2
Jump attempt! Current time = 862.750000 (Frametime = 0.002316), new "last jump input" time = 862.750000 (+0.002319)

The cooldown starts from 85% into the tick and again at the end of the tick, and this doesn't match the behavior of example 1.


The weirdness doesn't end, this weird behavior in example 3 can also happen if the tick right after contain a scroll jump input. Example 4: One scroll step per tick, but there are 3 jump attempts in 2 ticks.

cl: 55584 ===========================
cl: 55584: CSGOUserCmdPB
cl: 55584: {
cl: 55584: base {
cl: 55584:   command_number: 55584
cl: 55584:   tick_count: 55714
cl: 55584:   buttons_pb {
cl: 55584:     buttonstate1: 0
cl: 55584:     buttonstate2: 0
cl: 55584:     buttonstate3: 2
cl: 55584:   }
cl: 55584:   viewangles {
cl: 55584:     x: 16.6319809
cl: 55584:     y: 28.4767494
cl: 55584:     z: 0
cl: 55584:   }
cl: 55584:   forwardmove: 0
cl: 55584:   leftmove: 0
cl: 55584:   upmove: 0
cl: 55584:   random_seed: 568888718
cl: 55584:   mousedx: 0
cl: 55584:   mousedy: 0
cl: 55584:   pawn_entity_handle: 11173965
cl: 55584:   subtick_moves {
cl: 55584:     button: 2
cl: 55584:     pressed: true
cl: 55584:     when: 0.98557514
cl: 55584:   }
cl: 55584:   subtick_moves {
cl: 55584:     button: 2
cl: 55584:     pressed: false
cl: 55584:     when: 0.98557514
cl: 55584:   }
cl: 55584:   move_crc: "\032\006\010\000\020\000\030\002\"\017\rL\016\205A\025b\320\343A\035\000\000\000\000"
cl: 55584: }
cl: 55584: }
Jump attempt made via condition 2
Jump attempt! Current time = 870.546875 (Frametime = 0.000225), new "last jump input" time = 870.546875 (+0.062500)
cl: 55585 ===========================
cl: 55585: CSGOUserCmdPB
cl: 55585: {
cl: 55585: base {
cl: 55585:   command_number: 55585
cl: 55585:   tick_count: 55715
cl: 55585:   buttons_pb {
cl: 55585:     buttonstate1: 0
cl: 55585:     buttonstate2: 0
cl: 55585:     buttonstate3: 2
cl: 55585:   }
cl: 55585:   viewangles {
cl: 55585:     x: 16.6319809
cl: 55585:     y: 28.4767494
cl: 55585:     z: 0
cl: 55585:   }
cl: 55585:   forwardmove: 0
cl: 55585:   leftmove: 0
cl: 55585:   upmove: 0
cl: 55585:   random_seed: 1002333385
cl: 55585:   mousedx: 0
cl: 55585:   mousedy: 0
cl: 55585:   pawn_entity_handle: 11173965
cl: 55585:   subtick_moves {
cl: 55585:     button: 2
cl: 55585:     pressed: true
cl: 55585:     when: 0.70823139
cl: 55585:   }
cl: 55585:   subtick_moves {
cl: 55585:     button: 2
cl: 55585:     pressed: false
cl: 55585:     when: 0.70823139
cl: 55585:   }
cl: 55585:   move_crc: "\032\006\010\000\020\000\030\002\"\017\rL\016\205A\025b\320\343A\035\000\000\000\000"
cl: 55585: }
cl: 55585: }
Jump attempt made via condition 2
Jump attempt! Current time = 870.557922 (Frametime = 0.011066), new "last jump input" time = 870.557922 (+0.011047)
Jump attempt made via condition 2
Jump attempt! Current time = 870.562500 (Frametime = 0.004559), new "last jump input" time = 870.562500 (+0.004578)

Even worse, you can make those 2 inputs into 4 jump attempts if you do it from the ground. The first tick takes you off, which forces a subtick input at after exactly 1 tick (0.015625s). See example 5:

cl: 91874: CSGOUserCmdPB
cl: 91874: {
cl: 91874: base {
cl: 91874:   command_number: 91874
cl: 91874:   tick_count: 92004
cl: 91874:   buttons_pb {
cl: 91874:     buttonstate1: 0
cl: 91874:     buttonstate2: 0
cl: 91874:     buttonstate3: 2
cl: 91874:   }
cl: 91874:   viewangles {
cl: 91874:     x: 15.9807987
cl: 91874:     y: 29.831934
cl: 91874:     z: 0
cl: 91874:   }
cl: 91874:   forwardmove: 0
cl: 91874:   leftmove: 0
cl: 91874:   upmove: 0
cl: 91874:   random_seed: 163865773
cl: 91874:   mousedx: 0
cl: 91874:   mousedy: 0
cl: 91874:   pawn_entity_handle: 11173965
cl: 91874:   subtick_moves {
cl: 91874:     button: 2
cl: 91874:     pressed: true
cl: 91874:     when: 0.819698036
cl: 91874:   }
cl: 91874:   subtick_moves {
cl: 91874:     button: 2
cl: 91874:     pressed: false
cl: 91874:     when: 0.819698036
cl: 91874:   }
cl: 91874:   move_crc: "\032\006\010\000\020\000\030\002\"\017\rZ\261\177A\025\315\247\356A\035\000\000\000\000"
cl: 91874: }
cl: 91874: }
Jump attempt made via condition 2
Jump attempt! Current time = 1437.578125 (Frametime = 0.002817), new "last jump input" time = 1437.578125 (+10.802490)
cl: 91875 ===========================
cl: 91875: CSGOUserCmdPB
cl: 91875: {
cl: 91875: base {
cl: 91875:   command_number: 91875
cl: 91875:   tick_count: 92005
cl: 91875:   buttons_pb {
cl: 91875:     buttonstate1: 0
cl: 91875:     buttonstate2: 0
cl: 91875:     buttonstate3: 2
cl: 91875:   }
cl: 91875:   viewangles {
cl: 91875:     x: 15.9807987
cl: 91875:     y: 29.831934
cl: 91875:     z: 0
cl: 91875:   }
cl: 91875:   forwardmove: 0
cl: 91875:   leftmove: 0
cl: 91875:   upmove: 0
cl: 91875:   random_seed: 1372693000
cl: 91875:   mousedx: 0
cl: 91875:   mousedy: 0
cl: 91875:   pawn_entity_handle: 11173965
cl: 91875:   subtick_moves {
cl: 91875:     button: 2
cl: 91875:     pressed: true
cl: 91875:     when: 0.306026191
cl: 91875:   }
cl: 91875:   subtick_moves {
cl: 91875:     button: 2
cl: 91875:     pressed: false
cl: 91875:     when: 0.306026191
cl: 91875:   }
cl: 91875:   move_crc: "\032\006\010\000\020\000\030\002\"\017\rZ\261\177A\025\315\247\356A\035\000\000\000\000"
cl: 91875: }
cl: 91875: }
Jump attempt made via condition 2
Jump attempt! Current time = 1437.582886 (Frametime = 0.004782), new "last jump input" time = 1437.582886 (+0.004761)
Jump attempt made via condition 2
Jump attempt! Current time = 1437.590942 (Frametime = 0.008036), new "last jump input" time = 1437.590942 (+0.008057)
Jump attempt made via condition 2
Jump attempt! Current time = 1437.593750 (Frametime = 0.002808), new "last jump input" time = 1437.593750 (+0.002808)

The cooldown is reset at the end of the jumping tick, another at the start of the subtick jump input, one more at 0.015625s after the jump time of the last tick, then the last one at the end of the tick.

In short, this makes the 0.015625 cooldown actually way longer than its value would indicate, but it's not possible to reduce this value any further as there are bugs associated with doing it which makes the bhop ratio 100%.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment