Counter-Strike 2 introduced a new input system called Sub-Ticks, replacing the traditional approach where a user_cmd represented a single game tick. Instead, each command can now contain multiple sub-tick inputs, allowing the server to reconstruct player actions with much greater precision.
What are Sub-Ticks?
Prior to Source 2, player input was sampled once per server tick. Counter-Strike 2 instead records player actions at a higher temporal resolution by storing multiple sub-tick events inside a single user_cmd.
When the command reaches the server, these events are replayed in the order they occurred, allowing the server to accurately determine exactly when actions such as aiming or shooting took place between ticks.
One important consequence of this design is that simply modifying the view angles stored in user_cmd is no longer sufficient. Those values are overwritten by the individual sub-tick commands during processing, meaning each sub-tick must be updated instead.
Reconstructing the Sub-Tick structures
class c_cmd_qangle
{
public:
char pad1[0x18];
vec3_t angles;
};
static_assert(sizeof(c_cmd_qangle) == 0x24);
class c_sub_tick_cmd
{
public:
char pad1[0x18];
c_cmd_qangle* view;
};
static_assert(sizeof(c_sub_tick_cmd) == 0x20);
class c_sub_tick_container
{
public:
std::int32_t tick_count;
char pad1[0x4];
std::uint8_t* tick_pointer;
c_sub_tick_cmd* get_tick(std::int32_t index)
{
if (index < this->tick_count)
{
c_sub_tick_cmd** tick_list = reinterpret_cast<c_sub_tick_cmd**>(this->tick_pointer + 0x8);
return tick_list[index];
}
return nullptr;
}
};
static_assert(sizeof(c_sub_tick_container) == 0x10);
class c_user_cmd
{
public:
char pad1[0x20];
c_sub_tick_container sub_tick_container;
char pad2[0x20];
std::uint32_t buttons;
};
static_assert(sizeof(c_user_cmd) == 0x58);
The c_sub_tick_container is located at offset 0x20 inside c_user_cmd.
It stores the number of recorded sub-ticks together with a pointer to the underlying sub-tick array. The pointer references an internal structure where the array begins at offset 0x8.
Each individual c_sub_tick_cmd can be accessed by indexing this array, allowing its associated view angles to be inspected or modified.
Updating Sub-Tick view angles
void set_sub_tick_angles(c_user_cmd* cmd, const vec3_t& angles)
{
c_sub_tick_container container = cmd->sub_tick_container;
for (std::int32_t i = 0; i < container.tick_count; i++)
{
c_sub_tick_cmd* tick = container.get_tick(i);
if (tick && tick->view)
{
tick->view->angles = angles;
}
}
}
To force a specific viewing direction, every recorded sub-tick must be updated individually. Modifying only the base user_cmd view angles is insufficient, as the server reconstructs the player’s aim from the sub-tick data instead.