Entropic Thoughts

The Mystery of the Deterministic Super Shotgun

The Mystery of the Deterministic Super Shotgun

Okay, I've caught a cold. If I try to move, snot flies everywhere. My brain is working at 5% capacity and I need to entertain myself somehow, so here you go:

In a recent discussion, the case of the Doom random number generation popped up. I casually mentioned what happens when you exchange the random numbers for all zeroes or all 255s:

What does it play like? I tried two values, 0x00 and 0xFF. With either value, the screen “melt” effect that is used at the end of levels is replaced with a level vertical wipe: the randomness was used to offset each column. Monsters do not make different death noises at different times; only one is played for each category of monster.

The bullet-based (hitscan) weapons have no spread at all: the shotgun becomes like a sniper rifle, and the chain-gun is likewise always true. You’d think this would make the super-shotgun a pretty lethal weapon, but it seems to have been nerfed: the spread pattern is integral to its function.

Someone wanted an explanation of what "seems to have been nerfed" meant. I had no idea, but I got curious myself and I happened to know the Doom source code is publically available, so I set out on a quest to find out why the super shotgun needs the spread to do solid damage.

Being Lost

It was years since I last played Doom. I've never seen the code base before. Beginner programmers often ask how one navigates a completely foreign code base. Perhaps this can serve as a rough guide, or at least a case study.

I started by just poking my head into different files to see if I could get a sense for which abbreviations meant what, and how everything fit together. Some seemed less relevant than others (s_sound.c) while others more so (p_enemy.c?)

In a happy accident, I found a good place to start digging deeper. As I quickly scrolled through g_game.c my eyes got stuck on one declaration: int mousebfire;. That must be related to firing your weapon.

Seeing the Light

Sure enough. That declaration is used later in the same file:

 if (gamekeydown[key_fire]
    || mousebuttons[mousebfire]
    || joybuttons[joybfire])
    cmd->buttons |= BT_ATTACK;

This was found in a procedure that started with

