21Gorge behind the scenes: Why a control point map needs NPC entities

  • Site Migration: See bugs? Report them here. Want something changed or have an idea? Suggest it here.
Jul 26, 2015
For Startacker's 2020 April Fool's Day contest, I created a map named cp_21gorge. It's an edit of 5gorge but with 21 control points instead of 5. I actually came up with the idea for a control point map with an ungodly number of points several years ago, but was never able to make it due to TF2's hard-coded limit of 8 control point entities per map. This year I found myself without any new ideas, and almost considered sitting this year out. But I couldn't stand the thought of not submitting anything to an April Fool's Day contest, so at the beginning of March I finally decided to sit down and figure out how to make a map with more than 8 control points.

Bypassing Engine Limits

Most experienced mapmakers will be aware that the game will crash if you try to put more than 8 team_control_point entities in the same map. It simply isn't possible to have more than 8 points. So how did I make a map with 21?

The answer is that there actually aren't more than 8 team_control_point entities. In fact, there are only 5 - the same as the original map! Most of the control points in the map are faked: the holograms are prop_dynamics parented to func_rotating entities, and the capture sounds are made using the open / close sounds of invisible doors located below each point.


Each trigger_capture_area is in fact linked to a real control point - it's just that the point they're linked to changes each time a point is captured. At any time, only the two unlocked capture areas on the front lines are linked to the active points. When a point is captured, it is immediately set back to its previous owner using SetOwner, and the capture zones shift. Only when either team pushes all the way to the final capture area does the real, actual, final control point for the defending team become unlocked.

Creating the HUD

The next problem was to figure out how the hell to show a HUD for this. Obviously, I couldn't get the game to show an icon for all 21 points, since 16 of them don't even exist. Instead, I used env_screenoverlay to display a custom image on screen mimicking the control point HUD. Whenever a point is captured, it updates a math_counter keeping count of how many points Red owns, which sends that number to a logic_case that updates the overlay to the corresponding image.

Of course, I still wanted players to be able to see the capture progress for the two active points, and I couldn't do that with static images. So I decided to still show the two real points on the real HUD below the overlay. I realized that trying to use inputs to change the "Hide Control Point on HUD" flag didn't work, so the next best solution I came up with was changing the HUD layout to put the three other points on a new row and scooting the whole thing down so they became hidden off the bottom of the screen. I used "Custom cap position Y" to change the vertical location of the HUD and "SetCapLayout" to change the custom layout shape. Which brings me to the next section...

Changing the Cap Layout

So, the HUD layout is controlled by the team_control_point_master entity, which has a keyvalue called "Cap Layout" that lets you specify a custom shape for the way control point icons are displayed on the HUD. This is formatted by using the index number for each control point (0 1 2 3 and 4) with commas to separate different rows. For example, a layout string of " 0 1, 2 3 4 " would display the cap icons in a pyramid with the first two points at the top and the rest below. This is exactly what I wanted: two icons on top, and the rest below them, to be shoved off screen. But I also needed to be able to change the cap layout based on which two real control points are currently unlocked.

In total, there are 4 layouts I needed to switch between:

Layout | Use
0 1, 2 3 4 | Blu's final point contested
1 2, 0 3 4 | Somewhere in the middle, Red made the previous capture*
2 3, 0 1 4 | Somewhere in the middle, Blu made the previous capture*
3 4, 0 1 2 | Red's final point contested
*Mid (point 2) is owned by whichever team has just captured.

Luckily, team_control_point_master has an input called "SetCapLayout" which allows this string to be changed. However, there is one major oversight with this design:

Commas are also used to separate parameters of an input in a vmf file.

To understand why this is a big problem, consider this example: say we want to send an input to change the HUD layout so that it displays points 1 and 2 on top, with points 0, 3, and 4 on the bottom.

My Output | Target Entity | Target Input | Parameter
OnTrigger | team_control_point_master | SetCapLayout | 1 2, 0 3 4

If you were to compile the map and fire this input, the HUD would display points 1 and 2, and then... scatter the rest of the point icons wherever it felt like. Sometimes overlapping each other. Why does this happen? Let's look at the map file in a text editor:

"OnTrigger" "team_control_point_master,SetCapLayout,1 2, 0 3 4,0,-1"

As you can see, the line that stores the input uses commas to separate the different fields of the input. This means that the only part of the layout string it is able to interpret is what comes before the comma - anything after that gets parsed as a different field, like delay time. Therefore, the layout string that gets passed to the team_control_point_master is "1 2" and it doesn't know what to do with the rest of the points.

How did I manage to find a fix for this? Some amazing people in the TF2maps Discord (@Pdan4 @Sarexicus @Narpas @B!scuit @14bit) helped me figure out a solution.

I couldn't put the cap layout string anywhere in an input, but I could put it in a keyvalue. All I would need is an entity that was capable of passing one of its keyvalues as the parameter for an input. A math_counter is such an entity, but it's only capable of passing numerical values, not strings. So what entity did I end up using? This is where things get a little bit weird.

generic_actor is an entity that acts as a generic NPC for scripted sequences. You won't find this entity in TF2 hammer - you'll have to install the HL2 FGD to see it, but it does work in TF2. Why is it important? It has an output called "OnFoundEnemy" that gets fired whenever it establishes a line of sight with an enemy NPC. This output returns the targetname of the entity that it sees. And you know what? Targetnames can have commas in them!

And so, here is my HUD logic:


This is a secret chamber (I call it the dungeon) hidden below the map that has two rooms. In one room, there are four NPC entities (the tiny white balls). Each one is named after a different cap layout: '0 1, 2 3 4', '1 2, 0 3 4', '2 3, 0 1 4', and '3 4, 0 1 2'. In the other room is the watcher NPC (the skull). There is an ai_relationship entity that makes the watcher hate all of the layout NPCs. When I want to change the HUD layout, I teleport one of the layout NPCs into the watcher room. The watcher sees it, fires the OnFoundEnemy output and calls SetCapLayout on the team_control_point_master, passing the targetname of what it saw. Then I immediately teleport the layout NPC back into its room.

The result is seamless and most players probably have no idea how much effort went into such a simple feature. Now you know!


Oh, and if you see Jill, tell them to add escape characters for God's sake!
Last edited:


Server Staff
Nov 25, 2013
I'm really amazed you actually did keep using the skull model. Fantastic.

ALSO: there's a typo with the names. The names should have spaces within the single quotes, like this:

' 0 1, 2 3 4 '

Otherwise it won't work.
Last edited:


Stale air
Aug 31, 2014
any sufficiently advanced bullshittery is indistinguishable from logic


L69: Emoticon
Dec 15, 2013
That was quite the entertaining read. I love it when mappers have to brute force the engine to get something to work, often in the most jank ways possible.