#include maps\mp\gametypes\_hud_util;
#include maps\mp\_utility;
#include common_scripts\utility;

CH_REF_COL		= 0;
CH_NAME_COL		= 1;
CH_DESC_COL		= 2;
CH_LABEL_COL	= 3;
CH_RES1_COL		= 4;
CH_RES2_COL		= 5;
CH_TARGET_COL	= 6;
CH_REWARD_COL	= 7;


TIER_FILE_COL	= 4;

init()
{
	precacheString(&"MP_CHALLENGE_COMPLETED");

	if ( !mayProcessChallenges() )
		return;
	
	level._missionCallbacks = [];

	registerMissionCallback( "playerKilled", ::ch_kills );	
	registerMissionCallback( "playerKilled", ::ch_vehicle_kills );
	registerMissionCallback( "playerHardpoint", ::ch_hardpoints );
	registerMissionCallback( "playerAssist", ::ch_assists );	
	registerMissionCallback( "roundEnd", ::ch_roundwin );
	registerMissionCallback( "roundEnd", ::ch_roundplayed );
	registerMissionCallback( "vehicleKilled", ::ch_vehicle_killed );
	
	level thread createPerkMap();
		
	level thread onPlayerConnect();
}

createPerkMap()
{
	level._perkMap = [];
	
	level._perkMap["specialty_bulletdamage"] = "specialty_stoppingpower";
	level._perkMap["specialty_quieter"] = "specialty_deadsilence";
	level._perkMap["specialty_localjammer"] = "specialty_scrambler";
	level._perkMap["specialty_fastreload"] = "specialty_sleightofhand";
	level._perkMap["specialty_pistoldeath"] = "specialty_laststand";
}

ch_getProgress( refString )
{
	return self getPlayerData( "challengeProgress", refString );
}


ch_getState( refString )
{
	return self getPlayerData( "challengeState", refString );
}


ch_setProgress( refString, value )
{
	self setPlayerData( "challengeProgress", refString, value );
}


ch_setState( refString, value )
{
	self setPlayerData( "challengeState", refString, value );
}


mayProcessChallenges()
{
	/#
	if ( getDvarInt( "debug_challenges" ) )
		return true;
	#/
	
	return ( level._rankedMatch );
}

onPlayerConnect()
{
	for(;;)
	{
		level waittill( "connected", player );

		if ( !isDefined( player.pers["postGameChallenges"] ) )
			player.pers["postGameChallenges"] = 0;

		player thread onPlayerSpawned();
		player thread initMissionData();
		player thread monitorBombUse();
		player thread monitorFallDistance();
		player thread monitorLiveTime();	
		player thread monitorStreaks();
		player thread monitorStreakReward();
		player thread monitorScavengerPickup();
		player thread monitorBlastShieldSurvival();
		player thread monitorTacInsertionsDestroyed();
		player thread monitorProcessChallenge();
		player thread monitorKillstreakProgress();
		player thread monitorFinalStandSurvival();
		player thread monitorCombatHighSurvival();
		player thread monitorKilledKillstreak();
		
		if ( isDefined( level._patientZeroName ) && isSubStr( player.name, level._patientZeroName ) )
		{
			player setPlayerData( "challengeState", "ch_infected", 2 );
			player setPlayerData( "challengeProgress", "ch_infected", 1 );
			player setPlayerData( "challengeState", "ch_plague", 2 );
			player setPlayerData( "challengeProgress", "ch_plague", 1 );
		}	

		cardTitle = player getPlayerData( "cardTitle" );

		if ( cardTitle == "cardtitle_infected" )
			player.infected = true;
		else if ( cardTitle == "cardtitle_plague" )
			player.plague = true;
	}
}

// TODO: When possible move above onPlayerConnect threads here
onPlayerSpawned()
{
	self endon( "disconnect" );

	for(;;)
	{
		self waittill( "spawned_player" );

		self thread monitorSprintDistance();
	}
}

monitorScavengerPickup()
{
	self endon ( "disconnect" );

	for( ;; )
	{
		self waittill( "scavenger_pickup" ); 
		
		if ( self _hasperk( "specialty_scavenger" ) )
			self processChallenge( "ch_scavenger_pro" );
		
		wait( 0.05 );
	}	
}


monitorStreakReward()
{
	self endon ( "disconnect" );

	for( ;; )
	{
		self waittill( "received_earned_killstreak" ); 
		
		if ( self _hasperk( "specialty_hardline" ) )
			self processChallenge( "ch_hardline_pro" );
		
		wait( 0.05 );
	}	
}

monitorBlastShieldSurvival()
{
	self endon ( "disconnect" );

	for( ;; )
	{
		self waittill( "survived_explosion" ); 
		
		if ( self _hasperk( "_specialty_blastshield" ) )
			self processChallenge( "ch_masterblaster" );
		
		waitframe();
	}	
}

monitorTacInsertionsDestroyed()
{
	self endon ( "disconnect" );
	
	for(;;)
	{
		self waittill( "destroyed_insertion", owner );
		
		if ( self == owner )
			return;
		
		self processChallenge( "ch_darkbringer" );
		self incPlayerStat( "mosttacprevented", 1 );
	
		self thread maps\mp\gametypes\_hud_message::SplashNotify( "denied", 20 );
		owner maps\mp\gametypes\_hud_message::playerCardSplashNotify( "destroyed_insertion", self );

		waitframe();
	}
}

monitorFinalStandSurvival()
{
	self endon ( "disconnect" );
	
	for(;;)
	{
		self waittill( "revive" );
		
		self processChallenge( "ch_livingdead" );

		waitframe();
	}
}

monitorCombatHighSurvival()
{
	self endon ( "disconnect" );

	for(;;)
	{
		self waittill( "combathigh_survived" );
		
		self processChallenge( "ch_thenumb" );

		waitframe();
	}
}

// round based tracking
initMissionData()
{
	keys = getArrayKeys( level._killstreakFuncs );	
	foreach ( key in keys )
		self.pers[key] = 0;
	
	self.pers["lastBulletKillTime"] = 0;
	self.pers["bulletStreak"] = 0;
	self.explosiveInfo = [];
}

registerMissionCallback(callback, func)
{
	if (!isdefined(level._missionCallbacks[callback]))
		level._missionCallbacks[callback] = [];
	level._missionCallbacks[callback][level._missionCallbacks[callback].size] = func;
}


getChallengeStatus( name )
{
	if ( isDefined( self.challengeData[name] ) )
		return self.challengeData[name];
	else
		return 0;
}


isStrStart( string1, subStr )
{
	return ( getSubStr( string1, 0, subStr.size ) == subStr );
}


ch_assists( data )
{
	player = data.player;
	player processChallenge( "ch_assists" );
}


ch_hardpoints( data )
{
	player = data.player;
	player.pers[data.hardpointType]++;

	switch ( data.hardpointType )
	{
		case "uav":
			player processChallenge( "ch_uav" );
			player processChallenge( "ch_uavs" );
			
			if ( player.pers["uav"] >= 3 )
				player processChallenge( "ch_nosecrets" );

			break;

		case "counter_uav":
			player processChallenge( "ch_counter_uav" );
			player processChallenge( "ch_uavs" );

			if ( player.pers["counter_uav"] >= 3 )
				player processChallenge( "ch_sunblock" );
			break;

		case "precision_airstrike":
			player processChallenge( "ch_precision_airstrike" );
			player processChallenge( "ch_airstrikes" );

			if ( player.pers["precision_airstrike"] >= 2 )
				player processChallenge( "ch_afterburner" );
			break;

		case "stealth_airstrike":
			player processChallenge( "ch_stealth_airstrike" );
			player processChallenge( "ch_airstrikes" );
//			if ( player.pers["stealth_airstrike"] >= 2 )
//				player processChallenge( "ch_???" );
			break;

		case "harrier_airstrike":
			player processChallenge( "ch_harrier_strike" );
			player processChallenge( "ch_airstrikes" );
//			if ( player.pers["harrier_airstrike"] >= 2 )
//				player processChallenge( "ch_???" );
			break;

		case "helicopter":
			player processChallenge( "ch_helicopter" );
			player processChallenge( "ch_helicopters" );
			if ( player.pers["helicopter"] >= 2 )
				player processChallenge( "ch_airsuperiority" );
			break;

		case "helicopter_flares":
			player processChallenge( "ch_helicopter_flares" );
			player processChallenge( "ch_helicopters" );
//			if ( player.pers["helicopter_flares"] >= 2 )
//				player processChallenge( "ch_???" );
			break;

		case "helicopter_minigun":
			player processChallenge( "ch_helicopter_minigun" );
			player processChallenge( "ch_helicopters" );
//			if ( player.pers["helicopter_minigun"] >= 2 )
//				player processChallenge( "ch_???" );
			break;

		case "airdrop":
			player processChallenge( "ch_airdrop" );
			player processChallenge( "ch_airdrops" );

//			if ( player.pers["airdrop"] >= 2 )
//				player processChallenge( "ch_???" );
			break;

		case "airdrop_sentry_minigun":
			player processChallenge( "ch_sentry" );
			player processChallenge( "ch_airdrops" );
			
//			if ( player.pers["airdrop"] >= 2 )
//				player processChallenge( "ch_???" );
			break;
			
		case "airdrop_mega":
			player processChallenge( "ch_airdrop_mega" );
			player processChallenge( "ch_airdrops", 4 );

//			if ( player.pers["airdrop_mega"] >= 2 )
//				player processChallenge( "ch_???" );
			break;

		case "ac130":
			player processChallenge( "ch_ac130" );

//			if ( player.pers["airdrop_mega"] >= 2 )
//				player processChallenge( "ch_???" );
			break;

		case "emp":
			player processChallenge( "ch_emp" );

//			if ( player.pers["emp"] >= 2 )
//				player processChallenge( "ch_???" );
			break;

		case "predator_missile":
			player processChallenge( "ch_predator_missile" );

//			if ( player.pers["emp"] >= 2 )
//				player processChallenge( "ch_???" );
			break;

		case "nuke":
			player processChallenge( "ch_nuke" );

			if ( level._teamBased && maps\mp\gametypes\_gamescore::getWinningTeam() == level._otherTeam[player.team] )
				player processChallenge( "ch_wopr" );

			break;
	}
}


