Half-Life Physics Documentation · for practical speedrunning aspects of these games, namely...

53
Half-Life Physics Documentation Release Matherunner September 03, 2016

Transcript of Half-Life Physics Documentation · for practical speedrunning aspects of these games, namely...

Page 1: Half-Life Physics Documentation · for practical speedrunning aspects of these games, namely theSourceRuns Wiki. Even years after Half-Life’s release, one can still find casual

Half-Life Physics DocumentationRelease

Matherunner

September 03, 2016

Page 2: Half-Life Physics Documentation · for practical speedrunning aspects of these games, namely theSourceRuns Wiki. Even years after Half-Life’s release, one can still find casual
Page 3: Half-Life Physics Documentation · for practical speedrunning aspects of these games, namely theSourceRuns Wiki. Even years after Half-Life’s release, one can still find casual

Contents

1 Contents 31.1 Fundamentals . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 31.2 Basic physics . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 51.3 Collisions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 111.4 Ladder physics . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 121.5 Strafing physics . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 161.6 Algorithms . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 231.7 Health, damage, other entities . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 281.8 Explosions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 361.9 TasTools and utilities . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 381.10 Glossary . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 48

i

Page 4: Half-Life Physics Documentation · for practical speedrunning aspects of these games, namely theSourceRuns Wiki. Even years after Half-Life’s release, one can still find casual

ii

Page 5: Half-Life Physics Documentation · for practical speedrunning aspects of these games, namely theSourceRuns Wiki. Even years after Half-Life’s release, one can still find casual

Half-Life Physics Documentation, Release

Caution: This documentation is work in progress!

This is an unofficial documentation for the physics governing the Half-Life universe. There have been many verycomprehensive wikis for games in the Half-Life series, such as the Half-Life Wikia and the Combine OverWiki.These wikis focus on the storyline and casual gaming aspects of the Half-Life series video games. There is also a wikifor practical speedrunning aspects of these games, namely the SourceRuns Wiki. Even years after Half-Life’s release,one can still find casual gaming or speedrunning communities around the game.

Despite the wealth of strategy guides for Half-Life, it is next to impossible to find documentations describing thephysics of the game with a satisfying level of technical accuracy. One can only speculate about the reasons behind suchscarcity. Knowledge about the physics of Half-Life is important for developing tools for Half-Life TAS productionand for the process of TASing itself. The nature of the in-game physics demands highly precise tools. Perhaps moreimportantly, developing an understanding and intuition for Half-Life physics is vital in producing a highly optimisedTAS for the game.

Thus, this documentation strives to detail all aspects of the physics in a way that would help any curious minds togain a much deeper appreciation for Half-Life and its speedruns. The potential tool developers will also find thisdocumentation a helpful guide. This documentation should serve as a definitive reference material for Half-Lifephysics.

Contents 1

Page 6: Half-Life Physics Documentation · for practical speedrunning aspects of these games, namely theSourceRuns Wiki. Even years after Half-Life’s release, one can still find casual

Half-Life Physics Documentation, Release

2 Contents

Page 7: Half-Life Physics Documentation · for practical speedrunning aspects of these games, namely theSourceRuns Wiki. Even years after Half-Life’s release, one can still find casual

CHAPTER 1

Contents

1.1 Fundamentals

1.1.1 Notation

One of the most important mathematical objects in discussions of Half-Life physics is the Euclidean vector. All vectorsare in either R2 or R3, where R denotes the real numbers. This is sometimes not specified explicitly if the contextualclues are sufficient for disambiguation.

All vectors are written in boldface like so:

v

Every vector has an associated length, which is referred to as the norm. The norm of some vector v is thus denoted as

‖v‖

A vector of length one is called a unit vector. So the unit vector in the direction of some vector v is written with a hat:

v =v

‖v‖

There are three special unit vectors, namely

i j k

These vectors point towards the positive 𝑥, 𝑦 and 𝑧 axes respectively.

Every vector also has components in each axis. For a vector in R2, it has an 𝑥 component and a 𝑦 component. A vectorin R3 has an additional 𝑧 component. To write out the components of a vector explicitly, we have

v = ⟨𝑣𝑥, 𝑣𝑦, 𝑣𝑧⟩

This is equivalent to writing v = 𝑣𝑥i + 𝑣𝑦 j + 𝑣𝑧k. However, we never write out the components this way in thisdocumentation as it is tedious. Notice that we are writing vectors as row vectors. This will be important to keep inmind when we apply matrix transformations to vectors.

The dot product between two vectors a and b is written as

a · b

On the other hand, the cross product between a and b is

a× b

3

Page 8: Half-Life Physics Documentation · for practical speedrunning aspects of these games, namely theSourceRuns Wiki. Even years after Half-Life’s release, one can still find casual

Half-Life Physics Documentation, Release

1.1.2 Viewangles

The term viewangles is usually associated with the player entity. The viewangles refer to a group of three angles whichdescribe the player’s view orientation. We call these angles yaw, pitch and roll. Mathematically, we denote the yaw by

𝜗

and the pitch by

𝜙

Note that these are different from 𝜃 and 𝜑. We do not have a mathematical symbol for roll as it is rarely used. Inmathematical discussions, the viewangles are assumed to be in radians unless stated otherwise. However, do keep inmind that they are stored in degrees in the game.

One way to change the yaw and pitch is by moving the mouse. This is not useful for tool-assisted speedrunning,however. A better method for precise control of the yaw and pitch angles is by issuing the commands +left,+right, +up, or +down. When these commands are active, the game increments or decrements the yaw or pitch bya certain controllable amount per frame. The amounts can be controlled by adjusting the variables cl_yawspeedand cl_pitchspeed. For instance, when +right is active, the game multiplies the value of cl_yawspeed bythe frame time, then subtracts the result from the yaw angle.

1.1.3 View vectors

There are two vectors associated with the player’s viewangles. These are called the view vectors. For discussions in3D space, they are defined to be

f := ⟨cos𝜗 cos𝜙, sin𝜗 cos𝜙,− sin𝜙⟩s := ⟨sin𝜗,− cos𝜗, 0⟩

We will refer to the former as the unit forward vector and the latter as the unit right vector. The negative sign for 𝑓𝑧 isan idiosyncrasy of the GoldSrc engine inherited from Quake. This is the consequence of the fact that looking up givesnegative pitch and vice versa.

We sometimes restrict our discussions to the horizontal plane, such as in the description of strafing. In this case weassume 𝜙 = 0 and define

f := ⟨cos𝜗, sin𝜗⟩s := ⟨sin𝜗,− cos𝜗⟩

Such restriction is equivalent to projecting the f vector onto the 𝑥𝑦 plane, provided the original vector is not vertical.

The above definitions are not valid if the roll is nonzero. Nevertheless, such situations are extremely rare in practice.

1.1.4 Entities

1.1.5 Tracing

Tracing is one of the most important computations done by the game. Tracing is done countless times per frame, andit is vital to how entities interact with one another.

4 Chapter 1. Contents

Page 9: Half-Life Physics Documentation · for practical speedrunning aspects of these games, namely theSourceRuns Wiki. Even years after Half-Life’s release, one can still find casual

Half-Life Physics Documentation, Release

1.2 Basic physics

1.2.1 Walking through a frame

Caution: This section is incomplete and outdated.

In this section we will describe what the game does in each frame that are relevant to TASing. Do not skip this sectionif you intend to understand the rest of this document without much pain.

1. For our purposes we can consider PlayerPreThink in dlls/client.cpp to be the first relevant func-tion that gets called in each frame. You might be wondering about the StartFrame function. While usu-ally it seems to get called prior to PlayerPreThink, inexplicable things happen when one tries to outputg_ulFrameCount and gpGlobals->time from both StartFrame and PlayerPreThink. What youwill observe in 1000 fps is something like the following:

startframe, g_ulFrameCount = 3, gpGlobals->time = 1.001prethink, g_ulFrameCount = 3, gpGlobals->time = 1.003startframe, g_ulFrameCount = 4, gpGlobals->time = 1.002prethink, g_ulFrameCount = 4, gpGlobals->time = 1.004......

If we look only at g_ulFrameCount, it appears that StartFrame is indeed called prior toPlayerPreThink. However, the values for gpGlobals->time seem to tell us that the invocation ofStartFrame is delayed by 2 frames. We have no idea how to explain this contradiction, considering the factthat gpGlobals is a global pointer referencing the same memory location. There could be some evil code thatchanges the value of gpGlobals->time to deceive StartFrame. Furthermore, when the frametime (theinverse of framerate, hereafter denoted as 𝜏 ) is not a multiple of 0.001, sometimes StartFrame will be calledmultiple times in succession without executing the game physics at all, followed by several iterations of the gamephysics in sucession without calling StartFrame. Also, while a map is loading, the entire game physics isrun multiple times after each StartFrame call. From these observations, we conclude that one “frame” reallyrefers to one iteration of the game physics, which can be thought to begin with the call to PlayerPreThinkas far as player physics is concerned, rather than StartFrame.

2. PlayerPreThink will call CBasePlayer::PreThink located in dlls/player.cpp. Informationsuch as the player health and those related to HUD will be sent over to the client in a function calledCBasePlayer::UpdateClientData. Physics such as the using of trains, jumping animations, soundsand other cosmetic effects are run. The physics associated with other entities in the map are not run at this point.

3. The UpdateClientData, which is a completely different function located in dlls/client.cpp willget called a short while later. This is where TasTools send the player velocity, position and other data in fullprecision to the clientside. This is important because of lines like these in the delta.lst file located in thevalve directory:

DEFINE_DELTA( velocity[0], DT_SIGNED | DT_FLOAT, 16, 8.0 ),DEFINE_DELTA( velocity[1], DT_SIGNED | DT_FLOAT, 16, 8.0 ),......DEFINE_DELTA( velocity[2], DT_SIGNED | DT_FLOAT, 16, 8.0 ),

This means that each component of player velocity will be sent only with 16 bits of precision and rounded to amultiple of one-eighth. This is bad for optimal strafing computations done entirely at the clientside.

4. Assuming the string CL_SignonReply: 2 has been printed to the console at some point (this will only beprinted once). Now the client starts working. The messages sent from the server just a moment ago are now re-ceived, including one that was sent by TasTools from UpdateClientData. Once the messages are processed,

1.2. Basic physics 5

Page 10: Half-Life Physics Documentation · for practical speedrunning aspects of these games, namely theSourceRuns Wiki. Even years after Half-Life’s release, one can still find casual

Half-Life Physics Documentation, Release

the game calls CL_CreateMove in cl_dlls/input.cpp. This is where the user input gets processed. Theuser input can come from keyboard, mouse, joystick, or just console commands such as +attack. This is alsowhere TasTools does all TAS-related computations such as optimal strafing. The final viewangles, button presses(represented by a 16-bit integer) and other data are sent to the server at some point after CL_CreateMove re-turns, stored as usercmd_t defined in common/usercmd.h. Note that button presses do not record theactual keys pressed, but rather, they are represented by bits that are set when specific commands are issued. Forexample, when the +forward command is issued, the IN_FORWARD bit will be set. For some reasons, thegame developers named the variable as buttons which is confusing. The definition for all “button” bits canbe found in common/in_buttons.h.

5. Once the server receives the user input from the clientside, the PM_Move in pm_shared/pm_shared.cgets called. This is where all the fun begins, because the physics related to player movement are located here.Due to the importance of the code in pm_shared/pm_shared.c, we will devote a couple of paragraphs tothem. PM_Move calls PM_PlayerMove, which is an unfortunately large function.

6. The first thing PM_PlayerMove does is to call PM_CheckParamters [sic]. This is the function that scalesforwardmove, sidemove and upmove (hereafter written as 𝐹 , 𝑆 and 𝑈 respectively) in pmove->cmd sothat the magnitude of the vector ⟨𝐹, 𝑆, 𝑈⟩ does not exceed sv_maxspeed (denoted as 𝑀𝑚). This means if‖⟨𝐹, 𝑆, 𝑈⟩‖ > 𝑀𝑚 then

⟨𝐹, 𝑆, 𝑈⟩ ↦→ 𝑀𝑚

‖⟨𝐹, 𝑆, 𝑈⟩‖⟨𝐹, 𝑆, 𝑈⟩

Otherwise, 𝐹 , 𝑆 and 𝑈 will remain unchanged. This is why increasing cl_forwardspeed,cl_sidespeed and cl_upspeed may not result in greater acceleration. PM_DropPunchAngle is alsoinvoked to decrease the magnitude of punchangles. The full equation is

P′ = max

[‖P‖

(1 − 1

2𝜏

)− 10𝜏, 0

]P

where P = ⟨𝑃𝑝, 𝑃𝑦, 𝑃𝑟⟩. The punchangles are then added to the viewangles (which does not affect theviewangles at the clientside). PM_ReduceTimers is now called to decrement various timers such aspmove->flDuckTime which purpose will be explained later.

7. A very important function, PM_CatagorizePosition [sic], is then called. Here we will introduce a newconcept: onground. A player is said to be onground if the game thinks that the player is standing some“ground” so that certain aspects of the player physics will be different from that when the player is not on-ground. PM_CatagorizePosition checks whether the player is onground and also determines the player’swaterlevel (which carries a numerical value), yet another new concept. In short, the player physics will differsignificantly only when the waterlevel is more than 1, which happens when the player is immersed sufficientlydeeply into some water (the specific conditions will be described later).

The way by which PM_CatagorizePosition determins the player onground status is simple. A player isonground if 𝑣𝑧 (vertical velocity) is at most 180 ups, and if there exists a plane at most 2 units below the player,such that the angle between the plane and the global horizontal 𝑥𝑦 plane is at most arccos 0.7 ≈ 45.57 degreeswith the plane normal pointing upward (this is another way of saying that the 𝑧 component of the unit normalvector is at least 0.7). If the player is determined to be onground and his waterlevel is less than 2, then thegame will forcibly shift the player position downward so that the player is really standing on the plane and notcontinue floating in the air at most 2 units above the plane.

TODO describe checkwater here

8. After the first onground check, the game will store −𝑣𝑧 in pmove->flFallVelocity. Although thismay seem insignificant, this turned out to be how the game calculates fall damage. Next we have a call toPM_Ladder, which determines whether the player is on some ladder.

9. PM_Duck, as its name suggests, is pretty important. This is the function responsible for ducking physics. Herewe must introduce two new concepts: bounding box and duckstate. They are described in Ducking physicswhich must be read before moving on.

6 Chapter 1. Contents

Page 11: Half-Life Physics Documentation · for practical speedrunning aspects of these games, namely theSourceRuns Wiki. Even years after Half-Life’s release, one can still find casual

Half-Life Physics Documentation, Release

10. The game will now call PM_LadderMove if the player is on some ladder. The ladder physics is described inLadder physics.

11. If +use and the player is onground, then v will be scaled down by 0.3. This is the basis of USE braking.

12. The game will now do different things depending on pmove->movetype. If the player is on ladder then themovetype is MOVETYPE_FLY. Otherwise it will usually be the confusingly named MOVETYPE_WALK. We willassume the latter. If the player waterlevel is at most 1, the game makes the first gravity computation as done byPM_AddCorrectGravity. Looking inside this function, assuming basevelocity is 0 we see that the gameperforms this following computation:

𝑣′𝑧 = 𝑣𝑧 −1

2𝑔𝜏

where 𝑔 is the gravitational acceleration, ent_gravity times pmove->movevars->gravity. Sev-eral notes must be made here: ent_gravity is pmove->gravity if the latter is nonzero, other-wise the former is 1. pmove->gravity is usually 1, which can thought as a multiplier that scalespmove->movevars->gravity. For instance, it has a different value if we enter the Xen maps, which is howthe game changes the gravitational acceleration without directly modifying the sv_gravity cvar. Now lookcloser to the computation, we see that it does seem incorrect as noted in the rather unhelpful comment. How-ever, the game always makes a call to PM_FixupGravityVelocity towards the end of PM_PlayerMovewhich performs the exact same computation except it completely ignores basevelocity. Now the key idea is thatthe actual movement of player vertical position is done between these two calls. In other words, we want thefinal vertical position after PM_PlayerMove to be

𝑝′𝑧 = 𝑝𝑧 + 𝑣𝑧𝜏 − 1

2𝑔𝜏2

which is exactly what we know from classical mechanics. But by rewriting this equation ever so slightly, weobtain

𝑝′𝑧 = 𝑝𝑧 + 𝜏

(𝑣𝑧 −

1

2𝑔𝜏

)= 𝑝𝑧 + 𝜏𝑣′𝑧

where 𝑣′𝑧 is the new velocity computed by PM_AddCorrectGravity. It is now obvious why this functiondoes it in the seemingly incorrect way.

The final velocity after PM_PlayerMove must be 𝑣𝑧 − 𝑔𝜏 and not 𝑣𝑧 − 12𝑔𝜏 . This is where

PM_FixupGravityVelocity comes into play by subtracting another 12𝑔𝜏 . A final note: in both of these

functions the PM_CheckVelocity is called. This function ensures each component of v is clamped tosv_maxvelocity.

13. TODO: waterjump etc

14. Assuming the waterlevel is less than 2. If +jump is active then PM_Jump will be called. The jumping physicswill be dealt in a later section.

15. The game calls PM_Friction which reduces the magnitude of player horizontal velocity if the player isonground. The friction physics is discussed much later. Note that the vertical speed is zeroed out here. AnotherPM_CheckVelocity will be called regardless of onground status.

16. The game will now perform the main movement physics. They are very intricate and we devoted several sectionsto them.

17. The final PM_CatagorizePosition will be called after the movement physics. The basevelocity willbe subtracted away from the player velocity, followed by yet another PM_CheckVelocity. Then thePM_FixupGravityVelocity is called. Finally, if the player is onground the vertical velocity will be zeroedout again.

18. After PM_Move returns, the game will call CBasePlayer::PostThink shortly after. This is where falldamage is inflicted upon the player (only if the player is onground at this point), impulse commands are executed,various timers and counters are decremented, and usable objects in vicinity will be used if +use.

1.2. Basic physics 7

Page 12: Half-Life Physics Documentation · for practical speedrunning aspects of these games, namely theSourceRuns Wiki. Even years after Half-Life’s release, one can still find casual

Half-Life Physics Documentation, Release

