Class Based Mod Starter

Seems like everybody wants to make a class-based mod these days ...

Man, people sure do love to play and make class based mods these days, must be a fad or something.

Anyway, this tutorial will let you add 2 classes, and an observer mode to your mod.   Remember that the classes will only work in multiplayer because of the way it assigns them.  Make sure you add in only the code which is blue and take out the code that is pink.

First, let's set up the variable around which our classes are based.  Open up g_local.h and add this integer into the respawn structure so it looks like this:

// client data that stays across deathmatch respawns
typedef struct
{
    client_persistant_t    coop_respawn;   // what to set client->pers to on a respawn
    int             enterframe;          
               // level.framenum the client entered the game
    int             score;              
                    // frags, etc
    vec3_t      cmd_angles;  
                      // angles sent over in the last command
    int             game_helpchanged;
    int             helpchanged;

   
int             class;                                   // added for the class variable
} client_respawn_t;

Next open up g_cmds.c and go down to the client command section, and add these two commands near the bottom, right after the wave command:

    else if (Q_stricmp (cmd, "wave") == 0)
        Cmd_Wave_f (ent);
    else if (Q_stricmp (cmd, "class1") == 0)
    {
            ent->client->resp.class = 1;
            EndObserverMode(ent);
    }
    else if (Q_stricmp (cmd, "class2") == 0)
    {
            ent->client->resp.class = 2;
            EndObserverMode(ent);
    }
    else if (Q_stricmp (cmd, "class") == 0)
    {
        if (ent->client->resp.class == 1)
            gi.cprintf(ent, PRINT_HIGH, "You are Class 1.\n");
        else if (ent->client->resp.class == 2)
            gi.cprintf(ent, PRINT_HIGH, "You are Class 2.\n");
        else
            gi.cprintf(ent, PRINT_HIGH, "You are an OBSERVER.\n");
    }

    else    // anything that doesn't match a command will be a chat
        Cmd_Say_f (ent, false, true);

Those will be the commands that clients enter to switch to a class from observer mode, or the switch classes midgame. It also adds a "check" command so a player can just type "class" and it will tell him/her what class they are and if they're an observer.

Next open up p_client.c and add this whole new section to the InitClientPersistant function.  It defines what weapons your classes will have.

/*
==============
InitClientPersistant

This is only called when the game first initializes in single player,
but is called after each death and level change in deathmatch
==============
*/
void InitClientPersistant (gclient_t *client)
{   
    if (client->resp.class == 1)
    {
        //Class 1
        gitem_t         *item;

        memset (&client->pers, 0, sizeof(client->pers));

        item = FindItem("Blaster");
        client->pers.selected_item = ITEM_INDEX(item);
        client->pers.inventory[client->pers.selected_item] = 1;

        item = FindItem("Jacket Armor");
        client->pers.selected_item = ITEM_INDEX(item);
        client->pers.inventory[client->pers.selected_item] = 25;

        item = FindItem("Rockets");
        client->pers.selected_item = ITEM_INDEX(item);
        client->pers.inventory[client->pers.selected_item] = 30;

        item = FindItem("Rocket Launcher");
        client->pers.selected_item = ITEM_INDEX(item);
        client->pers.inventory[client->pers.selected_item] = 1;

        client->pers.weapon = item;
    }
    else if (client->resp.class == 2)
    {
        //Class 2
        gitem_t         *item;

        memset (&client->pers, 0, sizeof(client->pers));

        item = FindItem("Blaster");
   
    client->pers.selected_item = ITEM_INDEX(item);
        client->pers.inventory[client->pers.selected_item] = 1;
        item = FindItem("Combat Armor");
        client->pers.selected_item = ITEM_INDEX(item);
   
    client->pers.inventory[client->pers.selected_item] = 50;
        item = FindItem("Slugs");
   
    client->pers.selected_item = ITEM_INDEX(item);
        client->pers.inventory[client->pers.selected_item] = 30;
   
    item = FindItem("Railgun");
        client->pers.selected_item = ITEM_INDEX(item);
   
    client->pers.inventory[client->pers.selected_item] = 1;

        client->pers.weapon = item;
    }
    else
    {
        //Observer mode, doesn't really matter what they have
        gitem_t         *item;

        memset (&client->pers, 0, sizeof(client->pers));
   
        item = FindItem("Combat Armor");
        client->pers.selected_item = ITEM_INDEX(item);
        client->pers.inventory[client->pers.selected_item] = 1;

        client->pers.weapon = item;
    }


    gitem_t         *item;

    memset (&client->pers, 0, sizeof(client->pers));

    item = FindItem("Blaster");
    client->pers.selected_item = ITEM_INDEX(item);
    client->pers.inventory[client->pers.selected_item] = 1;

    client->pers.weapon = item;


    client->pers.health             = 100;
    client->pers.max_health        = 100;

    client->pers.max_bullets    = 200;
    client->pers.max_shells        = 100;
    client->pers.max_rockets    = 50;
    client->pers.max_grenades    = 50;
    client->pers.max_cells        = 200;
    client->pers.max_slugs        = 50;

    client->pers.connected = true;
}

