Button Glitch

From P2SR Wiki

Button Glitch


Button Glitch describes a class of glitches where a button remains activated despite neither a player or cube being on it as a result of incorrectly ordered entity IO. It is not to be confused with the similar but distinct Button Save Glitch.

There are 2 main types of button glitch: delayed output button glitches (DOBGs) and simultaneous output button glitches (SOBGs).

Delayed Output Button Glitches

DOBGs are a style of button glitch exclusive to coop. In this type, the button sends an OnPressed output after the corresponding OnUnPressed output. This results in the button activation being the last event the game processes, so anything activates by the button (doors, fizzlers, etc) remains activated. This glitch can normally only be triggered by Orange in coop, since it relies on certain networking quirks.

Simulatenous Output Button Glitches

SOBGs are an alternative style of button glitch whereby the OnPressed and OnUnPressed outputs are sent in the correct order, but both on the same server tick. This type of button glitch can be achieved both by Orange in coop, or in singleplayer - note that in singleplayer, a SOBG cannot be triggered by jumping on a button with keyboard/mouse input, so most button glitch setups involve walking at a precise angle so as to hit the button trigger very briefly. For most buttons, SOBGs are not possible; these outputs being triggered on the same tick has no effect on processing. However, there are two situations in which SOBGs are possible.

The first case is for buttons with pending outputs. Sometimes, a button will activate a component on a delay: for instance, buttons activating stairs or panels tend to have a small delay on the relevant output to allow a more clean visual effect. However, this raises potential issues where a button's outputs are triggered after it is deactivated. To prevent this, such buttons use a special entity input called CancelPending, which is able to remove all entity inputs triggered by a specific entity from the entity IO queue. A button with a pending output will use CancelPending whenever pressed or unpressed in order to prevent pending actions from previous presses/unpresses being executed. In normal scenarios, this doesn't cause any issues. However, as the entity IO queue is only processed at the end of each server tick, if the "button activated" and "button deactivated" inputs are queued on the same tick, then the "deactivated" inputs are already on the queue by the time the "activated" inputs are processed. This means the CancelPending removes the button deactivation inputs - scheduled for later this tick - from the queue, causing them never to happen and sticking the button.

The second case is for buttons triggering the SetPosition input of func_movelinear entities. When run, this input first checks if the target position given is equal the the entity's current position, and does nothing if so (presumably to prevent making sounds etc despite not visibly moving). However, since it checks against the current position rather than the target position, and does not update the current target position if they're the same, this means that if a func_movelinear is precisely at the position requested when the SetPosition input is processed, it will continue on its current path. If a button triggers such an entity, then performing a SOBG on the button means e.g. a SetPosition(1) and SetPosition(0) input will occur on the same tick. If the entity is already at position 0, the first event will be processed and the target position will be updated; however, the entity does not actually move until the following tick, meaning the SetPosition(0) input processed afterwards will see the entity already in the given position and do nothing, meaning the target position of 1 persists.

Technical Explanation

There are two main mechanisms by which button glitches can arise.

Prediction Tick Base Correction

When a client communicates with a server over the network, the packets containing input info (usercmds) are not transmitted every tick. Instead, to save data, the usercmds are bulked into packets which are only sent up to cl_cmdrate times per second (30 by default). Every fully simulated tick, the server will execute all new usercmds received. As part of this, the server attempts to determine "when" the first movement tick to be simulated happened (pretending the movement ticks are in sync with server ticks), and stores it into a field on the player entity called m_nTickBase.

When in singleplayer or running with cl_predict 0 (i.e. in splitscreen, or as the host), m_nTickBase is set so that the final command being run will be synchronised with the current server tick. However, when a client has prediction enabled (cl_predict 1), the server assumes the client is remote, and could therefore have unreliable network conditions. Therefore, if the code used the same logic, this could lead to tick numbers being randomly skipped or repeated due to network variances. So instead, the server does its best to keep m_nTickBase at its current value. Specifically, as long as the expected final tick number is within a certain range of the tick count, it will not modify m_nTickBase. This range is defined by the sv_clockcorrection_msecs cvar - this cvar is converted to a number of ticks (call it n), and the safe range is defined to be [servertick, servertick + 2n]. However, if the final expected tick base for a server tick ever exceeds this range, the tick base will be reset so that the final movement tick will fall on servertick + n, presumably since it's deemed better to have ticks that run "in the future" than it is to have them "in the past". Practically, this means that the final tick base will be at servertick + n most of the time. The default value of sv_clockcorrection_msecs is 60, so we can conclude that predicted clients will normally have m_nTickBase set several ticks in the future.

When a usercmd is run, the server's clock (specifically gpGlobals->curtime - see CPlayerMove::RunCommand) is temporarily set to the current value of m_nTickBase. During this processing, the server builds up a list of every entity the player collides with while moving. At the end of the command, the server reverts its timer back to the old value, and calls moveHelper->ProcessImpact(), which eventually fires off StartTouch callbacks. If the player has started touching the trigger on a button, this callback will trigger an OnPressed event, queuing its outputs for this tick. However, there is a bug in the timer reverting code which means that for all but the first usercmd processed in a tick, the timer is not reverted to the true server tick, but instead to the tick base of the previously simulated usercmd. Therefore, assuming we hit the button on the second (or later) usercmd in a server tick, the queued OnPressed outputs are queued for the previous tick base which, on predicted clients, will be in the future.

When the button is unpressed, the trigger is no longer hit by the player within the last processed usercmd. However, the EndTouch callback is not triggered until later in the server tick (see the CBaseEntity::PhysicsCheckForEntityUntouch call in CEntityTouchManager::FrameUpdatePostEntityThink), which happens after the timer has been correctly reverted by CBasePlayer::PhysicsSimulate. In normal circumstances, the OnPressed outputs would have been triggered on the same server tick, so the outputs would at least be ordered correctly. This happens if the button was pressed on the first usercmd in a server tick and unpressed in a later usercmd on the same tick, and it triggers an SOBG. However, if the button was pressed on the second usercmd in a server tick, the OnPressed output will be scheduled, say, 3 server ticks in the future (due to the faulty timer reverting logic). Then, 2 server ticks in the future, the server receives and processes another usercmd packet in which you exit the button, and triggers the OnUnPressed outputs - a tick before the OnPressed event is scheduled, causing a DOBG.

sv_alternateticks

When alternateticks is active (sv_alternateticks is set to 1, and the server is a singleplayer listen server), we see a similar situation to the SOBG case described above. Because of alternateticks, the client usercmds are processed in pairs by the server every other server tick. If on the first tick the button is activated, OnPressed outputs are scheduled for the current server tick (as the timer reverting succeeds for the first usercmd). Then, if the button is deactivated on the second tick, the OnUnPressed outputs are also scheduled for the current server tick. The server will therefore recognise both inputs as being triggered on the same tick, triggering an SOBG. Note that DOBGs are not possible to achieve through this mechanism.