1. Game basics¶
This documentation largely focuses on game mechanics which involve the player one way or another. Nevertheless, the player doesn’t exist in a vacuum: it interacts with the engine and core aspects of the game. For example, the frame rate (see Frame rate) plays an outsized role in the player movement physics. In this page, we will describe only aspects of the engine and “lower level” game mechanics necessary to understand and exploit Half-Life for speedrunning purposes.
1.1. Versioning¶
Half-Life was first released in 1998. The game has since undergone many changes, though not in the core casual gameplay mechanics and campaign. Changes are usually confined to bug fixes, console command changes, small movement physics alterations, server administration improvements, and quality of life updates. Nevertheless, some ostensibly minor tweaks or bug fixes can neuter critical speedrun tricks and exploits. The most notorious of which is none other than the “bunnyhop cap” (see Bunnyhop cap). Community knowledge points to exe version 1.1.0.8 released in 2001 as the first version carrying the “fix”. As of 2026, all subsequent versions have the bunnyhop cap, and it is very unlikely Valve will officially remove it.
In speedrunning, the term “version” is ambiguous and usually context dependent. When we talk about Half-Life versions, we are usually concerned with how the version impacts game mechanics (such as the aforementioned bunnyhop cap). The most pertinent is the engine version. The Valve developer community wiki contains a page listing out all Half-Life engine versions. On the page, observe that there isn’t a singular “version” as far as the engine is concerned. There are at least the following versioning dimensions:
engine or exe build number, e.g. 8684
exe version string, e.g. 1.1.0.8
protocol version, e.g. 48
The most precise way to refer to a specific official release of the game is by referencing the build number. To find out the build number of a game, we could launch the game and run the version command in the developer console, or reverse engineer the hw.dll to examine the value returned by the build_number function.
Many engine builds share a common set of characteristics which distinguish them from other builds. To help distinguish the most important characteristics among the engine builds, speedrunners categorise them most broadly as WON and Steam. Broadly speaking, the WON versions of Half-Life refer to versions of the game prior to being released on Steam, while Steam versions are those released on Steam. We can further divide Steam into pre-SteamPipe, SteamPipe, and 25th anniversary. Speedrunners adopt the following mental model to identify the distinctive characteristics of each category.
- WON
Maximum frame rate of 100 fps. Version 1.1.0.7 and earlier do not have the bunnyhop cap, while 1.1.0.8 and after do. No quickgauss (Quickgauss). No easy repeated action scripts with
_special. The first crowbar hit has a higher damage.- Steam, pre-SteamPipe
Maximum frame rate raised to 1000 fps. All builds under the pre-SteamPipe category have the bunnyhop cap. Quickgauss works. Scripting with
_specialpossible. The “501 fps slow down” trick works (see Slowdown on older engines).- Steam, SteamPipe
No limit to frame rate with some nuances detailed in Frame rate, but the code change caused the 501 fps slowdown trick to stop working. Scripting with
_specialdisabled by Valve despite pleas from the community. Later versions since some time in 2019 fixed an issue where NPCs turn very slowly at higher frame rates.
- Steam, 25th anniversary
Object boosting and manoeuvring (Object manoeuvre) no longer works. Use key braking removed entirely. The first crowbar hit has a higher damage similar to the WON versions.
This categorisation method will likely stay unmodified indefinitely. As Valve rarely intentionally reverts bug fixes, and the 25th anniversary is generally considered unattractive for speedrunning, future releases of Half-Life will very likely be ignored by the community and therefore do not call for new categorisation.
1.1.1. Version preference¶
Manual speedrunners or RTA runners generally prefer the WON version, with an important nuance which we will describe in the subsequent section. This is primarily due to levelling out the hardware playing field by forcing a cap of 100 fps, lack of bunnyhop cap, and lack of later “fixes” to NPC turn rate and object boosting. The lack of the extremely powerful quickgauss in WON is apparently not a big enough downside for RTA speedrunners, especially in light of uncapped bunnyhop. Exploiting the “infinite health door” in the Surface Tension chapter also takes several more seconds in WON compared to Steam, but the community considers this an acceptable trade off.
Tool-assisted speedrunners, however, generally prefer the Steam versions. Tooling is generally better for the Steam versions: for one, Linux binaries with DWARF debug symbols are only available in Steam versions. Official vanilla untouched versions are also considered, at least by the author of this documentation, to be “purer”, compared to what is commonly done for the WON version used by RTA runners (again, see Game/DLL version). This matters more to TASes as they run a higher risk of being accused of cheating. The deterrence of bunnyhop cap also matters less because the player could still maintain movement speed by precise ducktapping. Quickgauss is also much more exploitable in a TAS because inhuman precision is required to control the player at extreme speeds made possible by it. Many would consider creating a TAS on a WON version to be not making full use of the precision afforded by tool assistance. Most importantly, Steam versions allow pinning the frame rate at 1000 fps, which maximises strafing acceleration (see Strafing).
As this documentation is primarily focused on TAS applications, we will prioritise our analysis of Half-Life game mechanics on the Steam versions, particularly those before the 25th anniversary changes. As mentioned earlier, the changes brought upon by the 25h anniversary version are poorly received by the speedrunning community. The lack of object boosting to instantly max out the movement speed is too big of a trade off in exchange for nothing of speedrunning value. The inability to perform use key braking is also a deal breaker for high-speed precise TASing.
At the time of writing, the last Steam versions generally used for speedrunning are build 6153 and perhaps build 8684 or earlier. There is nothing fundamentally special about build 6153, except that it is the first SteamPipe version (according to tribal knowledge) and it comes with the commonly used GoldSrc Package 2.4 circulated among speedrunners.
1.1.2. Game/DLL version¶
Unfortunately, there is one aspect of Half-Life which makes matters even more confusing. Broadly speaking, the game is modular, consisting of an engine component and a “game/DLL” component. The engine component refers to the following binaries (and their macOS and Linux equivalents):
hw.dllsw.dll(if present)hl.exe
Meanwhile, the “game/DLL” component refers to the following and their equivalents:
hl.dllclient.dllopfor.dll(for Half-Life: Opposing Force)
In vanilla Half-Life, this distinction is less critical because when Valve releases a “build”, it comes bundled with the corresponding game/DLL component, which may or may not contain code changes specific to that release. However, in modding and speedrunning, the game/DLL of one release may be swapped for the game/DLL from a different release. There are multiple reasons for doing this.
The primary reason is that the game/DLL component is source-available on Github as the Half-Life SDK (HLSDK). Modding sometimes entails modifying the source code then distributing the compiled binaries as modified hl.dll and client.dll, often on ModDB. Sometimes, a mod only distributes a mod folder (akin to the valve folder) containing only the game/DLL component. The player could easily and conveniently install this into an existing Half-Life installation, which could be running any engine build.
In Half-Life speedrunning since the mid-2010s, this swap is often a solution for those who wish to speedrun on the WON version but are hindered by compatibility and tooling issues on modern systems when running the original game outright. Since many desirable characteristics, such as the absence of bunnyhop cap, are determined by the game/DLL component, a runner can take a more modern Steam-based engine and swap in the older WON game/DLL, creating a “mixed breed” setup.
There is, however, a risk that a runner using a mixed breed version might configure settings that are impossible in any vanilla version. For instance, increasing the frame rate beyond 100 fps while bunnyhopping without a cap. This impossible combination was used to produce the landmark and esteemed HL21 segmented speedrun, which modern community standards have since reevaluated as not wholly legitimate. To prevent this, the community established rules to ensure a speedrunning experience that remains as close to the original unmodified WON release as possible, while retaining the quality of life and compatibility updates of the Steam engine. The results of this approach can be seen in the GoldSrc Package and Half-Life 2005 Package.
Since Valve do not make large changes to Half-Life and speedruns have become ever closer to optimality, we do not expect the above setups and arrangements to change meaningfully for, perhaps, forever.
1.2. Tracing¶
Tracing is one of the most important computations done by the game. Tracing is done countless times per frame, and it is vital to how entities interact with one another.
Note
expansion needed
1.3. Randomness¶
The Half-Life universe is full of uncertainties, much like our universe at the level of quantum mechanics. Randomness in Half-Life is sourced in two ways: by means of the shared RNG and the non-shared RNG. These are custom written pseudo-RNGs that are powered by vastly different algorithms. The shared RNG is so named because it is computed by the game server and shared with the game clients, while the non-shared RNG is computed independently by the game server and clients without any kind of sharing or synchronisation between them.
1.4. Frame rate¶
When we think of the concept of frame rate, or sometimes somewhat incorrectly referred to by its unit of measurement frames per second or fps, we think of the refresh rate of the screen when playing Half-Life. However, it is crucial to distinguish between three different types of frame rate:
- rendering frame rate
This is the real-time rate at which graphics are painted on the screen, denoted as
. This definition maps to what is normally thought of as the frame rate. The rendering frame rate is usually limited by𝑓 𝑟 = 𝜏 − 1 𝑟 fps_maxin normal gameplay, though ifhost_framerateis set, thenfps_maxis ignored. Other factors can also limit the maximum frame rate, including, but not limited to, the “vertical sync” setting (in-game or otherwise) andfps_override.- game frame rate
This is the virtual rate at which the majority (with player movement being the important exception) of the game physics are run, denoted as
. The game frame rate is typically in sync with the rendering frame rate, though not always. For example, suppose a computer is not able to render the graphics beyond a rendering frame rate of 500 fps, but𝑓 𝑔 = 𝜏 − 1 𝑔 host_framerateis set to 0.001. This forces the physics to run at a virtual 1000 fps, though because the screen does not update that frequently, the game appears to run twice as slow in real time.- player frame rate
The player frame rate is the virtual frame rate at which the majority of the player movement physics (see Player movement basics) are run, denoted as
. The player frame rate roughly corresponds to the game frame rate. Depending on the engine version, whether the game is paused, and the value of the game frame rate itself, the player frame time𝑓 𝑝 = 𝜏 − 1 𝑝 may oscillate between different values, stay at zero, or be rounded towards zero to the nearest 0.001.𝜏 𝑝
1.4.1. Slowdown on older engines¶
Suppose the game frame rate is higher than 20 fps. On older game engines, roughly before build 6027, the player frame rate equals the game frame rate rounded towards zero to the nearest 0.001, as mentioned above. Namely, we have
Definition 1.1 (Slowdown factor)
The slowdown factor is defined as the fraction
When the slowdown factor is less than one, the actual movement speed of the player will be lower. The player’s position update described in Position update uses
For instance, a trick known as the “501 fps slowdown” was implemented in Half-Life 21 (see Half-Life 21 (2014) by quadrazid et al.) to permit opening and passing through doors in the Questionable Ethics chapter without stopping dead by the doors before they could be opened fully. The slowdown factor at 501 fps is
It’s also worth noting that on pre-Steam versions of Half-Life and its expansions, the default frame rate is 72 fps (and some speedrunners believe it should not be exceeded), which would give a slowdown factor of
The following theorem is a well known fact among speedrunners: by setting an appropriate value for the game frame rate
Theorem 1.1 (No-slowdown frame rate)
The slowdown factor
Proof. By the last equality in (1.1),
which is only possible if
1.5. Savestates¶
1.6. DELTA rounding¶
The DELTA mechanism is one of the ways Half-Life uses to save bandwidth in client-server communication. Data sent between them include, but not limited to, the player velocity, player position, viewangles (see Viewangles), entity positions, weapon states, and many others. Essentially, information about every entity and player input in the game. Curiously, the schema for all the data sent via the DELTA mechanism is stored as the delta.lst file under the valve or mod folder. To illustrate, the following code excerpt pertains to the player inputs:
usercmd_t none
{
DEFINE_DELTA( lerp_msec, DT_SHORT, 9, 1.0 ),
DEFINE_DELTA( msec, DT_BYTE, 8, 1.0 ),
DEFINE_DELTA( viewangles[1], DT_ANGLE, 16, 1.0 ),
DEFINE_DELTA( viewangles[0], DT_ANGLE, 16, 1.0 ),
DEFINE_DELTA( buttons, DT_SHORT, 16, 1.0 ),
DEFINE_DELTA( forwardmove, DT_SIGNED | DT_FLOAT, 12, 1.0 ),
DEFINE_DELTA( lightlevel, DT_BYTE, 8, 1.0 ),
DEFINE_DELTA( sidemove, DT_SIGNED | DT_FLOAT, 12, 1.0 ),
DEFINE_DELTA( upmove, DT_SIGNED | DT_FLOAT, 12, 1.0 ),
DEFINE_DELTA( impulse, DT_BYTE, 8, 1.0 ),
DEFINE_DELTA( viewangles[2], DT_ANGLE, 16, 1.0 ),
DEFINE_DELTA( impact_index, DT_INTEGER, 6, 1.0 ),
DEFINE_DELTA( impact_position[0], DT_SIGNED | DT_FLOAT, 16, 8.0 ),
DEFINE_DELTA( impact_position[1], DT_SIGNED | DT_FLOAT, 16, 8.0 ),
DEFINE_DELTA( impact_position[2], DT_SIGNED | DT_FLOAT, 16, 8.0 )
}
For speedrunning purpose, it is not necessary to understand every field defined in the delta file. Nevertheless, it is important to be aware of the highlighted lines in Listing 1.2. To understand why, we first need to understand what DEFINE_DELTA means.
The first parameter is the field name.
The second is the field type.
The third is the number of bits to represent the field value after converting to an integer type.
The fourth is a post-multiplier which the receiver must multiply by to restore the intended value. This name is seen in the DWARF symbol in the Linux binary.
Take the definition for forwardmove as an example. We see that the type is defined to be DT_SIGNED | DT_FLOAT. The number of bits to represent the value is only 12 and the post-multiplier is 1. This means that the value of forwardmove when received by the server-side code is an integer in the range of
Let’s see why. Recall that a single-precision floating point number requires 32 bits. This implies that the forwardmove undergoes a lossy compression when sent across the network. To be more precise, DT_SIGNED | DT_FLOAT means that the value will first be converted into an integer, thus truncating it or rounding towards zero. Then, the integer value will be represented as a sign and magnitude bits, rather two’s complement. In particular, the integer will be clamped or clipped to fit in the magnitude bits. In addition, a post-multiplier of 1 means that the serialised value will be multiplied by 1 when received to restore the intended value, which implies the intended value is exactly what is sent on the wire to begin with. (The reader may verify these facts by reverse engineering the DELTA_* and MSG_WriteSBits functions in hw.so or equivalents.) Since sidemove and upmove as per the highlighted lines in Listing 1.2.
Equally as important are the viewangles[*] fields. They have the data type DT_ANGLE, represented in 16 bits, with a post-multiplier of 1. According to MSG_WriteBitsAngle in hw.so, we may model the operation mathematically as follows.
Lemma 1.1
Let
where the meaning of
On the receiver side, the MSG_ReadBitsAngle is called to parse the wire format. Mathematically, it performs the following operation.
Lemma 1.2
Let
We may now examine the end-to-end or round trip conversion. Putting Lemma 1.1 and Lemma 1.2 together, we obtain the function
Theorem 1.2
If
Proof. The only difference between the end-to-end conversion and
In Theorem 1.2 we only need to concern ourselves with nonnegative
Footnotes