Zero press


Revision as of 20:37, 20 May 2024 by Inuk (talk | contribs) (small correction and adjustments)

The zero press is a technique that can be used to activate switches through walls and at farther distances than is normally possible. It is commonly used in speedrunning to reach normally inaccessible switches in order to skip sections of maps.

Performing it requires precise positioning parallel to the button linedef that is typically achieved by making very slight adjustments while pressing the use key repeatedly. This is helped along by shorttics, which cuts the player's range of possible turning angles down, making it significantly easier to aim parallel to buttons placed at right angles or even some diagonal angles.

The trick is named after Zero-Master, who discovered and shared it in 2017. This led to many speedrunning record improvements, such as on MAP07: Dead Simple which became beatable in three seconds.

Technical explanation

The bug is caused by imprecision in fixed point math where bits are discarded during multiplication. This leads to a linedef mistakenly being considered to be aimed at when it is not, which allows it to be included in distance calculation not designed for it. This bad input line is considered to be closest, so it is pressed even though it is not being aimed at.

When a player performs the use action, a line segment (hereby referred to as the trace) is made from the center of the player to the farthest point they can press a button at, and then the function PIT_AddLineIntercepts in p_maputl.c investigates every linedef near the trace, collecting the linedefs that intersect it, as this means they are being aimed at. The function P_PointOnDivlineSide determines whether a point is on one side of the line or the other. Two calls are made, one for each linedef vertex; if the two vertices are on different sides of the trace, this linedef is intersecting the trace, meaning this linedef is being aimed at.

An abridged version of P_PointOnDivlineSide modified for clarity has the following relevant logic, implementing the cross product formula which gives a negative or positive result depending on the ordering of vectors, which in this case is used for determining which side of the line a point is on:

   ( fixed_t	x,
     fixed_t	y,
     divline_t*	line )
       dx = (x - line->x);
       dy = (y - line->y);
       left = FixedMul ( line->dy>>8, dx>>8 );
       right = FixedMul ( dy>>8 , line->dx>>8 );
       if (left - right > 0) return FRONT;
       else return BACK;

The function FixedMul makes use of the property of the fixed point number format that simply multiplying them as though they were regular integers gives the correct result, albeit with bits in excess. There can be up to 32 excess bits, for a total of 64 bits, in a multiplication result, as a fixed point number is 32 bits. Downwards bitshifting is used to discard these excess bits, and is equivalent to floor division targeting the lowest fractional hex digits. The function FixedMul discards the 16 lowest bits of the product and leaves to the caller the discarding of an additional 16 bits from the factors, leading to a 32 bit final result. This precision loss is also responsible for some wiggle room when performing the glitch, as the least significant bits of all coordinates, and therefore those of the player's position, are discarded and are therefore treated identically.

This precision loss is in addition to the side-finding function's bias to the back side of a line; if the point rests precisely on the line, the cross product is 0, and the point is considered to be behind the line. As precision loss in the form of floor division between positive quantities brings results closer to 0 while discarding fractions, small quantities risk results becoming exactly zero, switching the side of the line they would have represented if there was no precision loss.

This collection of equations, representing fixed point numbers plainly as integers and borrowing the comment and hexadecimal syntax from the C language, is an example of calculations from a real zero press and how results differ depending on whether or not precision is lost, demonstrating that small quantities giving results of 0 can cause a side switch:

   //// Determining the side of both vertices with full precision:
   // 64 bit results are acceptable and no bitshifting is performed.
   // This results in correctly determining that both vertices are on the same line side.
   // The symbol _ is a digit separator for ease of reading.
   left  = 0x0000_0640 * 0x0010_0000 = 0x0000_0000_6400_0000
   right = 0x0000_0100 * 0x003F_FFC0 = 0x0000_0000_3FFF_C000
   vertex1side = FRONT // because left - right > 0
   left  = 0x0000_0640 * 0x0090_0000 = 0x0000_0003_8400_0000
   right = 0x0000_0100 * 0x003F_FFC0 = 0x0000_0000_3FFF_C000
   vertex2side = FRONT // because left - right > 0
   //// Determining the side of both vertices with precision loss:
   // 32 bit results are required and bitshifting was used to discard excess bits.
   // 8 bits have been discarded from the factors, and 16 bits have been discarded from the products.
   // This results in incorrectly determining that the vertices are on different sides.
   left  = FixedMul(0x00_0006, 0x00_1000) = 0x0000_0000
   right = FixedMul(0x00_0001, 0x00_3FFF) = 0x0000_0000
   vertex1side = BACK  // because left - right <= 0
   left  = FixedMul(0x00_0006, 0x00_9000) = 0x0000_0003
   right = FixedMul(0x00_0001, 0x00_3FFF) = 0x0000_0000
   vertex2side = FRONT // because left - right > 0

Linedefs that have been determined to intersect the trace go on to have their distance to the trace base, the player, calculated. This is done in the function P_InterceptVector, which takes the trace and a linedef and calculates relative distance between them by measuring how close the infinite linedef line intersects with the finite trace line base, the returned value being a number in the range [0, 1] where lower values represent linedefs intersecting closer to the base of the trace, and higher values represent linedefs intersecting closer to the end of the trace.

   ( divline_t* v2,
     divline_t* v1 )
       fixed_t frac;
       fixed_t num;
       fixed_t den;
       den = FixedMul (v1->dy>>8,v2->dx) - FixedMul(v1->dx>>8,v2->dy);
       if (den == 0) return 0;
       num =
       FixedMul ( (v1->x - v2->x)>>8 ,v1->dy )
       +FixedMul ( (v2->y - v1->y)>>8, v1->dx );
       frac = FixedDiv (num , den);
       return frac;

Ignoring the special zero denominator case, and the precision loss less important than in the the side-finding function from before, this distance-finding function boils down to the following equation:

   // T.1 and T.2 are the points of the use trace line (where T.1 is the base)
   // L.1 and L.2 are the points of the linedef
   // T.1.x is the x coordinate of the point T.1
   num = (L.1.x - T.1.x)*(L.2.y - L.1.y) + (T.1.y - L.1.y)*(L.2.x - L.1.x)
   den = (L.2.y - L.1.y)*(T.2.x - T.1.x) - (L.2.x - L.1.x)*(T.2.y - T.1.y)
   frac = num / den

This equation can be plugged into an interactive graphing tool to gain an intuitive understanding of what the frac value, the relative distance, represents. This does not require an understanding of the equation itself.

Results from this function are sorted, and the shortest button or wall linedef is chosen and pressed on.

Due to this distance-finding function regarding linedefs as infinite in length, it is crucial that it is only given linedefs that intersect the trace, as the intended behavior is to find the relative distance of the trace base to the point of linedef intersection. If the linedef does not intersect, as is the case with a zero press, this point of intersection is on a nonsensical infinite imaginary extension of the linedef. The following is an exaggerated and not-to-scale illustration of this situation:

   P represents the player, at the base of the trace.
   / represents the trace.
   | represents a wall linedef being aimed at.
   # represents a button linedef behind the wall.
   - represents the imaginary extension of the button linedef.
        / |
       /  |
     /    |
    /     |
   P      |

As the imaginary extension of the button linedef intersects closer to the base than any other linedef, it is chosen for the use action. It is also this pressing on the imaginary extension that causes the extended range of the use action for zero presses.

External links