19. The game will now execute the Think functions for all entities. This is also where other damages and objectboosting are handled.

1.2.2 Frame rate

The term “frame rate” is potentially ambiguous. If precision is desirable then we can differentiate between three kindsof frame rate: computational frame rate (CFR), usercmd frame rate (UFR) and rendering frame rate (RFR). The RFRis simply the rate at which frames are drawn on the screen. (Note that it is incorrect to define RFR as the “numberof screen refreshes per second”. This definition falls apart if r_norefresh 1, which prevents the screen fromrefreshing!)

TODO!!

1.2.3 Ducking physics

The bounding box is an imaginary cuboid, usually enclosing the player model. It is sometimes called the AABB, whichstands for axis-aligned bounding box, which means the edges of the box are always parallel to the corresponding axes,regardless of player position and orientation. The bounding box is used for collision detection. If a solid entitytouches this bounding box, then this entity is considered to be in contact with the player, even though it may notactually intersect the player model. The height of the player bounding box can change depending on the duckstate.

When the player is not ducking, we say the player is unducked and thus the duckstate is 0. In this state the boundingbox has width 32 units and height 72 units. If the duck key has been held down for no longer than 1 second and theplayer has been onground all the while, then we say that the player duckstate is 1. At this point the bounding boxheight remains the same as that when the player has duckstate of 0. If the duck key is held (regardless of duration) butnot onground, or if the duck key has been held down for more than 1 second while onground, the duckstate will be 2.Now the bounding box height will be 36 units, with the same width as before.

If the duck key is released while the player duckstate is 2, the duckstate will be changed back to 0 immediately, and thebounding box height will switch back to 72 units. However, if the key is released while the duckstate is 1, magic willhappen: the player position will be shifted instantaneously 18 units above the original position, provided that there aresufficient empty space above the player. This forms the basis of ducktapping, sometimes referred to as the imprecisename “doubleduck”. Doubleduck is really a ducktap followed by another duck.

Note that the bounding box is an actual concept in the game code, while duckstate is simply an easier ab-straction used in our literature. In the code, a duckstate of 0 means pmove->bInDuck == false and(pmove->flags & FL_DUCKING) == 0. A duckstate of 1 means pmove->bInDuck == true and(pmove->flags & FL_DUCKING) == 0 still. Finally, a duckstate of 2 means pmove->bInDuck ==false and (pmove->flags & FL_DUCKING) != 0. Whereas the type of bounding box is essentially selectedby modifying pmove->usehull.

Now that we have described the concept of bounding box and duckstate, we will now note that each of 𝐹 , 𝑆 and 𝑈will be scaled down by 0.333 if the duckstate is 2. After the scaling down, ‖⟨𝐹, 𝑆, 𝑈⟩‖ becomes 0.333𝑀 , ignoringfloating point errors and assuming original ‖⟨𝐹, 𝑆, 𝑈⟩‖ ≥ 𝑀 . However, this is done before any change in duckstatehappens in PM_Duck. Suppose the player has duckstate 0 before the call to PM_Duck, and after PM_Duck is calledthe duckstate changes to 2. In this case, the multiplication by 0.333 will not happen: the duckstate was not 2 beforethe change. Suppose the player has duckstate 2 and the call to PM_Duck makes the player unducks, hence changingthe duckstate back to 0. In this case the multiplication will happen.

1.2.4 Jumping physics

Assuming the player is ongorund. Then jumping is possible only if he is onground and the IN_JUMP bit is unset inpmove->oldbuttons.

8 Chapter 1. Contents

Page 13: Half-Life Physics Documentation · for practical speedrunning aspects of these games, namely theSourceRuns Wiki. Even years after Half-Life’s release, one can still find casual

Half-Life Physics Documentation, Release

1.2.5 Jumpbug

Jumpbug is one of a few exploits that can bypass fall damage when landing on any ground. The downside of jumpbugis that a jump must be made, which may be undesirable under certain circumstances. For example, when the playerjumps the bunnyhop cap will be triggered.

To begin a jumpbug sequence, suppose that the player is initially not onground (as determined by the first ongroundcheck) and that the duckstate is 2, as illustrated by the +duck bounding box in the figure above. Some time later theplayer unducks, hence PM_UnDuck will be called to change the duckstate back to 0 and the second onground checkwill be triggered. If there exists a ground 2 units below the player, then the player will now be onground (as shown bythe -duck box above), and if +jump happens to be active the player will jump when PM_Jump is called within thesame frame (shown by the +jump box). But recall that PM_Jump will always make the player to be not onground.Also, as the upward velocity is now greater than 180 ups, when the third onground check is made the player will againbe determined to be not onground. As a result, when the control passes to CBasePlayer::PostThink, the gamewill not inflict fall damage to the player.

Jumpbug can fail if the player was not able to unduck to make himself onground after the second groundcheck. Thechances of this happening is greater at lower frame rates and higher vertical speeds.

1.2.6 Edgebug

TODO

1.2. Basic physics 9

Page 14: Half-Life Physics Documentation · for practical speedrunning aspects of these games, namely theSourceRuns Wiki. Even years after Half-Life’s release, one can still find casual

Half-Life Physics Documentation, Release

1.2.7 Basevelocity and pushfields

In pm_shared.c the basevelocity is stored in pmove->basevelocity. This is nonzero usually when beinginside a trigger_push or conveyor belt, which are called pushfields. The way the player physics incorporatesbasevelocity is vastly different for its horizontal and vertical components.

The basevelocity is always added to the player velocity after acceleration. This means the new player position iscomputed by p′ = p + (v + b)𝜏 where b is the basevelocity. Shortly after that the basevelocity will be subtractedaway from v before PM_PlayerMove returns.

Interestingly, whenever the player leaves a pushfield the basevelocity of the pushfield will be added to the player’svelocity somewhere in the game engine. The added components will not be subtracted away. This is the basis of thefamous push trigger boost, whereby a player ducks and unducks in rapid succession so that the bounding box entersand leaves the pushfield repeatedly.

The 𝑏𝑧 is handled differently. It is incorperated into 𝑣𝑧 in PM_AddCorrectGravity without being subtracted awaylater. Instead, 𝑏𝑧 is set to zero in the function. Let us write 𝑣 to mean 𝑣𝑧 for now. The vertical velocity at the 𝑛-thframe would be 𝑣𝑛 = 𝑣0 +(𝑏−𝑔)𝑛𝜏 . But bear in mind that the position is computed using 𝑣𝑛 = 𝑣0 +(𝑏−𝑔)𝑛𝜏 + 1

2𝑔𝜏instead. Therefore, to find the position at an arbitrary 𝑛-th frame we must compute

𝑝𝑛 = 𝑝0 + 𝜏

𝑛∑𝑘=0

𝑣𝑛 = 𝑝0 +

(𝑣0 +

1

2𝑏𝜏

)𝑛𝜏 +

1

2𝑛2𝜏2(𝑏− 𝑔)

These formulae can be useful in planning.

1.2.8 Water physics

Water movement is unfortunately not optimisable in Half-Life. However, we will still include a description of itsphysics here.

If the point 1 unit above the bottom of bounding box is immersed in water, then the waterlevel is 1. If the player origin(centre of bounding box) is additionally in water, then the waterlevel will be increased to 2. If the player’s view (originplus view offset) is also in water, then the waterlevel will be 3. Depending on the existence of water current and thewaterlevel, the magnitude of basevelocity may be modified.

In water physics the acceleration vector is a = 𝐹 f +𝑆s+ ⟨0, 0, 𝑈⟩ provided at least one of 𝐹 , 𝑆, 𝑈 is nonzero. Other-wise a = ⟨0, 0,−60⟩. Note that a is an R3 vector. In the context of water physics we denote 𝑀 = 0.8 min (𝑀𝑚, ‖a‖),where it can be shown that, if not all 𝐹 , 𝑆, 𝑈 are zero, then

‖a‖ =

√𝐹 2 + 𝑆2 + 𝑈2 + ⟨0, 0, 2𝑈⟩ ·

(𝐹 f + 𝑆s

)Thus the water movement equation can be written as

v′ = v(1 − 𝑘𝜏) + 𝜇a

with

𝜇 =

