Hostage execution

Article by [furrycat] with updates from Birdie and Beckett.

AI scripts

Using UTPT, I decompiled the R6Terrorist and R6TerroristAI scripts which control terrorist behaviour.

UTPT disassembles .u files to produce scripts which may look a little strange at first. For example, it writes this

  local R6Pawn r6seen;

  r6seen=R6Pawn(seen);
  if (! r6seen == None ) goto JL001D;
  return;
JL001D:
  // Rest of code

It would be more intuitive to write the above as follows:

  local R6Pawn r6seen;

  r6seen = R6Pawn(seen);
  if (r6seen == None) return;

  // Rest of code

The reason for this discrepancy is that the program is disassembling optimised code and printing out a "literal translation" of what the code does. If you have programming experience yourself you will probably understand what's happening if you stop and think for a few moments. If you don't, I mention this because I want to let people know that I am aware that I may have made mistakes in my research and I invite people to check and correct what I have done...

Finding the "kill hostage" trigger

The game wouldn't be much fun if the terrorists executed their hostages immediately. There is clearly some kind of trigger which causes them to do so. I looked through the scripts to try to find out what that trigger could be.

The answer is the AttackHostage state:

state AttackHostage extends Attack
{
Begin:
	if (! (R6Hostage(Enemy) == None) || R6Hostage(Enemy).m_bExtracted ) goto JL002F;
	FindNextEnemy();
JL002F:
	if (!  !R6Pawn(Enemy).IsAlive() || CanSee(Enemy) ) goto JL005B;
	GotoStateAimedFire();
JL005B:
	SetReactionStatus(3,4);
	GotoStateMovingTo("Chase hostage",5,True,Enemy,,'AttackHostage','Begin');
}

This code is saying that when a terrorist enters the AttackHostage state, he should find a hostage who is not dead and has not been extracted, chasing after him if necessary, and shoot at that hostage.

A function, GotoStateAttackHostage(), can be used to set a terrorist to the AttackHostage state.

So there's the trigger but when is it fired?

CheckForInteraction()

The CheckForInteraction() function provides the only call to GotoStateAttackHostage() in the script.

function bool CheckForInteraction ()
{
	local Actor aGoal;

	if (! m_TriggeredIO != None ) goto JL0082;
	m_bCantInterruptIO=True;
	SetReactionStatus(5,0);
	if (! m_TriggeredIO.m_Anchor != None ) goto JL0048;
	aGoal=m_TriggeredIO.m_Anchor;
	goto JL0053;
JL0048:
	aGoal=m_TriggeredIO;
JL0053:
	GotoStateMovingTo("InteractionObject",5,False,aGoal,,'PrecombatAction','InteractiveObject',True);
	return True;
JL0082:
	if (! Pawn.m_bDroppedWeapon || (m_pawn.EngineWeapon == None) ) goto JL00AC;
	return False;
JL00AC:
	if (!  !UseRandomHostage() ) goto JL00E0;
	/*m_Hostage=m_pawn.m_DZone.UnknownFunction1838(m_pawn.Location);*/
	m_Hostage=m_pawn.m_DZone.GetClosestHostage(m_pawn.Location);
JL00E0:
	if (! (m_Hostage != None) &&  !m_Hostage.m_bExtracted ) goto JL011D;
	if (! Rand(100) < GetKillingHostageChance() ) goto JL011D;
	GotoStateAttackHostage(m_Hostage);
	return True;
JL011D:
	return False;
}

The comment is by me. UTPT couldn't determine that UnknownFunction1838 was R6DeploymentZone.GetClosestHostage, which is easily verifiable through decompiling some other scripts...

Although the function initially looks confusing, it turns out to be quite simple. If there's an "InteractionObject" nearby, the terrorist will move towards it and Interact with it. Otherwise there is a random chance (determined by GetKillingHostageChance()) that he will move towards and attack the nearest hostage instead.

If that still doesn't make sense, consider that Evidence and Bombs are InteractionObjects. This function is saying "Go and detonate the bomb OR kill the hostages."

Which hostage shall I kill and when?

The GetClosestHostage() and GetKillingHostageChance() functions determine which hostage a terrorist will aim for and under what conditions. If you open up a map in UnrealEd and examine the properties of a terrorist start point, you will see two interesting variables: m_HostageShootChance and m_HostageZoneToCheck.

