A Short Walkthrough of the "mc2" mapC

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.
When we said mapmaking for TF2 will be easier in some ways than TF1, we meant it... for mapmakers who want to do a map that mapC already exists for, it'll simply be a case of putting the entities around the place.

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 weapons
Here, 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.
The // Flag states section defines a bunch of constant ints... basically defines. We'll use these to make the mapC more readable.

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.
If the player is on the Flag's team, the next line checks to see if the Flag's origin is _not_ equal to the Flag's initial origin. If it isn't, it returns the Flag to its initial origin and tells everyone about it. The "print_team_updates" script is a script you can find towards the top of the file... it simply prints one of 3 strings to every player in the game. The "playwavs_to_teams" script is the same, and plays wav files.

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, entvar, "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);
                }
            }
        }
So, if the Flag is touched by someone on the same Team, and it _is_ at home, we do some slightly more complex stuff.
The first important line is:
    while ((flag = find_ent(flag, entvar, "item_script")) != NULL)
"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) tells "find_ent" that we want to check the "classname" variable... don't worry about the "entvar" bit for now.
The last parameter, "item_script", tells "find_ent" we're looking for any entity who's "classname" is "item_script". Now, remember that Custom Entity's are derived from a basic entity type... and that the Flag's Custom Entity is derived from the "item_script" entity. When Custom Entity's are spawned, their classname is set to their derived entity type... so all Flag's Custom Entity's classname's change to "item_script" after spawning.

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.
Don't worry too much about this stuff for the moment... until we've looked at flag pickups, this stuff is a little confusing... suffice to say that when a player touches his/her Flag when its at its initial position, we cycle through all the flags in the map, and capture any of them that are being carried by the player.

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.
We specify 0 because we don't know how many flags are in the map... a mapmaker might decide to create a ctf style map where each team has 3 flags.

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 Drop
First 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).
In this case, we check the drop reason, and if it's due to a player trying to voluntarily drop or trade it, we return 0... players cannot drop or trade Flags. Trading's a fairly special case... slightly beyond the scope of this doc.

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, entvar, "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]
            }
        }
This 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:
    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.
After that we restore their solidity, reset the flag's carrying state, and leave it lying wherever it's fallen. The final piece of the script is this:
        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".
As we mentioned above, the TF2 Code only provides two inbuilt Criteria's for entities... Team checking and Playerclass checking. The "team_no" variable for entities does the Team checking... if a team's bit is not set in the "team_no" variable, it's not allowed to pick it up (although a "team_no" of 0 allows anyone to pick it up).
So only members of team 1 can pick up this Command Marker.

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.
Whenever a player types in a command that TF2 does not recognise, it checks to see if the player has a Command Item with a Command String that matches it. If they have, TF2 calls the Command Function.
So, if a player with a Flaginfo item types "flaginfo" at the console, the "flaginfo_command" script will be called. Lets have a look at that script:
    void flaginfo_command(entity ent)
    {
        entity flag;

        // Cycle through all the flags, and dump the status of each
        while ((flag = find_ent(flag, entvar, "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");
            }
        }
    }
Command 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.

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.
This function takes a player (in this case the connecting player") and gives him/her an item. The Item given is specified by the second parameter, and the third parameter is the number of the items to give. "add_item" tries to find an Item Type with a name matching the second parameter, and then increments the player's count of that Item Type by the third parameter. Using this function, we could give the player ammo, armor, weapons, etc. The script then returns TRUE to allow the player to join.

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]