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:
case MONSTERSTATE_DEAD:
{
return GetScheduleOfType( SCHED_DIE );
break;
}
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[] =
{
{
tlDie1,
ARRAYSIZE( tlDie1 ),
0,
0,
"Die"
},
};
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:
{
RouteClear();
m_IdealActivity = GetDeathActivity();
pev->deadflag = DEAD_DYING;
break;
}
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.
-
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
withSetThink
. This causesCBaseMonster::MonsterThink
to not run for Nihilanth, which prevents the newACT_RESET
activity from overriding its animation. ↩︎