Terrorist start point

This would suggest that only certain terrorists will execute hostages and that the map maker can assign the probability that they will do so. Furthermore, the terrorist will look for the nearest hostage to kill by checking zones where hostages may be found.

Beckett writes:The m_HostageZonetoCheck setting doesn't assign a map zone for the terrorist to investigate. This setting actually assigns individual hostages (Click the "Pick" button and then click the eyedropper on the hostage you want the terrorist to guard).

Decompiling GetKillingHostageChance() confirms this:

function int GetKillingHostageChance ()
{
	local int iChance;

	if (! UseRandomHostage() ) goto JL0014;
	iChance=40;
	goto JL0031;
JL0014:
	iChance=m_pawn.m_DZone.m_HostageShootChance;
JL0031:
	if (! m_pawn.m_iDiffLevel == 1 ) goto JL004E;
	iChance -= 20;
JL004E:
	if (! m_pawn.m_iDiffLevel == 3 ) goto JL006C;
	iChance += 20;
JL006C:
	return iChance;
}

This function says that in Mission mode, terrorists are assigned a probability of shooting hostages from the map editor. You can confirm that the probability is set to zero for most terrorists. In Hostage Rescue games, the probability defaults to 40%. In both types of game, the difficulty level influences the likelihood of a terrorist executing a hostage. Add 20 to the probability factor if it's an ELITE level game, subtract 20 if it's a RECRUIT game and leave it unchanged if it's a VETERAN game.

Calls to CheckForInteraction()

We have now seen that certain terrorists may be triggered to detonate bombs or execute hostages. We are still not certain of the potential causes. We need to know when CheckForInteraction() is called to answer that question.

The answer is that it can be called in one of four situations.

I'm not certain what PerformAction_StopInteraction is. I would guess that it is called when a terrorist is sitting on a chair, leaning against a wall or Performing some other Action on the game world when he is Stopped from Interacting with the world further because of Rainbow's intervention. That, however, is just an assumption which I have not checked.

EngageBySight() and SeePlayer() should be self-explanatory. When a terrorist spots a player, the AI code tells him to do certain things, one of which is to call the CheckForInteraction() function and hence potentially kill hostages.

RecoverFromFlash is interesting. It's a label in the TransientStateCode state:

RecoverFromFlash:
	Disable('HearNoise');
	Disable('SeePlayer');
	StopMoving();
	Sleep(5.00);
	if (! m_bCantInterruptIO ) goto JL00B6;
	CheckForInteraction();

So if you use a flashbang on a terrorist he may panic and go after hostages when he recovers. I say "may" since this is effectively determined by his m_HostageShootChance, as you will recall.

TransientStateCode has some other interesting code too.

