Why only human grunts duplicate ammo drops

Popularised by quadrazid’s Opposing Force speedrun, ammo duplication is now a well known speedrunning trick in Half-Life. You may have noticed that this trick only works with human grunts. Once you understand how the trick works, it will be clear why it can’t be used on other NPC types.

To execute the trick, kill a human grunt and make a save before his dying animation ends but after dropping some ammo. Upon loading, the previously dropped ammo will still exist either in the air or on the ground. The dying animation will play again, and a copy of the ammo will be dropped. Congratulations, you have just duplicated the ammo drop from this NPC. This process can be repeated ad infinitum.

It is time to examine the Half-Life SDK. When a grunt’s health goes below zero after taking damage in CBaseMonster::TakeDamage, CSquadMonster::Killed will be called, which in turn calls the base class CBaseMonster::Killed. This method sets a couple of states to indicate death, but the most relevant to our discussion is setting m_IdealMonsterState to MONSTERSTATE_DEAD. The next time CBaseMonster::MaintainSchedule is called on the human grunt, it will update the actual monster state by

SetState( m_IdealMonsterState );

Now that the m_MonsterState is MONSTERSTATE_DEAD, CBaseMonster::GetSchedule will return the following:

        return GetScheduleOfType( SCHED_DIE );

which in turn returns the slDie schedule defined in defaultai.cpp.

// Die!
Task_t tlDie1[] =
    { TASK_STOP_MOVING,			0				 },
    { TASK_SOUND_DIE,		(float)0			 },
    { TASK_DIE,				(float)0			 },

Schedule_t slDie[] =
        ARRAYSIZE( tlDie1 ),

The most important part of this schedule is TASK_DIE. When CBaseMonster::StartTask is called with TASK_DIE, it sets m_IdealActivity to the death activity selected and returned by CBaseMonster::GetDeathActivity in the human grunt case.

case TASK_DIE:
        m_IdealActivity = GetDeathActivity();

        pev->deadflag = DEAD_DYING;

The GetDeathActivity function selects the appropriate animation sequence based on the last hitgroup the monster was hit on when it took damage, and various other conditions. When CBaseMonster::MaintainSchedule is called again, it sets the actual activity to the ideal activity:

SetActivity ( m_IdealActivity );

As animation continues to play, CBaseMonster::MonsterThink calls CBaseAnimating::DispatchAnimEvents at most 10 times per second. When a tagged frame is played, CHGrunt::HandleAnimEvent will be called with the event. When the event is HGRUNT_AE_DROP_GUN which is 11 numerically, CBaseMonster::DropItem will be called to drop a weapon or ammo.

Up to this point, I have just described the normal process of killing a human grunt to make him drop an item. What happens if we save and reload the game while the death animation is playing? The current animation frame is indexed by pev->frame, which is normally advanced by CBaseAnimating::StudioFrameAdvance. The pev data structure is saved to the ENTVARS name in the saved data. After loading from a save, pev will be fully restored along with pev->frame, which should have saved the progress of the current animation. However, in CBaseMonster::Restore, we see the following after restoration:

// Reset animation
m_Activity = ACT_RESET;

When CBaseMonster::MonsterThink is called again, the animation will be reset, giving a chance for the HGRUNT_AE_DROP_GUN event to be triggered once more.1

There is a way to slightly increase the rate of ammo duplication. I used Jed’s Half-Life Model Viewer to check out hgrunt.mdl, and I found that event 11 occurs at different frames for each death animation sequences. For example, event 11 is tagged at the 6th frame in diebackwards, and the 0th frame in diegutshot. If we are able to kill the human grunt in the right way so as to trigger the diegutshot animation, duplication can occur at a higher rate, because we would be able to perform a save load almost immediately after the animation started, rather than waiting for it to play the tagged frame.

Unlike the human grunt, other NPCs do not generally drop ammo on a tagged frame in the death animation. For example, Barney drops his items only when CBarney::Killed is called.

  1. If you try this with the Nihilanth, you will notice that the Nihilanth preserves the animation progress after a save load, so you are not able to cancel any of its animations. This is because, unlike most other monsters, Nihilanth replaces the think function pointed to by m_pfnThink with SetThink. This causes CBaseMonster::MonsterThink to not run for Nihilanth, which prevents the new ACT_RESET activity from overriding its animation. ↩︎