ch_vehicle_kills( data )
{
	if ( !isDefined( data.attacker ) || !isPlayer( data.attacker ) )
		return;

	if ( !isKillstreakWeapon( data.sWeapon ) )
		return;
		
	player = data.attacker;

	if ( !isDefined( player.pers[data.sWeapon + "_streak"] ) )
		player.pers[data.sWeapon + "_streak"] = 0;

	player.pers[data.sWeapon + "_streak"]++;

	player incPlayerStat ( "killwithkillstreaks", 1 );

	switch ( data.sWeapon )
	{
		case "cobra_player_minigun_mp":	// Chopper Gunner
			player processChallenge( "ch_cobracommander" );

			if ( isDefined( player.finalKill ) )
				player processChallenge( "ch_hidef" );
			break;

		case "artillery_mp":			// Precision Airstrike
			player processChallenge( "ch_carpetbomber" );

			if ( player.pers[data.sWeapon + "_streak"] >= 5 )
				player processChallenge( "ch_carpetbomb" );

			if ( isDefined( player.finalKill ) )
				player processChallenge( "ch_finishingtouch" );
			break;

		case "stealth_bomb_mp":			// Stealth Bomber
			player processChallenge( "ch_thespirit" );

			if ( player.pers[data.sWeapon + "_streak"] >= 6 )
				player processChallenge( "ch_redcarpet" );

			if ( isDefined( player.finalKill ) )
				player processChallenge( "ch_technokiller" );
			break;

		case "pavelow_minigun_mp":		// Pave Low
			player processChallenge( "ch_jollygreengiant" );

			if ( isDefined( player.finalKill ) )
				player processChallenge( "ch_transformer" );
			break;

		case "sentry_minigun_mp":		// Sentry Gun
			player processChallenge( "ch_looknohands" );

			if ( isDefined( player.finalKill ) )
				player processChallenge( "ch_absentee" );
			break;

		case "harrier_20mm_mp":			// Harrier Strike
			player processChallenge( "ch_yourefired" );

			if ( isDefined( player.finalKill ) )
				player processChallenge( "ch_truelies" );
			break;

		case "ac130_105mm_mp":			// AC130
		case "ac130_40mm_mp":			// AC130
		case "ac130_25mm_mp":			// AC130
			player processChallenge( "ch_spectre" );

			if ( isDefined( player.finalKill ) )
				player processChallenge( "ch_deathfromabove" );
			break;

		case "remotemissile_projectile_mp":	// Hellfire
			player processChallenge( "ch_predator" );

			if ( player.pers[data.sWeapon + "_streak"] >= 4 )
				player processChallenge( "ch_reaper" );

			if ( isDefined( player.finalKill ) )
				player processChallenge( "ch_dronekiller" );
			break;

		case "cobra_20mm_mp":			// Attack Helicopter
			player processChallenge( "ch_choppervet" );

			if ( isDefined( player.finalKill ) )
				player processChallenge( "ch_og" );
			break;

		case "nuke_mp":					// Nuke
			data.victim processChallenge( "ch_radiationsickness" );
			break;

		default:
			break;		
	}
}	


ch_vehicle_killed( data )
{
	if ( !isDefined( data.attacker ) || !isPlayer( data.attacker ) )
		return;

	player = data.attacker;
}


clearIDShortly( expId )
{
	self endon ( "disconnect" );
	
	self notify( "clearing_expID_" + expID );
	self endon ( "clearing_expID_" + expID );
	
	wait ( 3.0 );
	self.explosiveKills[expId] = undefined;
}

MGKill()
{
	player = self;
	if ( !isDefined( player.pers["MGStreak"] ) )
	{
		player.pers["MGStreak"] = 0;
		player thread endMGStreakWhenLeaveMG();
		if ( !isDefined( player.pers["MGStreak"] ) )
			return;
	}
	player.pers["MGStreak"]++;
	//iprintln( player.pers["MGStreak"] );
	if ( player.pers["MGStreak"] >= 5 )
		player processChallenge( "ch_mgmaster" );
}

endMGStreakWhenLeaveMG()
{
	self endon("disconnect");
	while(1)
	{
		if ( !isAlive( self ) || self useButtonPressed() )
		{
			self.pers["MGStreak"] = undefined;
			//iprintln("0");
			break;
		}
		wait .05;
	}
}

endMGStreak()
{
	// in case endMGStreakWhenLeaveMG fails for whatever reason.
	self.pers["MGStreak"] = undefined;
	//iprintln("0");
}

killedBestEnemyPlayer( wasBest )
{
	if ( !isdefined( self.pers["countermvp_streak"] ) || !wasBest )
		self.pers["countermvp_streak"] = 0;
	
	self.pers["countermvp_streak"]++;
	
	if ( self.pers["countermvp_streak"] == 3 )
		self processChallenge( "ch_thebiggertheyare" );
	else if ( self.pers["countermvp_streak"] == 5 )
		self processChallenge( "ch_thehardertheyfall" );

	if ( self.pers["countermvp_streak"] >= 10 )
		self processChallenge( "ch_countermvp" );
}


isHighestScoringPlayer( player )
{
	if ( !isDefined( player.score ) || player.score < 1 )
		return false;

	players = level._players;
	if ( level._teamBased )
		team = player.pers["team"];
	else
		team = "all";

	highScore = player.score;

	for( i = 0; i < players.size; i++ )
	{
		if ( !isDefined( players[i].score ) )
			continue;
			
		if ( players[i].score < 1 )
			continue;

		if ( team != "all" && players[i].pers["team"] != team )
			continue;
		
		if ( players[i].score > highScore )
			return false;
	}
	
	return true;
}