RunFromGrenade:
Begin:
	StopMoving();
	switch (m_pawn.m_iDiffLevel) {
	case 1:
	Sleep(1.00);
	goto JL0040;
	case 2:
	Sleep(0.50);
	goto JL0040;
	case 3:
	goto JL0040;
	default:
JL0040:
	GotoStateMovingTo("RunFromGrenade",5,True,m_aMovingToDestination,,'TransientStateCode','AfterRunFromGrenade',True);
AfterRunFromGrenade:
	m_bHeardGrenade=False;
	if (! Enemy == None ) goto JL0085;
	Sleep(3.00);
JL0085:
	goto ('ResumeAction');

This clearly shows that hearing or seeing frag grenades does not cause tangos to target hostages!

Hearing players

In-game testing using the PlayerInvisible didn't conclusively show whether hearing players would cause terrorists to react. The code for the HearNoise event tells us what happens.

event HearNoise (float Loudness, Actor NoiseMaker, ENoiseType eType)
{
  local R6Hostage hostage;
  local R6Pawn pPawn;

  if (! m_pawn.m_bHearNothing || m_pawn.m_bDontHearPlayer && R6Pawn(NoiseMaker.Instigator).m_bIsPlayer ) goto JL004A;
  return;
JL004A:
  ReconThreatCheck(NoiseMaker,eType);
  if (! m_bHearInvestigate && (vector(eType) == vector(1)) ) goto JL00F3;
  hostage=R6Hostage(NoiseMaker.Instigator);
  if (! hostage != None ) goto JL00A9;
  if (! IsAssigned(hostage) ) goto JL00A9;
  return;
JL00A9:
  if (!  !m_bAlreadyHeardSound ) goto JL00D2;
  m_bAlreadyHeardSound=True;
  m_VoicesManager.PlayTerroristVoices(m_pawn,13);
JL00D2:
  GotoPointAndSearch(NoiseMaker.Location,4,True,30.00,2);
  goto JL02F5;
JL00F3:
  if (! m_bHearThreat && (vector(eType) == vector(2)) ) goto JL0178;
  if (! m_iChanceToDetectShooter < 80 ) goto JL0123;
  m_iChanceToDetectShooter += 20;
JL0123:
  if (! Rand(100) + 1 < m_iChanceToDetectShooter ) goto JL014B;
  EngageBySight(NoiseMaker.Instigator);
  goto JL0175;
JL014B:
  if (!  !IsInState('EngageByThreat') ) goto JL0175;
  GotoStateEngageByThreat(NoiseMaker.Instigator.Location);
JL0175:
  goto JL02F5;
JL0178:
  if (! m_bHearGrenade && (vector(eType) == vector(3)) ) goto JL0268;
  if (! (UnknownFunction1851(vector(rotator(NoiseMaker.Location - Pawn.Location)).Yaw,Pawn.Rotation.Yaw) < 16000) || (UnknownFunction1851(vector(rotator(NoiseMaker.Instigator.Location - Pawn.Location)).Yaw,Pawn.Rotation.Yaw) < 16000) ) goto JL0265;
  if (!  !m_bHeardGrenade ) goto JL0251;
  m_VoicesManager.PlayTerroristVoices(m_pawn,5);
  m_bHeardGrenade=True;
JL0251:
  ReactToGrenade(NoiseMaker.Location);
JL0265:
  goto JL02F5;
JL0268:
  if (! m_bHearInvestigate && (vector(eType) == vector(4)) ) goto JL02F5;
  pPawn=R6Pawn(NoiseMaker.Instigator);
  if (! (pPawn != None) &&  !pPawn.m_bTerroSawMeDead ) goto JL02ED;
  pPawn.m_bTerroSawMeDead=True;
  GotoPointAndSearch(NoiseMaker.Location,4,True,30.00);
  goto JL02F5;
JL02ED:
  ChangeDefCon(2);
JL02F5:
}

There's large chunk of code to look at here. The first line can be rewritten as:

if (m_pawn.m_bHearNothing || (R6Pawn(NoiseMaker.Instigator).m_bIsPlayer && m_pawn.m_bDontHearPlayer)) return;

In other words "do nothing if this terrorist shouldn't hear anything OR if the noise maker was a player and players are set to inaudible."

Next, ReconThreatCheck() is called. This function holds the key.

ReconThreatCheck()

function ReconThreatCheck (Actor aThreat, ENoiseType eType)
{
  local R6Pawn aPawn;

  aPawn=R6Pawn(aThreat);
  if (! vector(eType) == vector(0) ) goto JL006E;
  if (! (aPawn != None) && m_pawn.IsEnemy(aPawn) ) goto JL006B;
  R6AbstractGameInfo(Level.Game).PawnSeen(aPawn,m_pawn);
JL006B:
  goto JL00E6;
JL006E:
  if (! (vector(eType) == vector(2)) || m_pawn.IsEnemy(aThreat.Instigator) && aThreat.IsA('R6Weapon') ) goto JL00E6;
  R6AbstractGameInfo(Level.Game).PawnHeard(aThreat.Instigator,m_pawn);
JL00E6:
}

If the tango hears a player moving, or hears a weapon fired by the player, the game calls the same code as it does when the tango sees the player. Effectively, hearing a player or his weapon is as good as seeing him. We have already seen that seeing a player is also sufficient cause to make the terrorist run to detonate a bomb or kill a prisoner.

Verifying my research

If you want to check what I've done, wait for UBI to release the SDK with the .uc scripts or use UTPT to decompile the R6Terrorist and R6TerroristAI scripts from R6Engine.u.

Conclusion

Decompilation of the AI controller code shows that tangos will detonate bombs or execute hostages if they see players, hear player movement or weapon fire or are flashbanged. Furthermore they will only detonate or execute if they have been configured by the map author to do so AND THEN only at random.


Jump to a section

intro | part 1: Anecdotal evidence | part 2: AI scripts | part 3: Conclusion