Ok, that was huge.  All it does is check to see which class they are based on the class variable, then it assigns the weapon, armor, items, or ammo when they spawn or respawn.  You can change what weapons a class starts with easily.  Just change the actual names of the items!  Make sure you have the correct spelling, else Quake2 will crash when you try to pick your class.  Next go back up to the player_die function, still in p_client.c, and add in one line like this:

/*
==================
player_die
==================
*/
void player_die (edict_t *self, edict_t *inflictor, edict_t *attacker, int damage, vec3_t point)
{
    int        n;

   
self->svflags &= ~SVF_NOCLIENT;

    VectorClear (self->avelocity);

    self->takedamage = DAMAGE_YES;
    self->movetype = MOVETYPE_TOSS;

    self->s.modelindex2 = 0;    // remove linked weapon model

    self->s.angles[0] = 0;
    self->s.angles[2] = 0;

    self->s.sound = 0;
    self->client->weapon_sound = 0;

    self->maxs[2] = -8;

    ...

All that does is make sure that a player isn't invisible when they switch to a class from observer mode.  You don't want invisible players running around, do you?
Next we need to do some serious work with ClientBeginDeathmatch.  Change it so it looks like this:

/*
=====================
ClientBeginDeathmatch

A client has just connected to the server in
deathmatch mode, so clear everything out before starting them.
=====================
*/
void ClientBeginDeathmatch (edict_t *ent)
{
    G_InitEdict (ent);

    InitClientResp (ent->client);

    // locate ent at a spawn point
    PutClientInServer (ent);

   
ent->client->ps.gunindex = 0;
    gi.linkentity (ent);


    if (level.intermissiontime)
    {
        MoveClientToIntermission (ent);
    }
    else
    {
        // send effect
        gi.WriteByte (svc_muzzleflash);
        gi.WriteShort (ent-g_edicts);
        gi.WriteByte (MZ_LOGIN);
        gi.multicast (ent->s.origin, MULTICAST_PVS);
    }

    gi.bprintf (PRINT_HIGH, "%s entered the game\n", ent->client->pers.netname);


    // make sure all view stuff is valid
    ClientEndServerFrame (ent);
}

Next add this function directly above ClientBeginDeathmatch:

void EndObserverMode(edict_t* ent)
{
    ent->movetype &= ~MOVETYPE_NOCLIP;
    ent->solid &= ~SOLID_NOT;
    ent->svflags &= ~SVF_NOCLIENT;

    PutClientInServer (ent);

    if (level.intermissiontime)
    {
        MoveClientToIntermission (ent);
    }
    else
    {
        // send effect
        gi.WriteByte (svc_muzzleflash);
        gi.WriteShort (ent-g_edicts);
        gi.WriteByte (MZ_LOGIN);
        gi.multicast (ent->s.origin, MULTICAST_PVS);
    }

    if (ent->client->resp.class == 1)
        gi.bprintf (PRINT_HIGH, "%s is Class 2\n", ent->client->pers.netname);

    else if (ent->client->resp.class == 2)
        gi.bprintf (PRINT_HIGH, "%s is Class 1\n", ent->client->pers.netname);

}

This is basically the same as the original ClientBeginDeathmatch.  When a player enters the game it now says nothing until they pick a class.  It's a more reliable way of doing this, and works much better than the original way of killing yourself and losing frags.  This is how the *good* mods do it.

Ok, just one more section needs to be changed.  Go to the client think function, also in p_client.c.  Add in the section as shown:

/*
==============
ClientThink

This will be called once for each client frame, which will
usually be a couple times for each server frame.
==============
*/
void ClientThink (edict_t *ent, usercmd_t *ucmd)
{
    gclient_t    *client;
    edict_t    *other;
    int        i, j;
    pmove_t    pm;

    level.current_entity = ent;
    client = ent->client;

    if (level.intermissiontime)
    {
        client->ps.pmove.pm_type = PM_FREEZE;
        // can exit intermission after five seconds
        if (level.time > level.intermissiontime + 5.0
            && (ucmd->buttons & BUTTON_ANY) )
            level.exitintermission = true;
        return;
    }

    pm_passent = ent;

    // set up for pmove
    memset (&pm, 0, sizeof(pm));

    if (ent->movetype == MOVETYPE_NOCLIP)
        client->ps.pmove.pm_type = PM_SPECTATOR;
    else if (ent->s.modelindex != 255)
        client->ps.pmove.pm_type = PM_GIB;
    else if (ent->deadflag)
        client->ps.pmove.pm_type = PM_DEAD;
    else
        client->ps.pmove.pm_type = PM_NORMAL;

    client->ps.pmove.gravity = sv_gravity->value;

    if (ent->client->resp.class < 1)
    {
        ent->solid = SOLID_NOT;
        ent->movetype = MOVETYPE_NOCLIP;
        ent->svflags |= SVF_NOCLIENT;
    }

    pm.s = client->ps.pmove;

    for (i=0 ; i<3 ; i++)
    {
        pm.s.origin[i] = ent->s.origin[i]*8;
        pm.s.velocity[i] = ent->velocity[i]*8;
    }

    if (memcmp(&client->old_pmove, &pm.s, sizeof(pm.s)))
    {
        pm.snapinitial = true;
//        gi.dprintf ("pmove changed!\n");
    }

    ...

You're done!  This last section added in some checks to see if they are an observer.  If they are, then they can move freely around the map, are invisble, and are invulnerable.  A player is in observer mode automatically upon entering the game, and must type either class1 or class2 in the console to switch to a class.  It's very easy to add more classes this way, just extend the else if statements in g_cmds.c and p_client.c. 

Now, if you want to make it so a class can't pick up other weapons, and only has what it starts with, you can prevent any weapons from spawning on the maps.  This is very easy to do, it just consists of telling the game to spawn ammo in the place of the weapons.  Just open up g_items.c and go to the spawnitem function.  Add in this big chunk of code:

/*
============
SpawnItem

Sets the clipping size and plants the object on the floor.

Items can't be immediately dropped to floor, because they might
be on an entity that hasn't spawned yet.
============
*/
void SpawnItem (edict_t *ent, gitem_t *item)
{
   
if (deathmatch->value)
    {
        if (strcmp(ent->classname, "weapon_shotgun") == 0)
        {
            ent->classname = "ammo_shells";
            item = FindItemByClassname ("ammo_shells");
        }

       
if (strcmp(ent->classname, "weapon_supershotgun") == 0)
        {
            ent->classname = "ammo_shells";
            item = FindItemByClassname ("ammo_shells");
        }

       
if (strcmp(ent->classname, "weapon_machinegun") == 0)
        {
            ent->classname = "ammo_bullets";
            item = FindItemByClassname ("ammo_bullets");
        }

       
if (strcmp(ent->classname, "weapon_chaingun") == 0)
        {
            ent->classname = "ammo_bullets";
            item = FindItemByClassname ("ammo_bullets");
        }

       
if (strcmp(ent->classname, "weapon_grenadelauncher") == 0)
        {
            ent->classname = "ammo_grenades";
            item = FindItemByClassname ("ammo_grenades");
        }

       
if (strcmp(ent->classname, "weapon_rocketlauncher") == 0)
        {
            ent->classname = "ammo_rockets";
            item = FindItemByClassname ("ammo_rockets");
        }

       
if (strcmp(ent->classname, "weapon_railgun") == 0)
        {
            ent->classname = "ammo_slugs";
            item = FindItemByClassname ("ammo_slugs");
        }
   
       
if (strcmp(ent->classname, "weapon_hyperblaster") == 0)
        {
            ent->classname = "ammo_cells";
            item = FindItemByClassname ("ammo_cells");
        }

       
if (strcmp(ent->classname, "weapon_bfg") == 0)
        {
            ent->classname = "ammo_cells";
            item = FindItemByClassname ("ammo_cells");
        }
    }

    PrecacheItem (item);

    if (ent->spawnflags)
    {
        if (strcmp(ent->classname, "key_power_cube") != 0)
        {
            ent->spawnflags = 0;
            gi.dprintf("%s at %s has invalid spawnflags set\n", ent->classname, vtos(ent->s.origin));
        }
    }

    ...

All this large section does is change what spawns on the maps.  If the map has a spawn point for the weapon, the code tells it to place the appropriate ammo there instead.  This will work for all items, so if you don't want quads spawning or something else, just change it making sure you have the correct names from the item list further down in the g_items.c file.

Now you need to make it so when a player dies, he doesn't drop his weapon.  This one is really easy, just open up p_client.c and go down to the player_die function.  Remove the one marked line as shown:

/*
==================
player_die
==================
*/
void player_die (edict_t *self, edict_t *inflictor, edict_t *attacker, int damage, vec3_t point)
{
    int        n;

    VectorClear (self->avelocity);

    self->takedamage = DAMAGE_YES;
    self->movetype = MOVETYPE_TOSS;

    self->s.modelindex2 = 0;    // remove linked weapon model

    self->s.angles[0] = 0;
    self->s.angles[2] = 0;

    self->s.sound = 0;
    self->client->weapon_sound = 0;

    self->maxs[2] = -8;

//    self->solid = SOLID_NOT;
    self->svflags |= SVF_DEADMONSTER;

    if (!self->deadflag)
    {
        self->client->respawn_time = level.time + 1.0;
        LookAtKiller (self, inflictor, attacker);
        self->client->ps.pmove.pm_type = PM_DEAD;
        ClientObituary (self, inflictor, attacker);
       
TossClientWeapon (self);
        if (deathmatch->value)
            Cmd_Help_f (self);         // show scores
    }

    // remove powerups
    self->client->quad_framenum = 0;
    self->client->invincible_framenum = 0;
    self->client->breather_framenum = 0;
    self->client->enviro_framenum = 0;

    ...

That's it.  Now no weapons spawn and no players drop weapons, your class based mod is complete.

Tutorial by Willi
Back to
Quake Style - Tutorials