In praise of SUB_CalcMoveDone

It comes up time and time again in map hacks, but why is SUB_CalcMoveDone such a versatile function? Today she gets her day in the sun, where we learn all the useful features of the function, revisit some old applications and see some new ones as well!

Recap – how do hacks call functions?

In Quake, entities can respond to various interactions, for example when they are killed or another entity touches them. Some entities respond to many interactions, others to just one or none. Entities have a field for each possible interaction, and the QuakeC creates a response by assigning a function name to that field.

Many map hacks work by finding a field the QuakeC leaves blank and assigning it a function, so that the entity responds in a new way. For example, the func_wall entity does not have a function in the touch field – meaning it does not respond to other entities colliding with it. By adding the key "touch" "trigger_damage_touch" to a func_wall, we cause it to react to collisions by running the trigger_damage_touch function. As you may have guessed from the name, this is the function normally run when an entity collides with a trigger_damage. The result is that the func_wall damages things that collide with it.

Although this technique enables some great map hacks to be performed, there are challenges as well. You can only run predefined functions, you can only run one, and you have to run the whole of the function even if you only want some of the effects. SUB_CalcMoveDone features in such a large number of map hacks because it can work around some of those issues.

What’s in the function?

The code for SUB_CalcMoveDone is just 5 lines long:

void() SUB_CalcMoveDone =
setorigin(self, self.finaldest);
self.velocity = '0 0 0';
self.nextthink = -1;
if (self.think1)

The simplicity of the function can be a boon. There’s nothing worse than finding a function which does dozens of useful things, then discovering it has a side effect you can’t work with. The first three things the function does are: setting the origin of the entity, stopping its movement, and setting a negative nextthink. These effects have frequently been helpful in map hacks I’ve created, and if one of the effects is not needed, it’s generally benign or easy to work around. For example, if you don’t need to change the origin of your entity, simply set the "finaldest" field equal to the entity’s starting position.

SUB_CalcMoveDone can be added for free

The bigger part of why SUB_CalcMoveDone is so useful comes in the latter two lines, where it runs the function in the think1 field if one is present. This lets us pair SUB_CalcMoveDone with a second function, and run both of them as a single reaction. If you set a function as a reaction, then find it does everything you want except setting the location of your entity, you can move that function to the think1 field, and put SUB_CalcMoveDone into the reaction field. When the entity reacts, it will run SUB_CalcMoveDone first, then the function in think1. It’s like you got the SUB_CalcMoveDone effects “for free”.


The practical part of the post can now begin. The following hacks are a mixture of never seen before hacks, things I posted on func_msgboard at some point, and subjects of previous blog posts – and all of them make use of SUB_CalcMoveDone in some manner. I’ll reproduce some of them in full, but for the more complex ones I’ll just link to the post dedicated to them and summarise what role SUB_CalcMoveDone plays.

The following entity hack was devised to create a rotating entity which does not appear until triggered

"classname" "info_notnull"
"avelocity" "0 256 0"
"movetype" "8"
"model" "*1"
"modelindex" "2"
"origin" "0 -4000 0"
"use" "SUB_CalcMoveDone"
"finaldest" "64 -32 128"

The first half of the hack creates an brush entity that rotates. It directly sets modelindex in order to become visible, so the oft-used trick of delaying the spawnfunction would not delay the entity appearing visible. This is where the SUB_MoveCalcDone is used, in particular the origin setting feature. The trick is that we start the entity with an origin that places it far below the map, then “teleport” it into position when needed.

The next entity allows us to repeatedly spawn gibs in from a specific point

"classname" "info_notnull"
"health" "-40"
"origin" "64 -32 128"
"use" "SUB_CalcMoveDone"
"think1" "PlayerDie"
"finaldest" "64 -32 128"

Using the first three lines with "use" "PlayerDie" creates an excellent gib fountain for a single use. The problem is that the head gib actually uses our info_notnull entity, rather than spawning a new entity. So each time you re-use the entity, the gibs spawn from the point that the head landed previously. SUB_CalcMoveDone is again being used to set the origin of an entity, repositioning the head to the finaldest location. In this case, we’re also making use of the chaining ability, which lets use get the bonus effects from SUB_CalcMoveDone on top of the code we actually want to run in PlayerDie. You can adjust the velocity of the gibs by setting health to a more-negative value.

One of the weirdest uses I’ve made of SUB_CalcMoveDone is Classname Masquerading. The idea is that if you change the classname of an entity to , then type the original classname into the think1 field, then the entity spawns like normal except that the classname string is now "SUB_CalcMoveDone" rather than the usual classname value. Essentially all we’re using about SUB_CalcMoveDone here is the ability to chain another function, and the fact that the outer function name gets saved as the classname!

This lets you subvert any part of the QuakeC which is expecting a specific classname. The example I’ve posted on the blog before was in Masquerading as a Door, where two entities just needed the same classname as each other, so they would become linked doors. That hack generalises to highly customised door entities, which previously seemed almost impossible. In addition, there are dozens of other places where the QuakeC tests classname, so this one has a lot of unrealised potential.

Another previously featured hack which uses SUB_CalcMoveDone appeared in the Reusable trigger_counter article. Here we wanted to run the multi_wait function to reset some aspects of our entity, but ran into a problem because it didn’t reset the nextthink field. By adding SUB_CalcMoveDone then chaining multi_wait in the think1 slot, we got to add that one missing ingredient. And it’s certainly surprising that it’s the self.nextthink = -1 line which we needed, when usually that seems like the least useful part of the function.

The final hack for today is a new one I call the “Duke of York”. It allows a func_door to start in a position which is neither up nor down, but a third position. It will still move between the open and closed positions when triggered, the third position is a one-off. This might be useful to start a lift mid-way between floors before it is activated for the first time. Create the func_door as usual then set the following extra keys:

"th_die" "SUB_CalcMoveDone"
"finaldest" "0 0 -80"
"takedamage" "1"
"think1" "multi_trigger"

You then need to set up some kind of spikeshooter or similar to attack the door at the start of the map. It’s a bit dodgy, but we don’t have many choices for running a function on a door! Again this is using the SUB_CalcMoveDone origin change to move the door, you can adjust the vector in finaldest to get the offset you’re looking for. multi_trigger is here to set takedamage to 0 so that it can’t be shot and killed again.

Phew! I hope that little tour of one my my favourite functions was illustrative, there are some neat new ideas here I hope to see in some maps soon. Until next time!


Leave a Reply

Fill in your details below or click an icon to log in: Logo

You are commenting using your account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.