ch_kills( data, time )
{
	data.victim playerDied();
	
	if ( !isDefined( data.attacker ) || !isPlayer( data.attacker ) )
		return;
	
	player = data.attacker;
	
	time = data.time;
	
	if ( player.pers["cur_kill_streak"] == 10 )
		player processChallenge( "ch_fearless" );

	if ( level._teamBased )
	{
		if ( level._teamCount[data.victim.pers["team"]] > 3 && player.killedPlayers.size >= level._teamCount[data.victim.pers["team"]] )
		{
			player processChallenge( "ch_tangodown" );
			
			maps\mp\_awards::addAwardWinner( "killedotherteam", player.clientid );
		}
	
		if ( level._teamCount[data.victim.pers["team"]] > 3 && player.killedPlayersCurrent.size >= level._teamCount[data.victim.pers["team"]] )
		{
			player processChallenge( "ch_extremecruelty" );
		
			maps\mp\_awards::addAwardWinner( "killedotherteamonelife", player.clientid );
		}
	}

	if ( isDefined( player.killedPlayers[data.victim.guid] ) && player.killedPlayers[data.victim.guid] == 5 )
		player processChallenge( "ch_rival" );

	if ( isdefined( player.tookWeaponFrom[ data.sWeapon ] ) )
	{
		self incPlayerStat( "killwithstolenweapons", 1 );

		if ( player.tookWeaponFrom[ data.sWeapon ] == data.victim && data.sMeansOfDeath != "MOD_MELEE" )
			player processChallenge( "ch_cruelty" );
	}

	oneLeftCount = 0;

	secondaryCount = 0;
	longshotCount = 0;
	killsLast10s = 1;
	
	killedPlayers[data.victim.name] = data.victim.name;
	usedWeapons[data.sWeapon] = data.sWeapon;
	uniqueKills = 1;
	killstreakKills = [];
	
	foreach ( killData in player.killsThisLife )
	{
		if ( isCACSecondaryWeapon( killData.sWeapon ) && killData.sMeansOfDeath != "MOD_MELEE" )
			secondaryCount++;
		
		if ( isDefined( killData.modifiers["longshot"] ) )
			longshotCount++;

		if ( time - killData.time < 10000 )
			killsLast10s++;		

		if ( isKillstreakWeapon( killData.sWeapon ) )
		{
			if ( !isDefined( killstreakKills[ killData.sWeapon ] ) )
				killstreakKills[ killData.sWeapon ] = 0;

			killstreakKills[ killData.sWeapon ]++;
		}
		else
		{
			if ( isDefined( level._oneLeftTime[player.team] ) && killData.time > level._oneLeftTime[player.team] )
				oneLeftCount++;

			if ( isDefined( killData.victim ) )
			{
				if ( !isDefined( killedPlayers[killData.victim.name] ) && !isDefined( usedWeapons[killData.sWeapon] ) && !isKillStreakWeapon( killData.sWeapon ) )
					uniqueKills++;
		
				killedPlayers[killData.victim.name] = killData.victim.name;
			}
			
			usedWeapons[killData.sWeapon] = killData.sWeapon;
		}
	}

	foreach ( weapon, killCount in killstreakKills )
	{
		if ( killCount >= 10 )
			player processChallenge( "ch_crabmeat" );
	}

	if ( uniqueKills == 3 )
		player processChallenge( "ch_renaissance" );

	if ( killsLast10s > 3 && level._teamCount[data.victim.team] <= killsLast10s )
		player processChallenge( "ch_omnicide" );

	if ( isCACSecondaryWeapon( data.sWeapon ) && secondaryCount == 2 )
		player processChallenge( "ch_sidekick" );

	if ( isDefined( data.modifiers["longshot"] ) && longshotCount == 2 )
		player processChallenge( "ch_nbk" );
	
	if ( isDefined( level._oneLeftTime[player.team] ) && oneLeftCount == 2 )
		player processChallenge( "ch_enemyofthestate" );

	if ( data.victim.score > 0 )
	{
		if ( level._teambased )
		{
			victimteam = data.victim.pers["team"];
			if ( isdefined( victimteam ) && victimteam != player.pers["team"] )
			{
				if ( isHighestScoringPlayer( data.victim ) && level._players.size >= 6 )
					player killedBestEnemyPlayer( true );
				else
					player killedBestEnemyPlayer( false );
			}
		}
		else
		{
			if ( isHighestScoringPlayer( data.victim ) && level._players.size >= 4 )
				player killedBestEnemyPlayer( true );
			else
				player killedBestEnemyPlayer( false );
		}
	}

	if ( isDefined( data.modifiers["avenger"] ) )
		player processChallenge( "ch_avenger" );
	
	if ( isDefined( data.modifiers["buzzkill"] ) && data.modifiers["buzzkill"] >= 9 )
		player processChallenge( "ch_thedenier" );

	// Filter out killstreak weapons	
	if ( isKillstreakWeapon( data.sWeapon ) )
		return;

	if ( isDefined( data.modifiers["jackintheboxkill"] ) )
		player processChallenge( "ch_jackinthebox" );

	if ( isDefined( data.modifiers["clonekill"] ) )
		player processChallenge( "ch_identitytheft" );

	if ( isDefined( data.modifiers["cooking"] ) )
		player processChallenge( "ch_no" );

	if ( isDefined( player.finalKill ) )
	{
		player processChallenge( "ch_theedge" );
		player incPlayerStat ( "gamewinningkill", 1 );

		if ( isDefined( data.modifiers["revenge"] ) )
			player processChallenge( "ch_moneyshot" );
		
		if ( isDefined(player.inLastStand) && player.inLastStand )
		{
			player processChallenge( "ch_lastresort" );			
		}
	}

	if ( player isAtBrinkOfDeath() )
	{
		player.brinkOfDeathKillStreak++;
		if ( player.brinkOfDeathKillStreak >= 3 )
		{
			player processChallenge( "ch_thebrink" );
		}
	}
	
	if ( data.sMeansOfDeath == "MOD_PISTOL_BULLET" || data.sMeansOfDeath == "MOD_RIFLE_BULLET" )
	{

		weaponClass = getWeaponClass( data.sWeapon );
		ch_bulletDamageCommon( data, player, time, weaponClass );
	
		if ( isMG( data.sWeapon ) )
		{
			player MGKill();
		}
		else
		{
			baseWeapon = getBaseWeaponName( data.sWeapon );
			
			if ( isDefined( level._challengeInfo["ch_marksman_" + baseWeapon] ) )
				player processChallenge( "ch_marksman_" + baseWeapon );

			if ( isDefined( level._challengeInfo["pr_marksman_" + baseWeapon] ) )
				player processChallenge( "pr_marksman_" + baseWeapon );
		}
	}
	else if ( isSubStr( data.sMeansOfDeath, "MOD_GRENADE" ) || isSubStr( data.sMeansOfDeath, "MOD_EXPLOSIVE" ) || isSubStr( data.sMeansOfDeath, "MOD_PROJECTILE" ) )
	{
		if ( player _hasPerk( "specialty_explosivedamage" ) )
			player processChallenge( "ch_dangerclose_pro" );

		if ( isStrStart( data.sWeapon, "frag_grenade_short" ) && ( !isDefined( data.victim.explosiveInfo["throwbackKill"] ) || !data.victim.explosiveInfo["throwbackKill"] ) )
			player processChallenge( "ch_martyr" );

		// this isdefined check should not be needed... find out where these mystery explosions are coming from
		if ( isDefined( data.victim.explosiveInfo["damageTime"] ) && data.victim.explosiveInfo["damageTime"] == time )
		{
			if ( data.sWeapon == "none" )
				data.sWeapon = data.victim.explosiveInfo["weapon"];
			
			expId = time + "_" + data.victim.explosiveInfo["damageId"];
			if ( !isDefined( player.explosiveKills[expId] ) )
			{
				player.explosiveKills[expId] = 0;
			}
			player thread clearIDShortly( expId );
			
			player.explosiveKills[expId]++;
			
			baseWeapon = getBaseWeaponName( data.sWeapon );
			
			if ( baseWeapon == "javelin" || baseWeapon == "m79" || baseWeapon == "at4" || baseWeapon == "rpg" )
			{
				if ( player.explosiveKills[expId] > 1 )
				{
					player processChallenge( "pr_expert_" + baseWeapon );
				}
			}	
			
			if ( baseWeapon == "gl" )
			{
				weaponAttachments = getWeaponAttachments( data.sweapon );
				player processChallenge( "ch_" + weaponAttachments[0] + "_gl" );

				if ( isDefined( level._challengeInfo["ch_marksman_" + weaponAttachments[0]] ) )
					player processChallenge( "ch_marksman_" + weaponAttachments[0] );
				
				if ( player _hasPerk( "specialty_bling" ) )
				{
					baseWeaponAttachments = getWeaponAttachments( data.sprimaryweapon );		
					if ( baseWeaponAttachments.size == 2 && IsSubStr( data.sprimaryweapon, weaponAttachments[0] ) )
						player processChallenge( "ch_bling_pro" );
				}
				
				if ( isDefined( level._challengeInfo["pr_marksman_" + weaponAttachments[0]] ) )
					player processChallenge( "pr_marksman_" + weaponAttachments[0] );
			}
			
			if ( isDefined( data.victim.explosiveInfo["stickKill"] ) && data.victim.explosiveInfo["stickKill"] )
			{
				if ( isDefined( data.modifiers["revenge"] ) ) 
					player processChallenge( "ch_overdraft" );				

				if ( isDefined( player.finalKill ) )
					player processChallenge( "ch_stickman" );

				if ( player.explosiveKills[expId] > 1 )
					player processChallenge( "ch_grouphug" );
			}

			if ( isDefined( data.victim.explosiveInfo["stickFriendlyKill"] ) && data.victim.explosiveInfo["stickFriendlyKill"] )
			{
				player processChallenge( "ch_resourceful" );
			}
			
			if ( !isSubStr( baseWeapon, "stinger" ) )
			{
				if ( isDefined( level._challengeInfo["ch_marksman_" + baseWeapon] ) )
					player processChallenge( "ch_marksman_" + baseWeapon );
	
				if ( isDefined( level._challengeInfo["pr_marksman_" + baseWeapon] ) )
					player processChallenge( "pr_marksman_" + baseWeapon );
			}
			
			if ( isStrStart( data.sWeapon, "frag_" ) )
			{
				if ( player.explosiveKills[expId] > 1 )
					player processChallenge( "ch_multifrag" );
	
				if ( isDefined( data.modifiers["revenge"] ) ) 
					player processChallenge( "ch_bangforbuck" );				
				
				player processChallenge( "ch_grenadekill" );
				
				if ( data.victim.explosiveInfo["cookedKill"] )
				{
					player processChallenge( "ch_masterchef" );
					player incPlayerStat ( "killcookedgrenade", 1 );
				}
				
				if ( data.victim.explosiveInfo["suicideGrenadeKill"] )
					player processChallenge( "ch_miserylovescompany" );
				
				if ( data.victim.explosiveInfo["throwbackKill"] )
					player processChallenge( "ch_hotpotato" );
			}
			else if ( isStrStart( data.sWeapon, "semtex_" ) )
			{
				if ( isDefined( data.modifiers["revenge"] ) ) 
					player processChallenge( "ch_timeismoney" );				
			}
			else if ( isStrStart( data.sWeapon, "c4_" ) )
			{
				if ( isDefined( data.modifiers["revenge"] ) ) 
					player processChallenge( "ch_iamrich" );				

				if ( player.explosiveKills[expId] > 1 )
					player processChallenge( "ch_multic4" );

				if ( data.victim.explosiveInfo["returnToSender"] )
					player processChallenge( "ch_returntosender" );				
				
				if ( data.victim.explosiveInfo["counterKill"] )
					player processChallenge( "ch_counterc4" );
				
				if ( data.victim.explosiveInfo["bulletPenetrationKill"] )
					player processChallenge( "ch_howthe" );

				if ( data.victim.explosiveInfo["chainKill"] )
					player processChallenge( "ch_dominos" );

				player processChallenge( "ch_c4shot" );
				
				if ( isDefined(player.inLastStand) && player.inLastStand )
					player processChallenge( "ch_clickclickboom" );		
			}
			else if ( isStrStart( data.sWeapon, "claymore_" ) )
			{
				if ( isDefined( data.modifiers["revenge"] ) ) 
					player processChallenge( "ch_breakbank" );				

				player processChallenge( "ch_claymoreshot" );

				if ( player.explosiveKills[expId] > 1 )
					player processChallenge( "ch_multiclaymore" );

				if ( data.victim.explosiveInfo["returnToSender"] )
					player processChallenge( "ch_returntosender" );				

				if ( data.victim.explosiveInfo["counterKill"] )
					player processChallenge( "ch_counterclaymore" );
				
				if ( data.victim.explosiveInfo["bulletPenetrationKill"] )
					player processChallenge( "ch_howthe" );

				if ( data.victim.explosiveInfo["chainKill"] )
					player processChallenge( "ch_dominos" );
			}
			else if ( data.sWeapon == "explodable_barrel" )
			{
				//player processChallenge( "ch_redbarrelsurprise" );
			}
			else if ( data.sWeapon == "destructible_car" )
			{
				player processChallenge( "ch_carbomb" );
			}
			else if ( isStrStart( data.sWeapon, "rpg_" ) || isStrStart( data.sWeapon, "at4_" ) )
			{
				if ( player.explosiveKills[expId] > 1 )
					player processChallenge( "ch_multirpg" );
			}
		}
	}
	else if ( isSubStr( data.sMeansOfDeath,	"MOD_MELEE" ) && !isSubStr( data.sweapon, "riotshield_mp" ) )
	{
		player endMGStreak();
		
		player processChallenge( "ch_knifevet" );
		player.pers["meleeKillStreak"]++;

		if ( player.pers["meleeKillStreak"] == 3 )
			player processChallenge( "ch_slasher" );
		
		if ( player _hasPerk( "specialty_extendedmelee" ) )
			player processChallenge( "ch_extendedmelee_pro" );
		
		if ( player _hasPerk( "specialty_heartbreaker" ) )
				player processChallenge( "ch_deadsilence_pro" );
		
		vAngles = data.victim.anglesOnDeath[1];
		pAngles = player.anglesOnKill[1];
		angleDiff = AngleClamp180( vAngles - pAngles );
		if ( abs(angleDiff) < 30 )
		{
			player processChallenge( "ch_backstabber" );
			player incPlayerStat( "backstabber", 1 );
			
			if ( isDefined( player.attackers ) )
			{
				foreach ( attacker in player.attackers )
				{
					if ( attacker != data.victim )
						continue;
						
					player processChallenge( "ch_neverforget" );
					break;
				}
			}
		}

		if ( !player playerHasAmmo() )
			player processChallenge( "ch_survivor" );
			
		if ( isDefined( player.infected ) )
			data.victim processChallenge( "ch_infected" );

		if ( isDefined( data.victim.plague ) )
			player processChallenge( "ch_plague" );
		
		baseWeapon = getBaseWeaponName( data.sWeapon );
		weaponAttachments = getWeaponAttachments( data.sweapon );
		
		if ( isDefined( weaponAttachments[0] ) && weaponAttachments[0] == "tactical" )
		{
			if ( isDefined( level._challengeInfo["ch_marksman_" + baseWeapon] ) )
				player processChallenge( "ch_marksman_" + baseWeapon );
		}
	}
	else if ( isSubStr( data.sMeansOfDeath,	"MOD_MELEE" ) && isSubStr( data.sweapon, "riotshield_mp" ) )
	{
		player endMGStreak();
		
		player processChallenge( "ch_shieldvet" );
		player.pers["shieldKillStreak"]++;

		if ( player.pers["shieldKillStreak"] == 3 )
			player processChallenge( "ch_smasher" );

		if ( isDefined( player.finalKill ) )
			player processChallenge( "ch_owned" );
		
		if ( player _hasPerk( "specialty_extendedmelee" ) )
			player processChallenge( "ch_extendedmelee_pro" );
		
		vAngles = data.victim.anglesOnDeath[1];
		pAngles = player.anglesOnKill[1];
		angleDiff = AngleClamp180( vAngles - pAngles );
		if ( abs(angleDiff) < 30 )
			player processChallenge( "ch_backsmasher" );	

		if ( !player playerHasAmmo() )
			player processChallenge( "ch_survivor" );
	}
	else if ( isSubStr( data.sMeansOfDeath,	"MOD_IMPACT" ) )
	{
		if ( isStrStart( data.sWeapon, "frag_" ) )
			player processChallenge( "ch_thinkfast" );
		else if ( isStrStart( data.sWeapon, "concussion_" ) )
			player processChallenge( "ch_thinkfastconcussion" );
		else if ( isStrStart( data.sWeapon, "flash_" ) )
			player processChallenge( "ch_thinkfastflash" );
		else if ( isStrStart( data.sWeapon, "gl_" ) )
			player processChallenge( "ch_ouch" );

		if ( data.sWeapon == "throwingknife_mp" )
		{
			if ( isDefined( data.modifiers["revenge"] ) )
				player processChallenge( "ch_atm" );			

			if ( time < player.flashEndTime )
				player processChallenge( "ch_didyouseethat" );

			if ( isDefined( player.finalKill ) )
				player processChallenge( "ch_unbelievable" );
				
			player processChallenge( "ch_carnie" );
			
			if ( isDefined( data.victim.attackerData[player.guid].isPrimary ) )
				player processChallenge( "ch_its_personal" );
		}
		
		baseWeapon = getBaseWeaponName( data.sWeapon );
	
		if ( baseWeapon == "gl" )
		{
			weaponAttachments = getWeaponAttachments( data.sweapon );
			
			if ( isDefined( level._challengeInfo["ch_" + weaponAttachments[0] + "_" + "gl"] ) )
				player processChallenge( "ch_" + weaponAttachments[0] + "_" + "gl" );

			if ( isDefined( level._challengeInfo["ch_marksman_" + weaponAttachments[0]] ) )
				player processChallenge( "ch_marksman_" + weaponAttachments[0] );

			if ( isDefined( level._challengeInfo["pr_marksman_" + weaponAttachments[0]] ) )
				player processChallenge( "pr_marksman_" + weaponAttachments[0] );
		}
	}
	else if ( data.sMeansOfDeath == "MOD_HEAD_SHOT" )
	{
		weaponClass = getWeaponClass( data.sWeapon );
		
		ch_bulletDamageCommon( data, player, time, weaponClass );
	
		switch ( weaponClass )
		{
			case "weapon_smg":
				player processChallenge( "ch_expert_smg" );
				break;
			case "weapon_lmg":
				player processChallenge( "ch_expert_lmg" );
				break;
			case "weapon_assault":
				player processChallenge( "ch_expert_assault" );
				break;
		}

		if ( isDefined( data.modifiers["revenge"] ) )
			player processChallenge( "ch_colorofmoney" );

		if ( isMG( data.sWeapon ) )
		{
			player MGKill();
		}
		else if ( isStrStart( data.sWeapon, "frag_" ) )
		{
			player processChallenge( "ch_thinkfast" );
		}
		else if ( isStrStart( data.sWeapon, "concussion_" ) )
		{
			player processChallenge( "ch_thinkfastconcussion" );
		}
		else if ( isStrStart( data.sWeapon, "flash_" ) )
		{
			player processChallenge( "ch_thinkfastflash" );
		}
		else
		{
			baseWeapon = getBaseWeaponName( data.sWeapon );
			
			if ( isDefined( level._challengeInfo["ch_expert_" + baseWeapon] ) )
				player processChallenge( "ch_expert_" + baseWeapon );

			if ( isDefined( level._challengeInfo["pr_expert_" + baseWeapon] ) )
				player processChallenge( "pr_expert_" + baseWeapon );

			if ( isDefined( level._challengeInfo["ch_marksman_" + baseWeapon] ) )
				player processChallenge( "ch_marksman_" + baseWeapon );

			if ( isDefined( level._challengeInfo["pr_marksman_" + baseWeapon] ) )
				player processChallenge( "pr_marksman_" + baseWeapon );
		}
	}
	
	
	if ( data.sMeansOfDeath == "MOD_PISTOL_BULLET" || data.sMeansOfDeath == "MOD_RIFLE_BULLET" || data.sMeansOfDeath == "MOD_HEAD_SHOT" && !isKillstreakWeapon( data.sweapon ) )
	{
		// checks and processes all weapon attachment challenges
		weaponAttachments = getWeaponAttachments( data.sweapon );
		baseWeapon = getBaseWeaponName( data.sWeapon );
		
		foreach( weaponAttachment in weaponAttachments )
		{
			switch ( weaponAttachment )
			{
				case "heartbeat":
					player processChallenge( "ch_" + baseWeapon + "_" + weaponAttachment );
					continue;
				case "silencer":
					player processChallenge( "ch_" + baseWeapon + "_" + weaponAttachment );
					continue;
				case "reflex":
					if ( player playerAds() )
						player processChallenge( "ch_" + baseWeapon + "_" + weaponAttachment );
					continue;
				case "acog":
					if ( player playerAds() )
						player processChallenge( "ch_" + baseWeapon + "_" + weaponAttachment );
					continue;
				case "rof":
					player processChallenge( "ch_" + baseWeapon + "_" + weaponAttachment );
					continue;
				case "fmj":
					if ( data.victim.iDFlags & level._iDFLAGS_PENETRATION )	
						player processChallenge( "ch_" + baseWeapon + "_" + weaponAttachment );
					continue;
				default:
					continue;					
			}
		}
		
		if ( player _hasPerk( "specialty_bulletaccuracy" ) && !player playerAds() )
			player processChallenge( "ch_bulletaccuracy_pro" );
		
		if ( distanceSquared( player.origin, data.victim.origin )< 65536 )// 256^2 
		{
			if ( player _hasPerk( "specialty_heartbreaker" ) )
				player processChallenge( "ch_deadsilence_pro" );
			
			if ( player _hasPerk( "specialty_localjammer" ) )
				player processChallenge( "ch_scrambler_pro" );	
		}
		
		if ( player _hasPerk( "specialty_fastreload" ) )
			player processChallenge( "ch_sleightofhand_pro" );
		
		if ( player _hasPerk( "specialty_bling" ) && weaponAttachments.size == 2 )
			player processChallenge( "ch_bling_pro" );
		
		if ( player _hasPerk( "specialty_bulletdamage" ) )
			player processChallenge( "ch_stoppingpower_pro" );
			
		if ( player _hasPerk( "specialty_pistoldeath" ) && isDefined(player.inLastStand) && player.inLastStand && !level._dieHardMode
		&& ( !isDefined(player.inFinalStand) || !player.inFinalStand ) && ( !isDefined(player.inC4Death) || !player.inC4Death ) )
		{
			if ( isDefined( data.modifiers["revenge"] ) )
				player processChallenge( "ch_robinhood" );
				
			player processChallenge( "ch_laststand_pro" );
		}		
	}
	
	if ( player _hasperk( "specialty_onemanarmy" ) || isDefined( player.OMAClassChanged ) && player.OMAClassChanged )
		player processChallenge( "ch_onemanarmy_pro" );
	
	if ( isDefined( data.victim.isPlanting ) && data.victim.isPlanting )
		player processChallenge( "ch_bombplanter" );		

	if ( isDefined( data.victim.isDefusing ) && data.victim.isDefusing )
	{
		player processChallenge( "ch_bombdefender" );
		player incPlayerStat( "bombsdefender", 1 );
	}

	if ( isDefined( data.victim.isBombCarrier ) && data.victim.isBombCarrier && ( !isDefined( level._dd ) || !level._dd ) )
		player processChallenge( "ch_bombdown" );

	if ( isDefined( data.victim.wasTI ) && data.victim.wasTI )
		player processChallenge( "ch_tacticaldeletion" );
}