{min(𝛾1, 𝛾2) if 𝛾2 > 0

0 otherwise𝛾1 = 𝑘𝜏𝑀𝐴𝑔 𝛾2 = 𝑀 − ‖v‖(1 − 𝑘𝜏)

The first thing we should notice is that 𝛾2 is independent of a, which means as the speed increases 𝛾2 will inevitablydecrease until it is negative. The speed can be written as

‖v′‖ =√‖v‖2(1 − 𝑘𝜏)2 + 𝜇2 + 2‖v‖(1 − 𝑘𝜏)𝜇 cos 𝜃

10 Chapter 1. Contents

Page 15: Half-Life Physics Documentation · for practical speedrunning aspects of these games, namely theSourceRuns Wiki. Even years after Half-Life’s release, one can still find casual

Half-Life Physics Documentation, Release

If 𝜃 = 0 and ‖v‖ is sufficiently high so that 𝛾2 < 𝛾1, then we see that ‖v′‖ = 𝑀 . This means the maximum possibleswimming speed is simply 0.8𝑀𝑚. Moreover, assuming 𝜇 = 𝛾1 then the acceleration is independent of frame rate:

accel =[‖v‖(1 − 𝑘𝜏) + 𝑘𝜏𝑀𝐴𝑔] − ‖v‖

𝜏= 𝑘 (𝑀𝐴𝑔 − ‖v‖)

Also observe that the player always experience geometric friction while in the water.

1.3 Collisions

Many entities in Half-Life collide with one another. The velocity of the colliding entity usually changes as a result,while the position and velocity of the entity receiving the collision usually stay constant, countering real world New-tonian physics. The process of changing the velocity is usually referred to as velocity clipping. Collision is one of themost common events in Half-Life, so it is worthwhile to study its physics.

Collision is detected by determining the planes that run into the way of a line traced from the moving entity’s positionto the future position. The future position depends on the frame rate, the velocity and the base velocity associatedwith the colliding entity. Let n be the plane normal and let v be the velocity at the instant of collision. Let 𝑏 be thebounce coefficient which, under certain conditions, depends on sv_bounce (denoted as 𝐵) and 𝑘𝑒 (see Friction).The bounce coefficient controls how the velocity is reflected akin to a light ray. If v′ is the velocity resulting from thecollision, then the general collision equation (GCE) can be written as

v′ = v − 𝑏(v · n)n

Before we proceed, we must point out that this equation may be applied multiple times per frame. The functionsresponsible of actually displacing entities are SV_FlyMove for non-players and PM_FlyMove for players. Thesefunctions perform at most four aforementioned line tracing, each time potentially calling the velocity clipping function.

In most cases, players have 𝑏 = 1 because 𝑘𝑒 = 1 and so is 𝐵. In general, 𝑏 for players is computed by 𝑏 =1+𝐵(1−𝑘𝑒). The case of 𝑏 = 1 is more common for other entities. For example, snarks have 𝑏 = 3/2 and 𝑘𝑒 = 1/2.In general, if the movement type of an entity is designated as MOVETYPE_BOUNCE, then 𝑏 = 2 − 𝑘𝑒.

Care must be taken when 𝑏 < 1. To understand why, we first observe that v · n < 0, because otherwise there wouldnot be any collision events. With

v′ · n = (1 − 𝑏)v · n

we see that if 𝑏 < 1 then the angle between the resultant velocity and the plane normal is obtuse. As a result, collisionswill occur indefinitely with an increasing v. To prevent this, the game utilises a safeguard immediately after the linetracing process in the respective FlyMove functions to set v′ = 0.

Hence, assuming 𝑏 ≥ 1 we employ the following trick to quickly find ‖v′‖: write ‖v′‖2 = v′ · v′ and expanding eachv′ in the RHS to give

‖v′‖ = ‖v‖√

1 − 𝑏(2 − 𝑏) cos2 𝛼

where 𝛼 is the smallest angle between v and n confined to [−𝜋/2, 𝜋/2]. Observe that the resulting speed is strictlyincreasing with respect to 𝑏 in [1,∞). In fact, the curve of resultant speed against 𝑏 is hyperbolic provided 𝛼 = 0 and𝛼 = ±𝜋/2. When 𝛼 does equal zero, the resultant speed will be linear in 𝑏 like so:

‖v′‖ = ‖v‖(𝑏− 1)

Again, this result assumes 𝑏 ≥ 1. On the other hand, for the very common case of 𝑏 = 1 we have

‖v′‖ = ‖v‖ |sin𝛼|

Observe that the resultant velocity is always parallel to the plane, as one can verify that v′ · n = 0 is indeed true.

1.3. Collisions 11

Page 16: Half-Life Physics Documentation · for practical speedrunning aspects of these games, namely theSourceRuns Wiki. Even years after Half-Life’s release, one can still find casual

Half-Life Physics Documentation, Release

1.3.1 Speed preserving circular walls

In Half-Life we can sometimes find concave walls made out of multiple planes to approximate an arc. Examples canbe found in the c2a1 map. Circular walls can be a blessing for speedrunners because they allow making sharp turnswithout losing too much speed. In fact, if the number of planes increases, the approximation will improve, and sothe speed loss will decrease. Let 𝑛 be the number of walls and let 𝛽 be the angle subtended by the arc joining themidpoints of every wall. For example, with 𝛽 = 𝜋/2 the first and the last walls will be perpendicular, and with 𝛽 = 𝜋the they will be opposite and parallel instead. Let v𝑖 be the velocity immediatley after colliding with the 𝑖-th wall, andassuming v0 is parallel to and coincident with the first wall. Assume also that 0 ≤ 𝛽/(𝑛 − 1) ≤ 𝜋/2, which meansthat the angle between adjacent planes cannot be acute. If the velocity does not change due to other external factorsthroughout the collisions, then

‖v𝑖+1‖ = ‖v𝑖‖ cos

(𝛽

𝑛− 1

)The general equation is simply

‖v𝑛‖ = ‖v0‖ cos𝑛−1

(𝛽

𝑛− 1

)It can be verified that lim𝑛→∞‖v𝑛‖ = ‖v0‖, hence the speed preserving property of circular walls. Observe also thatthe final speed is completely independent of the radius of the arc. Perfectly circular walls are impossible in Half-Lifedue to the inherent limitations in the map format, so some amount of speed loss is unavoidable. Nevertheless, evenwith 𝑛 = 3 and 𝛽 = 𝜋/2 we can still preserve half of the original speed.

1.4 Ladder physics

It is widely known that the ladder climbing speed is optimisable. We first introduce ℱ and 𝒮, which are analogues of𝐹 and 𝑆 from the standard movement physics. Issuing +forward adds 200 to ℱ , and issuing +back subtracts 200from it. Thus, when both +forward and +back are issued we have ℱ = 0. Similarly, executing +moveright adds200 to 𝒮 and +moveleft subtracts 200 from it. Note that the value of 200 cannot be modified without recompilation.For ladder physics, it does not matter what 𝐹 and 𝑆 are. If the duckstate is 2, then ℱ ↦→ 0.333ℱ and 𝒮 ↦→ 0.333𝒮 innewer Half-Life versions. This is not true for earlier versions such as NGHL. Regardless of viewangles, jumping offthe ladder always sets v′ = 270n.

If u = ℱ f + 𝒮 s and n = ⟨0, 0,±1⟩ then

v′ = u− (u · n)

(n + n× ⟨0, 0, 1⟩ × n

‖⟨0, 0, 1⟩ × n‖

)where n is the unit normal vector of the ladder’s climbable plane.

1.4.1 Optimal angle between u and n

To optimise the vertical climbing speed, we assume n = ⟨𝑛𝑥, 0, 𝑛𝑧⟩. We further assume that 𝑢𝑦 = 0. Now we have

v′ = u− ‖u‖ cos𝛼(⟨𝑛𝑥, 0, 𝑛𝑧⟩ + ⟨−𝑛𝑧, 0, 𝑛𝑥⟩)

where 𝛼 is the angle between u and n. ⟨−𝑛𝑧, 0, 𝑛𝑥⟩ is actually n rotated by 𝜋/2 anticlockwise when viewing into thepositive direction of 𝑦-axis. Expanding ‖v′‖ =

√v′ · v′,

‖v′‖ = ‖u‖√

1 − 2√

2 cos𝛼 cos(𝛼− 𝜋/4) + 2 cos2 𝛼

=√ℱ2 + 𝒮2

√1 − 2

√2 cos2 𝛼 cos(𝜋/4) − 2

√2 cos𝛼 sin𝛼 sin(𝜋/4) + 2 cos2 𝛼

=√ℱ2 + 𝒮2

√1 − sin(2𝛼)

12 Chapter 1. Contents

Page 17: Half-Life Physics Documentation · for practical speedrunning aspects of these games, namely theSourceRuns Wiki. Even years after Half-Life’s release, one can still find casual

Half-Life Physics Documentation, Release

We conclude that 𝛼 = 3𝜋/4 maximises ‖v′‖. If |ℱ| = |𝒮| = 200, we have ‖v′‖ = 400.

Knowing the optimal angle 𝛼 is useful for theoretical understanding, but in practice we must be able to calculatethe player’s yaw and pitch angles that maximises vertical climbing speed. For ladders that are perfectly vertical theoptimal viewangles are trivial to find, but we need explicit formulae for slanted ladders.

1.4.2 Formulae for optimal yaw and pitch

Let n = ⟨𝑛𝑥, 𝑛𝑦, 𝑛𝑧⟩ with the constraint 𝑛2𝑥 + 𝑛2

𝑦 + 𝑛2𝑧 = 1, so that

n + n× ⟨0, 0, 1⟩ × n

‖⟨0, 0, 1⟩ × n‖= ⟨𝑁𝑥, 𝑁𝑦, 𝑁𝑧⟩

We are concerned with the vertical velocity, 𝑣′𝑧 . Written in full and simplifying,

𝑣′𝑧 = −ℱ sin𝜙(1 −𝑁𝑧𝑛𝑧) −𝑁𝑧 (ℱ cos𝜙 cos(𝜗− 𝜃) + 𝒮 sin(𝜗− 𝜃))√𝑛2𝑥 + 𝑛2

𝑦 (1.1)

where 𝜃 = atan2(𝑛𝑦, 𝑛𝑥). To maximise this quantity, we compute

𝜕𝑣′𝑧𝜕𝜙

= −ℱ cos𝜙(1 −𝑁𝑧𝑛𝑧) + ℱ𝑁𝑧 sin𝜙 cos(𝜗− 𝜃)√𝑛2𝑥 + 𝑛2

𝑦

𝜕𝑣′𝑧𝜕𝜗

= −𝑁𝑧(−ℱ cos𝜙 sin(𝜗− 𝜃) + 𝒮 cos(𝜗− 𝜃))√

𝑛2𝑥 + 𝑛2

𝑦

Setting them to zero and simplifying, we obtain the following equations respectively

(1 −𝑁𝑧𝑛𝑧) cos𝜙 = 𝑁𝑧 sin𝜙 cos(𝜗− 𝜃)√𝑛2𝑥 + 𝑛2

𝑦 (1.2)

ℱ cos𝜙 sin(𝜗− 𝜃) = 𝒮 cos(𝜗− 𝜃) (1.3)

To solve these equations, we begin by assuming |ℱ| = |𝒮| = 0 and rewriting equation (1.3) as

tan𝜙 = ±√

1 − 2 cos2(𝜗− 𝜃)

cos(𝜗− 𝜃)

Eliminating 𝜙 from equation (1.2), we get

1 −𝑁𝑧𝑛𝑧

𝑁𝑧

√𝑛2𝑥 + 𝑛2

𝑦

= ±√

1 − 2 cos2(𝜗− 𝜃)

Squaring both sides and simplifying gives

tan2(𝜗− 𝜃) =1

2𝑛𝑧

√𝑛2𝑥 + 𝑛2

𝑦(1.4)

Immediately we observe that 𝑛𝑧 ≥ 0 is required for this equation to have real solutions. We will deal with this in alater section. At this point we are required to take square roots. This is a critical step and we must carefully choosethe signs for the numerator and the denominator, as they will determine the quadrant in which (𝜗− 𝜃) resides.

We define three free variables:

• The sign of 𝒮. Positive if rightward and negative if leftward.

• The sign of ℱ . Positive if forward and negative if backward.

1.4. Ladder physics 13

Page 18: Half-Life Physics Documentation · for practical speedrunning aspects of these games, namely theSourceRuns Wiki. Even years after Half-Life’s release, one can still find casual

Half-Life Physics Documentation, Release

• The sign of 𝑣′𝑧 . Positive if upward and negative if downward.

The motivation is that we want to be able to automatically determine the correct signs for the numerator and thedenominator given our choices of the signs of the free variables. This is useful in practice because we often makeconscious decisions regarding the directions in which we want to strafe when climbing ladders. For example, we maychoose to invoke +forward and +moveleft, or +back and +moveright. In both cases the resulting velocity isidentically optimal, and yet the viewangles are different. By declaring the signs of 𝒮 and ℱ as free variables, we canchoose the strafing directions mathematically by simply setting the correct signs.

Optimal ladder climbing can go in two possible directions, that is upward or downward. Again, the maximum climbingspeed does not depend on the direction, though the viewangles do. Hence we declare the sign of 𝑣′𝑧 as a free variable.

We will now attempt to formulate the final viewangles in terms of these free variables. To begin, we examine Equation(1.1) more closely. We make three observations:

1. We have 1 −𝑁𝑧𝑛𝑧 ≥ 0 when 0 ≤ 𝑛𝑧 ≤ 1/√

2 and 1 −𝑁𝑧𝑛𝑧 < 0 when 1/√

2 < 𝑛𝑧 ≤ 1.

2. We have 𝑁𝑧 > 0.

3. We have cos𝜙 ≥ 0 for −𝜋/2 ≤ 𝜙 ≤ 𝜋/2.

We start by considering the sign of 𝑣′𝑧 . Obviously, the right hand side of Equation (1.1) must have the same sign asthe 𝑣′𝑧 . But observe that there are two terms in the right hand side. Therefore, both terms should also be as large aspossible in the direction indicated by the sign of 𝑣′𝑧 . For example, if we choose 𝑣′𝑧 < 0, then the terms on the righthand side should be as negative as possible, and vice versa.

We will deal with the angle (𝜗− 𝜃) first, which appears only in the second term, so we will assume that the first termhas been dealt with (that is, conforming to the sign of 𝑣′𝑧 while being as large as possible in magnitude). Now, we want

sgn(𝑣′𝑧) = sgn(−𝑁𝑧(ℱ cos𝜙 cos(𝜗− 𝜃) + 𝒮 sin(𝜗− 𝜃))

√𝑛2𝑥 + 𝑛2

𝑦

)By one of the observations we made, we have 𝑁𝑧 > 0 and cos𝜙 ≥ 0. Also,

√𝑛2𝑥 + 𝑛2

𝑦 is always positive. Hence,equivalently we need

sgn(𝑣′𝑧) = − sgn(ℱ cos(𝜗− 𝜃) + 𝒮 sin(𝜗− 𝜃))

And further,

sgn(𝑣′𝑧) = − sgn(ℱ cos(𝜗− 𝜃))

sgn(𝑣′𝑧) = − sgn(𝒮 sin(𝜗− 𝜃))

And thus,

sgn(sin(𝜗− 𝜃)) = − sgn(ℱ𝑣′𝑧)

sgn(cos(𝜗− 𝜃)) = − sgn(𝒮𝑣′𝑧)

Observe that the required signs of sin(𝜗− 𝜃) and cos(𝜗− 𝜃) depends on the chosen signs of ℱ and 𝒮 respectively, inaddition to the sign of 𝑣′𝑧 . If we look at Equation (1.4) again, notice that the signs of sin(𝜗−𝜃) and cos(𝜗−𝜃) determinethe signs of the numerator and denominator respectively after removing the squares, because tan(𝑥) = sin(𝑥)/ cos(𝑥)for all 𝑥.

Deriving from Equation (1.4), the formula for the optimal yaw is thus, in all its glory,

𝜗 = atan2(𝑛𝑦, 𝑛𝑥) + atan2

(− sgn(𝒮𝑣′𝑧), − sgn(ℱ𝑣′𝑧)

√2𝑛𝑧

√𝑛2𝑥 + 𝑛2

𝑦

)(1.5)

We can adopt the same line of attack for the final formula for 𝜙. Combining Equation (1.3) and Equation (1.4) gives

cos𝜙 = cot(𝜗− 𝜃) =

√2𝑛𝑧

√𝑛2𝑥 + 𝑛2

𝑦

14 Chapter 1. Contents

Page 19: Half-Life Physics Documentation · for practical speedrunning aspects of these games, namely theSourceRuns Wiki. Even years after Half-Life’s release, one can still find casual

Half-Life Physics Documentation, Release

Note that the positive square root is taken for the cotangent term because we want −𝜋/2 ≤ 𝜙 ≤ 𝜋/2. This is followedby a simple rewrite:

𝜙 = ± arccos

√2𝑛𝑧

√𝑛2𝑥 + 𝑛2

𝑦

Here, we only need to determine the sign of the right hand side as a whole, rather than considering the numerator andthe denominator separately. The sign of 𝜙 will indicate whether the player should look upward or downward whenclimbing. Going back to Equation (1.1) again, we assume the second term has been dealt with, in the same way weassumed the first term to have been dealt with when deducing the signs for the optimal yaw. Now we must have

sgn(𝑣′𝑧) = sgn(−ℱ sin𝜙(1 −𝑁𝑧𝑛𝑧))

Since the sign of sin𝜙 is completely determined by the sign of 𝜙, the relation is simplified to

sgn(𝑣′𝑧) = − sgn(ℱ𝜙(1 −𝑁𝑧𝑛𝑧))

And equivalently,

sgn(𝜙) = − sgn(ℱ𝑣′𝑧(1 −𝑁𝑧𝑛𝑧))

Notice that the sign of (1−𝑁𝑧𝑛𝑧) plays a role here. In practice, however, 1−𝑁𝑧𝑛𝑧 is less efficient to compute. Usingone of the observations, we see that sgn(1 − 𝑁𝑧𝑛𝑧) = sgn

(1/√

2 − 𝑛𝑧

). So we are done and we can write out the

complete formula for the optimal pitch as follows:

𝜙 = − sgn(ℱ𝑣′𝑧

(1/√

2 − 𝑛𝑧

))arccos

√2𝑛𝑧

√𝑛2𝑥 + 𝑛2

𝑦(1.6)

1.4.3 Optimal yaw and pitch when 𝑛𝑧 < 0

When 𝑛𝑧 < 0, the derivatives will never be zero. However, we can observe that |𝜙| increases when 𝑛𝑧 decreases. Wealso note we constrain the range of 𝜙 to [−𝜋/2, 𝜋/2] while the value of 𝜗 is unrestricted. Hence we can substitute themaximum value |𝜙| = 𝜋/2 into 𝜕𝑣′𝑧/𝜕𝜙 = 0 and solve for 𝜗. It is found to be

𝜗 = 𝜃 ± 𝜋

2

We need to determine what the sign of 𝜋/2 means. Substituting 𝜙 = ±𝜋/2 and 𝜗 − 𝜃 = ±𝜋/2 into the originalvertical velocity equation gives

𝑣′𝑧 = −ℱ sgn(𝜙)(1 −𝑁𝑧𝑛𝑧) −𝑁𝑧𝒮 sgn(𝜗− 𝜃)√𝑛2𝑥 + 𝑛2

𝑦

Note that 𝑁𝑧 < 0 when 𝑛𝑧 < −1/√

2. Now we can use the similar technique to deduce the required signs of 𝜙 and(𝜗− 𝜃), which results in

𝜗 = atan2(𝑛𝑦, 𝑛𝑥) + sgn(𝒮𝑣′𝑧(𝑛𝑧 + 1/√

2))𝜋

2

𝜙 = − sgn(ℱ𝑣′𝑧)𝜋

2

Again, we wrote these formulae so that they give the correct angles given the freely chosen signs of 𝒮, ℱ and 𝑣′𝑧 .

1.4. Ladder physics 15

Page 20: Half-Life Physics Documentation · for practical speedrunning aspects of these games, namely theSourceRuns Wiki. Even years after Half-Life’s release, one can still find casual

Half-Life Physics Documentation, Release

1.4.4 Optimal yaw and pitch when 𝑛𝑧 = 1

Up to this point we have been assuming the normal vector not being vertical. If n = ⟨0, 0,±1⟩, then the second termin the bracket vanishes (since VectorNormalize in pm_shared/pm_math.c returns a zero vector if the input,which is ⟨0, 0, 1⟩ × n, is also a zero vector) instead of being indeterminate, leaving only

v′ = u− ‖u‖ cos𝛼⟨0, 0,±1⟩

thus

‖v′‖ =√

ℱ2 + 𝒮2√

1 − cos2 𝛼

which is maximised when 𝛼 = 𝜋/2. This can be achieved by setting 𝜙 = 0. If |ℱ| = |𝒮| = 0 then the yaw should be45 or 135 degrees away from the intended direction, depending on the signs.

1.5 Strafing physics

Here we will be discussing movement physics primarily associated with the player. Physics of other entities such asmonster and boxes are presumed to share some similarities with the player, though they are usually not important.

1.5.1 Fundamental movement equation

For subsequence analyses that involve mathematics, we will not concern ourselves with the details of how they wereimplemented in PM_AirMove and PM_WalkMove. For example, we will not be thinking in terms of wishvel,addspeed, pmove->right and so on.

Denote v the player velocity in the current frame and v′ the new velocity. Denote a the unit acceleration vector. Let 𝜃the angle between v and a. Then

v′ = v + 𝜇a (1.7)

with 𝜇 =

{min(𝛾1, 𝛾2) if 𝛾2 > 0

0 otherwise𝑀 = min

(𝑀𝑚,

√𝐹 2 + 𝑆2

)𝛾1 = 𝑘𝑒𝜏𝑀𝐴 𝛾2 = 𝐿− v · a = 𝐿− ‖v‖ cos 𝜃

where 𝜏 is called the frame time, which is just the inverse of frame rate. 𝐿 = 30 when airstrafing and 𝐿 = 𝑀 whengroundstrafing. 𝐴 is the value of sv_airaccelerate when airstrafing and sv_accelerate when groundstraf-ing. Lastly, 𝑘𝑒 is called the environmental friction which is usually 1 and will be explained in Friction.

Ignoring the roll angle, the unit acceleration vector is such that a = 𝐹 f + 𝑆s, which is in R2. We have unit forwardvector f = ⟨cos𝜗, sin𝜗⟩ where 𝜗 is the yaw angle. This means f is essentially directed parallel to the player view.s is directed perpendicular to f rightward, or s = ⟨sin𝜗,− cos𝜗⟩. The magnitude of a is simply

√𝐹 2 + 𝑆2. In

the following analysis we will not concern ourselves with the components of a, but instead parameterise the entireequation in 𝜃.

Besides, we will assume that ‖⟨𝐹, 𝑆⟩‖ ≥ 𝑀 . If this is not the case, we must replace 𝑀 ↦→ ‖⟨𝐹, 𝑆⟩‖ for all appearancesof 𝑀 below. Throughout this document we will assume that 𝑀 , 𝐴, 𝜏 , 𝑘, 𝑘𝑒 and 𝐸 are positive.

1.5.2 Optimal strafing

The new speed ‖v′‖ can be expressed as

‖v′‖ =√v′ · v′ =

√(v + 𝜇a) · (v + 𝜇a) =

√‖v‖2 + 𝜇2 + 2‖v‖𝜇 cos 𝜃 (1.8)

16 Chapter 1. Contents

Page 21: Half-Life Physics Documentation · for practical speedrunning aspects of these games, namely theSourceRuns Wiki. Even years after Half-Life’s release, one can still find casual

Half-Life Physics Documentation, Release

Equation (1.8), sometimes called the scalar FME, is often used in practical applications as the general way to computenew speeds, if 𝜃 is known. To compute new velocity vectors, given 𝜃, we can rewrite Equation (1.7) as

v′ = v + 𝜇v

(cos 𝜃 ∓ sin 𝜃± sin 𝜃 cos 𝜃

)if v = 0 (1.9)

which expresses a as a rotation of v clockwise or anticlockwise, depending on the signs of sin 𝜃. Equation (1.9) canbe useful when computing line strafing.

If 𝜇 = 𝛾1 and 𝜇 = 𝛾2 we have

‖v′‖𝜇=𝛾1=√‖v‖2 + 𝑘𝑒𝜏𝑀𝐴 (𝑘𝑒𝜏𝑀𝐴 + 2‖v‖ cos 𝜃)

‖v′‖𝜇=𝛾2=

√‖v‖2 sin2 𝜃 + 𝐿2

respectively. Let 𝜃 the independent variable, then notice that these functions are invariant under the transformation𝜃 ↦→ −𝜃. Hence we will consider only 𝜃 ≥ 0 for simplicity. Observe that

1. ‖v′‖𝜇=𝛾1and ‖v′‖𝜇=𝛾2

intersects only at 𝜃 = 𝜁 where cos 𝜁 = (𝐿 − 𝑘𝑒𝜏𝑀𝐴)‖v‖−1 is obtained by solving𝛾1 = 𝛾2

2. ‖v′‖𝜇=𝛾1is decreasing in 0 ≤ 𝜃 ≤ 𝜋

3. ‖v′‖𝜇=𝛾2is increasing in 0 ≤ 𝜃 ≤ 𝜋/2 and decreasing in 𝜋/2 ≤ 𝜃 ≤ 𝜋

4. 𝜇 = 𝛾2 if 0 ≤ 𝜃 ≤ 𝜁, and 𝜇 = 𝛾1 if 𝜁 < 𝜃 ≤ 𝜋.

Therefore, we claim that to maximise ‖v′‖ we have optimal angle

𝜃 =

⎧⎪⎨⎪⎩𝜋/2 if 𝐿− 𝑘𝑒𝜏𝑀𝐴 ≤ 0

𝜁 if 0 < 𝐿− 𝑘𝑒𝜏𝑀𝐴 ≤ ‖v‖0 otherwise

To see this, suppose 0 < 𝜃 < 𝜋/2. This implies the second condition described above. When this is the case, thealways decreasing curve of ‖v′‖𝜇=𝛾1 intersects that of ‖v′‖𝜇=𝛾2 at the point where the latter curve is increasing. Tothe left of this point is the domain of the latter curve, which is increasing until we reach the discontinuity at the pointof intersection, beyond which is the domain of the former curve. Therefore the optimal angle is simply at the peak:the point of intersection 𝜃 = 𝜁.

If 𝜃 ≥ 𝜋/2, the former curve intersects the latter curve at the point where the latter is decreasing. 0 ≤ 𝜃 ≤ 𝜁 is thedomain of the latter curve which contains the maximum point at 𝜋/2. Have a look at the graphs below:

1.5. Strafing physics 17

Page 22: Half-Life Physics Documentation · for practical speedrunning aspects of these games, namely theSourceRuns Wiki. Even years after Half-Life’s release, one can still find casual

Half-Life Physics Documentation, Release

Note that these are sketches of the real graphs, therefore they are by no means accurate. However, they do illustratethe four observations made above accurately. The green dashed lines represent the curve of ‖v′‖𝜇=𝛾1

, which is alwaysdecreasing (observation 2). The blue dashed lines represent ‖v′‖𝜇=𝛾2

, which fits observation 3. Now focus on the redlines: they represent the graph of ‖v′‖ if the restriction 𝜇 = min(𝛾1, 𝛾2) is factored in, rather than considering eachcase in isolation. In other words, the red lines are what we expect to obtain if we sketch them using Equation (1.8).Notice that the region 0 ≤ 𝜃 ≤ 𝜁 is indeed the domain of ‖v′‖𝜇=𝛾2

, and vice versa (observation 4). Finally, the blueline and green line intersect only at one point. Now it is clear where the maximum points are, along with the optimal𝜃s associated with them.

Having these results, for airstrafing it is a matter of simple substitutions to obtain

‖v𝑛‖ =

⎧⎪⎨⎪⎩√‖v‖2 + 900𝑛 if 𝜃 = 𝜋/2√‖v‖2 + 𝑛𝑘𝑒𝜏𝑀𝐴𝑎(60 − 𝑘𝑒𝜏𝑀𝐴𝑎) if 𝜃 = 𝜁

‖v‖ + 𝑛𝑘𝑒𝜏𝑀𝐴𝑎 if 𝜃 = 0

These equations can be quite useful in planning. For example, to calculate the number of frames required to airstrafefrom 320 ups to 1000 ups at default Half-Life settings, we solve 10002 = 3202+𝑛·0.001·320·10·(60−0.001·320·10)=⇒ 𝑛 ≈ 4938

For groundstrafing, however, the presence of friction means simple substitution may not work.

1.5.3 Friction

Let 𝑘 the friction coefficient, 𝑘𝑒 the environmental friction and 𝐸 the stopspeed. The value of 𝑘 in the gamesv_friction while 𝐸 is sv_stopspeed. As mentioned previously, in most cases 𝑘𝑒 = 1 unless the player

18 Chapter 1. Contents

Page 23: Half-Life Physics Documentation · for practical speedrunning aspects of these games, namely theSourceRuns Wiki. Even years after Half-Life’s release, one can still find casual

Half-Life Physics Documentation, Release

is standing on a friction modifier. If friction is present, then before any physics computation is done, the velocity mustbe multiplied by 𝜆 such that

𝜆 = max(1 − max(1, 𝐸‖v‖−1)𝑘𝑒𝑘𝜏, 0) (1.10)

In 𝐸𝑘𝜏 ≤ ‖v‖ ≤ 𝐸, the kind of friction is called arithmetic friction. It is so named because if the player is allowedto slide freely on the ground, the successive speeds form an arithmetic series. In other words, given initial speed, thespeed at the 𝑛-th frame ‖v𝑛‖ is

‖v𝑛‖ = ‖v0‖ − 𝑛𝐸𝑘𝑒𝑘𝜏

Let 𝑡 = 𝑛𝜏 , then notice that the value of ‖v𝑡‖ is independent of the frame rate. If ‖v‖ > 𝐸, however, the friction iscalled geometric friction

‖v𝑛‖ = ‖v0‖(1 − 𝑘𝑒𝑘𝜏)𝑛

Again, let 𝑡 = 𝑛𝜏 , then ‖v𝑡‖ = ‖v0‖(1 − 𝑘𝜏)𝑡/𝜏 . Observe that

𝑑

𝑑𝜏‖v𝑡‖ = − 𝑡

𝜏‖v𝑡‖

(𝑘𝑒𝑘

1 − 𝑘𝑒𝑘𝜏+

ln|1 − 𝑘𝑒𝑘𝜏 |𝜏

)≤ 0 for 𝑡 ≥ 0

which means ‖v𝑡‖ is strictly increasing with respect to 𝜏 at any given positive 𝑡. By increasing 𝜏 (or decreasing theframe rate), the deceleration as a result of geometric friction becomes larger.

There is a limit to the speed achievable by perfect groundstrafing alone. There will be a critical speed such that theincrease in speed exactly cancels the friction, so that ‖v𝑛+1‖ = ‖v𝑛‖. For example, suppose optimal 𝜃 = 𝜁 andgeometric friction is at play. Then if

‖v‖2 = (1 − 𝑘𝑒𝑘𝜏)2‖v‖2 + 𝑘𝑒𝜏𝑀2𝐴𝑔(2 − 𝑘𝑒𝜏𝐴𝑔)

we have maximum groundstrafe speed

𝑀

√𝐴𝑔(2 − 𝑘𝑒𝜏𝐴𝑔)

𝑘(2 − 𝑘𝑒𝑘𝜏)

Strafing at this speed effectively degenerates perfect strafing into speed preserving strafing, which will be discussedshortly after. If 𝑘 < 𝐴𝑔 , which is the case in default Half-Life settings, the smaller the 𝜏 the higher the maximumgroundstrafe speed. If 𝜃 = 𝜋/2 instead, then the expression becomes

𝑀√𝑘𝑒𝑘𝜏(2 − 𝑘𝑒𝑘𝜏)

1.5.4 Bunnyhop cap

We must introduce 𝑀𝑚, which is the value of sv_maxspeed. It is not always the case that 𝑀𝑚 = 𝑀 , since 𝑀 canbe affected by duckstate and the values of 𝐹 , 𝑆 and 𝑈 .

All Steam versions of Half-Life have an infamous “cap” on bunnyhop speed which is triggered only when jumpingwith player speed greater than 1.7𝑀𝑚. Note that the aforementioned speed is not horizontal speed, but rather, themagnitude of the entire R3 vector. When this mechanism is triggered, the new velocity will become 1.105𝑀𝑚v.

It is impossible to avoid this mechanism when jumping. In speedruns a workaround would be to ducktap instead, buteach ducktap requires the player to slide on the ground for one frame, thereby losing a bit of speed due to friction.In addition, a player cannot ducktap if there is insufficient space above him. In this case jumping is the only way tomaintain speed, though there are different possible styles to achieve this.

One way would be to move at constant horizontal speed, which is 1.7𝑀𝑚. The second way would be to acceleratewhile in the air, then backpedal after landing on the ground until the speed reduces to 1.7𝑀𝑚 before jumping off

1.5. Strafing physics 19

Page 24: Half-Life Physics Documentation · for practical speedrunning aspects of these games, namely theSourceRuns Wiki. Even years after Half-Life’s release, one can still find casual

Half-Life Physics Documentation, Release

again. Yet another way would be to accelerate in the air and on the ground, though the speed will still decrease whileon the ground as long as the speed is greater than the maximum groundstrafe speed. To the determine the most optimalmethod we must compare the distance travelled for a given number of frames. We will assume that the maximumgroundstrafe speed is lower than 1.7𝑀𝑚.

It turns out that the answer is not as straightforward as we may have thought.

TODO!!

1.5.5 Air-ground speed threshold

The acceleration of groundstrafe is usually greater than that of airstrafe. It is for this reason that groundstrafing isused to initiate bunnyhopping. However, once the speed increases beyond 𝐸 the acceleration will begin to decrease,as the friction grows proportionally with the speed. There will be a critical speed beyond which the accelerationof airstrafe exceeds that of groundstrafe. This is called the air-ground speed threshold (AGST), admittedly a rathernon-descriptive name.

Analytic solutions for AGST are always available, but they are cumbersome to write and code. Sometimes the speedcurves for airstrafe and groundstrafe intercepts several times, depending even on the initial speed itself. A morepractical solution in practice is to simply use Equation (1.8) to compute the new airstrafe and groundstrafe speeds thencomparing them.

1.5.6 Speed preserving strafing

Speed preserving strafing can be useful when we are strafing at high 𝐴. It takes only about 4.4s to reach 2000 upsfrom rest at 𝐴 = 100. While making turns at 2000 ups, if the velocity is not parallel to the global axes the speed willexceed sv_maxvelocity. Ocassionally, this can prove cumbersome as the curvature decreases with increasingspeed, making the player liable to collision with walls or other obstacles. Besides, as the velocity gradually becomesparallel to one of the global axes again, the speed will drop back to sv_maxvelocity. This means, under certainsituations, that the slight speed increase in the process of making the turn has little benefit. Therefore, it can sometimesbe helpful to simply make turns at a constant sv_maxvelocity. This is where the technique of speed preservingstrafing comes into play. Another situation might be that we want to groundstrafe at a constant speed. When the speedis relatively low, constant speed groundstrafing can produce a very sharp curve, which is sometimes desirable in a veryconfined space.

We first consider the case where friction is absent. Setting ‖v′‖ = ‖v‖ in Equation (1.8) and solving,

cos 𝜃 = − 𝜇

2‖v‖

If 𝜇 = 𝛾1 then we must have 𝛾1 ≤ 𝛾2, or

𝑘𝑒𝜏𝑀𝐴 ≤ 𝐿− ‖v‖ cos 𝜃 =⇒ 𝑘𝑒𝜏𝑀𝐴 ≤ 2𝐿

At this point we can go ahead and write out the full formula for 𝜃 that preserves speed while strafing

cos 𝜃 =

⎧⎪⎨⎪⎩−𝑘𝑒𝜏𝑀𝐴

2‖v‖if 𝑘𝑒𝜏𝑀𝐴 ≤ 2𝐿

− 𝐿

‖v‖otherwise

On the other hand, if friction is present, we let ‖u‖ = 𝜆‖v‖ be the speed immediately after friction is applied, where𝜆 is given in (1.10). Now we have

‖v‖2 = ‖u‖2 + 𝜇2 + 2𝜇‖u‖ cos 𝜃

20 Chapter 1. Contents

Page 25: Half-Life Physics Documentation · for practical speedrunning aspects of these games, namely theSourceRuns Wiki. Even years after Half-Life’s release, one can still find casual

Half-Life Physics Documentation, Release

By the usual line of attack, we force 𝜇 = 𝛾1 which implies that 𝛾1 ≤ 𝛾2, giving the formula

cos 𝜃 =1

2‖u‖

(‖v‖2 − ‖u‖2

𝑘𝑒𝜏𝑀𝐴− 𝑘𝑒𝜏𝑀𝐴

)and the necessary condition

‖v‖2 − ‖u‖2

𝑘𝑒𝜏𝑀𝐴+ 𝑘𝑒𝜏𝑀𝐴 ≤ 2𝐿

If that condition failed, then we instead have

cos 𝜃 = −√

𝐿2 − (‖v‖2 − ‖u‖2)

‖u‖

Note that we took the negative square root, because 𝜃 needs to be as large as possible so that the curvature of the strafingpath is maximised, which is one of the purposes of speed preserving strafing. To derive the necessary condition for theformula above, we again employ the standard strategy, yielding

𝑘𝑒𝜏𝑀𝐴− 𝐿 >√

𝐿2 − (‖v‖2 − ‖u‖2)

Observe that we need 𝑘𝑒𝜏𝑀𝐴 > 𝐿 and 𝐿2 ≥ ‖v‖2 − ‖u‖2. Then we square the inequality to yield the converse ofthe condition for 𝜇 = 𝛾1, as expected. Putting these results together, we obtain

cos 𝜃 =

⎧⎪⎪⎨⎪⎪⎩1

2‖u‖

(‖v‖2 − ‖u‖2

𝑘𝑒𝜏𝑀𝐴− 𝑘𝑒𝜏𝑀𝐴

)if‖v‖2 − ‖u‖2

𝑘𝑒𝜏𝑀𝐴+ 𝑘𝑒𝜏𝑀𝐴 ≤ 2𝐿

−√𝐿2 − (‖v‖2 − ‖u‖2)

‖u‖otherwise, if 𝑘𝑒𝜏𝑀𝐴 > 𝐿 and 𝐿2 ≥ ‖v‖2 − ‖u‖2

Note that, regardless of whether friction is present, if |cos 𝜃| > 1 then we might resort to using the optimal angleto strafe instead. This can happen when, for instance, the speed is so small that the player will always gain speedregardless of strafing direction. Or it could be that the effect of friction exceeds that of strafing, rendering it impossibleto prevent the speed reduction. If ‖v‖ is greater than the maximum groundstrafe speed, then the angle that minimisesthe inevitable speed loss is obviously the optimal strafing angle.

1.5.7 Curvature

The locus of a point obtained by strafing is a spiral. Intuitively, at any given speed there is a limit to how sharp a turncan be made without lowering acceleration. It is commonly known that this limit grows harsher with higher speed. Astight turns are common in Half-Life, this becomes an important consideration that preoccupies speedrunners at almostevery moment. Learning how navigate through tight corners by strafing without losing speed is a make-or-break skillin speedrunning.

It is natural to ask exactly how this limit can be quantified for the benefit of TASing. The simplest way to do sois to consider the radius of curvature of the path. Obviously, this quantity is not constant with time, except forspeed preserving strafing. Therefore, when we talk about the radius of curvature, precisely we are referring to theinstantaneous radius of curvature, namely the radius at a given instant in time. But time is discrete in Half-Life, sothis is approximated by the radius in a given frame.

90 degrees turns

Passageways in Half-Life commonly bend perpendicularly, so we frequently make 90 degrees turns by strafing. Wecan imagine how the width of a passage limits the maximum radius of curvature one can sustain without colliding withthe walls. This implies that the speed is limited as well. When planning for speedruns, it can prove useful to be able toestimate this limit for a given turn without running a simulation or strafing by hand. In particular, we want to compute

1.5. Strafing physics 21

Page 26: Half-Life Physics Documentation · for practical speedrunning aspects of these games, namely theSourceRuns Wiki. Even years after Half-Life’s release, one can still find casual

Half-Life Physics Documentation, Release

the maximum speed for a given passage width. We start by making some simplifying assumptions that will greatlyreduce the difficulty of analysis while closely modelling actual situations in practice. Refer to the figure below.

The first assumption we make is that the width of the corridor is the same before and after the turn. This width isdenoted as 𝑑, as one can see in the figure. This assumption is justified because this is often true or approximatelytrue in Half-Life maps. The second assumption is that the path is circular. The centre of this circle, also named thecentre of curvature, is at point 𝐶. As noted earlier, the strafing path is in general a spiral with varying radius ofcurvature. Nevertheless, the total time required to make such a turn is typically very small. Within such short timeframe, the radius would not have changed significantly. Therefore it is not absurd to assume that the radius of curvatureis constant while making the turn. The third assumption is that the positions of the player before and after making theturn coincide with the walls. This assumption is arguably less realistic, but the resulting path is the larger circular arcone can fit in this space.

By trivial applications of the Pythagorean theorem, it can be shown that the relationship between the radius of curvature𝑟 and the width of the corridor 𝑑 is given by

𝑟 =(

2 +√

2)𝑑 ≈ 3.414𝑑

This formula may be used to estimate the maximum radius of curvature for making such a turn without collision.However, the radius of curvature by itself is not very useful. We may wish to further estimate the maximum speedcorresponding to this 𝑟.

Radius-speed relationship

The following figure depicts the positions of the player at times 𝑡 = 0, 𝑡 = 𝜏 and 𝑡 = 2𝜏 . The initial speed is ‖v‖. Allother symbols have their usual meaning.

22 Chapter 1. Contents

Page 27: Half-Life Physics Documentation · for practical speedrunning aspects of these games, namely theSourceRuns Wiki. Even years after Half-Life’s release, one can still find casual

Half-Life Physics Documentation, Release

Based on the figure, the radius of curvature may be approximated as the 𝑦-intercept, or 𝑐. Obviously, a more accurateapproximation may be achieved by averaging 𝑐 and BC . However, this results in a clumsy formula with little benefit.Empirically, the approximation by calculating 𝑐 is sufficiently accurate in practice. In consideration of this, it can becalculated that

𝑟 ≈ 𝑐 =𝜏

sin 𝜃

(2

𝜇‖v‖2 + 3‖v‖ cos 𝜃 + 𝜇

)(1.11)

Note that this is the most general formula, applicable to any type of strafing. From this equation, observe that theradius of curvature grows with the square of speed. This is a fairly rapid growth. On the other hand, under maximumspeed strafing, the speed grows with the square root of time. Informally, the result of these two growth rates conspiringwith one another is that the radius of curvature grows linearly with time. We also observe that the radius of curvatureis directly influenced by 𝜏 , as experienced strafers would expect. Namely, we can make sharper turns at higher framerates.

From Equation (1.11) we can derive formulae for various types of strafing by eliminating 𝜃. For instance, in Type 2strafing we have 𝜃 = 𝜋/2. Substituting, we obtain a very simple expression for the radius:

𝑟 ≈ 𝜏

(2

𝐿‖v‖2 + 𝐿

)Or, solving for ‖v‖, we obtain a more useful equation:

‖v‖ ≈√

𝐿

2

( 𝑟𝜏− 𝐿

)For Type 1 strafing, the formula is clumsier. Recall that we have 𝜇 = 𝑘𝑒𝜏𝑀𝐴 and

cos 𝜃 =𝐿− 𝑘𝑒𝜏𝑀𝐴

‖v‖

To eliminate sin 𝜃, we can trivially rewrite the cos 𝜃 equation in this form

sin 𝜃 =

√‖v‖2 − (𝐿− 𝑘𝑒𝜏𝑀𝐴)2

‖v‖

Then we proceed by substituting, yielding

𝑟 ≈ 𝜏‖v‖√‖v‖2 − (𝐿− 𝑘𝑒𝜏𝑀𝐴)2

(2

𝑘𝑒𝜏𝑀𝐴‖v‖2 + 3𝐿− 2𝑘𝑒𝜏𝑀𝐴

)We cannot simplify this equation further. In fact, solving for ‖v‖ is non-trivial as it requires finding a root to arelatively high order polynomial equation. As per the usual strategy when facing similar difficulties, we resort toiterative methods.

1.6 Algorithms

1.6.1 Line strafing

Line strafing refers to the act of strafing along a fixed line towards a particular direction. It is not possible to strafeso that the path is a straight line, therefore we have to choose the strafing directions (either left or right) carefully toapproximate a straight path and minimise the deviation from the path. We will describe the approach used by TasToolsmod here.

Recall that every line in R𝑛 can be represented as a parametric equation r = a + 𝑠b, such that the line is the locus ofr, 𝑠 is the parameter, a is a known point on the line and b is a vector parallel to the line. In the context of line strafing

1.6. Algorithms 23

Page 28: Half-Life Physics Documentation · for practical speedrunning aspects of these games, namely theSourceRuns Wiki. Even years after Half-Life’s release, one can still find casual

Half-Life Physics Documentation, Release

a is called the origin of line strafe (OLS) and b is the direction of line strafe (DLS). For simplicity we often normaliseb. Let p be the player position, then the distance of the player from the line is given by

(a− p) −[(a− p) · b

]b

To line strafe, we would use Equation (??) to compute two different player positions corresponding to left and rightstrafe. Then the distances of these positions from the line is compared, the direction that causes the smallest deviationfrom the line is chosen to be the final strafing direction for this particular frame.

The advantage of this method is that it allows some control over the amplitude of line strafe. If the initial playerposition is relatively far away from the line, then the amplitude of the subsequent path will be approximately equal tothe initial point-to-line distance.

In practice, the method to compute b depends on the value of v. If v = 0, then we might set b = ⟨cos𝜗, sin𝜗⟩, whichmeans line strafing towards the initial viewing direciton. Otherwise, we would set b = v. At times we might want tooverride b manually. In TasTools, this can be accomplished by issuing tas_yaw while +linestrafe is active.

1.6.2 Computing strafing inputs

Up to this point we have been analysing strafing in terms of 𝜃, the angle between velocity and acceleration vectors. Toactually strafe in practice, we need to adjust the yaw angle and provide the input (such as +moveright) correctly. Inthis section we describe a simple algorithm to produce such input.

We start off by computing the intended 𝜃 in degrees, which is the foundation of all. Now we must set 𝐹 and 𝑆, whichcan be achieved by issuing the correct commands (+forward, +moveleft, +moveright, +back). Assume that|𝑆| = |𝐹 | and ‖⟨𝐹, 𝑆⟩‖ ≥ 𝑀 before PM_CheckParamters is called. In principle it does not matter which of thefour commands are issued, but to minimise screen jittering we can adopt the following guideline:

• If 0 ≤ 𝜃 < 22.5 then +back with negative cl_backspeed.

• If 22.5 ≤ 𝜃 < 67.5 then +back and +move(right|left) with negative cl_backspeed.

• If 67.5 ≤ 𝜃 then +move(right|left).

where 22.5 is midway between 0 and 45, and 67.5 is midway between 45 and 90. You must be wondering about the+back and negative cl_backspeed as well. The rationale is this: we want to avoid accidentally pushing movableentities. If you look at the CPushable::Move function in dlls/func_break.cpp, notice that an object can bepushed only if +forward or +use is active.

To compute the new yaw angle, in the most general way we compute 𝛼, 𝛽 and 𝜑, all in degrees, where

𝜑 = arctan

(𝑆

𝐹

)𝛼 = atan2(𝑣𝑦, 𝑣𝑥) 𝛽 = 𝛼± (𝜑− 𝜃)

Notice the ± sign in 𝛽? It should be replaced by a + if right strafing and − if left strafing. We must add that ifv = 0 then we are better off setting 𝜃 = 0 and 𝛼 = 𝜗. In practice, when the speed is zero what we want is to strafetowards the yaw direction. In this case it does not make sense to have 𝜃 carrying nonzero values, even though it ismathematically valid. The atan2 function is available in most sensible programming languages. Do not use the plainarc tangent to compute 𝛼 or you will land yourself in trouble. In addition, we often do not need to compute 𝜑 in themanner presented above if |𝑆| = |𝐹 |, unless vectorial compensation is employed (will be discussed later).

Now we can compute the following trial new yaw angles, denoted as 𝜗1 and 𝜗2, such that

𝜗1 = anglemod(𝛽) 𝜗2 = anglemod(𝛽 + sgn(𝛽)𝑢)

We see a new function anglemod and a new constant 𝑢 = 360/65536. Anglemod is a function defined inpm_shared/pm_math.c to wrap angles to [0, 360) degrees. Except, it accomplishes this by means of a fasterapproximation, instead of a more expensive but correct method which may involve fmod. The spirit is not dissimilar

24 Chapter 1. Contents

Page 29: Half-Life Physics Documentation · for practical speedrunning aspects of these games, namely theSourceRuns Wiki. Even years after Half-Life’s release, one can still find casual

Half-Life Physics Documentation, Release

to the famous fast inverse square root approximation. The angles produced by anglemod is always a multiple of 𝑢.Consequently the strafing may be less accurate, now that the actual 𝜃 will only be an approximation of the intended 𝜃.

The anglemod function truncates the input angle, instead of rounding it. The following optimisation can be done toimprove the accuracy slightly. Set 𝜃1 = 𝜗1 ∓ 𝜑 and 𝜃2 = 𝜗2 ∓ 𝜑 which are the actual 𝜃s, then use these 𝜃s and thescalar FME to compute the new speeds. Now compare the new speeds to determine the 𝜗 that gives the higher speed.This will be the final yaw angle.

1.6.3 Autoactions

An autoaction refers to a set of input that are generated automatically when certain conditions are met. In TasToolsthey are (in order of precedence) tas_jb, tas_lgagst, tas_db4l, tas_db4c, tas_dtap, tas_dwj andtas_cjmp. The tables below show the conditions and corresponding actions. If a condition is not displayed, no-opis assumed. Key: og is onground status, d is user +duck input, j is user +jump input, and dst is duckstate. Theabbreviation “a.u.” stands for “after unducking”, while “a.d.” stands for “after ducking”.

When we say a command is taken precedence over the other, it means if the former command performs an action alllower precedence commands will be inhibited.

Implementing automatic jumpbug can be tricky. Suppose +duck is not active, player is not onground and falling.We want to make sure the player is not onground after the final groundcheck, which requires predicting the newposition after PM_AddCorrectGravity and movement physics. The following is the action table for jumpbugimplementation. Jumpbug is usually prioritised over other autoactions. If 𝑣𝑧 > 180 then jumpbug is impossible.

1. If dst 2 AND unduckable AND jumpable AND onground with dst 0, stop with -duck and +jump.

2. If new position is unduckable AND new position is onground with dst 0, stop with +duck and -jump.

Automatic ducktap is taken priority over automatic jump unless stated otherwise. Recall that ducktapping only worksif there exists sufficient space to accomodate the player bounding box if he is moved vertically up by 18 units, whilethe duckstate is not 2. If the duckstate is 2 then the player must first unduck for this frame. Ducktap is relativelycomplex:

og d j dst action0 0 1 2 -jump if unduckable AND onground a.u..1 – – 0 -jump and +duck if sufficient space, automatic jump otherwise.1 – – 1 -duck and decrement if sufficient space., automatic jump otherwise.1 – – 2 -jump and -duck if unduckable AND sufficient space a.u., automatic jump otherwise.

For automatic jumping, the need to handle pmove->oldbuttons complicates matters. At the time of writing,TasTools assumes IN_JUMP is unset in pmove->oldbuttons. A rare use case for this would be to temporarilydisable automatic jumping simply by issuing +jump.

og d j dst action0 0 1 0 -jump if new position is onground with dst 0.0 1 1 2 -jump if new position is onground with dst 2.0 0 – 2 Decrement and +jump if unduckable AND onground a.u. AND jumpable. -jump if new

position is unduckable AND new position is onground with dst 0.0 1 1 0 -jump if new position a.d. is onground.1 0 – 1 Decrement and +jump if jumpable AND insufficient space, -jump otherwise.1 1 – 1 Decrement and +jump if jumpable, -jump otherwise.1 – – 0/2 Decrement and +jump if jumpable, -jump otherwise.

Next we have DB4L. As with jumpbug, if 𝑣𝑧 > 180 then this is impossible.

1.6. Algorithms 25

Page 30: Half-Life Physics Documentation · for practical speedrunning aspects of these games, namely theSourceRuns Wiki. Even years after Half-Life’s release, one can still find casual

Half-Life Physics Documentation, Release

og dst action1 2 Decrement and set state to 0 if state is 10 0 +duck and set state to 1 if new position is obstructed by ground.0 2 +duck and set state to 1 if unduckable AND (onground a.u. OR new position a.u. is obstructed by

onground). Otherwise, decrement and set state to 0 if state is 1.

Then we have DB4C.

og d dst action0 0 0 Decrement and +duck if new position is obstructed OR (new position a.d. is obstructed AND

new speed is less than new speed a.d.).

We also have DWJ, which is inserting +duck at the instant the player jumps. This can be useful for longjump andas a jumping style itself. To selfgauss with headshot immediately after jumping usually requires this jumping styleto work. There is no action table for this – the counter is decremented and +duck is inserted whenever the playersuccessfully jumps.

1.6.4 Vectorial compensation

Vectorial compensation (VC) is a novel technique developed to push the strafing accuracy closer to perfection by fur-ther compensating the effects of anglemod. It is called vectorial as it manipulates the values for cl_forwardspeedand cl_sidespeed, thereby changing the direction of a slightly. This technique is not implemented in TasTools,however, as its use can significantly reduce the enjoyability of the resulting TAS due to the screen shaking haphazardly,effectively transforming the resulting videos into some psychedelic art. Furthermore, the advantages of VC over thesimple anglemod compensation described previously are largely negligible. It is for these reasons that we decidedagainst implementing VC in TasTools, though the technique will still be described here for academic interests.

The idea is the following: while the yaw angle in degrees is always a multiple of 𝑢, we can adjust the values ofcl_forwardspeed and cl_sidespeed in combination with cl_yawspeed so that the polar angle of a canreside between any two multiples of 𝑢. As a result, the actual 𝜃 can now be better approximated, hence higher strafingaccuracy.

Have a look at the illustration below.

The a in the figure is the intended a. The actual a being computed by the game will likely be an approximation of theintended vector. Also, the spaces between the lines corresponding to the multiples of 𝑢 are exaggerated so that theyare easier to see. The figure above depicts strafing to the right.

26 Chapter 1. Contents

Page 31: Half-Life Physics Documentation · for practical speedrunning aspects of these games, namely theSourceRuns Wiki. Even years after Half-Life’s release, one can still find casual

Half-Life Physics Documentation, Release

The algorithm would begin with the decision to strafe left or right, then compute 𝜃 in degrees, along with

𝛼 = atan2(𝑣𝑦, 𝑣𝑥) 𝛽 = 𝛼∓ 𝜃 𝜎 =

{⌈𝛽𝑢−1⌉𝑢− 𝛽 if right strafing𝛽 − ⌊𝛽𝑢−1⌋𝑢 if left strafing

where the ∓ in 𝛽 should be replaced by − if right strafing and vice versa. The quantity 𝜎 has the following meaning:⌈𝛽𝑢−1⌉𝑢 − 𝛽 represents the difference between 𝛽 and the smallest multiple of 𝑢 not lower than 𝛽, while the otherrepresents the difference between 𝛽 and the largest multiple of 𝑢 not greater than 𝛽. It is exactly these differences thatwe are compensating in this algorithm.

Now we must find 𝜑 = arctan(𝑆/𝐹 ) with 𝐹 ≥ 0 and 𝑆 ≥ 0 so that 𝜑 − ⌊𝜑𝑢−1⌋𝑢 closely approximates 𝜎. A naive,inefficient and hackish way would be: for all (𝐹, 𝑆) pairs with ‖⟨𝐹, 𝑆⟩‖ ≥ 𝑀 , compute the associated 𝜑− ⌊𝜑𝑢−1⌋𝑢and then find the one which approximates 𝜎 to the desired accuracy. The problem with this approach, leaving asideits crudeness, is that there are about 4 million (𝐹, 𝑆) pairs that satisfy the above constraint with 𝑀 = 320, whichtranslates to 4 million arc tangent computations per frame. This takes about 0.25s on a modern 2.0 GHz Core i7.

TODO

1.6.5 Delicious recipes

We will provide some implementations of basic strafing functions in Python. import math is required.

The following function returns speed after one frame of optimal strafing.

def fme_spd_opt(spd, L, tauMA):tmp = L - tauMAif tmp < 0:

return math.sqrt(spd * spd + L * L)if tmp < spd:

return math.sqrt(spd * spd + tauMA * (L + tmp))return spd + tauMA

If computing the velocity vector is required, instead of just the speed, then one might use the following implementation,where d is the direction: 1 for right and -1 for left.

def fme_vel_opt(v, d, L, tauMA):tmp = L - tauMAspd = math.hypot(v[0], v[1])ax = 0ay = 0if tmp < 0:

ax = L * v[1] * d / spday = -L * v[0] * d / spd

elif tmp < spd:ct = tmp / spdst = d * math.sqrt(1 - ct * ct)ax = tauMA * (v[0] * ct + v[1] * st) / spday = tauMA * (-v[0] * st + v[1] * ct) / spd

else:ax = tauMA * v[0] / spday = tauMA * v[1] / spd

v[0] += axv[1] += ay

On the other hand, if we want to compute the velocity as a result of an arbitrary 𝜃 then we would instead use

def fme_vel_gen(v, theta, L, tauMA):spd = math.hypot(v[0], v[1])

1.6. Algorithms 27

Page 32: Half-Life Physics Documentation · for practical speedrunning aspects of these games, namely theSourceRuns Wiki. Even years after Half-Life’s release, one can still find casual

Half-Life Physics Documentation, Release

ct = math.cos(theta)mu = L - spd * ctif mu < 0:

returnif tauMA < mu:

mu = tauMAst = math.sin(theta)ax = mu * (v[0] * ct + v[1] * st) / spday = mu * (-v[0] * st + v[1] * ct) / spdv[0] += axv[1] += ay

Note that these two implementations will no work if the speed is zero. This is a feature and not a bug: when thespeed is zero the direction is undefined. In other words, the meaning of “rightstrafe” or “leftstrafe” will be lost withoutspecifying additional information.

For backpedalling, we have

def fme_spd_back(spd, L, tauMA):return abs(spd - min(tauMA, L + spd))

Then we have the following function which applies friction. This function must be called before calling the speedfunctions when groundstrafing.

def apply_fric(spd, E, ktau):if spd > E:

return spd * (1 - ktau)tmp = E * ktauif spd > tmp:

return spd - tmpreturn 0

1.7 Health, damage, other entities

1.7.1 Health and damage

Suppose ℋ and 𝒜 are the health and armour respectively, while ℋ′ and 𝒜′ are new health and armour. Suppose 𝐷 isthe original damage received by the player. Assuming the damage type is not DMG_FALL and not DMG_DROWN, then

ℋ′ = ℋ− ∆ℋ 𝒜′ =

{0 if 𝒜 = 0

max(0,𝒜− 2𝐷/5) otherwise

where

∆ℋ =

{trunc(𝐷 − 2𝒜) if 𝒜′ = 0

trunc(𝐷/5) otherwise

If the damage type if DMG_FALL or DMG_DROWN, then the armour value will remain the same, with ∆ℋ = trunc(𝐷).Sometimes the player velocity will be modified upon receiving damage, forming the foundation for a variety ofdamage boosts. First we have the concept of an “inflictor” associated with a damage, which may or may notexist. Drowning damage, for example, does not have an inflictor. Inflictor could be a grenade entity, a satchelcharge entity, a human grunt, or even the player himself (in the case of selfgaussing). It is the first argument toCBaseMonster::TakeDamage in dlls/combat.cpp.

Suppose v is the player velocity and p the player position. If an inflictor with position pinflictor exists, then with

d = p− pinflictor + ⟨0, 0, 10⟩

28 Chapter 1. Contents

Page 33: Half-Life Physics Documentation · for practical speedrunning aspects of these games, namely theSourceRuns Wiki. Even years after Half-Life’s release, one can still find casual

Half-Life Physics Documentation, Release

we have

v′ = v +

{min(1000, 10∆ℋ)d if duckstate = 2

min(1000, 5∆ℋ)d otherwise

We can immediately see that if the duckstate is 2 the change in velocity is greater. It is sad to see that the maximumpossible boost given by a single damage is 1000 ups and not infinite.

1.7.2 Object manoeuvre

Let 𝑉 the value of sv_maxvelocity. Define

clip(v) := [min(𝑣𝑥, 𝑉 ),min(𝑣𝑦, 𝑉 ),min(𝑣𝑧, 𝑉 )]

which is basically PM_CheckVelocity. Assuming the player is not accelerating, ‖v‖ > 𝐸 and the use key ispressed then with v0 = v0 the subsequence player velocities v𝑘 and object velocities u𝑘 is given by

v𝑘+1 = (1 − 𝑘𝜏) clip(0.3v𝑘)

v𝑘+1 = u𝑘 + v𝑘

u𝑘+1 = (1 − 𝑘𝜏) clip(0.3v𝑘+1)

The physics of object boosting is well understood with trivial implementation. A trickier technique is fast objectmanoeuvre, which is the act of “bringing” an object with the player at extreme speed for a relatively long duration.

The general idea is to repeatedly activate +use for just one frame then deactive it for subsequent 𝑛 frames whilepulling an object. Observe that when +use is active the player velocity will be reduced significantly. And yet, when+use is deactivated, the player velocity will be equal to the object velocity, which may well be moving very fast. Theplayer will then continue to experience friction.

One important note to make is that despite the player velocity being scaled down by 0.3 when +use is active, theobject velocity will actually increase in magnitude. An implication of this is that the object will gradually overtakethe player, until it goes out of the player’s use radius. To put it another way, we say that the mean object speed isgreater than the mean player speed. To control the mean player speed, 𝑛 must be adjusted. If 𝑛 is too low or too high,the mean player speed will be very low. Therefore there must exist an optimal 𝑛 at which the mean player speed ismaximised.

However, we do not often use the optimal 𝑛 when carrying out this trick. Instead, we would use the smallest possible𝑛 so that the object mean speed will be as high as possible while ensuring the object stays within the use radius. Thismeans the object will hit an obstruction as soon as possible, so that we can change direction as soon as that happens.

1.7.3 Damage boosting

Handgrenades

The handgrenade is one of the most useful weapons for damage boosting in Half-Life. It is versatile and can be usedin many situations. Interestingly, the initial speed and direction of the grenade when it is tossed depend on the playerpitch in a subtle way. For example, when 𝜙 = 𝜋/2 (i.e. the player is facing straight down) the initial speed anddirection are 0 and 𝜋/2 respectively. However, when 𝜙 = 0 the initial speed and direction now become 400 and−𝜋/18 = −10∘ respectively. Another notable aspect of handgrenades is that its initial velocity depends on the playervelocity at the instant of throwing. This is unlike MP5 grenades.

In general, we can describe the initial velocity and direction of handgrenades in the following way. Assuming allangles are in degrees. First of all, the player pitch will be clamped within (−180∘, 180∘]. Let 𝜙𝑔 be the handgrenade’s

1.7. Health, damage, other entities 29

Page 34: Half-Life Physics Documentation · for practical speedrunning aspects of these games, namely theSourceRuns Wiki. Even years after Half-Life’s release, one can still find casual

Half-Life Physics Documentation, Release

initial pitch, then we have

𝜙𝑔 = −10∘ +

{(8/9)𝜙 if 𝜙 < 0

(10/9)𝜙 otherwise

And if v𝑔 is its initial velocity and f𝑔 is the unit forward vector constructed using 𝜙𝑔 , then

v𝑔 = v + min(500, 360 − 4𝜙𝑔)f𝑔

To visualise this equation, we plotted a graph of the handgrenade’s horizontal speed and vertical velocity relative tothe player against the player pitch.

TODO

Distribution of health

Health is a scarce resource in any speedrun because medkits and health chargers are relatively rare. Despite this harshconstraint, it is common to want to perform multiple damage boosts using whatever health that is available until thehealth becomes too low. A natural question to ask is: what is the optimal way to distribute the limited health overthese damage boosts, so that the total time taken to reach the destination is minimised?

Intuitively, this question seems to have a simple answer. Suppose there are two straight paths we need to travel toreach the destination. We want to perform damage boosts at the very beginning of each path. Let the lengths of thesetwo paths be 250 and 750 units. Assume that the initial horizontal speed at the beginning of each path is 100 ups. Forsimplicity, we will assume that we can consume up to 100 HP in total without dying.

Now observe that the length ratio is 1:3, so it is natural to guess that the health should also be distributed in 1:3proportion for each straight path. Namely, allocate 25 HP to the damage boost for the shorter path and 75 HP for the

30 Chapter 1. Contents

Page 35: Half-Life Physics Documentation · for practical speedrunning aspects of these games, namely theSourceRuns Wiki. Even years after Half-Life’s release, one can still find casual

Half-Life Physics Documentation, Release

longer path. Thus, we calculate that the total time taken to travel both paths is 1.597 seconds. However, what if weallocate 34 HP for the shorter path and 66 HP for the longer path instead? Then the total time is 1.555 seconds. Infact, we claim that this is the optimal distribution which minimises the total time. Even though the difference is smallin this particular scenario, it is not at all obvious why the 1:3 distribution is suboptimal.

To find out the optimal health distribution, we construct a model which closely reflects actual situations. We firstassume that we are required to perform damage boosts for 𝑛 distance segments. We define a distance segment as astraight line path which directly benefits from a damage boost done at the beginning of the path. To take a concreteexample, imagine an extremely narrow L-shaped path where the turn is extremely sharp. Since the turn is very sharp,the player’s horizontal speed will be reduced to a fixed value after making the turn. Thus, we consider the L-shapedpath to be comprised of two distance segments, one for each straight path. Notice that no matter how much healthis allocated to the initial boost, the speed gained will be lost after making the turn. Thus, the two straight paths areof distinct distance segment: the time taken to travel across the second straight path is independent of whatever thathappens while travelling in the first straight path.

In practice, there is, of course, no perfect distance segment. Turns are rarely so sharp that all boosts in the horizontalspeed are nullified. Nevertheless, the concept of distance segments can serve as a helpful guide and approximation topractical situations. Note also that the distance segments need not be continuous as is the case in the L-shaped pathexample described previously. Indeed, distance segments are completely independent of each other.

Let 𝑠1, . . . , 𝑠𝑛 be the lengths of the distance segments. Let 𝑢1, . . . , 𝑢𝑛 be the initial horizontal speeds are the begin-ning of each distance segment before damage boosting. These initial speeds are assumed to be fixed, independentof previous damage boosts. They are typically approximated in practice. And let ∆𝑣1, . . . ,∆𝑣𝑛 be the change inhorizontal speed as a result of the damage boost at the beginning of each distance segment. Now assume that the speedstays constant after boosting. We can then compute that the total time required to traverse all distance segments is

𝑇 (∆𝑣1, . . . ,∆𝑣𝑛) =𝑠1

𝑢1 + ∆𝑣1+ · · · +

𝑠𝑛𝑢𝑛 + ∆𝑣𝑛

Here, the total time is written as a function with parameters ∆𝑣1, . . . ,∆𝑣𝑛. We want to minimise this quantity byfinding the optimal values for each of ∆𝑣𝑖. Note also that we have a constraint, namely the amount of health given atthe beginning of everything, before any boosting is done. We may express this constraint simply as

𝐻(∆𝑣1, . . . ,∆𝑣𝑛) = ∆𝑣1 + · · · + ∆𝑣𝑛 = 10ℋ

where ℋ is the total health amount that will be consumed. Here, the coefficient of 10 reflects the assumption that theplayer will duck for each damage boosting. Indeed, recall that by ducking the player will receive twice the amount ofspeed boost compared to that received in upright position. By stating the optimisation problem this way, it may readilybe solved via the method of Lagrange multipliers.

This optimisation method is particularly useful when we have a multivariate objective function and an equation con-straining the parameters. In this optimisation problem, we want to solve the 𝑛+1 equations consisting of the constraintalong the equations encoded as ∇𝑇 = −𝜆∇𝐻 where 𝜆 is the Lagrange multiplier. Writing out the latter explicitly,we have

𝑠𝑖(𝑢𝑖 + ∆𝑣𝑖)2

= 𝜆 (1.12)

for all 1 ≤ 𝑖 ≤ 𝑛. To proceed, we introduce a temporary variable ℋ such that

10ℋ = ℋ − 𝑢1 − · · · − 𝑢𝑛

As a result, the constraint equation may be written as

(𝑢1 + ∆𝑣1) + · · · + (𝑢𝑛 + ∆𝑣𝑛) = ℋ

Using (1.12), we then eliminate all 𝑢𝑖 + ∆𝑣𝑖, yielding√𝑠1𝜆

+ · · · +

√𝑠𝑛𝜆

= ℋ

1.7. Health, damage, other entities 31

Page 36: Half-Life Physics Documentation · for practical speedrunning aspects of these games, namely theSourceRuns Wiki. Even years after Half-Life’s release, one can still find casual

Half-Life Physics Documentation, Release

Or equivalently, by eliminating the temporary variable,( √𝑠1 + · · · +

√𝑠𝑛

10ℋ + 𝑢1 + · · · + 𝑢𝑛

)2

= 𝜆

Eliminating 𝜆 using (1.12) again, we have the solution for each ∆𝑣𝑖 in the following form:

∆𝑣𝑖 =

√𝑠𝑖∑𝑛

𝑘=1

√𝑠𝑘

(10ℋ +

𝑛∑𝑘=1

𝑢𝑘

)− 𝑢𝑖

Looking at this equation, we observe the rather counterintuitive ratio. In particular, the ratio is not given by

𝑠𝑖∑𝑛𝑘=1 𝑠𝑖

as one would have guessed.

We want to remark that this model makes the assumption that the speed is constant after boosting. This is normally nottrue in practice. However, consider that the speed after a damage boost is typically very high, and recall from strafingphysics that the acceleration at higher speeds is noticeably lower.

1.7.4 Gauss boost and quickgauss

Gauss is a very powerful weapon by virtue of its damage and recoil, which can be exploited to change the playervelocity significantly. When the secondary fire is shot, the new player velocity will become

v′ = v + 250𝑡⟨cos𝜗, sin𝜗, 0⟩ cos𝜙

where 0.5 ≤ 𝑡 ≤ 4 is the charging time in seconds. The damage produced is 𝐷 = 50𝑡.

Starting with the later versions of GoldSrc Half-Life, and including all Steam versions, there is a magnificent exploitthat makes the weapon fire in secondary mode as though 𝑡 = 4, but taking only half a second to charge plus one cell.This technique is called quickgauss, so named because it is quick to produce the maximum possible boost and damage,rather than having to charge for a full 4 seconds and consuming a lot of cells. This can be achieved by charging up theweapon, then save and reload the game before the weapon completes its minimum 0.5s charge. Upon reloading, theweapon will complete the 0.5s charge followed by a quickgauss shot.

1.7.5 Gauss reflect boost

In secondary mode, explosions happen at the points at which the gauss beam reflects. Moreover, gauss beams onlyreflect if the angle of incidence 𝛼 (the smallest angle between the incoming beam and the plane normal) is greater than60 degrees. Let 𝐷 the initial damage. When the beam reflects for the first time, then a blast damage is produced with𝐷 cos𝛼. The damage is then reduced to 𝐷(1 − cos𝛼). So on so forth.

The maximum blast damage is obviously 100. This is true when the damage from the gauss beam is 200 and 𝛼 is 60degrees. Suppose a player is ducked and shoots the flat floor beneath him with 𝛼 = 𝜋/3. The height of the source ofgauss beam will be 18+12 = 30 units above the ground. Therefore the horizontal distance from the point of reflectionto the player is 90/

√3, and it can be shown that the health loss is 75-77 (assuming no armour). Interestingly, the

direction of boost is vertically upward.

Another more dramatic type of reflect boost is the following: facing the wall (might need to adjust the yaw angleslightly) with 60 degrees pitch and shoot while unducked. When the gauss is shot, the beam will reflect at 16

√3 units

below the gun position. Hence the blast damage is around 86-91. It turns out that the reflected beam will directly hitthe player, dealing another 100 damage. The combined damage is therefore 186-191, giving 930-955 ups of verticalboost. If the player DWJ before boosting, the resultant vertical speed would be approximately 1198-1223 ups, which isenormous. Note that normal jumping will not work, it has to be DWJ. This is to prevent the normal jumping animationfrom playing. Presumably, movement of the legs in the jumping animation causes the beam to miss the player hitboxes.

32 Chapter 1. Contents

Page 37: Half-Life Physics Documentation · for practical speedrunning aspects of these games, namely theSourceRuns Wiki. Even years after Half-Life’s release, one can still find casual

Half-Life Physics Documentation, Release

1.7.6 Selfgauss

Selfgauss happens when a gauss beam hits certain obstructions. The beam will shoot out of the player’s body, therebyhitting the head from within. In normal situations the beam will ignore the player unless it has been reflected at leastonce. In this case the damage inflictor is the player himself, therefore d = ⟨0, 0, 1⟩, thus the boost is vertically upward.If the player is ducked, the pitch angle must be sufficiently negative for the beam to headshot the player.

There are two possibilities that trigger selfgauss. If a beam enters an obstruction and exits it, the distance between thepoint of entry and the point of exit must be numerically greater than the damage of the beam. This distance is ℓ in thefigure above. For example, if the distance is 100 units and the damage is 110, then selfgauss will not be triggered. Inaddition, the beam must be able to exit the obstruction. Thus selfgaussing does not work if the obstruction touchesa wall with no gap in between. On the other hand, if the beam crosses at least one non-worldspawn entities (such asfunc_wall) aside from the first obstruction, the distance between the point of entry into the first obstruction and thepoint of exit out of the last non-worldspawn entity is now considered.

Unfortunately, in some occasions if the obstruction is very thick selfgauss may not be triggered, even if the conditionsdescribed above are fulfilled. The exact reason remains a mystery.

Selfgaussing can be very powerful. It is possible to obtain a 1000 ups upward boost using just 67 damage. However,if the pitch angle is too high or too low, the beam might not hit the head, hence reducing the boost significantly.

1.7.7 Quick weapon switch and gauss rapid fire

Weapons in Half-Life does moderate damage by default. The quick weapon switch (QWS) trick is one way to dramat-ically boost one’s damage rate. There is a small delay immediately after switching to any weapon before one couldfire it. To eliminate this delay, one saves the game and reloads it. This is the essence of the QWS trick.

There is an instance variable of float type in the CBasePlayer class named m_flNextAttack. Upon switchingto a new weapon, this variable is set by CBasePlayerWeapon::DefaultDeploy to 0.5. This effectively addsa delay of half a second to any weapon switching. However, this value is not written to the savestate when the gameis saved. After reloading, the variable will be set to zero and therefore the weapon can start firing immediately.Contrary to popular beliefs, the trick does not work by cancelling the animation, because the delay is independent ofthe animation.

1.7. Health, damage, other entities 33

Page 38: Half-Life Physics Documentation · for practical speedrunning aspects of these games, namely theSourceRuns Wiki. Even years after Half-Life’s release, one can still find casual

Half-Life Physics Documentation, Release

Though there is quick weapon switch, we do not have “weapon rapid fire” to eliminate the delay between consecu-tive fires. This is because this delay is set by m_flNextPrimaryAttack or m_flNextSecondaryAttack,which are written to the savestate. Fortunately, the gauss weapon is a remarkable exception to this. It is widelyknown that there is no firing delay in secondary mode. More importantly, the delay in primary mode is enforced bym_flNextAttack instead of m_flNextPrimaryAttack. This allows one to save and reload the game imme-diately after a primary fire to bypass the delay. This is called gauss rapid fire (GRF) or fastfire.

The gauss weapon in rapid fire has an impressive damage rate. One primary fire inflicts a damage of 20. Thus at 1000fps, the damage rate is 20000 HP per second, dealing a total damage of 1000 HP limited by the ammunition. Thedamage rate is significantly higher than that of any other weapon, including gauss with the quickgauss trick. WithGRF most enemies will be annihilated within a fraction of a second. This can be useful for clearing paths in confinedspaces and destroying monsters that block the progress of the game, such as the Nihilanth.

1.7.8 Box item duplication

Box item duplication is a trick useful for duplicating items dropped by crates. To perform this trick, we simply firea shotgun simultaneously at two adjacent crates, one of which is explosive and the other will spawn the desired itemupon destruction. Consequently, the desired item will be duplicated. It seems straightforward to understand how thistrick works: the crate in question breaks twice due to damages inflicted simultanously by the explosive crate and theshotgun pellets. Such explanation implies that any type of simultaneous damages inflicted in the same frame cantrigger the glitch. Unfortunately, this is false. For instance, if we shoot the crate while a hand grenade explodes in thesame frame, the box items will not be duplicated.

Building blocks

Understanding the correct explanation requires a detailed knowledge of the Half-Life damage system and the sequenceof events when performing the trick. Often, damages in Half-Life are not inflicted immediately. Instead, a series ofdamages may be accumulated by the multidamage mechanism. There are three important functions associated withthis mechanism: ClearMultiDamage, AddMultiDamage and ApplyMultiDamage. Each of these functionsworks on a global variable called gMultiDamage which has the following type

typedef struct{

CBaseEntity *pEntity;float amount;int type;

} MULTIDAMAGE;

The pEntity field is the entity on which damages are inflicted while the amount field is the accumulated damage.The type field is not important.

ClearMultiDamage is the simplest function out of the three. It simply assigns NULL togMultiDamage->pEntity and zeros out gMultiDamage->amount and gMultiDamage->type.This function accepts no parameter.

ApplyMultiDamage is straightforward. When called, it will invokegMultiDamage->pEntity->TakeDamage with the damage specified by gMultiDamage->amount.As the name suggests, TakeDamage simply subtracts the entity’s health by the given damage. When the entity is abreakable crate and its health is reduced to below zero, it will turn into a SOLID_NOT, which renders itself invisibleto any tracing functions. Then, the crate will fire any associated targets, schedule its removal from memory after 0.1s,then spawn its associated item. At this point you may be confused: if the crate becomes SOLID_NOT, then how canany further damages be dealt to it if the crate cannot be found by tracing functions? Continue reading.

AddMultiDamage is slightly trickier. One of the parameters accepted by this function is the target en-tity on which damages are to be inflicted. When this function is invoked, it checks whether the current

34 Chapter 1. Contents

Page 39: Half-Life Physics Documentation · for practical speedrunning aspects of these games, namely theSourceRuns Wiki. Even years after Half-Life’s release, one can still find casual

Half-Life Physics Documentation, Release

gMultiDamage->pEntity differs from the supplied entity. If so, it will call ApplyMultiDamage to deal thecurrently accumulated damages on the current gMultiDamage->pEntity. After that, it assigns the supplied en-tity to gMultiDamage->pEntity and the supplied damage to gMultiDamage->amount. On the other hand,if the supplied entity is the same as the current gMultiDamage->pEntity, then the supplied damage will simplybe added to gMultiDamage->amount.

When an explosive crate detonates, damage is dealt to the surrounding entities. The function responsible of inflictingthis blast damage is RadiusDamage. This function looks for entities within a given radius. For each entity, it usuallydoes a ClearMultiDamage, followed by TraceAttack (which simply calls AddMultiDamage on the targetentity) and then ApplyMultiDamage.

Finally, we come to the final building block toward understanding the trick: FireBulletsPlayer. This function iscalled whenever a shotgun is fired. At the very beginning of this function, ClearMultiDamage is called, followedby a loop in which each pellet is randomly assigned a direction to simulate spread, then a tracing function is called foreach pellet to determine what entity has been hit. Then, this entity’s TraceAttack is called. After the loop ends,the function concludes with a call to ApplyMultiDamage.

Process

We can now make use of the knowledge we learnt above to understand how the trick works. Suppose we have twocrates, one explosive and the other carrying the desired item. To perform the trick we fire the shotgun so that bothcrates are simultaneously broken. First of all, FireBulletsPlayer will be called. The ClearMultiDamageat the beginning of the function ensures that any leftover multidamage will not interfere with our current situation.Suppose the first few pellets strike the explosive crate. For each of these pellets, TraceAttack is being called onthe explosive crate. This in turns call AddMultiDamage which accumulates the damage dealt to the explosive crate.Suppose now the loop comes to a pellet that is set to deal damage on the desired crate. As a result, TraceAttackand so AddMultiDamage is called on the desired crate, which is a different entity than the explosive crate. Since thedesired crate is not the same as gMultiDamage->pEntity, AddMultiDamagewill call ApplyMultiDamageto inflict the accumulated damage against the explosive crate. This is the moment where the explosive crate explodes.

The explosive crate calls RadiusDamage which in turn inflicts damage onto the desired crate. When this happens,the TakeDamage associated with the desired crate will be called, which causes the associated item to spawn. Thedesired crate now turns into SOLID_NOT. Once RadiusDamage returns, we go back to the last AddMultiDamagecall mentioned in the previous paragraph. Here, gMultiDamage->pEntity will be made to point to the desiredcrate, and the damage for the current pellet will be assigned to gMultiDamage->amount.

Remember the FireBulletsPlayer at the beginning of this series of events? The loop in this function willcontinue to iterate. However, since the desired crate is of SOLID_NOT type, the tracing functions will completelymiss the crate. In other words, the rest of the shotgun pellets will not hit the desired crate, and that in total only onepellet hits the crate. When the loop finally completes, the final ApplyMultiDamage then inflicts the damage dealtby the one pellet onto the desired crate. Since ApplyMultiDamage does not rely on tracing functions to determinethe target entity, but rather, it uses gMultiDamage->pEntity set a moment ago, the damage will be successfullyinflicted which triggers the second TakeDamage call for the desired crate. This will again causes it to spawn theassociated item.

One assumption we made in the description above is that the loop in FireBulletsPlayer breaks the explosioncrate first. If this is not the case, then the item will not be duplicated. To see this, notice that the desired crate becomesSOLID_NOT as soon as the first set of pellets breaks it, which causes the later explosion to miss the crate.

Limited applicability

So why does shooting the target crate when a grenade explodes not work? To see this, suppose the grenade explodesfirst. The grenade will call RadiusDamage to inflict blast damage onto the target crate. After that, the crate becomesSOLID_NOT. The bullets will therefore miss the crate. On the other hand, suppose the bullets hit the crate first. The

1.7. Health, damage, other entities 35

Page 40: Half-Life Physics Documentation · for practical speedrunning aspects of these games, namely theSourceRuns Wiki. Even years after Half-Life’s release, one can still find casual

Half-Life Physics Documentation, Release

crate will then break and becomes SOLID_NOT again. When the grenade later calls RadiusDamage, the tracingfunctions within RadiusDamage will again miss the crate.

To put it simply, this trick does not work in cases like this because usually there is no way for the second damage tofind the crate, since they depend on tracing functions and they do not save the pointer to the desired crate before thecrate becomes SOLID_NOT.

1.8 Explosions

This page details the physics of explosion and explosive weapons, along with some speedrunning tricks that arise ofout these aspects of the game. Familiarity with the health and damage system is assumed.

1.8.1 General physics

An explosion is a phenomenon in Half-Life that inflicts damage onto surrounding entities. An explosion needs not bevisible, though it is normally accompanied with a fiery visual effect. We may describe an explosion in terms of threefundamental properties. Namely, an explosion has an origin, a source damage, and a radius.

Suppose an explosion occurs. Let 𝐷 be its source damage and 𝑅 its radius. Suppose there is an entity adjacentto the explosion origin. From gaming experience, we know that the further away this entity is from the explosionorigin, the lower the damage inflicted on this entity. In fact, the game only looks for entities within a sphere ofradius 𝑅 from the explosion origin, ignoring all entities beyond. In the implementation, this is achieved by callingUTIL_FindEntityInSphere with the radius as one of the parameters.

Assume the entity in question is within 𝑅 units from the explosion origin. First, the game traces a line from theexplosion origin to the entity’s body target. Recall from Entities that the body target of an entity is usually, but notalways, coincident with the entity’s origin. Then, the game computes the distance between this entity’s body targetand the explosion origin as ℓ. The damage inflicted onto this entity is thus computed to be

𝐷

(1 − ℓ

𝑅

)(0 ≤ ℓ ≤ 𝑅)

Observe that the damage inflicted falls off linearly with distance, and not with the square of distance as is the case inthe real world. This process is repeated with other entities found within the sphere.

Interestingly, the computed distance ℓ may not equal to the actual distance between this entity and explosion origin. Inparticular, if the line trace is startsolid, then the game computes ℓ = 0. As a result, the damage inflicted on the entityis exactly the source damage of the explosion. Indeed, all entities within the sphere will receive the same damage.

The case where the line trace is startsolid is seemingly impossible to achieve. Fortunately, this edge case is not hard toexploit in game, the act of which is named nuking as will be detailed in Nuking. The key to understanding how suchexploits might work is to observe that the explosion origin may not coincide with the origin of the entity just before itdetonates. The exact way the explosion origin is computed depends on the type of entity generating the explosion.

1.8.2 Explosion origin

Explosions are always associated with a source entity. This entity could be a grenade (of which there are three kinds)or an env_explosion.

Denote r the position of the associated entity. When an explosion occurs, the game will trace a line from 𝐴 to 𝐵. Theexact coordinates of these two points depend on the type of the associated source entity, but they are always, in oneway or the other, offset from the source entity’s origin. In general, we call c the end position from the line trace. Ifthe trace fraction is not 1, the game will modify the position of the source entity. Otherwise, the position will not bechanged.

36 Chapter 1. Contents

Page 41: Half-Life Physics Documentation · for practical speedrunning aspects of these games, namely theSourceRuns Wiki. Even years after Half-Life’s release, one can still find casual

Half-Life Physics Documentation, Release

The new position of the source entity is computed to be

r′ := c +3

5(𝐷 − 24)n

All numerical constants are hardcoded. Call the coefficient of n the pull out distance, as per the comments in theimplementation in ggrenade.cpp. This is so named because if the source entity is a grenade, it is typically incontact with some plane or ground when it explodes. By modifying the origin this way, the source entity is beingpulled out of the plane by that distance. Remarkably, this distance depends on the source damage of the explosion.For instance, MP5 grenades create explosions with a source damage of 𝐷 = 100, therefore MP5 grenades are pulledout of the plane by 45.6 units at detonation.

Subsequently, the source entity will begin to properly explode. The physics driving the rest of this event has beendescribed in General physics. Most importantly, the explosion origin is set to be r′ + k. Observe how the k is addedto the entity’s origin, the purpose of which is to pull non-contact grenades out of the ground slightly, as noted in thecomments. In the implementation, the addition of this term is done in the function responsible for applying explosivedamage, namely RadiusDamage. Since all explosion code invoke this function, this term is always added to theorigin for any explosion that happens.

Contact grenades

A contact grenade is a type of grenade which detonates upon contact with a solid entity. This includes the MP5grenades and RPGs.

Let r be the origin of a contact grenade moving in space. Assuming the map is closed, the grenade will eventually hitsome entity and then detonate. Denote unit vector n the normal to the plane on the entity that got hit. Note that at theinstant the grenade collides with the plane, its position will be on the plane. Thus at this instant, let v be the velocityof the grenade.

Then, the start and end points of the line trace are given by

𝐴 := r− 32v

𝐵 := r + 32v

Here, 𝐴 is 32 units away from the position of the grenade at collision, in the opposite direction of its velocity. And 𝐵is 32 units away from that position, but in the direction of the velocity. It is easy to imagine that, more often than not,the end position of the line trace will coincide with the grenade position. This line trace will also rarely be startsolid.This is because the grenade has to pass through open space before hitting the plane, and 𝐴 is approximately one of thegrenade’s past positions.

Timed grenades

Timed grenades are grenades that detonate after a specific amount of time. This includes hand grenades, which explodethree seconds after the pin is pulled.

Denote r the origin of a timed grenade. At detonation, the grenade may or may not be lying on a plane. Since thegrenade could well be resting on the ground with zero velocity, it does not make sense to use the velocity in computingthe start and end points for the line trace. Instead, Valve decided to use k to offset those points from the grenade origin.So, we have

𝐴 := r + 8k

𝐵 := r− 32k

Now, 𝐴 is simply 8 units above the grenade and 𝐵 is 32 units below the grenade. This means that there is a greaterchance that this line trace is startsolid and also that the trace fraction is 1. The former can occur if there is a solid entityabove the grenade, while the latter can occur if the grenade is sufficiently high above the ground.

1.8. Explosions 37

Page 42: Half-Life Physics Documentation · for practical speedrunning aspects of these games, namely theSourceRuns Wiki. Even years after Half-Life’s release, one can still find casual

Half-Life Physics Documentation, Release

Explosions by env_explosion

Tripmines

1.8.3 Nuking

Nuking refers to the trick of placing explosives in locations confined in a particular way so as to disable damage falloff.The result is that, for all entities found within the sphere of radius 𝑅 from the explosion origin, the damage inflictedwill be the maximum damage 𝐷, effectively with ℓ = 0. The usefulness of this trick is obvious.

It is important to keep in mind that the explosion radius does not change when nuking. Entities outside the sphere willremain untouched by the explosion.

1.9 TasTools and utilities

TasTools is a mod written for the latest version of Half-Life. TasTools on its own is hardly useful, there are severalsmall utilities that enhance one’s productivity in TAS creation.

1.9.1 New commands and cvars

TasTools mod added several new TAS-related commands and some cvars for testing purposes. Notice that some of thecommands accept a COUNT argument. For these commands, they perform their respective actions for at most COUNTtimes. For instance, by specifying tas_dtap 5 the player will only ducktap 5 times. To perform an action for apractically infinite number of times, specify -1 for COUNT. To disable a command immediately, specify 0 for COUNT.

+/-linestrafe Perform line strafing.

+/-backpedal Perform braking by backpedalling.

+/-leftstrafe Perform strafing to the left.

+/-rightstrafe Perform strafing to the right.

tas_cjmp COUNT Jump whenever the player lands on the ground.

tas_dtap COUNT Ducktap whenever the player lands on the ground. If tas_cjmp is also active, then tas_dtapwill be taken precedence.

tas_dwj COUNT Press the duck key whenever the player jumps, then release it after one frame. This can be usefulfor longjumping and DWJ jumping style (sometimes in conjunction with tas_db4l).

tas_lgagst COUNT Leave the ground by ducktapping or jumping, depending on where tas_cjmp or tas_dtap isactive at the moment, when the acceleration of airstrafe exceeds that of groundstrafe. cl_mtype 1 is requiredfor this to work. “lgagst” stands for “leave ground at air-ground speed threshold”.

tas_jb COUNT Perform a jumpbug before landing to avoid fall damage. This command has the highest priorityover all other autoaction commands.

tas_db4c COUNT Duck to avoid collision with non ground planes. If collision still happens after ducking, then itwill not duck unless +duck is active. “db4c” stands for “duck before collision”.

tas_db4l COUNT Duck to avoid landing on ground planes, unless the player has ducked anyway. “db4l” standsfor “duck before landing”.

tas_yaw YAW Set the yaw angle to YAW immediately. If the player is line strafing, then this command overrides theDLS instead.

tas_pitch PITCH Set the pitch angle to PITCH immediately.

38 Chapter 1. Contents

Page 43: Half-Life Physics Documentation · for practical speedrunning aspects of these games, namely theSourceRuns Wiki. Even years after Half-Life’s release, one can still find casual

Half-Life Physics Documentation, Release

tas_olsshift DIST Shift the OLS perpendicularly to the right by DIST units. If DIST is negative, then shift tothe left instead. This is mainly used to make the player line strafe with approximately DIST amplitude.

tas_sba ANGLE Perform left or right strafing automatically until the velocity polar angle has changed by ANGLEdegrees. Note that either +leftstrafe or +rightstrafe must be active for this to work, and it re-quires a wait followed by exec waitscript.cfg to work as intended. It relies on the ability to createwaitscript.cfg (called the waitscript) in the mod directory. This is a rather special command as it preventsfurther execution of the script that invokes it until the condition is met, accomplished by writing wait; execwaitscript.cfg to waitscript so that the waitscript executes itself forever. Once the condition is fulfilled,TasTools will stop the loop by clearing the content of waitscript, allowing the main script to resume execution.

tas_s2y YAW Similar to tas_sba, except that the command stops when the velocity polar angle becomes YAWdegrees.

ch_health HEALTH Change the health amount to HEALTH. This is a cheat and should be used for testing purposesonly.

ch_armor ARMOR Change the armour amount to ARMOR. As with ch_health, this is a cheat.

cl_mtype 1/2 If 1, then optimal strafing is performed when +linestrafe, +leftstrafe or+rightstrafe is activated. If 2, then speed preserving strafing is performed instead.

cl_db4c_ceil 0/1 If 1, then the ceiling will be considered as a plane to avoid when tas_db4c is active.

sv_taslog 0/1 Dump a lot of useful information to the console.

sv_bcap 0/1 Enable or disable bunnyhop cap.

sv_sim_qg 0/1 Simulate a quickgauss shot when releasing the gauss charge. This is a cheat.

sv_sim_grf 0/1 Simulate gauss rapidfire when firing the gauss cannon in primary mode. This is a cheat.

sv_sim_qws 0/1 Simulate quick weapon switch upon switching weapons. This is a cheat.

When strafing commands (+linestrafe, +leftstrafe, +rightstrafe, +backpedal) are active, do notactivate the “normal” movement commands (such as +moveleft), or else you are asking for trouble. Also, pleaseavoid doing things like mixing +leftstrafe and +backpedal together.

1.9.2 TasTools HUD

TasTools adds several custom HUD elements that can be useful for TAS planning and quick monitoring during scriptexecution.

1.9. TasTools and utilities 39

Page 44: Half-Life Physics Documentation · for practical speedrunning aspects of these games, namely theSourceRuns Wiki. Even years after Half-Life’s release, one can still find casual

Half-Life Physics Documentation, Release

These new elements are located at the top-left corner. At the time of writing, the positions cannot be changed, nor cantheir display be disabled.

item descriptionH horizontal speed (or more accurately, the magnitude of the change in position per second, which is different

from the velocity stored in variables like pmove->velocity)V vertical speedZA angle arccos𝑛𝑧 in degrees, where 𝑛𝑧 is the 𝑧-component of the plane normal under crosshairHD horizontal distance from the player origin to the point under crosshairVD vertical distance from the player origin to the point under crosshairY yaw angleP pitch angleEH health of the entity under crosshairCN pev->classname of the entity under crosshairDS a purple square will appear if the duckstate is 2, empty otherwiseST strafetype, which shows “F” when linestrafing, “L” when leftstrafing, “R” when rightstrafing, and “B” when

backpedallingSG the maximum damage the gauss beam can have to trigger selfgauss

1.9.3 TAS logging

If sv_taslog 1, TasTools mod will dump mostly player-related information to the console each frame, which canbe highly useful to the runner to analyse what exactly happened during the run, especially for very complex and fastpaced sequences. To save the console output to a file, one must either specify the -condebug switch when executingthe game, or issue the condebug command in the console while the game is running. The output will be appendedto qconsole.log which resides in the Half-Life directory.

Perhaps even more importantly, this TAS log is essential in generating the final legit script as needed by the genlegit.pyscript.

The log file is usually not read directly, but instead fed to the qconread program for easier reading, but we will describethe format here. For each frame the following information will be printed:

[cl_yawspeed YAWSPEED]prethink FRAMENO FRAMETIMEhealth HP AP

40 Chapter 1. Contents

Page 45: Half-Life Physics Documentation · for practical speedrunning aspects of these games, namely theSourceRuns Wiki. Even years after Half-Life’s release, one can still find casual

Half-Life Physics Documentation, Release

usercmd MSEC BUTTONS PITCH YAWfsu FMOVE SMOVE UMOVEfg FRICMULT GRAVMULTpa PUNCHPITCH PUNCHYAWpos 1 PX PY PZpmove 1 VX VY VZ BX BY BZ INDUCK FLAGS ONGROUND WATERLVLntl NUMTOUCH LADDERpos 2 PX PY PZpmove 2 VX VY VZ BX BY BZ INDUCK FLAGS ONGROUND WATERLVL[obj PUSH OVX OVY][dmg DAMAGE DMGTYPE DIRX DIRY DIRZ][expld SRCX SRCY SRCZ TARGETX TARGETY TARGETZ ENDX ENDY ENDZ]

The tokens in uppercase here are replaced by the actual value, while those in lowercase are literal. The lines in squarebrackets may or may not appear in a particular frame.

cl_yawspeed YAWSPEED is the yaw speed needed to set the yaw angle to the current value. This line will onlybe displayed after CL_SignonReply: 2. genlegit.py relies on this line to generate legitimate scripts(see below).

prethink This line gives the frame number (FRAMENO) which is not necessarily unique and FRAMETIME isthe duration of this frame, or the CFR. The frame number is the value of g_ulFrameCount defined indlls/globals.cpp, which is incremented only when StartFrame in dlls/client.cpp is called.The frame time is grabbed from gpGlobals->frametime.

health The health information is straightforward. Note that the values are printed inCBasePlayer::PreThink, which is before any damage inflictions or other actions that might alterthe health take place.

usercmd This line is printed before any player physics happen in pm_shared.c. MSEC is the UFR, BUTTONSis pmove->cmd.buttons which contains bits that correspond to button presses, while PITCH and YAW arethe original pitch and yaw inputs from the clientside before punchpitch modification.

fsu FMOVE, SMOVE and UMOVE are pmove->cmd.forwardmove, pmove->cmd.sidemove andpmove->cmd.upmove respectively. As with the usercmd line, these are original inputs from the clientside,before alterations.

fg FRICMULT and GRAVMULT are pmove->friction and pmove->gravity respectively. These are multi-pliers that change the effective friction coefficient 𝑘 and gravitational acceleration 𝑔 when computing groundmovement and gravity. For example, FRICMULT is observed to be 0.15 when standing on the films of water atthe beginning of c1a1 map. This means the actual friction coefficient is 0.15 times the default sv_friction.

pa This line is straightforward. They are the punchangles before PM_DropPunchAngle is called.

pos 1 This line gives the player position before physics computations.

pmove 1 The VX, VY and VZ are components of the player velocity. BX, BY and BZ are components of player baseve-locity. INDUCK can be 1 or 0, which tells whether the player duckstate is 1. FLAGS is pmove->flags, whichcan be used to test if the FL_DUCKING bit is set to determine whether the player duckstate is 2. ONGROUNDcan be -1 (not onground) or anything else (onground). Lastly, WATERLVL can be 0, 1 or 2, depending on howdeep the player has immersed into some water. The values in this line are prior to any physics computations.

ntl When this line is printed, the physics computations have been completed for this frame. NUMTOUCH gives thenumber of entities the player is touching. LADDER (0 or 1) tells whether the player is climbing on some ladder.

pos 2 Similar to pos 1, except this is printed after physics computations.

pmove 2 Similar to pmove 1, except this is printed after physics computations.

obj This line is printed only when pushing or pulling an object. PUSH can be 0 or 1, which says whether theinteraction with this object is a pull or a push. OVX and OVY are the components of the horizontal object velocity

1.9. TasTools and utilities 41

Page 46: Half-Life Physics Documentation · for practical speedrunning aspects of these games, namely theSourceRuns Wiki. Even years after Half-Life’s release, one can still find casual

Half-Life Physics Documentation, Release

before pulling or pushing. This line is printed from CPushable::Move in dlls/func_break.cpp.

dmg This line is printed only when the player receives damage. DAMAGE is the amount of damage received, DMGTYPEcontains bits defined in dlls/cbase.h which describe the types of damage, while DIRX, DIRY and DIRZare the components of the unit direction vector associated with the damage. The first two fields in this lineare printed from CBasePlayer::TakeDamage in dlls/player.cpp, while the rest are printed fromCBaseMonster::TakeDamage in dlls/combat.cpp.

expld This line is printed only when the damage received is a blast damage. SRCX, SRCY and SRCZ are thecomponents of the position of explosion source, TARGETX, TARGETY and TARGETZ are the components ofthe position as returned by the BodyTarget function, while ENDX, ENDY and ENDZ are the components ofthe position upon which the damage ultimately inflicts.

Parsing the TAS log is straightforward.

1.9.4 Half-Life execution script

In Linux it is not possible to execute hl_linux directly, as it depends on the values of specific environment variablesthat are set by the Steam process. This is because Steam games are dynamically linked to the runtime librariesdistributed by Steam itself in place of the system-wide versions. As TAS runners we often want to specify additionalswitches to the game. One way is to utilise the Steam GUI, but this process requires several mouse clicks. It is muchmore convenient to be able to specify the switches and launch Half-Life via the command line.

To set up the environment correctly before executing hl_linux, we need to determine the values of these environ-ment variables. To do this, we run Half-Life via Steam then grab the values by issuing:

ps ex | grep '[h]l_linux'

This process has been done for you, and the resulting script, named runhl.sh, is

#!/bin/bash

export STEAM_ROOT=~/.local/share/Steamexport PLATFORM=ubuntu12_32export STEAM_RUNTIME=$STEAM_ROOT/$PLATFORM/steam-runtimeexport HL_ROOT=$STEAM_ROOT/steamapps/common/Half-Life

export LD_LIBRARY_PATH=\$HL_ROOT:\$STEAM_ROOT/$PLATFORM:\$STEAM_RUNTIME/i386/lib/i386-linux-gnu:\$STEAM_RUNTIME/i386/lib:\$STEAM_RUNTIME/i386/usr/lib/i386-linux-gnu:\$STEAM_RUNTIME/i386/usr/lib:\$STEAM_RUNTIME/amd64/lib/x86_64-linux-gnu:\$STEAM_RUNTIME/amd64/lib:\$STEAM_RUNTIME/amd64/usr/lib/x86_64-linux-gnu:\$STEAM_RUNTIME/amd64/usr/lib:\/usr/lib32

export PATH=$PATH:\$STEAM_ROOT/$PLATFORM:\$STEAM_RUNTIME/amd64/bin:\$STEAM_RUNTIME/amd64/usr/bin

export LD_PRELOAD=

42 Chapter 1. Contents

Page 47: Half-Life Physics Documentation · for practical speedrunning aspects of these games, namely theSourceRuns Wiki. Even years after Half-Life’s release, one can still find casual

Half-Life Physics Documentation, Release

cd $HL_ROOTexec ./hl_linux -steam "$@"

To run hooking-based mods, one needs to specify the path of the associated shared library to LD_PRELOAD.

Nevertheless, there is no guarantee that the script above will run successfully in every Linux system. The script hasonly been tested on recent versions of Arch Linux at the time of writing.

1.9.5 Scripting

There are two kinds of script as far as TasTools is concerned: the simulation script and the legitimate script. Simulationscripts use TasTools-specific commands and cvars heavily to “simulate” a run. The console output, which is usuallysaved to qconsole.log, can then be parsed to produce the legitimate script. This legitimate script can be run ineither Minimod or unmodded Half-Life, depending on whether the bhop cap is meant to be present.

One should define the following commonly used aliases in userconfig.cfg to reduce keystrokes when writingsimulation scripts:

alias +f +linestrafe; alias -f -linestrafealias +l +leftstrafe; alias -l -leftstrafealias +r +rightstrafe; alias -r -rightstrafealias +b +backpedal; alias -b -backpedalalias +j +jump; alias -j -jumpalias +d +duck; alias -d -duck

Along with these recommended settings:

cl_bob 0clockwindow 0sv_aim 0cl_forwardspeed 10000cl_backspeed 10000cl_sidespeed 10000cl_upspeed 10000

The 10000 for the last four cvars is to max out values of forwardmove, sidemove and upmove in pmove->cmd.According to the file delta.lst, the range for these variables is [−2047, 2047]. With 10000 they will always havethe maximum value.

One very convenient aspect of simulation script is that we do not need to write out the waits explicitly. Instead, we canwrite mathematical expressions in RPN in place of them. Then we use gensim.py which evaluates the expressionsand replaces the expressions by the correct number of waits. It also ignores lines containing only comments andblank lines. Suppose we have myscript.cfg_ which contains the following lines:

+f101 98 - 1 +-f+attack1-attack

In Linux we can simply run gensim.py < myscript.cfg_ which prints the following output to stdout

+fwaitwait-f+attack

1.9. TasTools and utilities 43

Page 48: Half-Life Physics Documentation · for practical speedrunning aspects of these games, namely theSourceRuns Wiki. Even years after Half-Life’s release, one can still find casual

Half-Life Physics Documentation, Release

wait-attack

This example is meant to be trivial, but suppose exactly 5452 waits are needed. The traditional means of us-ing wait aliases becomes cumbersome as one needs to define an enormous amount of them. Suppose we writew2000;w2000;w1000;w452 instead. What if after analysing the log file we decided that 5452 waits are toolong by 1738 frames? As helpful as mental computation is for shopping in supermarkets, it will rarely be quicker thanjust writing an expression which subtracts 1738 from the original value, unless you calculates at John von Neumann’sspeed.

If gensim.py encounters a line with this format: @U N1 N2 where N1 and N2 are integers, then it will output N2of the following in place of that line:

<N1 waits>+usewait-use

This is immensely useful for object manoeuvring, instead of copying the same lines manually over and over again,resulting in an unmaintainable script.

As noted earlier, the correct usage of tas_sba and tas_s2y requires the exec waitscript.cfg statement tobe present after at least one wait. Fortunately, we do not need to write the statement by hand in simulation scriptsas gensim.py is able to recognise those commands and insert it automatically. Nevertheless, we must avoid usingaliases that contain wait immediately after tas_sba or tas_s2y, as gensim.py would not know the definitionof these aliases, hence not able to insert the statement in the correct frame.

In general, very often r_norefresh 1 can come in handy as it disables screen refreshing (though not rendering).This can dramatically increase the frame rate to skip over long sequences or parts that have been completed/finalised.

1.9.6 Script execution

We will focus on script execution in the latest version of Half-Life. The technique for older versions is simpler andeasier to carry out.

First of all, we must bind a key in userconfig.cfg which executes the script upon pressing. Then the content ofgame.cfg must have the following format:

sv_taslog 1<waitpad 1>pause<waitpad 2>[save SAVE]

where waitpad 1 and waitpad 2 are lines containing only wait commands. For the first waitpad, the number ofwaits, called the wait number, must be determined experimentally, usually 20 for maps that are not too complex.The second waitpad should normally be empty and the save statement should be absent, except when handling leveltransitions (described in Segmentation).

Yet another Python script called gamecfg.py is written to allow easy generation of game.cfg conforming to theformat described above, though it prints to stdout rather than writing to the file directly. It accepts two mandatoryarguments, N1 and N2 which correspond to the wait number of waitpads 1 and 2. It also accepts the optional flag--save which causes it to output the final save statement if specified. In some rare cases we might not want toenable logging, hence the --nolog switch.

To run the game we should have some means of executing the following sequence of commands quickly (this is justan example that works most of the time):

44 Chapter 1. Contents

Page 49: Half-Life Physics Documentation · for practical speedrunning aspects of these games, namely theSourceRuns Wiki. Even years after Half-Life’s release, one can still find casual

Half-Life Physics Documentation, Release

rm -f $HLP/qconsole.loggamecfg.py 20 0 > $HLP/valve/game.cfgrunhl.sh -game tastools -condebug +host_framerate 0.0001 +load <savename>

where $HLP should be defined somewhere in .bashrc to point to the Half-Life directory. Having this variabledefined can save a lot of keystrokes when typing in the terminal. Note that modifications to core variables such assv_maxspeed should be done by passing switches to runhl.sh as well. After starting the game, we must holdthe bound key while the game is still loading, then release the key after the script starts executing. This is to ensure thescript gets executed as soon as the game engine does CL_SignonReply: 2. It would not work if we execute thescript directly in game.cfg.

After executing the script we should close the game and have qconsole.log opened in qconread for analysis.We should check the beginning of the log to verify that the script has been executed correctly.

Assuming that the simulation script has been finalised. The legitimate script must then be generated us-ing genlegit.py by reading the log file from stdin and emitting the final script to stdout. It always setscl_forwardspeed, cl_sidespeed and cl_upspeed to 10000 as hardcoded into the code. It also insertsa host_framerate 0.0001 before the final wait by default, unless --noendhfr or a different --hfrval isspecified. This is needed for handling level transitions correctly and is harmless for traditional segmenting within thesame map.

1.9.7 Segmentation

When we say a run is “segmented”, it simply means it was done piece by piece where each piece is loaded from asaved game (also known as savestate). One of the main motivations to segmenting a run is to allow human runners tobetter optimise the run, though another reason is to exploit glitches introduced when saving and loading the game inthe middle of some event or process. For Half-Life TASes, segmentation is always needed for level transition if therun is entirely scripted.

We distinguish between two kinds of segment : hard segment and soft segment. Soft segments are done only in longsimulated runs. The game is always saved in TasTools and the segments will eventually be merged as soon as the scriptis finalised. The purpose of soft segement is to decrease development cycle time, just like what r_norefresh 1does. On the other hand, hard segment is applicable to both kinds of scripts and the game must be saved instead in theunmodded game or other legitimate mods. Hard segments are used for exploiting savestate-related glitches and leveltransitions.

Segmentation is usually straightforward, except for hard segments at level transitions which are trickier to handle.For this we need host_framerate 0.0001 before the final wait in the legitimate script which is generated bydefault by genlegit.py. Then, before the level transition begins we must modify game.cfg so that it containsabout 50 waits for waitpad 1 and 100 waits for waitpad 2, plus the final save statement. Obviously, the exact waitnumbers must be determined experimentally. To check if the game was saved correctly we must utilise qconread.We should verify, after the script ended, that

1. “Player paused the game” is found somewhere before “CL_SignonReply: 1” and not “Player unpaused thegame”

2. “Saving game to” and another “Player paused the game” appear within the same frame as “CL_SignonReply:2”

3. UFT is always 0 until “CL_SignonReply: 2”

If “Can’t save during transition” is found, then the wait number for waitpad 2 must be increased. Assuming theresulting savestate is correct, the method to execute the script for the next segment will be as normal.

1.9. TasTools and utilities 45

Page 50: Half-Life Physics Documentation · for practical speedrunning aspects of these games, namely theSourceRuns Wiki. Even years after Half-Life’s release, one can still find casual

Half-Life Physics Documentation, Release

1.9.8 taslaunch tool

Up to this point we have been describing the manual way of executing scripts. In this section we will introduce anew tool called taslaunch.py which automates routine actions. It reads a configuration file (taslaunch.iniby default) and performs actions depending on the settings stored in it. It makes use of the Python scripts mentionedabove. taslaunch is the standard tool in TAS creation, and there is no reason not to use it unless you need completecontrol.

A configuration file contains many sections. Each section defines a segment. Within each section contains keys withcorresponding values. Have a look at the following:

[c2a1_seg_2]sim_waitpads = 20 0legit_save = c2a1_seg_3

[c2a1_seg_3]sim_waitpads = 15 0load_from = %(seg_name)s_test

In this example, [c2a1_seg_2] and [c2a1_seg_3] indicates the beginning of their respective sections. Thestring within a square bracket defines the segment name. For the first section, sim_waitpads and legit_saveare the keys with 20 0 and c2a1_seg_3 as the respective values. Note that the value of load_from contains thesubstring %(seg_name)s. This is a predefined string which will be resolved to the segment name, or c2a1_seg_3in this case. As a result, the final value of load_from will be c2a1_seg_3_test.

These are all the recognised settings:

load_from = CMD Specifies the map or savestate to load. To load a map, specify map <mapname> for CMD. Toload a savestate, specify load <savename>. This is mandatory.

host_framerate = FRAMETIME Set the initial value of host_framerate to FRAMETIME. The default is0.0001.

lines_per_file = N Limit the number of lines per generated script file to N. The default is 700.

sim_dest_prefix = PREFIX Set the prefix for generated script files to PREFIX. The default is tscript.

sim_waitpads = N1 N2 Generate game.cfg with wait numbers N1 and N2 for waitpad 1 and waitpad 2 re-spectively. This is mandatory.

sim_mod = MOD Run MOD. The default is valve.

sim_src_script = SCRIPT Use SCRIPT as the source simulation script. The default is%(seg_name)s_sim.cfg.

sim_log = LOGFILE Copy qconsole.log to LOGFILE after Half-Life exits. The defaults is%(seg_name)s_sim.log.

sim_hl_args = ARGS Specify ARGS as additional arguments to Half-Life. The default is an empty string.

dont_gen_legit This setting takes no arguments. If specified, the legitimate script will not be generated. Thiscan be useful if the user wishes to preserve manual tweaks done to the legitimate script generated previously.

legit_waitpads = N1 N2 Same as sim_waitpads, except this is for legitimate runs.

legit_lvl_waitpads = N1 N2 Generate game.cfg with wait numbers N1 and N2 for waitpad 1 and wait-pad 2 respectively after the run is started. This setting is vital in handling level transitions, and is useless withoutalso specifying legit_lvl_save.

legit_save = SAVE Save the game to SAVE at the end of script. This is mostly used for segmenting in themiddle of a map. Do not use this for saving at level transition.

46 Chapter 1. Contents

Page 51: Half-Life Physics Documentation · for practical speedrunning aspects of these games, namely theSourceRuns Wiki. Even years after Half-Life’s release, one can still find casual

Half-Life Physics Documentation, Release

legit_lvl_save = SAVE Save to SAVE upon level transition. This is useless without specifyinglegit_lvl_waitpads.

legit_dest_prefix = PREFIX Same as sim_dest_prefix, except this is for legitimate runs.

legit_mod = MOD Same as sim_mod, except this is for legitimate runs.

legit_demo = DEMO Record to DEMO throughout the script execution.

legit_hl_args = ARGS Same as sim_hl_args, except this is for legitimate runs.

legit_prepend = CMDS Prepend CMDS to the legitimate script.

legit_append = CMDS Append CMDS to the legitimate script.

To execute taslaunch.py, the user needs to specify two positional arguments. The first argument can be eithersim or legit, which specifies whether to initiate a simulation or a legitimate run. The second positional argumentspecifies the segment name. For example:

taslaunch.py legit c1a1_seg_2

1.9.9 qconread program

The qconread program is a simple GUI application which parses and presents the qconsole.log in an accessible wayso that the runner can have complete knowledge of the player information for each frame. It is a C++ program usingQt as the GUI toolkit, which happens to serve the need to display a few hundred thousand elements efficiently.

It has many columns with succinct labels, including one which is rated PG. Upon reading a log file, the player infor-mation will be populated, with each row representing one frame.

First of all, we have the frame number column, which displays the g_ulFrameCount values grabbed fromclient.cpp. They may not be sequential, and if this is the case in the middle of a run then you may have sometrouble. If a frame number is in bold appended with an asterisk, then you can turn on the display of “Extra lines” bygoing to View->Extra lines, and click on the frame number to have unrecognised lines displayed in the text box. Theunrecognised lines are lines that appear between this and the next prethink lines, except for the very first frame,which contains lines above the first prethink line.

1.9. TasTools and utilities 47

Page 52: Half-Life Physics Documentation · for practical speedrunning aspects of these games, namely theSourceRuns Wiki. Even years after Half-Life’s release, one can still find casual

Half-Life Physics Documentation, Release

We have “fr” and “ms”, which are CFR and UFT. Then we have “hp” and “ap” which are health and armour amounts.They are displayed in different colours than the rest to make them stand out. They may be displayed in red back-ground, white foreground and in bold if the player receives damage. Hover your mouse over them so that the damageamount and type are displayed in the status bar. We also have “hspd”, which is the horizontal speed computed frompmove->velocity after PM_PlayerMove returns, while “ang” is the polar angle of horizontal velocity and“vspd” is the vertical speed. If “hspd” and “ang” are in bold, then it means an object is being pulled or pushed. Youcan also hover the mouse cursor over the fields to have the object horizontal speed displayed. In addition, if an asteriskis displayed at the end of each of “hspd” and “ang” then horizontal basevelocity is nonzero and you can hover themouse cursor over them to see the basevelocity in the status bar. The similar is true if “vspd” is bold. Sometimes thebackground of all “hspd”, “ang” and “vspd” turns light red, in this case the player has collided with some solid entity,which usually changes the velocity.

For “yaw” and “pitch”, they may have light purple background. In this case the corresponding punchangle is nonzero,and again, the mouse cursor can be hovered over them to have the punchangle value displayed.

“fm”, “sm” and “um” stands for forwardmove, sidemove and upmove. They do not currently display the actual values,but rather, the signs of the values. Blue denotes positive values and red denotes negative values. “og” is green if theplayer is onground, and “ds” shows the duckstate, which is white, grey and black for duckstate 0, 1 and 2 respectively,both being states after PM_PlayerMove. “d”, “j”, “u”, “at”, “at2” and “rl” are not white if +duck, +jump, +use,+attack, +attack2 and +reload are active respectively. “wl” shows the waterlevel in white, light blue and bluefor 0, 1 and 2 respectively, while “ld” tells whether the player is onladder.

Lastly, we have the player positions. The 𝑧 component is generally more useful.

1.10 Glossary

ducktapping One of the ground avoidance tricks which involves tapping the duck key while on the ground, resultingin the player popping out 18 units above the ground.

explosion origin, explosion radius An explosion may be modelled as a sphere in which entities receive damage.The explosion origin is then the centre of this sphere, and the explosion radius is its radius.

unit acceleration vector The result of

viewangles A group of three angles comprising of yaw, pitch and roll. This is associated with every entity, represent-ing the view orientation.

48 Chapter 1. Contents

Page 53: Half-Life Physics Documentation · for practical speedrunning aspects of these games, namely theSourceRuns Wiki. Even years after Half-Life’s release, one can still find casual

Index

Dducktapping, 48

Eexplosion origin, 48explosion radius, 48

Uunit acceleration vector, 48

Vviewangles, 48

49