Missing items and the restart command

Quake doesn’t give mappers direct control over the items a player starts with. If you want the player to have a nailgun from the moment the level starts, overlapping the weapon with the spawn point seems like a good idea. Most of the time the player picks up the weapon immediately and all is well, but a bug occurs if the player uses the restart command to load the level from the beginning – the item falls out of the level! This article takes a brief look at why, and then suggests how to modify the QuakeC code to work around it.

I admit up front that I haven’t dug into the engine-side reasons that the map command behaves differently to the restart command. The code paths depend on lots of interaction between the map loading code, and the client-server handshake (which happens even in a single player game). What can be observed by experimenting in QuakeC is that when you run the map command the player is physically added to the server on frame 4. In contrast, the player is added on frame 2 when the restart command is used.

What a difference two frames make! The items drop to the floor on frame 2 to allow func_plat or similar entities spawn first. If the player is loaded on frame 2 as well, then that will happen before the items are dropped. Now the item is inside a solid object (the player) when it is dropped to the floor, so it attempts to drop through the obstruction, causing it to fall out of the level entirely. Of course, if the player is loaded on frame 4, then the spawn point is empty at the point that the item drops (and the player isn’t troubled by sharing with an item).

With the problem identified, one solution we can implement in QuakeC is to delay adding the player until frame 4. The player is added in the function PutClientInServer, and so my first thought was to detect when this function runs too early, and delay it until frame 4 using the player’s think function. The trouble is that PutClientInServer does lots of other things, like setting the player’s health, weapon and spawn location, and it’s very noticeable if you delay that. In particular if you run restart from the console, then when the map loads the game will be paused on frame 2 with the console still down, and without PutClientInServer the player’s first view of the map is dead, tilted sideways, in some random point in the void.

So what we need to do is carve this function into two pieces. Keep the majority of PutClientInServer as normal, so the player has a health value and is correctly located. Separate out the parts which make the player physically solid into a new function called MakeClientSolid, and make sure that function doesn’t run until frame 4. Here’s what I came up with

void() MakeClientSolid =
	if(framecount < 4)
	// if the player has a movetype but isn't solid then the engine puts out
	// "player is stuck" debug messages, so we set movetype in this function
	self.movetype = MOVETYPE_WALK;
	self.solid = SOLID_SLIDEBOX;

	setsize (self, VEC_HULL_MIN, VEC_HULL_MAX);
	// the telefrag effect is intended to prevent players getting stuck so
	// we must wait until the player is able to get stuck before running it
	if (deathmatch || coop)
		spawn_tfog (self.origin + v_forward*20);
	spawn_tdeath (self.origin, self);

void() PutClientInServer =
	local	entity spot;

	spot = SelectSpawnPoint ();

	self.classname = "player";
	self.health = 100;
	self.takedamage = DAMAGE_AIM;
	self.show_hostile = 0;
	self.max_health = 100;
	self.flags = FL_CLIENT;
	self.air_finished = time + 12;
	self.dmg = 2;   		// initial water damage
	self.super_damage_finished = 0;
	self.radsuit_finished = 0;
	self.invisible_finished = 0;
	self.invincible_finished = 0;
	self.effects = 0;
	self.invincible_time = 0;

	DecodeLevelParms ();
	W_SetCurrentAmmo ();

	self.attack_finished = time;
	self.th_pain = player_pain;
	self.th_die = PlayerDie;
	self.deadflag = DEAD_NO;
// paustime is set by teleporters to keep the player from moving a while
	self.pausetime = 0;
//	spot = SelectSpawnPoint ();

	self.origin = spot.origin + '0 0 1';
	self.angles = spot.angles;
	self.fixangle = TRUE;		// turn this way immediately

// oh, this is a hack!
	setmodel (self, "progs/eyes.mdl");
	modelindex_eyes = self.modelindex;

	setmodel (self, "progs/player.mdl");
	modelindex_player = self.modelindex;
	self.view_ofs = '0 0 22';

	player_stand1 ();

You might have noticed that the above code doesn’t ever revisit the MakeClientSolid function if we skip it because it ran too soon. We could set it as a think function, but something might change that player’s think function before it had a chance to run – for example taking damage and starting a pain animation, or attacking with a weapon before frame 4. Then we’ve introduced a far worse bug than before, the player would be entirely unable to move, rather than just dropping an item. The way to ensure it runs is to add it to PlayerPreThink instead. Add the highlighted lines as follows:

void() PlayerPreThink =
	if (intermission_running)
		IntermissionThink ();	// otherwise a button could be missed between
		return;					// the think tics

	if (self.view_ofs == '0 0 0')
		return;		// intermission or finale

The code here doesn’t bother testing whether framecount has hit 4 yet, the check inside MakeClientSolid still handles that fine. We’re also testing if movetype is set, this was an arbitrary choice of one of the things set in MakeClientSolid. You might worry that the player has no movetype in the intermission, which is why the call must be below all the intermission code. Compile all this up, and another bug should be fixed.


4 thoughts on “Missing items and the restart command

  1. Interesting. Is it frame 4 in all enignes or just certain ones?

    Learned a long while back that during Quake C’s WorldSpawn () funct, there are no map entities loaded at that stage and the only entities that are available are whatever number of player slots are available as next ent from world.

    I always thought it depended on the engine and or ticrate to figure out when the map specific entities start to spawn.

  2. Well, in theory any engine might change when a player joins – maybe some even alter “restart” to behave like the other commands do. Not to mention that in co-op and deathmatch players often join at later points. The article doesn’t make this point, but the important thing that items always drop on frame 2. So as long as you delay MakeClientSolid longer than that you won’t experience this particular bug, regardless of which frame the engine adds the player. We could have tried using frame 3 instead, but making it 4 frames to line up with standard engines may help mitigate other bugs.

  3. Since we’re changing the QuakeC anyway, i’d prefer to use a solution where – for example – you would just add an “items” field to worldspawn, to dictate what the player starts with.

    • This is also a good point, although there may be other effects that we can make more consistent through this fix.

      Food for thought: should the items field be read from the spawnpoint entity instead of the worldspawn? This would allow you to vary the starting load-out on different skills using spawnflags.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s