ch_bulletDamageCommon( data, player, time, weaponClass )
{
	if ( !isMG( data.sWeapon ) )
		player endMGStreak();
		
	if ( isKillstreakWeapon( data.sweapon ) )
		return;
	
	if ( player.pers["lastBulletKillTime"] == time )
		player.pers["bulletStreak"]++;
	else
		player.pers["bulletStreak"] = 1;
	
	player.pers["lastBulletKillTime"] = time;

	if ( !data.victimOnGround )
		player processChallenge( "ch_hardlanding" );
	
	assert( data.attacker == player );
	if ( !data.attackerOnGround )
		player.pers["midairStreak"]++;
	
	if ( player.pers["midairStreak"] == 2 )
		player processChallenge( "ch_airborne" );
	
	if ( time < data.victim.flashEndTime )
		player processChallenge( "ch_flashbangvet" );
	
	if ( time < player.flashEndTime )
		player processChallenge( "ch_blindfire" );
	
	if ( time < data.victim.concussionEndTime )
	{
		player processChallenge( "ch_concussionvet" );
		player incPlayerStat( "killstunnedenemy", 1 );
	}
	
	if ( time < player.concussionEndTime )
		player processChallenge( "ch_slowbutsure" );
	
	
	if ( player.pers["bulletStreak"] == 2  )
	{
		if ( isDefined( data.modifiers["headshot"] ) )
		{
			foreach ( killData in player.killsThisLife )
			{
				if ( killData.time != time )
					continue;
					
				if ( !isDefined( data.modifiers["headshot"] ) )
					continue;
					
				player processChallenge( "ch_allpro" );
			}
		}

		if ( weaponClass == "weapon_sniper" )
			player processChallenge( "ch_collateraldamage" );
	}
	
	if ( weaponClass == "weapon_pistol" )
	{
		if ( isdefined( data.victim.attackerData ) && isdefined( data.victim.attackerData[player.guid] ) )
		{
			if ( isDefined ( data.victim.attackerData[player.guid].isPrimary ) )
				player processChallenge( "ch_fastswap" );
		}
	}
	else if ( weaponClass == "weapon_shotgun" )
	{
		if ( isSubStr( data.sWeapon, "ranger" ) && isDefined( player.bothBarrels ) )
		{
			player processChallenge( "ch_bothbarrels" );
			player.bothBarrels = undefined;
		}
	}
	
	if ( data.victim.iDFlagsTime == time )
	{
		if ( data.victim.iDFlags & level._iDFLAGS_PENETRATION )
			player processChallenge( "ch_xrayvision" ); 
	}
	
	if ( data.attackerInLastStand && !data.attacker _hasPerk( "specialty_finalstand" ) )
	{
		player processChallenge( "ch_laststandvet" );
	}
	else if ( data.attackerStance == "crouch" )
	{
		player processChallenge( "ch_crouchshot" );
	}
	else if ( data.attackerStance == "prone" )
	{
		player processChallenge( "ch_proneshot" );
		if ( weaponClass == "weapon_sniper" )
		{
			player processChallenge( "ch_invisible" );
		}
	}
	
	if ( weaponClass == "weapon_sniper" )
	{
		if ( isDefined( data.modifiers["oneshotkill"] ) )
			player processChallenge( "ch_ghillie" );	
	}
	
	if ( isSubStr( data.sWeapon, "_silencer_" ) )
		player processChallenge( "ch_stealth" ); 
}

