Button Glitch

From P2SR Wiki

Revision as of 19:51, 30 March 2022 by Mlugg (talk | contribs) (Fix unclosed tags)

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: due to a desynchronisation between server and client tick counts over the network, or as a consequence of sv_alternateticks.

Network Timer Desync

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). When the server processes these usercmds, it temporarily reverts the server's timer (specifically, gpGlobals->curtime - see CPlayerMove::RunCommand). During usercmd processing, the game builds up a list of every entity the player hits while moving, and at the end of the command - importantly, before reverting the timer - calls moveHelper->ProcessImpacts(), 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 all its outputs for this tick. However, in doing that, the tick count is calculated from curtime, meaning it uses the temporarily modified value.

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, after the timer has been reverted (see the CBaseEntity::PhysicsCheckForEntityUntouch call in CEntityTouchManager::FrameUpdatePostEntityThink). In normal circumstances, this isn't a problem, because usercmd processing would only ever move the server clock backwards. However, when cl_cmdrate is causing usercmds to be bulked together in packets, it is possible for the client and server to desynchronise, causing the client to send a usercmd too early (TODO: why?). If a packet is sent 2 ticks early (i.e. the final usercmd in the packet is for 2 ticks in the future), it is possible to activate the button on the second usercmd of the packet, and deactivate it on the third, so an activation event is queued for a tick in the future and the deactivation for now (since the EndTouch was processed after the timer was reverted to the current tick). This causes a DOBG, since the queued OnPressed outputs aren't processed until the tick after the OnUnPressed outputs. Similarly, if a packet is sent only 1 tick early, the inputs occur on the same tick, causing a SOBG.

This method of button stick seems to be fairly random, although capping the host's FPS seems to slightly improve the odds of getting it. Getting a SOBG is relatively common - around 50/50 (TODO: does this imply *every* command context is sent a tick early, and DOBGs are caused by inconsistent frame times causing 3 usercmds to be bulked together?) - however DOBGs are quite rare. This means that buttons on which SOBG works, while not required, are much easier to stick in coop.

TODO: why can the client send a usercmd too early under cmdrate?
TODO: does the 50/50 SOBG thing imply *every* command context is sent [nticks-1] ticks early, and DOBGs are caused by inconsistent frame times causing 3 usercmds to be bulked together?

sv_alternateticks

When alternateticks is active (sv_alternateticks is set to 1, and the server is a singleplayer listen server), we see a similar story to the "1 tick early" case above. Because of alternateticks, the client usercmds are processed in pairs by the server every other server tick. One of these ticks is for the future. If on the first tick the button is activated, OnPressed outputs are scheduled for the movement tick currently being simulated, which happens to match the server tick. Then, if the button is deactivated on the second tick, the OnUnPressed outputs are scheduled for the current server tick. Therefore, the server will recognise both inputs as being triggered on the same tick, triggering a SOBG. Note that DOBGs are not possible to achieve through this mechanism.