This text file is designed to give you a short decription of the mapC language, along with a walkthrough of the mc2 map, and its mapC. As a mapmaker, if you don't understand how mapC works, or don't want to understand, then don't... the only reason we provide you with this is so you can gain a better understanding of what the TF2 map engine is capable of. We will be providing large chunks of example mapC code with TF2, handling all the standard map types... 2 flag ctf, 1 flag ctf, command point system, president, etc.
This does _not_ tell you how to program mapC. If you have programming experience, then you'll grasp it easily... if not, then look elsewhere for programming help.
Please note this is a badly cut version of a text file supplied to our mapmakers, and may make references to files that we haven't made available to you. Please don't mail us asking for these files.
MapC
The mapC language is basically C. You write a header for, which is essentially a makefile, for your project. In this case, it's mc2.mapH, which looks like this:
#mapinfo "name" "mapC test 2" "outputname" "mc2" "description" "simple script stuff" #interface "int6.mapI" #libraries // none #scripts "mc2.mapC"For your map, just copy the mapH and change the #mapinfo section. The #script section tells the compiler which files to include, so you'll probably want to include your map's .mapC file. Apart from that, ignore the rest.
The only other bit of interest is the int6.mapI file. This file defines the Scripting Interface... basically, all the variables and functions the TF2 engine provides the mapC. Have a browse through it... skip past all the start and take a look at the #interface_vars section. This section contains all the interface variables and functions... for instance:
void dprint(string cstring);This interface function is Debug Print, which takes a string variable, returns nothing. It simply prints that string to the server's console. A more interesting one is:
void team_set_name(int team_number, string team_name);This sets the name of a Team to whatever string you specify. Browse through the interface funcs... so far, all you really have are the various functions we needed for the ctf mapC.
This is the main area we want feedback from you mapmakers... if you have interface functions you require for a particular map, then by all means tell us, and we'll look into including them.
When TF2 starts a map, it looks for a mapC script file to match that map... first it looks inside the map's Worldspawn entity. If there's a "scriptfile" key, such as "ctf", it'll look for that .mcB file (ctf.mcB). If that "scriptfile" key isn't specified, it looks for a .mcB file matching the name of the map file (mc2.mcB).
MC2
The mc2 map is a simple map which we're using to test mapC on. At the moment, it contains mapC for CTF, Command Point, and a cute scoreboard that Midori wanted.
It'd take ages to describe it all, and you'll probably figure out more just by reading it... so I'll begin by just describing the behaviour of one entity, a CTF Flag... the most complex entity in the mc2 mapC, and indeed, probably the most complex use of mapC there is.
Custom Entity Definitions
Start by looking at this code:
////////////////////////////////////////////////////// // Custom Ent Definitions for Flags entity_def { "classname" "item_flag_team1" "spawnclass" "item_script" "script_spawn" "spawn_flag" "script_postspawn" "postspawn_flag" "script_activate" "touched_flag" "script_getitem" "get_flag" "script_dropitem" "drop_flag" "owned_by_team" "1" "name" "Blue Flag" }This defines a new type of entity... a "Custom Entity" that can only be used on a map which includes this mapC. The first line in the definition sets the classname of the Custom Entity to "item_flag_team1". This means you can open up your map, put an entity somewhere, call it "item_flag_team1", and include this mapC, and bang, that entity will be a ctf flag for team 1.
Anyway, back to the flag. The "spawnclass" tells TF what type of entity this entity really is. Basically, Custom Entities are derived from an existing entity... in this case, an item_script. You've probably already seen John's initial TF2 mapmaking ent specs... in it, the "item_script" is defined as:
"An item who's properties are mainly described by scripts. It can be picked up, carried and used by players."So, we're deriving our flag entity from that basic entity type, and we're going to override its basic behaviour with some of our own mapC code.
Then we define a bunch of scripts for this entity... you'll be able to find all the scripts listed here throughout the mc2.mapC file. Currently, every entity can use any or all of the following scripts:
ALL ENTITIES script_use // called when targeted script_activate // called when activated (touched, etc.) script_usetargets // called after activated, after delayed, when using target/killtarget script_kill // called when killtargeted script_damaged // called when damaged script_think // requires a nextthink script_prespawn // yes/no to whether it should spawn script_spawn // called just after spawn script_die // called when player/button/etc dies ITEM SPECIFIC script_postspawn // called after items have spawned and dropped script_dropitem // called when player tries to drop this item script_useitem // called when player tries to use this item script_getitem // called when player tries to pick up the item PLAYER SPECIFIC script_attack // called when player attempts to attack script_changeweapon // called when player changes weaponsHere, we're defining 5 scripts for this flag. The next line ("owned_by_team" "1") sets a variable in this entity called "owned_by_team" to 1. Then we set the "name" of the entity to "Blue Flag".
Note that all of these settings (scripts, owned_by_team, name) are set exactly as if they were key/value pairs inside the .map file. Note that Custom Entity definitions are overridden by .map file key/value pairs, so if you put an "item_flag_team1" entity in your .map file, with two keys, as follows:
"owned_by_team" "16" "name" "Team 16's Flag"The .map's keys would override the mapC's entity definition, and you'd effectively have a flag for team 16. We could provide an entity definition along these lines:
entity_def { "classname" "item_flag" "spawnclass" "item_script" "script_spawn" "spawn_flag" "script_postspawn" "postspawn_flag" "script_activate" "touched_flag" "script_getitem" "get_flag" "script_dropitem" "drop_flag" // Mapmaker must provide a "owned_by_team" and a "name" }And there, we've created a Custom Entity definition for a generic flag. Provided the mapmakers sets the "owned_by_team" and "name" keys in the .map file, the map could use this Custom Entity to makes flags for any team.
Spawn Scripts
Now, lets look at the Flag's first script, the spawn script. As the above description says, the spawn script is called when the entity is spawned, at the start of the map... before players have joined even. Here's the flag's spawn script:
int entity::spawn_flag(int flags) { //self.model = "models/items/tf_flag/tf_flag.md2"; self.model = "models/items/healing/medium/tris.md2"; self.flag_state = FLAG_HOME; return TRUE; }Ignore the comment... you don't have the tf_flag model, so it's just using the health box model for now, which the second line sets. Note that the "self" variable points to the entity the script is being run for... in this case, it's the Flag itself. To understand the third line (self.flag_state = FLAG_HOME), we need to take a look up near the top of the file, at these lines:
// new vars vector entity::init_origin; // For tracking flag movement int entity::flag_state; // Allow us to keep track of all flags on the map entity entity::carrier; // Allow us to keep track of all flags on the map // Flag states const int FLAG_HOME = 1; const int FLAG_AWAY = 2; const int FLAG_BEING_CARRIED = 3;The // new vars section defines a bunch of new variables. The second one (int entity::flag_state) defines a new integer for every entity in the game. Its a mapC var only, so you can do anything you want with it, and be assured that the dll code won't ever change it on you.
Now, back down to the spawn function again... now we can see that the (self.flag_state = FLAG_HOME) line is setting the Flag's flag_state variable to FLAG_HOME (1). As you've probably figured out by now, we're going to use the Flag's flag_state variable to keep track of what the status of the Flag is... whether it's at it's Home, Lying around somewhere, or Being Carried by someone.
The last line of the spawn function is important. Almost all built in scripts return an int, which is usually TRUE or FALSE, and the TF engine uses that return value to do something. In the case of Spawn Scripts, if the spawn function returns FALSE, the TF Engine understands that you don't want this entity to exist in the game, so it removes it. This allows the mapC to check server settings, like deathmatch, teamplay, etc, and remove entities that shouldn't exist under those settings.
Postspawn Scripts
Onto the next script... the "postspawn" script. This is mainly used just for
items... and to understand why it's needed, we need to understand just a little
about how Quake2 works. When Quake2 starts a new level, it moves through the
entities in the map and spawns them all, calling their spawn functions. The
TF engine calls all the mapC spawn functions at this time. All spawn functions
are called at the same point in the game time... so that none of the entities
quite exists until the next game time "tick".
Now, the tricky part is that even the map itself, the world, is considered an
entity. So, until the next game time "tick", not even the world exists. This
means that if you wanted to do something with your entity in the spawn that
involved interaction with the world/map itself, it would fail.
And that's where the "postspawn" script comes in... it is called shortly after
the initial spawning of entities, and you can safely perform functions which
rely upon other entities, like the world.
Here's the flag's postspawn script:
int entity::postspawn_flag(int flags) { droptofloor(self); self.init_origin = self.origin; return TRUE; }The first line (droptofloor(self)) calls an interface function called "droptofloor". As it's name implies, this function simply makes the entity passed to it (in this case, the flag) drop to the floor. This had to be done in the "postspawn" script, not in the "spawn" script, because as we described above, the world does not exist in the "spawn" script.
Why do we drop this item to the floor? It's standard practice... in Quake2,
all items are dropped to the floor upon spawning, so that mapmakers don't
have to worry about putting their in exactly the right place on the floor...
they can just suspend them up in the air a bit, and let the engine handle
the rest.
Why do we have to do it in the mapC then? Because the "item_script" entity,
from which the Flag's Custom Entity is derived, does almost nothing on it's
own... it designed to let the mapC define it's behaviour.
Now, the second line (self.init_origin = self.origin) sets another mapC variable we defined at the top of the mapC code... init_origin. An entity's .origin variable is a vector containing the current position of the entity in the world. As we all know, in ctf style maps we need to return the flag to it's original position on occasion. So, we need to store this initial position somewhere... and that's why we defined a new vector for every entity: init_origin.
Activate Scripts
Now, onto the really interesting Flag functions... the "activate" script.
Activate scripts are called when something has attempted to activate this
entity. These scripts provide you with the capability TF1 provided with
its extensively ugly Criteria.
TF2 provides entities with only 2 basic criteria... team checking and
playerclass checking. If the activating entity passes both those Criteria,
the activate script is called (if it exists), and if that script returns
TRUE, the entity is activated.
So, lets look at the Flag's activate script,
which will be called whenever a player has touched the Flag. It's a long
function, so I'll just describe snippets of it:
int entity::touched_flag(entity other, int flags) { entity msgent, flag; string fstr, estr, astr;Look at the parameters this script takes... an entity and some flags. The flags simply provide you with some more information about this activation... ignore them for now. The entity parameter is important though: it's a pointer to the entity that is attempting to activate this entity... in this case, it's the player who has touched the Flag.
The first two lines define local variables we need in this script. They work just like any local variable in C. The next few lines:
if (other.team_no == self.owned_by_team) { if (self.init_origin != self.origin) { // Return Flag self.origin = self.init_origin; self.flag_state = FLAG_HOME; // Setup and Print Messages fstr = other.name + " Returned Your flag!\n"; estr = "Your Flag was Captured!\n"; astr = other.name + " Returned the " + other.name + "!\n"; print_team_updates(other, other.team_no, other.team_no, fstr, NULL, astr); cprint(other, "You returned your flag\n"); // Play Wav files playwavs_to_teams(self, other.team_no, other.team_no, "tf/maps/ctf/t_fl_return.wav", "", "tf/maps/ctf/t_en_fl_return.wav"); }The first line (if (other.team_no == self.owned_by_team) is fairly self explanatory... it checks to see if the other's (the player) team number is the same as the self's (the Flag) "owned_by_team". Remember that the Custom Entity definition of the Flag set the "owned_by_team" variable to "1" for Team 1's Flag. So, we're checking to see if the a member of the team who own this Flag has touched it.
After that is a large chunk of code, so I've snipped out some of the simple stuff that we've seen above.
else { // If they've got any enemy flags, capture them while ((flag = find_ent(flag, entvarSo, if the Flag is touched by someone on the same Team, and it _is_ at home, we do some slightly more complex stuff., "item_script")) != NULL) { if (flag.flag_state == FLAG_BEING_CARRIED AND flag.carrier == other) { // Setup and Print Messages [snip] // Play Wav files [snip] remove_item(other, flag.name, 0); flag.origin = flag.init_origin; flag.svflags = flag.svflags - (flag.svflags & SVF_NOCLIENT); flag.solid = SOLID_TRIGGER; flag.flag_state = FLAG_HOME; flag.carrier = NULL; iscore += 1; set_scoreboard("my_board", iscore); } } }
while ((flag = find_ent(flag, entvar"find_ent" is an interface function that takes 3 parameters... an entity to start from, a variable to check, and a value to check the variable against. In this case, we start from flag, which was a local variable, and hence is initialised as NULL... and if the starting entity passed to "find_ent" is NULL, it starts from the start of the entity list. The second parameter (entvar, "item_script")) != NULL)
So, basically, this function simply returns a pointer to the first entity it find with a classname of "item_script". The line starts looking in the entity list from the "flag" variable's position, and since we set the "flag" variable to it's return variable, the while loop will cycle nicely through all the "item_script" entities in the map.
The next line is as follows:
if (flag.flag_state == FLAG_BEING_CARRIED AND flag.carrier == other)It checks the state of the current flag returned by the "find_ent" function. If the flag's state is being carried, AND the flag's carrier variable is equal to the other (the player who touched this flag), then we want to capture this flag.
The next important bit is the (return FALSE) line. We return FALSE because we don't want to allow the player to pick up the Flag... it's his flag after all, and in ctf style modes, we don't allow player's to carry their flags.
Now, onto the last bit of the script, which handles what happens if a player _not_ on the Flag's team touches it... obviously, we want the player to pick up the flag, so we return TRUE, and let the TF2 code give the player the Flag.
GetItem Scripts
Now, we're starting to get into the trickiest part of mapC... item handling. Lets look at the Flag's "getitem" script:
int entity::get_flag(entity other, int no_of_flags) { self.flag_state = FLAG_BEING_CARRIED; self.carrier = other; // Since we're not going to let the Code remove this item, // we need to hide it ourselves self.svflags |= SVF_NOCLIENT; self.solid = SOLID_NOT; // Return False to prevent the Code from removing the item // now its been picked up return FALSE; }If you're following things nicely, you'll probably wonder why we need a "getitem" script at all... after all, couldn't we just put all this code at the end of the "activate" script, before we return TRUE? To understand exactly what's going on here, we need to look at how TF2 handles items, and here's where things get ugly.
Item Handling
For every player in the game, TF2 records a single number for every Item
Type in the game, which represents the number of those Item Types the
player has. For instance, if a player gets a weapon, the player's count for
that weapon Type is incremented, and the actual weapon Entity itself is
_removed_.
It'd be a terrible waste of memory to keep all the entities that player's
are carrying around, when we can simply keep track of a number of them
a player has.
So, way back when we declared a new Custom Entity definition, TF2 took note
of the fact that we were deriving it from an "item_script". Whenever TF2
spots an "item_script" in a map, it creates a new Item Type based upon
that item_script. If there are 20 of those item's in the map, it still only
creates 1 Item Type.
Then, whenever a player gets one of those Item Entities, it simply increments
the player's count of that Item Type, and nukes the Item Entity.
And whenever a player drops one of those item's, it simply decrements the
player's count of that Item Type, and then, and this is important, creates
a new Item Entity Based Upon The Original Item Type.
So, after the Item Type is created, which occurs during the spawning of the "item_script", any changes to any Item Entities derived from this Item Type do not affect the Item Type. This is the fundamental thing to understand when it comes to handling items in TF2 mapC.
Now, this creates a problem for us in our ctf mapC... because we don't want the item (in this case, the Flag) destroyed and the player's count simply incremented... because we store information in that item. In the Flag's "postspawn" script, we store the Flag's initial origin. The Item Type is created during spawning, which means that initial origin, and the flag state for that matter, is _not_ stored inside the Item Type. Which means that if we allowed the Flag entity to be destroyed when the player picked it up, and then recreated when the player drops it, we'd lose that initial origin and flag state.
Item Pickup Summary
So, to make things simple for mapC coders, there's two scripts related to item
pickups, and each has a distinct purpose.
The "activate" script is called before the pickup, and is designed to return
TRUE if you want the TF2 code to handle the pickup and increment the player's
count of this Item Type.
The "getitem" script is called after the pickup, and is designed to return
TRUE if you want the TF2 code to destroy the Item Entity.
In this case, we're happy to let TF2 handle the pickup and the item count incrementing and all that crap... we just don't want it to remove the Flag entity afterwards.
So, we return TRUE in the Flag's "activate" script, and we return FALSE in the Flag's "getitem" script. But we don't want to just leave the flag there, so we need to do a bit more work... here's the code again:
int entity::get_flag(entity other, int no_of_flags) { self.flag_state = FLAG_BEING_CARRIED; self.carrier = other; // Since we're not going to let the Code remove this item, // we need to hide it ourselves self.svflags |= SVF_NOCLIENT; self.solid = SOLID_NOT; // Return False to prevent the Code from removing the item // now its been picked up return FALSE; }The first line (self.flag_state = FLAG_BEING_CARRIED) updates the status of the flag, and the following line (self.carrier = other) sets the Flag's carrier variable, which we defined up at the top of the mapC, to point to the player who's now "carrying" the Flag.
The next two lines are more involved in the Quake2 dll code than we'd like
to get into at this time, so I'll describe them only in what they do, not
in how they do it.
The (self.svflags |= SVF_NOCLIENT) line sets a bit in the Flag's serverflags
which prevents the game server from telling any clients that it exists...
effectively making it invisible to everyone.
The (self.solid = SOLID_NOT) line makes the Flag non-solid, making it
impossible for any entities to touch it.
So, these two lines effectively "remove" the Flag, but still leave the actual
Flag's entity intact.
And then finally, we return FALSE, preventing the TF2 code from removing the Flag entity. And we're done with Flag pickups.
Flag Capturing
Now, lets backtrack just a bit, to the bit in the Flag's "activate" script where we capture flags being carried by the player... the code again:
if (flag.flag_state == FLAG_BEING_CARRIED AND flag.carrier == other) { // Setup and Print Messages [snip] // Play Wav files [snip] remove_item(other, flag.name, 0); flag.origin = flag.init_origin; flag.svflags = flag.svflags - (flag.svflags & SVF_NOCLIENT); flag.solid = SOLID_TRIGGER; flag.flag_state = FLAG_HOME; flag.carrier = NULL; iscore += 1; set_scoreboard("my_board", iscore); }The first line (remove_item(other, flag.name, 0)) calls an interface script called "remove_item". This function takes a player (in this case other, the player who touched the flag), and removes items from him/her. The Item Type to be removed is specified by the second parameter, and the third parameter is the number of that type to remove... or 0 if we want to remove however many the player's got.
The (flag.origin = flag.init_origin) sets the Flag's origin back to the
initial origin that we saved in the "postspawn" script.
The (flag.svflags = flag.svflags - (flag.svflags & SVF_NOCLIENT)) line removes
the bit we set in the Flag's serverflags, effectively making it visible again.
The (flag.solid = SOLID_TRIGGER) line makes the Flag solid again, ready to
be touched by players. Then we update the Flag's status, and clear the Flag's
carrier variable, since it's not being carried by anyone.
The following lines just update the scoreboard... they're not part of the CTF code. I just needed a way of incrementing the scoreboard while I was testing it. Capture the flag a few times and you'll see the scoreboard keep count of how many captures you've made.
DropItem Scripts
Now lets look at the last piece of the Flag puzzle... the "dropitem" script.
An item's "dropitem" script can be called for a few reasons, so the second
parameter passed to the "dropitems" script specifies the cause for the drop.
The last parameter specifies the number of these Item Types the player has...
more on this shortly.
Let's look at the first part:
int entity::drop_flag(entity other, int flags, int no_of_flags) { entity flag; string fstr, estr, astr; if (flags & SF_DI_PLAYER_DROP OR flags & SF_DI_PLAYER_TRADE) return 0; // Prevent DropFirst we define a couple of local vars, and then we take a look at the flags passed to us by the TF2 code. The flags will be set to one of the following (which are defined in the mapC interface file, .mapI):
const int SF_DI_PLAYER_DEATH = 1; const int SF_DI_PLAYER_DROP = 2; const int SF_DI_PLAYER_TRADE = 4;The "dropitems" script is called for one of three reasons... the Item Type is being 'dropped' because either the player died, the player voluntarily tried to drop the item, or the player tried to trade it to a teammate. The "dropitems" script simply returns the number of this Item Type to 'drop' (or trade).
Now, unfortunately things are going to get tricky again here, because we're about to discover the other part of Item Handling... Item dropping. When the "getitems" script is called, the "self" variable is set to point to the Item Entity... the one the player has just picked up. When the "dropitems" script is called, the "self" variable is NOT set to point to an Item Entity... why? Because there may not be one. Remember... a player does not have a list of Item Entities he/she is carrying. All a player has is a count of the number of Item TYPES he/she is carrying.
So, when the "dropitems" script is called for an Item, it is called for an
Item TYPE. Remember... when a map spawns, TF2 creates an Item Type for each
new Custom Item Definition it finds in a map. This Item Type is a template
that is never altered after it's initial creation.
So in the "dropitems" script the "self" variable is set to point at that Item
Type... at that template. You can read information out of it as much
as you like, but you cannot alter it.
Now, lets continue:
// Play Wav files [snip] // We want to handle it ourselves remove_item(other, self.name, no_of_flags);After playing some wav files, we remove all of this Item Type that the player has. After that we use a loop as follows:
// Cycle through all the flags, and find the correct flags while ((flag = find_ent(flag, entvarThis simply cycles through all the Flag's in the map, and finds out if they're being carried by this player. If they are, we call this function:, "item_script")) != NULL) { if (flag.flag_state == FLAG_BEING_CARRIED AND flag.carrier == other) { throw_object(flag, other.origin); flag.solid = SOLID_TRIGGER; flag.flag_state = FLAG_AWAY; flag.carrier = NULL; // Setup and Print Messages [snip] } }
throw_object(flag, other.origin);This is an interface function that takes an entity and a point. It moves the entity to that point, and throws it upwards in a random direction. It also removes the entity's serverflag NOCLIENT bit... making the entity visible again.
return 0; }This tells the TF2 code that we don't want to drop any of this Item Type... because we've already handled it ourselves. If we were to return 1, the TF2 code would create a new Item Entity, based upon this Item Type, and drop it. If we were to return 10, the TF2 code would create 10 new Item Entities, all based on the Item Type template, and drop them all.
Now you might be wondering at this moment... why are we using the "dropitems"
script of one Type of Flag to drop all the Flag's the player is carrying?
Isn't that kind of illogical, not to mention stupid if the player has more
than one flag, because this script will be called for each Flag Type?
Well... yes.
The correct way of handling this would be to define all the Flag's "dropitem" scripts as nothing but a simple function that returned 0 and do the actual dropping of the Items in the player's "script_die" script, which would be called whenever a player dies.
But, that'd be easy... and I wanted to try and push the difference between an Item Entity and an Item Type.
Item Handling Summary
While the whole Item Handling concept isn't simple, the implications for mapC coders are. Its important to keep in your head the difference between an Item Entity and an Item Type... do that, and you shouldn't have any trouble.
- Player's carry numerical counts of Item Types. - Players pickup Item Entities, which increments their counts of the corresponding Item Types, and destroys the Item Entities. - Players drop Item Types, which decrements their counts of those Item Types, and creates new Item Entities based upon those Item Types.
Simpler Items
Now, you're probably thinking mapC looks fairly daunting. Not so... as I said, the ctf style flags are among the most complex uses of mapC there is, because they're items that you want to keep track of even when they're being carried. That's fairly rare... you'll usually be happy to let the TF2 code do the item handling.
Lets look at a far simpler item, and one that's more like the standard item's
most mapmakers will want to add.
Here's the entity definition:
entity_def { "classname" "item_comm_marker_team1" "spawnclass" "item_script" "script_postspawn" "comm_marker_postspawn" "script_activate" "comm_marker_touched" "script_getitem" "comm_marker_getitem" "script_dropitem" "comm_marker_dropitem" "owned_by_team" "1" "name" "Blue Command Marker" "model" "models/items/healing/medium/tris.md2" // Criteria "team_no" "1" }This defines a new Custom Entity... the "item_comm_marker_team1". It's a Command Marker, used in the Canalzone Command Point scoring system. It's derived from the "item_script" class, like the ctf Flags. It's owned by team 1, has a health box as a model, and unlike the ctf Flags, it's got some Criteria: a "team_no".
The Command Marker's scripts are very simple. No spawn script, and a postspawn script as follows:
int entity::comm_marker_postspawn(int flags) { droptofloor(self); return TRUE; }Like the ctf Flags, we want Command Markers to drop to the floor after spawning.
The "activate" script called when a player tries to pick up a Command Marker is as follows:
int entity::comm_marker_touched(entity other, int flags) { // Only allow them to carry 1 at a time if (get_num_items(other, self.name) == 0) return TRUE; return FALSE; }The "get_num_items" function is an interface func that returns the number of an Item Type that a player is carrying. In this case, we want to see if the player has any of this type of Command Marker... if not, we allow them to pick it up.
The "getitem" script for the Command Marker's is also simple:
int entity::comm_marker_getitem(entity other, int no_of_flags) { return FALSE; }Quite simply, we don't want the TF2 code to remove this item... it simply acts as a kind of dispenser. It'll just give out 1 Command Marker item to any of its team that touch it.
The "dropitem" script is just as simple:
int entity::comm_marker_dropitem(entity other, int flags, int no_of_flags) { return 0; // Lost when player dies }Player's cannot trade or drop Command Markers... when player's die, Command Marker's are just lost.
And that's it. In their "activate" scripts, the Command Points check to see
if player's have any Command Markers, and if so, allow the player to capture
that Command Point. Command Markers themselves provide player's with no other
capabilities.
So, lets look at an item that does.
Command Items
Command Items are a special type of item that adds a command to any player carrying it. The ctf mapC uses a Command Item for the "flaginfo" command, so we'll look at that. Here's it's entity definition:
entity_def { "classname" "item_flaginfo" "spawnclass" "item_script" "script_spawn" "spawn_flaginfo" "command_func" "flaginfo_command" "command_str" "flaginfo" "name" "flaginfo" "item_flags" "96" // IT_NOT_VISIBLE | IT_RESPAWN_KEEP }Like all previous items, it's derived from the "item_script" class. It only has one script, and that's it's spawn script, which looks like this:
int entity::spawn_flaginfo(int flags) { return FALSE; }Returning FALSE from a spawn function causes the entity to be removed... we don't want "flaginfo" entities in the map, so we just remove them. They don't actually need to be in the map... the spawn function's just in case a mapmaker puts one in the map.
Now, lets look at the interesting parts of the Flaginfo Entity Definition:
"command_func" "flaginfo_command" "command_str" "flaginfo"The first line specifies a Command Function. The second specifies a Command String. That's all Command Items need.
void flaginfo_command(entity ent) { entity flag; // Cycle through all the flags, and dump the status of each while ((flag = find_ent(flag, entvarCommand Functions take a single parameter... a pointer to the player who used it. The "self" variable points to the Item Type of the Command Item. In the case of the Flaginfo item, the Command function just cycles through all the Flags in the map, and displays the status of each of them to "ent", which is the player who typed the "flaginfo" command., "item_script")) != NULL) { if (flag.flag_state == FLAG_HOME) { cprint(ent, flag.name); cprint(ent, " is in its base.\n"); } else if (flag.flag_state == FLAG_AWAY) { cprint(ent, flag.name); cprint(ent, " is lying around somewhere.\n"); } else if (flag.flag_state == FLAG_BEING_CARRIED) { cprint(ent, flag.name); cprint(ent, " is being carried by "); cprint(ent, flag.carrier.name); cprint(ent, "\n"); } } }
There's only one catch... the player must have one or more of the Command
Items. So how does a player get the Flaginfo item, since the spawn function
automatically removes it from the map?
Easy... through the use of a Global Script.
Global Scripts
Global Scripts are scripts that aren't attached to a specific entity... they're generally event based. An event occurs which has an attached Global Script, and if the Global Script exists in the mapC file, it's called.
The first Global script we'll look at is PlayerConnect. As it's name suggests, it's called whenever a Player connects to the server. Here's mc2's PlayerConnect script:
int entity::script_PlayerConnect(int flags) { add_item(self, "flaginfo", 1); return TRUE; // yes, the player is allowed to join the game }The "self" variable is set to point to the connecting Player. If the PlayerConnect script returns FALSE, the player is prevented from joining the server. In mc2, we use the PlayerConnect to give the player the Flaginfo Command Item, using the "add_item" interface function.
There are currently only two other Global Scripts: Worldspawn, called at the initial spawning of the map, and Precaches, called when the server begins to precache sounds and models. We will be adding more... for when Player's disconnect, join teams, leave teams, etc. Like Interface Functions, we'd like to hear your requests for Global Scripts.
Think Functions
And last but not least, lets look at Think functions. The Command Point
System uses a Think function to check the status of every Command Point
in the map.
Every entity can have a think function and a nextthink time. The TF2
engine will automatically call that think function when the time is reached.
Lets look at the WorldSpawn Global Script:
int entity::script_Worldspawn(int flags) { entity ent; number_of_teams = 2; team_set_name(1, "Blue"); team_set_name(2, "Red"); // Command Point Scoring Entity ent = create_entity(); ent.think = comm_scorer_think; ent.nextthink = time + 300; // 30 Seconds return TRUE; }The Worldspawn Global Script is called when the map first loads. First, we initialise the number of teams the map is designed for, and set the team's names. These values could be overriden by setting values inside the "worldspawn" entity in the .map file, but don't worry about that for now.
The (ent = create_entity()) line creates a new entity and sets the "ent"
variable to point at it. The next two lines set that entity's think
and nextthink.
We set the think to "comm_scorer_think" and the nextthink to time + 300.
What this means is that the TF2 code will automatically call the
"comm_scorer_think" script 30 seconds from now.
You might want to take a look at the
"comm_scorer_think" script.
Other MC2 Code of Interest
We'll let you look over the mc2 mapC and mess with what's there. Take a look at the scoreboard... it's an example of a more complex interaction between mapC and brushes inside a map.
You might notice the use of the "entlist" variable in some places, such as
the "print_team_updates" script. An "entlist"
is an Entity List. Entities can be added and removed from lists simply and
effectively, and any operation applied to a list is applied individually to
all entities in the list. Handy for messaging everyone on a team, or providing
bonuses to a subset of players.
More complex uses of them will come later.
Also, check out the entity definitions for the spawnpoints. Players attempting to spawn on a spawnpoint have their criteria checked just like everything else... a spawnpoint's "activate" script can check any of the player's details, and return TRUE if the player is allowed to spawn on it.
Results
So what have we got here?
We've got a chunk of mapC code that, among other things, will handle any
CTF map, with any number of Flags per team, with upto 32 teams. And all the
mapmaker has to do is set the worldspawn's "scriptfile" key to "ctf", and
put the Flag and Spawnpoint entities around the map.
Robin. [email protected]
[Front][News][Download][TF
Info][TF Clans][Links][Contact][Maps][Classes][Supporters]