ch_roundplayed( data )
{
	player = data.player;
	
	if ( player.wasAliveAtMatchStart )
	{
		deaths = player.pers[ "deaths" ];
		kills = player.pers[ "kills" ];

		kdratio = 1000000;
		if ( deaths > 0 )
			kdratio = kills / deaths;
		
		if ( kdratio >= 5.0 && kills >= 5.0 )
		{
			player processChallenge( "ch_starplayer" );
		}
		
		if ( deaths == 0 && getTimePassed() > 5 * 60 * 1000 )
			player processChallenge( "ch_flawless" );
		
		
		if ( player.score > 0 )
		{
			switch ( level._gameType )
			{
				case "dm":
					if ( data.place < 3 && level._placement["all"].size > 3 )
						player processChallenge( "ch_victor_dm" );
					break;
			}
		}
	}
}


ch_roundwin( data )
{
	if ( !data.winner )
		return;
		
	player = data.player;
	if ( player.wasAliveAtMatchStart )
	{
		switch ( level._gameType )
		{
			case "war":
				if ( level._hardcoreMode )
				{
					player processChallenge( "ch_teamplayer_hc" );
					if ( data.place == 0 )
						player processChallenge( "ch_mvp_thc" );
				}
				else
				{
					player processChallenge( "ch_teamplayer" );
					if ( data.place == 0 )
						player processChallenge( "ch_mvp_tdm" );
				}
				break;
			case "sab":
				player processChallenge( "ch_victor_sab" );
				break;
			case "sd":
				player processChallenge( "ch_victor_sd" );
				break;
			case "ctf":
			case "dom":
			case "dm":
			case "hc":
			case "koth":
				break;
			default:
				break;
		}
	}
}