// Builds a ticcmd from all of the available inputs
// or reads it from the demo buffer.
// If recording a demo, write it out
void G_BuildTiccmd(ticcmd_t* cmd) {

At that point, I could only assume a ticcmd is a data structure that contains all the actions a player can perform in a single frame (or tick) which are then to be executed by the game engine (and perhaps sent over to other players in a multiplayer session.)

One sneaky benefit of building a ticcmd from the inputs instead of converting the inputs directly to game actions is that you can build ticcmds from pretty much anything (such as network data or a previously recorded demo file) and then from that point on use the same code as if the player was actively playing those characters affected.

In either case, this BT_ATTACK constant seemed like a good thing to search for in all files. Whatever code handles the shooting bit of a ticcmd would probably also mention the BT_ATTACK constant. Searching* gives a few results:

./p_pspr.c:315:    if (player->cmd.buttons & BT_ATTACK)
./p_pspr.c:350:    if ( (player->cmd.buttons & BT_ATTACK)
./wi_stuff.c:1480:          if (player->cmd.buttons & BT_ATTACK)
./g_game.c:332: cmd->buttons |= BT_ATTACK;
./d_event.h:75:    BT_ATTACK            = 1,

By taking a quick peek into d_event.h one discovers that that is the header file that defines, among other things, the value of the BT_ATTACK constant. wi_stuff.c appears to handle acceleration or otherwise movement physics – probably not relevant to us. g_game.c is the occurrence that started this search.

The two remaining hits sound interesting though! Both of them lead pretty quickly to a P_FireWeapon(player) procedure, which is our next target. It's a very short method:

void P_FireWeapon(player_t* player) {
    statenum_t newstate;

    if (!P_CheckAmmo(player))
        return;

    P_SetMobjState(player->mo, S_PLAY_ATK1);
    newstate = weaponinfo[player->readyweapon].atkstate;
    P_SetPsprite(player, ps_weapon, newstate);
    P_NoiseAlert(player->mo, player->mo);
}

Most of this is actually irrelevant to the problem at hand, though. The ammo check doesn't matter to us. The last three lines in the function are just concerned with graphics and monster AI (NoiseAlert is the thing that makes monsters turn onto you when you make sound – something I found out when I was just randomly checking into files at the start of this.)

So the only thing that might be relevant here is the line that says

P_SetMobjState(player->mo, S_PLAY_ATK1);

and even so, that isn't actually executing an attack, it's just setting some sort of attack state on the character object. So let's search all files for that attack state!

./p_pspr.c:253:    P_SetMobjState (player->mo, S_PLAY_ATK1);
./p_pspr.c:290:    if (player->mo->state == &states[S_PLAY_ATK1]
./info.c:290:    {SPR_PLAY,4,12,{NULL},S_PLAY,0,0},     // S_PLAY_ATK1
./info.c:291:    {SPR_PLAY,32773,6,{NULL},S_PLAY_ATK1,0,0},     // S_PLAY_ATK2
./info.c:1120:  S_PLAY_ATK1,            // missilestate
./f_finale.c:418:       if (caststate == &states[S_PLAY_ATK1])
./f_finale.c:427:         case S_PLAY_ATK1:     sfx = sfx_dshtgn; break;
./info.h:330:    S_PLAY_ATK1,

I'm not particularly interested in f_finale.c, because that has something to do with when you complete a level… or the game… or something like that. It has nothing to do with shooting and causing damage.

Unfortunately, the info.h and info.c files are also not particularly interesting, because they only contain definitions of animations and such, from what I gather. So the only really relevant hit is the second one in p_pspr.c – the file we just were in!

Here's the bummer:

// get out of attack state
if (player->mo->state == &states[S_PLAY_ATK1]
    || player->mo->state == &states[S_PLAY_ATK2] ) {
    P_SetMobjState (player->mo, S_PLAY);
}

That code is irrelevant too.

Lost Again

This probably means that some of the games functions are cycled through by a proper state machine. The info.c definitions might very well specify how states transition into each other, and how attacking somehow happens when the player is in the S_PLAY_ATK1 state… but I'm not going to put in the time and energy into understanding what all the arrays and numbers and constants mean in those definitions.

So at this point, I almost gave up.

Almost.

Hipshot

As a last-ditch effort, I thought I'd search through all files for S_PLAY_ATK2, the other attack state that is mentioned in a few places.


./p_pspr.c:291: || player->mo->state == &states[S_PLAY_ATK2] )
./p_pspr.c:453:    P_SetMobjState (player->mo, S_PLAY_ATK2);
./p_pspr.c:653:    P_SetMobjState (player->mo, S_PLAY_ATK2);
./p_pspr.c:676:    P_SetMobjState (player->mo, S_PLAY_ATK2);
./p_pspr.c:706:    P_SetMobjState (player->mo, S_PLAY_ATK2);
./p_pspr.c:742:    P_SetMobjState (player->mo, S_PLAY_ATK2);
./info.c:291:    {SPR_PLAY,32773,6,{NULL},S_PLAY_ATK1,0,0},     // S_PLAY_ATK2
./info.h:331:    S_PLAY_ATK2,

That is a bit odd. S_PLAY_ATK2 is mentioned a bunch in very quick succession somewhere in p_pspr.c. I wonder what that is about. Let's check it out.

Success!

As it turns out, the procedures that set that state have names like A_FirePistol, A_FireShotgun, A_FireShotgun2 and so on. Can I assume Shotgun2 means super shotgun?

player->ammo[weaponinfo[player->readyweapon].ammo]-=2;

I remember the super shotgun is double-barreled in the game, so that it consumes two units of ammunation at each shot would make a lot of sense. I'm fairly sure I've found what I'm looking for!

After the sounds have started playing, and the graphics are set, the code that deals with the effects of shooting is executed:

P_BulletSlope(player->mo);

for (i = 0; i < 20; i++) {
    damage = 5 * (P_Random() % 3 + 1);
    angle = player->mo->angle;
    angle += (P_Random() - P_Random()) << 19;
    P_LineAttack(
        player->mo,
        angle,
        MISSILERANGE,
        bulletslope + ((P_Random() - P_Random()) << 5),
        damage
    );
}

The first call to BulletSlope appears to determine the height at which the bullet marks should be drawn. Recalling that Doom has what is essentially a 2D engine – only graphics are drawn three-dimensionally – that makes sense. The engine only knows which direction the player is facing in the 2D plane. To be able to draw bullet marks at the correct height it guesses a height based on which enemy the player seems to be aiming at, and how high up that enemy must be based on level geometry.

Either way, that is irrelevant for damage. It is an aesthetic concern.

The Answer

We don't need to look long beyond that for the answer. The for loop is telling me the super shotgun fires 20 projectiles with a random spread. The damage of each projectile is determined by

5 * (P_Random() % 3 + 1);

Normally, P_Random() is going to be a random number between 0 and 255. When we mod that down with % 3 we get a random number between 0 and 2. This means that the lowest damage a single projectile will do normally is 5 * (0 + 1) which is 5. The highest damage a single projectile will do is 5 * (2 + 1), or 15.

Extrapolating for all 20 projectiles, a single shot with the super shotgun, when all projectiles hit, can make as little as 100 damage, or as much as 300 damage. On average, you can expect to make 200 damage when all projectiles hit.

What happens if we set the array of random numbers to either all zeroes or all 255s? Obviously if every random number generated is zero, the super shotgun will always do minimum damage (100 if all projectiles hit.)

What about 255? Unlucky choice of number! 255 % 3 is zero too! So whether you fill the random array with 0 or 255, you'll do the minimum possible damage with the super shotgun, which is half of what you'd expect normally. Quite the nerf indeed!

This of course also means that you can cleverly pick a number (like 2, or 254) to fill the random array with to do maximum damage with the super shotgun every time.

Conclusion

So in the end, it's not that the super shotgun is dependent on the spread to function, it's that the damage it deals is randomised, and the two numbers tested both happened to give bad results.

Somewhere in there, there's a lesson about relying too much on testing and not doing enough reasoning about your code…