TUTORIAL 39 - Server only InstaGib
by Steven Conway
Well in the original tutorial, Malcolm Lim presented the bare bones of what to do to
remove all of the items without breaking the bot code. In this
tutorial, I will show you how to break the bot code, make the whole mod server side only,
make sure that we can still play CTF, and then fix up the bot code. I will be assuming that
we all read the first tutorial.
As it stands at the moment, this is railgun only gameplay. Since health counts
down from 125, the first hit on a just spawned player won't be fatal. To remedy this,
you'll need to add section 3 of Malcolm's tutorial.
Files that will change:
g_client.c
g_items.c
g_combat.c
ai_dmq3.c
ai_main.c
1. G_CLIENT.C
As we already know Malcolm gave us all 999 slugs to go play with. This is the
maximum amount that the client can display (it's limited to 3 digits), hence the reason why
people like to use 999 slugs. So another way of doing this is the following...
void ClientSpawn(gentity_t *ent) {
*** Code Snipped ***
client->ps.stats[STAT_WEAPONS] |= ( 1 << WP_GAUNTLET );
client->ps.ammo[WP_GAUNTLET] = -1;
client->ps.ammo[WP_GRAPPLING_HOOK] = -1;
//SCO
client->ps.stats[STAT_WEAPONS] = (1 << WP_RAILGUN );
client->ps.ammo[WP_RAILGUN] = INFINITE;
//end SCO
// health will count down towards max_health
*** Code Snipped ***
As Malcolm said, using -1 breaks the bot code (but this is an easy fix)
and more annoyingly clients would have the "LOW AMMO" message printed on their
display permanently. Getting rid of the "LOW AMMO" message would
require a client modification, so thats out of the question for a server side mod.
If you set it to 999 the client will still predict taking
away ammo for every shot, and this will be displayed. I don't like this
method since the client will display some changes in the railgun ammo.
My method is to use a number that is big enough so that it doesn't overload in memory,
and won't display any ammo changes. The bonus with this method is that all clients will
see that they have 999 slugs, but they will actually have 1,000,000 (one million)
slugs. Now if any one can use all of them, without dying they really need help since
it will take them over 17 days straight to run out of ammo.
2. G_ITEMS.C
Well Malcolm did suggest there was room for improvement, so here it is. In the
function Touch_Item() touching items was prevented by just returning.
But what it we touched a flag like in CTF? Well a simple check for the type of object
will fix this.
void Touch_Item (gentity_t *ent, gentity_t *other, trace_t *trace) {
int respawn;
qboolean predict;
//SCO if ent-item is some sort of team item.
if (ent->item->giType != IT_TEAM)
return;
if (!other->client)
return;
***Code Snipped ***
Well that was easy enough, but what about when the items spawn? Now Malcolm hasn't
spawned CTF flags for us to pick up, so let's check this bit of code. In
G_SpawnItem() the spawning of the items was disabled, but they were also registered
too. i.e. when you load the map it loaded the plasma gun, health, and all the rest.
We'll fix this too.
void G_SpawnItem (gentity_t *ent, gitem_t *item) {
G_SpawnFloat( "random", "0", &ent->random );
G_SpawnFloat( "wait", "0", &ent->wait );
//SCO
//If it's a team item then we want to spwan it. Otherwise make it invisible.
if (item->giType == IT_TEAM) {
RegisterItem( item );
if ( G_ItemDisabled(item) )
return;
}
else {
//Ahh what does this do then
ent->r.svFlags = SVF_NOCLIENT;
//setting this flag makes the item invisible.
ent->s.eFlags |= EF_NODRAW;
}
ent->item = item;
*** Code snipped ***
Well it's pretty self explanatory with the comments. But what's the chop with
this ent->r.svFlags = SVF_NOCLIENT? If we do a search for it we find this in g_public.h...
// entity->svFlags
// the server does not know how to interpret most of the values
// in entityStates (level eType), so the game must explicitly flag
// special server behaviors
// don't send entity to clients,
//even if it has effects
//bots also can't see this item
#define SVF_NOCLIENT 0x00000001
// set if the entity is a bot
#define SVF_BOT 0x00000008
// send to all connected clients
#define SVF_BROADCAST 0x00000020
// merge a second pvs at origin2 into snapshots
#define SVF_PORTAL 0x00000040
// entity->r.currentOrigin instead of entity->s.origin
// for link position (missiles and movers)
#define SVF_USE_CURRENT_ORIGIN 0x00000080
// only send to a single client
#define SVF_SINGLECLIENT 0x00000100
I've moved the comments so that they take up less line width.
Ahhh! So when we set SVF_NOCLIENT, all of the clients won't even know the item
exists. It also means that this item is not sent to the clients for item
prediction. As an added "bonus" of doing this, we've managed to break the bot code.
What a great excuse to do some bot coding.
Now we head over to ClearRegisteredItems to register the railgun when the map
loads. We do this to just tidy up the mod.
void ClearRegisteredItems( void ) {
memset( itemRegistered, 0, sizeof( itemRegistered ) );
// players always start with the base weapon
//We don't want the machinegun or the Gauntlet
//RegisterItem( BG_FindItemForWeapon( WP_MACHINEGUN ) );
//RegisterItem( BG_FindItemForWeapon( WP_GAUNTLET ) );
//register that rail gun
RegisterItem( BG_FindItemForWeapon( WP_RAILGUN ) );
*** Code Snipped ***
That will register the rail gun and also remove the machinegun and the
gauntlet from being registered.
Now remember those powerups that are in normal Quake3? They don't get
spawned with all the other items when the map starts. They are deferred a little, and are spawned
about 45 seconds into the game. With the changes made so far the
powerups will still respawn later in the game. So lets head on over to the FinishSpawningItem()
function to fix this problem.
void FinishSpawningItem( gentity_t *ent ) {
*** Code Snipped ***
// team slaves and targeted items aren't present at start
if ( ( ent->flags & FL_TEAMSLAVE ) || ent->targetname ) {
ent->s.eFlags |= EF_NODRAW;
ent->r.contents = 0;
return;
}
//SCO We don't want the powerups to be spawning later in the game.
/*
// powerups don't spawn in for a while
if ( ent->item->giType == IT_POWERUP ) {
float respawn;
respawn = 45 + crandom() * 15;
ent->s.eFlags |= EF_NODRAW;
ent->r.contents = 0;
ent->nextthink = level.time + respawn * 1000;
ent->think = RespawnItem;
return;
}*/
//end SCO
trap_LinkEntity (ent);
}
By commenting out this statement you will be making sure that the powerups
don't respawn later on in the game.
Now we can finally save g_items.c and move onto the next file.
3. G_COMBAT.C
Just to keep things clean on the map, we're going to remove all dropping of weapons.
So now we have to look at the TossClientItems() function.
void TossClientItems( gentity_t *self ) {
gitem_t *item;
//SCO int weapon;
float angle;
int i;
gentity_t *drop;
//SCO commented out so that we don't drop any weapons
/*
// drop the weapon if not a gauntlet or machinegun
weapon = self->s.weapon;
*** Code Snipped ***
// spawn the item
Drop_Item( self, item, 0 );
}
*/
// drop all the powerups if not in teamplay
Since we're not spawning powerups, the only items that will be dropped are the CTF flags.
Summary of what we have done so far:
- made the CTF flags touchable, and left it predictable on the client,
- ensured that weapons aren't dropped,
- made all modifications server side only,
- managed to break the bot code.
4. AI_MAIN.C
Now it's time to fix up that bot code. If compiled and ran at the moment, the bots would
stand still (until they see you).
Let's start by thinking what the bot code is trying to achieve. It is trying to
provide similar processes to what a human would do without accessing information that a normal player wouldn't have (i.e. health of other clients,
exact location of players and items such as CTF flags). Knowing such information
would be classed as cheating.
An example of how the bot reacts like a human would be the respawn sound of a
powerup. If we (as humans) hear the powerup respawn sound we head for the powerup (generally).
We can see this in the bot code, pretty much exactly the same way a human gets this
information. It comes through events (specifically in this case EV_GLOBAL_SOUND). The client
receives this information and plays the appropriate sound, and we have the intelligence
(cough...) to make the decision whether or not to go and get the powerup.
Bots do exactly the same thing. Have a look in
ai_q3dm.c in the BotCheckEvents() function. It shows us that for most of the
events that are broadcasted to clients (such as EV_GLOBAL_SOUND) the bot will do
something. This can be seen from the extract from the code...
case EV_GLOBAL_SOUND:
{
*** Code Snipped ***
//Was the sound the powerup respawn sound
if (!strcmp(buf, "sound/items/poweruprespawn.wav")) {
//powerup respawned... go get it
BotGoForPowerups(bs);
}
break;
*** Code Snipped ***
The same goes for items spread around the map. Even when they're waiting to respawn, we
still know where they are. But if the item (or rather the entity) had the
SVF_NOCLIENT bit set, then the item never appears to the client. So the bots should
have the same sort of thinking and not go for these items.
So in a nut shell, the bots simply don't move because they don't see an item
that can be used as an objective. This is why the bots don't move until they
spot another player to kill.
So we do a little search for SVF_NOCLIENT, and we find two instances of it
in the bot code. They both appear in ai_main.c. So let's see what's happening
in BotAI_GetEntityState():
int BotAI_GetEntityState( int entityNum, entityState_t *state ) {
gentity_t *ent;
ent = &g_entities[entityNum];
memset( state, 0, sizeof(entityState_t) );
if (!ent->inuse) return qfalse;
if (!ent->r.linked) return qfalse;
//if (ent->r.svFlags & SVF_NOCLIENT) return qfalse;//Comment out this line
memcpy( state, &ent->s, sizeof(entityState_t) );
return qtrue;
}
Why did we comment out that line for? Because the bot thinks that the
item/entity is not there so it returns qfalse, which means that the item has
isn't on the map. But we still want the bot to move around so it will
now try and use these items as objectives.
The second one is found in BotAIStartFrame():
int BotAIStartFrame(int time) {
*** Code snipped ***
trap_BotLibUpdateEntity(i, NULL);
continue;
}
//SCO
/*if (ent->r.svFlags & SVF_NOCLIENT) {
trap_BotLibUpdateEntity(i, NULL);
continue;
}*/ //comment out the entire if statement
// do not update missiles
if (ent->s.eType == ET_MISSILE) {
*** Code snipped ***
Why did we do that one as well? The bots keep all the information about all
of the items in what is known as the bot library. Just like humans, we know
where all the items are in the map, and we know how many players that are
playing etc.. We also can remember that when we see an item and if its not there we
would move on. In the bots case, if the SVF_NOCLIENT bit is set, then the entity is removed from
the library permanently.
5. Extra Bot Coding
The following is optional, but it will make your bot code a little more
efficient. BotChooseWeapon() is of course a function that could be edited, since
we only have one weapon. We could get rid of it entirely, but I have chosen not
to since I want to demonstrate something. If you've ever seen the message
"ERROR: Weapon out of range", then this function where this message occurs.
You've probably done a search for the error message and couldn't find it. Well
the error message is actually sent from the executable (quake3.exe). So to
eliminate this problem we would do the following.
void BotChooseWeapon(bot_state_t *bs) {
int newweaponnum;
if (bs->cur_ps.weaponstate == WEAPON_RAISING ||
bs->cur_ps.weaponstate == WEAPON_DROPPING) {
trap_EA_SelectWeapon(bs->client, bs->weaponnum);
}
else {
//SCO we want to comment out this line since we only have the railgun
//newweaponnum = trap_BotChooseBestFightWeapon(bs->ws, bs->inventory);
newweaponnum = WP_RAILGUN; //We only have one weapon so let's choose it.
if (bs->weaponnum != newweaponnum) bs->weaponchange_time = FloatTime();
bs->weaponnum = newweaponnum;
//BotAI_Print(PRT_MESSAGE, "bs->weaponnum = %d\n", bs->weaponnum);
trap_EA_SelectWeapon(bs->client, bs->weaponnum);
}
}
You shouldn't get the error message any more.
Now a lot of the bot code could be cut back quite drastically since we only
have one weapon, unlimited ammo, and no real need for health and armor. The
following functions are examples of what I have done to optimize the code. Even
if they don't make any kind of a speed difference, they're useful for when you're
trying to teach bots about new weapons.
I have choosen to edit the BotAggression() function since it is called in all of the
major decision making parts of the code, and is therefore pretty important. I've edited mine to
look like this. It is a complete replacement of the original BotAggression function.
float BotAggression(bot_state_t *bs) {
//if the enemy is located way higher than the bot
if (bs->inventory[ENEMY_HEIGHT] > 200) return 0;
return 100;
}
The BotAggression() function was full of if statements checking health, weapons
ammo and the Quad. So as I mentioned before it is cut back quite drastically. I
suggest commenting out iD's code instead of deleting it.
Another function that can be cut back would be BotSelectActivateWeapon. Here's what I
think it should do...
int BotSelectActivateWeapon(bot_state_t *bs) {
return WEAPONINDEX_RAILGUN;
}
There are many other functions that are easily edited and concern weapons.
Some functions that could undergo some modification are...
- BotHasPersistantPowerupAndWeapon,
- BotFellingBad (this function is only used in a Team Arena Mod),
- BotCheckAttack (Removing any signs of Radial damage weapons),
- BotAimAtEnemy (Only if you feel adventurous).
Editing most of these functions will make the bot code more efficient rather
than make the bots harder.
6. Conclusion:
Well that moves the whole instagib mod into a server only version. I hope you learnt
something new, and even implement some of the features in your mod. If you
do use any of this code please give credit to me (Steven Conway a.k.a Coners)
and Malcolm Lim (a.k.a CyberKewl).
If you have any problems about this
tutorial contact me at niq3@planetquake.com,
Oh, and don't forget to checkout my mod NIQ3.
|