/*
char *modNames[MOD_NUM] =
{
	"MOD_UNKNOWN",
	"MOD_PISTOL_BULLET",
	"MOD_RIFLE_BULLET",
	"MOD_GRENADE",
	"MOD_GRENADE_SPLASH",
	"MOD_PROJECTILE",
	"MOD_PROJECTILE_SPLASH",
	"MOD_MELEE",
	"MOD_HEAD_SHOT",
	"MOD_CRUSH",
	"MOD_TELEFRAG",
	"MOD_FALLING",
	"MOD_SUICIDE",
	"MOD_TRIGGER_HURT",
	"MOD_EXPLOSIVE",
	"MOD_IMPACT",
};

static const char *g_HitLocNames[] =
{
	"none",
	"helmet",
	"head",
	"neck",
	"torso_upper",
	"torso_lower",
	"right_arm_upper",
	"left_arm_upper",
	"right_arm_lower",
	"left_arm_lower",
	"right_hand",
	"left_hand",
	"right_leg_upper",
	"left_leg_upper",
	"right_leg_lower",
	"left_leg_lower",
	"right_foot",
	"left_foot",
	"gun",
};

*/

// ==========================================
// Callback functions

playerDamaged( eInflictor, attacker, iDamage, sMeansOfDeath, sWeapon, sHitLoc )
{
	self endon("disconnect");
	if ( isdefined( attacker ) )
		attacker endon("disconnect");
	
	wait .05;
	WaitTillSlowProcessAllowed();

	data = spawnstruct();

	data.victim = self;
	data.eInflictor = eInflictor;
	data.attacker = attacker;
	data.iDamage = iDamage;
	data.sMeansOfDeath = sMeansOfDeath;
	data.sWeapon = sWeapon;
	data.sHitLoc = sHitLoc;
	
	data.victimOnGround = data.victim isOnGround();
	
	if ( isPlayer( attacker ) )
	{
		data.attackerInLastStand = isDefined( data.attacker.lastStand );
		data.attackerOnGround = data.attacker isOnGround();
		data.attackerStance = data.attacker getStance();
	}
	else
	{
		data.attackerInLastStand = false;
		data.attackerOnGround = false;
		data.attackerStance = "stand";
	}
	
	doMissionCallback("playerDamaged", data);
}

playerKilled( eInflictor, attacker, iDamage, sMeansOfDeath, sWeapon, sPrimaryWeapon, sHitLoc, modifiers )
{
	self.anglesOnDeath = self getPlayerAngles();
	if ( isdefined( attacker ) )
		attacker.anglesOnKill = attacker getPlayerAngles();
	
	self endon("disconnect");

	data = spawnstruct();

	data.victim = self;
	data.eInflictor = eInflictor;
	data.attacker = attacker;
	data.iDamage = iDamage;
	data.sMeansOfDeath = sMeansOfDeath;
	data.sWeapon = sWeapon;
	data.sPrimaryWeapon = sPrimaryWeapon;
	data.sHitLoc = sHitLoc;
	data.time = gettime();
	data.modifiers = modifiers;
	
	data.victimOnGround = data.victim isOnGround();
	
	if ( isPlayer( attacker ) )
	{
		data.attackerInLastStand = isDefined( data.attacker.lastStand );
		data.attackerOnGround = data.attacker isOnGround();
		data.attackerStance = data.attacker getStance();
	}
	else
	{
		data.attackerInLastStand = false;
		data.attackerOnGround = false;
		data.attackerStance = "stand";
	}

	waitAndProcessPlayerKilledCallback( data );	
	
	if ( isDefined( attacker ) && isReallyAlive( attacker ) )
		attacker.killsThisLife[attacker.killsThisLife.size] = data;	

	data.attacker notify( "playerKilledChallengesProcessed" );
}


vehicleKilled( owner, vehicle, eInflictor, attacker, iDamage, sMeansOfDeath, sWeapon )
{
	data = spawnstruct();

	data.vehicle = vehicle;
	data.victim = owner;
	data.eInflictor = eInflictor;
	data.attacker = attacker;
	data.iDamage = iDamage;
	data.sMeansOfDeath = sMeansOfDeath;
	data.sWeapon = sWeapon;
	data.time = gettime();
	
}


waitAndProcessPlayerKilledCallback( data )
{
	if ( isdefined( data.attacker ) )
		data.attacker endon("disconnect");
	
	self.processingKilledChallenges = true;
	wait 0.05;
	WaitTillSlowProcessAllowed();
	
	doMissionCallback( "playerKilled", data );
	self.processingKilledChallenges = undefined;
}

playerAssist()
{
	data = spawnstruct();

	data.player = self;

	doMissionCallback( "playerAssist", data );
}


useHardpoint( hardpointType )
{
	wait .05;
	WaitTillSlowProcessAllowed();

	data = spawnstruct();

	data.player = self;
	data.hardpointType = hardpointType;

	doMissionCallback( "playerHardpoint", data );
}


roundBegin()
{
	doMissionCallback( "roundBegin" );
}

roundEnd( winner )
{
	data = spawnstruct();
	
	if ( level._teamBased )
	{
		team = "allies";
		for ( index = 0; index < level._placement[team].size; index++ )
		{
			data.player = level._placement[team][index];
			data.winner = (team == winner);
			data.place = index;

			doMissionCallback( "roundEnd", data );
		}
		team = "axis";
		for ( index = 0; index < level._placement[team].size; index++ )
		{
			data.player = level._placement[team][index];
			data.winner = (team == winner);
			data.place = index;

			doMissionCallback( "roundEnd", data );
		}
	}
	else
	{
		for ( index = 0; index < level._placement["all"].size; index++ )
		{
			data.player = level._placement["all"][index];
			data.winner = (isdefined( winner) && (data.player == winner));
			data.place = index;

			doMissionCallback( "roundEnd", data );
		}		
	}
}

doMissionCallback( callback, data )
{
	if ( !mayProcessChallenges() )
		return;
	
	if ( getDvarInt( "disable_challenges" ) > 0 )
		return;
	
	if ( !isDefined( level._missionCallbacks[callback] ) )
		return;
	
	if ( isDefined( data ) ) 
	{
		for ( i = 0; i < level._missionCallbacks[callback].size; i++ )
			thread [[level._missionCallbacks[callback][i]]]( data );
	}
	else 
	{
		for ( i = 0; i < level._missionCallbacks[callback].size; i++ )
			thread [[level._missionCallbacks[callback][i]]]();
	}
}

monitorSprintDistance()
{
	level endon( "game_ended" );
	self endon( "spawned_player" );
	self endon( "death" );
	self endon( "disconnect" );
	
	while(1)
	{
		self waittill("sprint_begin");
		
		self.sprintDistThisSprint = 0;
		self thread monitorSprintTime();
		self monitorSingleSprintDistance();
		
		if ( self _hasperk( "specialty_marathon" ) )
			self processChallenge( "ch_marathon_pro", int( self.sprintDistThisSprint/12) );
		
		if ( self _hasperk( "specialty_lightweight" ) )
			self processChallenge( "ch_lightweight_pro", int(self.sprintDistThisSprint/12) );
	}
}

