Some time last year, someone passed on to me a copy of a very unusual demo. It demonstrates a bug in a Doom deathmatch game on a particular PWAD. The text file supplied with the demo describes the bug:
In the demo Toke and I [Waldon] play a 1on1 game, and after his first frag I can't respawn but my corpse is running around the map. My health is zero and I have no weapons (like you can expect from a corpse in doom) but I can move and open doors. I was unable to respawn. From Toke's point of view you can see the moving corpse.
Essentially, what happens is that player 1 dies, and when he tries to respawn his health and motion are restored but his person is not. Instead the corpse scurries around the floor in response to the player's actions - but cannot shoot or be shot at.
You won't see this bug at any of the original Doom levels, so it's not
hard to guess that there's something strange about the level which causes, or
contributes to the bug. The PWAD is vex6d.wad
, a ripoff of
Toke's Vexation Doom2 deathmatch wad.
At a first glance it is looks like a very straightforward DM level, with a
roughly square playing area divided up into some open yards, and 4 deathmatch
starts at the corners.
The first odd thing is shown by the demo itself - player 1 dies several
times but he respawns in the middle of one of the yards, not at any DM start.
A quick look in an editor shows that he's respawning at the player 1
single-player start. So is there something wrong with the DM starts? Yes -
Doom reckons the starts are placed too close to the corner walls, so it
refuses to spawn a player there because the start is blocked. Doom contains a
fallback in case all the DM starts are blocked - it uses the single/coop
start for that player instead and takes a chance that the spot is vacant
(cf g_game.c:G_DeathMatchSpawnPlayer
).
But in the demo the players do start correctly at the DM starts initially.
Reading the source, it turns out that Doom trusts the level author when it
first spawns the DM players, and doesn't do the full check to see whether the
DM start is blocked (it just checks that it hasn't already spawned a DM
player on that spot - cf g:game.c:G_CheckSpot
). So when the
level first loads both players start on DM starts, but it refuses to use the
DM starts after that.
But if the DM starts are considered too close to the walls, how can the players move when they are first spawned? That I don't know. I know the starts are just touching the wall, so it may be a rounding error or boundary condition in Doom's collision detection code. But that's not relevant to the bug so I leave it for a separate investigation.
Player 1 dies and respawns 3 or 4 times at the start of the demo. The bug occurs when player 2 is first fragged. Since the DM starts are blocked, Doom tries to use the player 2 cooperative start. There is no such start in the level.
So having established everything leading to the bug, how does Doom end up with a corpse running around? At this point I had to trace the code itself and run the demo and the engine (well, linuxdoom-1.10 which is close enough to the original) under a debugger.
Doom keeps a record of the cooperative starts, so that it can respawn
players when necessary without having to reread the whole list of THINGS for
the level. This array (g_game.c:playerstarts[]
) is global and
not initialised by Doom, so it will usually be initialised to all zeroes by
the protected memory manager. When Doom tries to respawn player 2, it does so
by trying to respawn the second cooperative start (which creates a new player
2 object to replace the old one). However, in this case
playerstarts[1]
is not a player 2 start - as all the fields are
zero, Doom will treat it as a thing of type 0 at (0,0).
So the chain of events is this. Player 2 tries to respawn. Doom marks the
player as respawning (state PST_REBORN
). It finds no DM start,
so it tries instead to respawn the non-existent cooperative player 2 start.
Creating a new object of type 0 has no effect on player 2 (in fact, Doom gets
confused by an object of type 0 and starts writing to memory before the start
of the players[]
array, but that doesn't seem to affect
anything). It then believes that it has finished respawning player 2.
The end result is that player 2 has entered a reborn state, but the monster object representing the player in the game has not been replaced, so the player's embodiment in the game is the same object as before the attempted respawn - the player 2 corpse. As the player is no longer flagged as dead, Doom allows it to move around and won't allow the player to respawn again, but it's still a corpse so cannot shoot or be shot at, and remains at floor height.
The demo, PWAD and text file are available here. Thanks to Waldon and Toke.
This research and document by Colin Phipps. Written 2002/08/26.