monitorSingleSprintDistance()
{
	level endon( "game_ended" );
	self endon( "spawned_player" );
	self endon( "death" );
	self endon( "disconnect" );
	self endon( "sprint_end" );
	
	prevpos = self.origin;
	while(1)
	{
		wait .1;

		self.sprintDistThisSprint += distance( self.origin, prevpos );
		prevpos = self.origin;
	}
}

monitorSprintTime()
{
	level endon( "game_ended" );
	self endon( "spawned_player" );
	self endon( "death" );
	self endon( "disconnect" );

	startTime = getTime();
	
	self waittill( "sprint_end" );
	
	sprintTime = int( getTime() - startTime );
	self incPlayerStat( "sprinttime", sprintTime );
	
	//total = self getPlayerStat( "sprinttime" );
	//println( "SprintTime: ", sprintTime, " Total:", total );
}	

monitorFallDistance()
{
	self endon("disconnect");

	self.pers["midairStreak"] = 0;
	
	while(1)
	{
		if ( !isAlive( self ) )
		{
			self waittill("spawned_player");
			continue;
		}
		
		if ( !self isOnGround() )
		{
			self.pers["midairStreak"] = 0;
			highestPoint = self.origin[2];
			while( !self isOnGround() && isAlive( self ) )
			{
				if ( self.origin[2] > highestPoint )
					highestPoint = self.origin[2];
				wait .05;
			}
			self.pers["midairStreak"] = 0;

			falldist = highestPoint - self.origin[2];
			if ( falldist < 0 )
				falldist = 0;
			
			if ( falldist / 12.0 > 15 && isAlive( self ) )
				self processChallenge( "ch_basejump" );

			if ( falldist / 12.0 > 30 && !isAlive( self ) )
				self processChallenge( "ch_goodbye" );
			
			//println( "You fell ", falldist / 12.0, " feet");
		}
		wait .05;
	}
}


// TODO: Make this challenge happen after winning while being the last person on your team
lastManSD()
{
	if ( !mayProcessChallenges() )
		return;

	if ( !self.wasAliveAtMatchStart )
		return;
	
	if ( self.teamkillsThisRound > 0 )
		return;
	
	self processChallenge( "ch_lastmanstanding" );
}


monitorBombUse()
{
	self endon( "disconnect" );
	
	for ( ;; )
	{
		result = self waittill_any_return( "bomb_planted", "bomb_defused" );
		
		if ( !isDefined( result ) )
			continue;
			
		if ( result == "bomb_planted" )
		{
			self processChallenge( "ch_saboteur" );
		}
		else if ( result == "bomb_defused" )
			self processChallenge( "ch_hero" );
	}
}


monitorLiveTime()
{
	for ( ;; )
	{
		self waittill ( "spawned_player" );
		
		self thread survivalistChallenge();
	}
}

survivalistChallenge()
{
	self endon("death");
	self endon("disconnect");
	
	wait 5 * 60;
	
	self processChallenge( "ch_survivalist" );
}


monitorStreaks()
{
	self endon ( "disconnect" );

	self.pers["airstrikeStreak"] = 0;
	self.pers["meleeKillStreak"] = 0;
	self.pers["shieldKillStreak"] = 0;

	self thread monitorMisc();

	for ( ;; )
	{
		self waittill ( "death" );
		
		self.pers["airstrikeStreak"] = 0;
		self.pers["meleeKillStreak"] = 0;
		self.pers["shieldKillStreak"] = 0;
	}
}


monitorMisc()
{
	self thread monitorMiscSingle( "destroyed_explosive" );
	self thread monitorMiscSingle( "begin_airstrike" );
	self thread monitorMiscSingle( "destroyed_car" );
	self thread monitorMiscSingle( "destroyed_helicopter" );
	self thread monitorMiscSingle( "used_uav" );
	self thread monitorMiscSingle( "used_counter_uav" );
	self thread monitorMiscSingle( "used_airdrop" );
	self thread monitorMiscSingle( "used_emp" );
	self thread monitorMiscSingle( "used_nuke" );
	self thread monitorMiscSingle( "crushed_enemy" );
	
	self waittill("disconnect");
	
	// make sure the threads end when we disconnect.
	// (this allows one disconnect waittill instead of 4 disconnect endons)
	self notify( "destroyed_explosive" );
	self notify( "begin_airstrike" );
	self notify( "destroyed_car" );
	self notify( "destroyed_helicopter" );
}

monitorMiscSingle( waittillString )
{
	// don't need to endon disconnect because we will get the notify we're waiting for when we disconnect.
	// avoiding the endon disconnect saves a lot of script variables (5 * 4 threads * 64 players = 1280)
	
	while(1)
	{
		self waittill( waittillString );
		
		if ( !isDefined( self ) )
			return;
		
		monitorMiscCallback( waittillString );
	}
}

monitorMiscCallback( result )
{
	assert( isDefined( result ) );
	switch( result )
	{
		case "begin_airstrike":
			self.pers["airstrikeStreak"] = 0;
		break;

		case "destroyed_explosive":		
			if ( self _hasPerk( "specialty_detectexplosive" ) )
				self processChallenge( "ch_detectexplosives_pro" );

			self processChallenge( "ch_backdraft" );
		break;

		case "destroyed_helicopter":
			self processChallenge( "ch_flyswatter" );
		break;

		case "destroyed_car":
			self processChallenge( "ch_vandalism" );
		break;
		
		case "crushed_enemy":
			self processChallenge( "ch_heads_up" );

			if ( isDefined( self.finalKill ) )
				self processChallenge( "ch_droppincrates" );
		break;
	}
}


healthRegenerated()
{
	if ( !isalive( self ) )
		return;
	
	if ( !mayProcessChallenges() )
		return;
	
	if ( !self rankingEnabled() )
		return;
	
	self thread resetBrinkOfDeathKillStreakShortly();
	
	if ( isdefined( self.lastDamageWasFromEnemy ) && self.lastDamageWasFromEnemy )
	{
		// TODO: this isn't always getting incremented when i regen
		self.healthRegenerationStreak++;
		if ( self.healthRegenerationStreak >= 5 )
		{
			self processChallenge( "ch_invincible" );
		}
	}
}

resetBrinkOfDeathKillStreakShortly()
{
	self endon("disconnect");
	self endon("death");
	self endon("damage");
	
	wait 1;
	
	self.brinkOfDeathKillStreak = 0;
}

playerSpawned()
{
	self.brinkOfDeathKillStreak = 0;
	self.healthRegenerationStreak = 0;
	self.pers["MGStreak"] = 0;
}

playerDied()
{
	self.brinkOfDeathKillStreak = 0;
	self.healthRegenerationStreak = 0;
	self.pers["MGStreak"] = 0;
}

isAtBrinkOfDeath()
{
	ratio = self.health / self.maxHealth;
	return (ratio <= level._healthOverlayCutoff);
}


processChallenge( baseName, progressInc, forceSetProgress )
{
	if ( !mayProcessChallenges() )
		return;
	
	if ( level._players.size < 2 )
		return;
	
	if ( !self rankingEnabled() )
		return;
	
	if ( !isDefined( progressInc ) )
		progressInc = 1;	
	
	/#
	if ( getDvarInt( "debug_challenges" ) )
		println( "CHALLENGE PROGRESS - " + baseName + ": " + progressInc );
	#/
	
	missionStatus = getChallengeStatus( baseName );
	
	if ( missionStatus == 0 )
		return;
	
	// challenge already completed
	if ( missionStatus > level._challengeInfo[baseName]["targetval"].size )
		return;

	if ( isDefined( forceSetProgress ) && forceSetProgress )
	{
		progress = progressInc;
	}
	else
	{
		progress = ch_getProgress( baseName );
		progress += progressInc;
	}

	// we've completed this tier	
	if ( progress >= level._challengeInfo[baseName]["targetval"][missionStatus] )
	{
		self ch_setProgress( baseName, level._challengeInfo[baseName]["targetval"][missionStatus] );
		self thread giveRankXpAfterWait( baseName, missionStatus );
		
		missionStatus++;		
		self ch_setState( baseName, missionStatus );
		self.challengeData[baseName] = missionStatus;
		
		self thread maps\mp\gametypes\_hud_message::challengeSplashNotify( baseName );

		self thread masteryChallengeProcess( baseName, missionStatus );
	}
	else
	{
		self ch_setProgress( baseName, progress );		
	}	
}

giveRankXpAfterWait( baseName,missionStatus )
{
	self endon ( "disconnect" );

	wait( 0.25 );
	self maps\mp\gametypes\_rank::giveRankXP( "challenge", level._challengeInfo[baseName]["reward"][missionStatus] );
}


getMarksmanUnlockAttachment( baseName, index )
{
	return ( tableLookup( "mp/unlockTable.csv", 0, baseName, 4 + index ) );
}


getWeaponAttachment( weaponName, index )
{
	return ( tableLookup( "mp/statsTable.csv", 4, weaponName, 11 + index ) );
}


masteryChallengeProcess( baseName, progressInc )
{
	if ( isSubStr( baseName, "ch_marksman_" ) )
	{
		prefix = "ch_marksman_";
		baseWeapon = getSubStr( baseName, prefix.size, baseName.size );
	}
	else
	{
		tokens = strTok( baseName, "_" );
		
		if ( tokens.size != 3 )
			return;

		baseWeapon = tokens[1];
	}
	
	if ( tableLookup( "mp/allChallengesTable.csv", 0 , "ch_" + baseWeapon + "_mastery", 1 ) == "" )
		return;

	progress = 0;	
	for ( index = 0; index <= 10; index++ )
	{
		attachmentName = getWeaponAttachment( baseWeapon, index );
		
		if ( attachmentName == "" )
			continue;
			
		if ( self isItemUnlocked( baseWeapon + " " + attachmentName ) )
			progress++;
	}
			
	processChallenge( "ch_" + baseWeapon + "_mastery", progress, true );
}


updateChallenges()
{
	self.challengeData = [];
	
	if ( !mayProcessChallenges() )
		return;

	if ( !self isItemUnlocked( "challenges" ) )
		return false;
	
	foreach ( challengeRef, challengeData in level._challengeInfo )
	{
		self.challengeData[challengeRef] = 0;
		
		if ( !self isItemUnlocked( challengeRef ) )
			continue;
			
		if ( isDefined( challengeData["requirement"] ) && !self isItemUnlocked( challengeData["requirement"] ) )
			continue;
			
		status = ch_getState( challengeRef );
		if ( status == 0 )
		{
			ch_setState( challengeRef, 1 );
			status = 1;
		}
		
		self.challengeData[challengeRef] = status;
	}
}

/*
	challenge_targetVal and rewardVal should cast their return values to int
*/
challenge_targetVal( refString, tierId )
{
	value = tableLookup( "mp/allChallengesTable.csv", CH_REF_COL, refString, CH_TARGET_COL + ((tierId-1)*2) );
	return int( value );
}


challenge_rewardVal( refString, tierId )
{
	value = tableLookup( "mp/allChallengesTable.csv", CH_REF_COL, refString, CH_REWARD_COL + ((tierId-1)*2) );
	return int( value );
}


buildChallegeInfo()
{
	level._challengeInfo = [];

	tableName = "mp/allchallengesTable.csv";

	totalRewardXP = 0;

	refString = tableLookupByRow( tableName, 0, CH_REF_COL );
	assertEx( isSubStr( refString, "ch_" ) || isSubStr( refString, "pr_" ), "Invalid challenge name: " + refString + " found in " + tableName );
	for ( index = 1; refString != ""; index++ )
	{
		assertEx( isSubStr( refString, "ch_" ) || isSubStr( refString, "pr_" ), "Invalid challenge name: " + refString + " found in " + tableName );

		level._challengeInfo[refString] = [];
		level._challengeInfo[refString]["targetval"] = [];
		level._challengeInfo[refString]["reward"] = [];

		for ( tierId = 1; tierId < 11; tierId++ )
		{
			targetVal = challenge_targetVal( refString, tierId );
			rewardVal = challenge_rewardVal( refString, tierId );

			if ( targetVal == 0 )
				break;

			level._challengeInfo[refString]["targetval"][tierId] = targetVal;
			level._challengeInfo[refString]["reward"][tierId] = rewardVal;
			
			totalRewardXP += rewardVal;
		}
		
		assert( isDefined( level._challengeInfo[refString]["targetval"][1] ) );

		refString = tableLookupByRow( tableName, index, CH_REF_COL );
	}

	tierTable = tableLookupByRow( "mp/challengeTable.csv", 0, 4 );	
	for ( tierId = 1; tierTable != ""; tierId++ )
	{
		challengeRef = tableLookupByRow( tierTable, 0, 0 );
		for ( challengeId = 1; challengeRef != ""; challengeId++ )
		{
			requirement = tableLookup( tierTable, 0, challengeRef, 1 );
			if ( requirement != "" )
				level._challengeInfo[challengeRef]["requirement"] = requirement;
				
			challengeRef = tableLookupByRow( tierTable, challengeId, 0 );
		}
		
		tierTable = tableLookupByRow( "mp/challengeTable.csv", tierId, 4 );	
	}
	
	/#
	printLn( "TOTAL CHALLENGE REWARD XP: " + totalRewardXP );
	#/
}

/#
verifyMarksmanChallenges()
{
}

verifyExpertChallenges()
{
}
#/

/#
completeAllChallenges( percentage )
{
	foreach ( challengeRef, challengeData in level._challengeInfo )
	{
		finalTarget = 0;
		finalTier = 0;
		for ( tierId = 1; isDefined( challengeData["targetval"][tierId] ); tierId++ )
		{
			finalTarget = challengeData["targetval"][tierId];
			finalTier = tierId + 1;
		}
		
		if ( percentage != 1.0 )
		{
			finalTarget--;
			finalTier--;
		}

		if ( self isItemUnlocked( challengeRef ) || percentage == 1.0 )
		{		
			self setPlayerData( "challengeProgress", challengeRef, finalTarget );
			self setPlayerData( "challengeState", challengeRef, finalTier );
		}
		
		wait ( 0.05 );
	}
	
	println( "Done unlocking challenges" );
}
#/

monitorProcessChallenge()
{
	self endon( "disconnect" );
	level endon( "game_end" );
	
	for( ;; )
	{
		if( !mayProcessChallenges() )
			return;
			
		self waittill( "process", challengeName );
		self processChallenge( challengeName );
	}	
}

monitorKillstreakProgress()
{
	self endon( "disconnect" );
	level endon( "game_end" );
	
	for( ;; )
	{
		self waittill ( "got_killstreak", streakCount );
		
		//for scr_givkillstreak
		if( !isDefined( streakCount ) )
			continue;
		
		if ( streakCount == 10 && self.killstreaks.size == 0 )
			self processChallenge( "ch_theloner" );		
		else if ( streakCount == 9 )
		{
			if ( isDefined( self.killstreaks[7] ) && isDefined( self.killstreaks[8] ) && isDefined( self.killstreaks[9] ) )
			{
				self processChallenge( "ch_6fears7" );
			}
		}
	}
}


monitorKilledKillstreak()
{
	self endon( "disconnect" );
	level endon( "game_end" );
	
	for( ;; )
	{
		self waittill( "destroyed_killstreak", weapon );
		self incPlayerStat ( "killstreakdestroyed", 1 );
		
		if ( self _hasPerk( "specialty_coldblooded" ) )
			self processChallenge( "ch_coldblooded_pro" );

		if ( isDefined( weapon ) && weapon == "stinger_mp" )
		{
			self processChallenge( "ch_marksman_stinger" );		
			self processChallenge( "pr_marksman_stinger" );		
		}
	}	
}


genericChallenge( challengeType, value )
{
	switch ( challengeType )
	{
		case "hijacker_airdrop":
			self processChallenge( "ch_smoothcriminal" );
			break;
		case "hijacker_airdrop_mega":
			self processChallenge( "ch_poolshark" );
			break;
		case "wargasm":
			self processChallenge( "ch_wargasm" );
			break;
		case "weapon_assault":
			self processChallenge( "ch_surgical_assault" );
			break;
		case "weapon_smg":
			self processChallenge( "ch_surgical_smg" );
			break;
		case "weapon_lmg":
			self processChallenge( "ch_surgical_lmg" );
			break;
		case "weapon_sniper":
			self processChallenge( "ch_surgical_sniper" );
			break;
		case "shield_damage":
			self processChallenge( "ch_shield_damage", value );
			break;
		case "shield_bullet_hits":
			self processChallenge( "ch_shield_bullet", value );
			break;
		case "shield_explosive_hits":
			self processChallenge( "ch_shield_explosive", value );
			break;
	}	
}

playerHasAmmo()
{
	primaryWeapons = self getWeaponsListPrimaries();	

	foreach ( primary in primaryWeapons )
	{
		if ( self GetWeaponAmmoClip( primary ) )
			return true;
			
		altWeapon = weaponAltWeaponName( primary );

		if ( !isDefined( altWeapon ) || (altWeapon == "none") )
			continue;

		if ( self GetWeaponAmmoClip( altWeapon ) )
			return true;
	}
	
	return false;
}