#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_LEVEL_COL	= 6;
CH_CHALLENGE_COL = 7;
CH_PRESTIGE_COL	= 8;
CH_TARGET_COL	= 9;
CH_REWARD_COL	= 10;
CH_PARENTCHALLENGE_COL = 42;
CH_SKIPVALIDATION_COL	= 43;


TIER_FILE_COL	= 4;

CH_REGULAR		= 0;
CH_DAILY			= 1;
CH_WEEKLY			= 2;

UNLOCK_TABLE_REF = "mp/unlockTable.csv";
CHALLENGE_TABLE_REF = "mp/challengeTable.csv";
ALL_CHALLENGES_TABLE_REF = "mp/allChallengesTable.csv";
DAILY_CHALLENGES_TABLE_REF = "mp/dailychallengesTable.csv";
WEEKLY_CHALLENGES_TABLE_REF = "mp/weeklychallengesTable.csv";

init()
{
	/#
	SetDevDvarIfUninitialized( "debug_challenges_by_name", "" );
	SetDevDvarIfUninitialized( "debug_challenges", 0 );
	#/
	
	precacheString(&"MP_CHALLENGE_COMPLETED");
	
	if( !mayProcessChallenges() )
		return;

	/#
	if( GetDvarInt( "debug_challenges", 0 ) == 1 )
		if( inVirtualLobby() )
			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 onPlayerConnect();
}

/#
debug_challenges_by_name()
{
	self endon( "disconnect" );
	
	while (true )
	{
		wait( 0.1 );
		
		challenge_name = GetDvar( "debug_challenges_by_name", "" );
		if( challenge_name == "" )
			continue;
		else
		{
			self processChallenge( challenge_name );
			SetDevDvar( "debug_challenges_by_name", "" );
		}
	}
}
#/

mayProcessChallenges()
{
	/#
	if( getDvarInt( "debug_challenges", 0 ) )
		return true;
	#/
	
	if( practiceRoundGame() )
		return false;
	
	return( level.rankedMatch );
}

onPlayerConnect()
{
	for(;;)
	{
		level waittill( "connected", player );
		
		if( IsBot( player ) )
			continue;
		
		/#
		if( IsPlayer( player ) && !IsAgent( player ) && !isAI( player ) && !IsTestClient( player ) )
			if( getDvarInt( "debug_challenges", 0 ) == 1 )
				player thread debug_challenges_by_name();
		#/
		
		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 monitorProcessChallenge();
		player thread monitorKillstreakProgress();
		player thread monitorFinalStandSurvival();
//		player thread monitorKilledKillstreak();
		player thread monitorADSTime();
		player thread monitorProneTime();
		player thread monitorPowerSlideTime();
		player thread monitorWeaponSwap();
		player thread monitorFlashbang();
		player thread monitorConcussion();
		player thread monitorMineTriggering();
		player thread monitorBoostJumpDistance();
		player thread monitorPlayerMatchChallenges();
		
		player NotifyOnPlayerCommand( "hold_breath", "+breath_sprint" );
		player NotifyOnPlayerCommand( "hold_breath", "+melee_breath" ); // needed for lefty button layout
		player NotifyOnPlayerCommand( "release_breath", "-breath_sprint" );
		player NotifyOnPlayerCommand( "release_breath", "-melee_breath" ); // needed for lefty button layout
		player thread monitorHoldBreath();
		
		player NotifyOnPlayerCommand( "jumped", "+goStand" );
		player thread monitorMantle();
	}
	
}

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

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

		self thread onPlayerDeath();
		self thread monitorSprintDistance();
	}
}

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

	self waittill( "death" );
		
	if( IsDefined ( self.hasScavengedAmmoThisLife ))
		self.hasScavengedAmmoThisLife = 0;
	
}

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

	for( ;; )
	{
		self waittill( "scavenger_pickup" ); 
		
		if ( self IsItemUnlocked( "specialty_scavenger" ) && self _hasPerk( "specialty_scavenger" ) && !self isJuggernaut() )
		{
			self processChallenge( "ch_scavenger_pro" );
			self.hasScavengedAmmoThisLife = 1;
		}
		
		wait( 0.05 );
	}	
}


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

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

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

	for( ;; )
	{
		self waittill( "survived_explosion", attacker ); 
		
		// don't let the player boost themselves
		if( IsDefined( attacker ) && IsPlayer( attacker ) && self == attacker )
			continue;

		if ( self IsItemUnlocked( "_specialty_blastshield" ) && self _hasPerk( "_specialty_blastshield" ) )
			self processChallenge( "ch_blastshield_pro" );
		
		waitframe();
	}	
}


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

		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 )
{
	assertEx( isDefined(self.challengeData), "Player: " + self.name + " doesnt have challenge data." );
	
	if ( isDefined( self.challengeData[name] ) )
		return self.challengeData[name];
	else
		return 0;
}


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

ch_streak_kill( event )
{
	switch( event )
	{
		case "vulcan_kill":
			processChallenge("ch_streak_orbitallaser");
			break;
		case "warbird_kill":
			processChallenge("ch_streak_warbird");
			break;
		case "paladin_kill":
			processChallenge("ch_streak_paladin");
			break;
		case "missile_strike_kill":
			processChallenge("ch_streak_missle");//ugh
			break;
		case "sentry_gun_kill":
			processChallenge("ch_streak_sentry");
			break;
		case "strafing_run_kill":
			processChallenge("ch_streak_strafing");
			break;
		case "assault_drone_kill":
			processChallenge("ch_streak_assault");
			break;
		case "goliath_kill":
			processChallenge("ch_streak_goliath");
			break;
// there is no map killstreak challenge
//		case "map_killstreak_kill":
//			processChallenge("ch_streak_assault");
//			break;
		default:
			break;
	}
}


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

	switch ( data.hardpointType )
	{
		// Assault Killstreaks
		case "uav":
			player processChallenge( "ch_uav" );
			player processChallenge( "ch_assault_streaks" );
			if ( player.pers["uav"] >= 3 )
				player processChallenge( "ch_nosecrets" );
			break;
		case "airdrop_assault":
			player processChallenge( "ch_airdrop_assault" );
			player processChallenge( "ch_assault_streaks" );
			//if ( player.pers["airdrop_assault"] >= 2 )
			//	player processChallenge( "ch_???" );
			break;
		case "airdrop_sentry_minigun":
			player processChallenge( "ch_airdrop_sentry_minigun" );
			player processChallenge( "ch_assault_streaks" );
			//if ( player.pers["airdrop_sentry_minigun"] >= 2 )
			//	player processChallenge( "ch_???" );
			break;
		// End Assault Killstreaks


		// Secret Killstreaks
		case "nuke":
			player processChallenge( "ch_nuke" );
			break;
	}
}


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

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

	// we need to reset the streak if there has been time in between airstrike kills because the challenge is for killing with 1 airstrike but this was accumulating during the entire match
	if( !isDefined( player.pers[data.sWeapon + "_streak"] ) || 
		( IsDefined( player.pers[data.sWeapon + "_streakTime"] ) && GetTime() - player.pers[data.sWeapon + "_streakTime"] > 7000 ) )
	{
		player.pers[data.sWeapon + "_streak"] = 0;
		player.pers[data.sWeapon + "_streakTime"] = GetTime();
	}

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

	switch ( data.sWeapon )
	{
		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 "sentry_minigun_mp":		// Sentry Gun
			player processChallenge( "ch_looknohands" );

			if ( isDefined( player.finalKill ) )
				player processChallenge( "ch_absentee" );
			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 "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;
	
	baseWeapon = getBaseWeaponName( data.sWeapon );
	if( isLootWeapon( baseWeapon ) )
		baseWeapon = maps\mp\gametypes\_class::getBaseFromLootVersion( baseWeapon );
	
	weaponClass = get_challenge_weapon_class( data.sWeapon, baseWeapon );
	
	// WEAPON CLASS,  ATTACHMENT UNLOCK, CAMO UNLOCK
	// LAUNCHERS HAVE A VEHICLE KILL MARKSMAN CHALLENGE
	if( weaponClass == "weapon_launcher" )
	{
		player processChallenge( "ch_launcher_kill" );
		
		if( IsDefined( level.challengeInfo["ch_vehicle_" + baseWeapon] ) )
			player processChallenge( "ch_vehicle_" + baseWeapon );
		
		if( isDefined( level.challengeInfo["ch_marksman_" + baseWeapon] ) )
			player processChallenge( "ch_marksman_" + baseWeapon );
	}
	
	// PRECISION CHALLENGES
	if( player _hasPerk( "specialty_coldblooded" ) && player _hasPerk( "specialty_spygame" ) && player _hasPerk( "specialty_heartbreaker" ) )
	{
		//no airhunt credit for a sentry
		if( !IsDefined( data.vehicle ) || !IsDefined( data.vehicle.sentrytype ) || data.vehicle.sentrytype != "prison_turret" )
			player processChallenge( "ch_precision_airhunt" );
	}
	 
	if( IsDefined( data.vehicle ) && IsDefined( data.vehicle.vehicletype ) && data.vehicle.vehicletype == "drone_recon" && IsSubStr( baseWeapon, "exoknife" ) )
		player processChallenge( "ch_precision_knife" );
	
	if( player _hasPerk( "specialty_class_blindeye" ) && (!IsDefined( data.vehicle.vehicleInfo ) || ( data.vehicle.vehicleInfo != "vehicle_tracking_drone_mp" )))
		player processChallenge( "ch_perk_blindeye" );
}


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_old( data, time ) // DEPRICATED - See "ch_kills" below for S1
{
	data.victim playerDied();
	
	if ( !isDefined( data.attacker ) || !isPlayer( data.attacker ) )
		return;
	
	player = data.attacker;
	
	// don't run challenges on bots for now, no need and causes issues
	if ( IsBot(player) )
		return;
	
	time = data.time;
	
//	// UNDONE: STREAK // X kills in a life
//	if ( player.pers["cur_kill_streak"] == 10 )
//		player processChallenge( "ch_fearless" );
	
	// UNDONE: precision - Clean House
	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 the victim is within the attackers smoke when they are killed
	if( IsDefined( data.victim.inPlayerSmokeScreen ) && data.victim.inPlayerSmokeScreen == player )
		player processChallenge( "ch_smokeemifyougotem" );

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

//	// UNDONE: FOUND WEAPON // Kill players with picked up weapon
//	if ( isdefined( player.tookWeaponFrom[ data.sWeapon ] ) )
//	{
//		if ( player.tookWeaponFrom[ data.sWeapon ] == data.victim && !isMeleeMOD( data.sMeansOfDeath) )
//			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 ) && !isMeleeMOD(killData.sMeansOfDeath) )
			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;
		}		
	}

// TODO: Track kills after weapon is planted. Using player.riotshieldentity and possible add timer to _riotshield.gsc to process ch_attach_unlock_special_postplant 
	
//	// UNDONE: SCORESTREAK KILL // challenges here?
//	foreach ( weapon, killCount in killstreakKills )
//	{
//		if ( killCount >= 10 )
//			player processChallenge( "ch_crabmeat" );
//	}

	// UNDONE: RAPID KILL // challegnes here?
	// Frenzy for 5 in a row kills with beam weapon
	if ( killsLast10s > 5 && level.teamCount[data.victim.team] <= killsLast10s )
	{
		baseWeapon = getBaseWeaponName( data.sWeapon );
		weaponClass = getWeaponClass( baseWeapon );
		//if (getWeaponClass( baseWeapon ) == "weapon_heavy" )
			//player processChallenge("ch_attach_unlock_heavy_frenzy");
//		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" );

//	// UNDONE: WILDCARD // two primaries
//	if ( player IsItemUnlocked( "specialty_twoprimaries" ) && player _hasPerk( "specialty_twoprimaries" ) && player.secondaryWeapon == data.sWeapon )
//		player processChallenge( "ch_twoprimaries_pro" );

	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" );

	// this needs to be processed before we filter out for killstreak weapons because you could the final kill with a killstreak weapon
	if ( isDefined( player.finalKill ) )
		player processChallenge( "ch_theedge" );

	// Filter out killstreak weapons... as they have already been processed?
	if ( isKillstreakWeapon( data.sWeapon ) )
		return;

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

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

	if ( player isAtBrinkOfDeath() )
	{
		player.brinkOfDeathKillStreak++;
		if ( player.brinkOfDeathKillStreak >= 3 )
		{
			player processChallenge( "ch_thebrink" );
		}
	}
	
	if( IsDefined( player.inFinalStand ) && player.inFinalStand )
	{
		if( IsDefined( data.modifiers["revenge"] ) )
			player processChallenge( "ch_robinhood" );

		if( IsDefined( player.finalKill ) )
			player processChallenge( "ch_lastresort" );			

		if( isStrStart( data.sWeapon, "c4_" ) )
			player processChallenge( "ch_clickclickboom" );		

		player processChallenge( "ch_laststandvet" );
	}		

	// Bullet weapon kills
	if ( data.sMeansOfDeath == "MOD_PISTOL_BULLET" || data.sMeansOfDeath == "MOD_RIFLE_BULLET" )
	{
		weaponClass = getWeaponClass( data.sWeapon );
		ch_bulletDamageCommon( data, player, time, weaponClass );
	
		if ( isEnvironmentWeapon( data.sWeapon ) )
		{
			player MGKill();
		}
		else
		{
			baseWeapon = getBaseWeaponName( data.sWeapon );
			if( isLootWeapon( baseWeapon ) )
				baseWeapon = maps\mp\gametypes\_class::getBaseFromLootVersion( baseWeapon );
			
			if ( isDefined( level.challengeInfo["ch_marksman_" + baseWeapon] ) )
				player processChallenge( "ch_marksman_" + baseWeapon );

//			// Camo - Longshots longshotCount
//			if( IsDefined( longshotCount ) && longshotCount > 0 )
//				if( IsDefined( level.challengeInfo["ch_camo_long_" + baseWeapon] ) )
//					player processChallenge( "ch_camo_long_" + baseWeapon );
//			
//			// Camo - Headshots
//			if( IsDefined( player.adsTime ) && player.adsTime > 0 )
//				if( IsDefined( level.challengeInfo["ch_camo_head_" + baseWeapon] ) )
//					player processChallenge( "ch_camo_head_" + baseWeapon );			
			
			if ( isDefined( level.challengeInfo["pr_marksman_" + baseWeapon] ) )
				player processChallenge( "pr_marksman_" + baseWeapon );
				
			//track daily and attachment weaponclass challenges
			weaponClass = getWeaponClass( baseWeapon );
			
			switch ( weaponClass )
			{
				case "weapon_smg":
					player processChallenge( "ch_smg_kill" );
					player processChallenge( "ch_attach_unlock_smg_kills" );
					if ( ! (player PlayerAds()) )
						player processChallenge( "ch_attach_unlock_smg_hipfire" );
					break;
				case "weapon_assault":
					player processChallenge( "ch_ar_kill" );
					player processChallenge( "ch_attach_unlock_ar_kills" );
					if ( ! (player PlayerAds()) )
						player processChallenge( "ch_attach_unlock_ar_hipfire" );
					break;
				case "weapon_shotgun":
					player processChallenge( "ch_shotgun_kill" );
					player processChallenge( "ch_attach_unlock_shotgun_kills" );
					if ( ! (player PlayerAds()) )		
						player processChallenge( "ch_attach_unlock_shotgun_hipfire" );	
					break;
				case "weapon_sniper":
					player processChallenge( "ch_sniper_kill" );
					player processChallenge( "ch_attach_unlock_sniper_kills" );
					if ( ! (player PlayerAds()) )		
						player processChallenge( "ch_attach_unlock_sniper_hipfire" );
					if (player PlayerAds())
						player processChallenge( "ch_attach_unlock_sniper_ads" );
					break;
				case "weapon_pistol":
					player processChallenge( "ch_pistol_kill" );
					player processChallenge( "ch_attach_unlock_pistol_kills" );
					if ( ! (player PlayerAds()) )		
						player processChallenge( "ch_attach_unlock_pistol_hipfire" );
					break;
				case "weapon_heavy":
					player processChallenge( "ch_heavy_kill" );
					player processChallenge( "ch_attach_unlock_heavy_kills" );
					if ( ! (player PlayerAds()) )
						player processChallenge( "ch_attach_unlock_heavy_hipfire" );
					if (player PlayerAds())
						player processChallenge( "ch_attach_unlock_heavy_ads" );
					break;
				case "weapon_special":
					player processChallenge( "ch_special_kill" );
					player processChallenge( "ch_attach_unlock_special_kills" );
					if (player PlayerAds())
						player processChallenge( "ch_attach_unlock_special_ads" );
					break;
				case "weapon_sec_special": // should only be crossbow
					if ( baseWeapon == "iw5_exocrossbow" || baseWeapon == "iw5_exocrossbowblops2")
					{
						player processChallenge( "ch_attach_unlock_special_kills" );
						if (player PlayerAds())
							player processChallenge( "ch_attach_unlock_special_ads" );
					}
					break;
				default:
			}
			
		}
	}
	else if ( isSubStr( data.sMeansOfDeath, "MOD_GRENADE" ) || isSubStr( data.sMeansOfDeath, "MOD_EXPLOSIVE" ) || isSubStr( data.sMeansOfDeath, "MOD_PROJECTILE" ) )
	{
		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 ) && 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 == "m79" || baseWeapon == "rpg" )
			{
				//daily/weekly challenge
				player processChallenge( "ch_launcher_kill" );	
				
				if ( player.explosiveKills[expId] > 1 )
				{
					player processChallenge( "pr_expert_" + baseWeapon );
				}
			}	
			
			weaponAttachments = getWeaponAttachments( data.sWeapon );
			foreach( weaponAttachment in weaponAttachments )
			{
				switch ( weaponAttachment )
				{
				case "gl":
				case "m320":
				case "gp25":
					if( isStrStart( data.sWeapon, "alt_" ) )
						player processChallenge( "ch_" + baseWeapon + "_" + weaponAttachment );
					continue;
				}
			}
			
			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" );
				
				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 ( IsDefined( data.victim.explosiveInfo["bulletPenetrationKill"] ) && data.victim.explosiveInfo["bulletPenetrationKill"] )
					player processChallenge( "ch_howthe" );

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

				player processChallenge( "ch_c4shot" );
			}
			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 ( IsDefined( data.victim.explosiveInfo["bulletPenetrationKill"] ) && data.victim.explosiveInfo["bulletPenetrationKill"] )
					player processChallenge( "ch_howthe" );

				if ( data.victim.explosiveInfo["chainKill"] )
					player processChallenge( "ch_dominos" );
			}
			else if ( isStrStart( data.sWeapon, "bouncingbetty_" ) )
			{
				player processChallenge( "ch_bouncingbetty" );
			}
			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_" ) )
			{
				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 IsItemUnlocked( "specialty_quieter" ) && player _hasPerk( "specialty_quieter" ) )
			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" );
			
			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" );
		
		baseWeapon = getBaseWeaponName( data.sWeapon );
		weaponAttachments = getWeaponAttachments( data.sWeapon );
		foreach( weaponAttachment in weaponAttachments )
		{
			switch ( weaponAttachment )
			{
			case "tactical":
				player processChallenge( "ch_" + baseWeapon + "_" + weaponAttachment );
				if( isDefined( level.challengeInfo[ "ch_marksman_" + baseWeapon ] ) )
					player processChallenge( "ch_marksman_" + baseWeapon );
				if( isDefined( level.challengeInfo[ "pr_marksman_" + baseWeapon ] ) )
					player processChallenge( "pr_marksman_" + baseWeapon );
				continue;
			}
		}
		if (baseWeapon == "iw5_riotshieldt6")
		{
			player processChallenge("ch_attach_unlock_special_meleekills");
		}
	}
	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" );
		
		//IW5 daily/weekly
		player processChallenge( "ch_riot_kill" );
		
		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" );

		if ( data.sWeapon == "throwingknife_mp" ) // JC-Todo: Ask Jordan about challenges and juggernaut
		{
			if ( isDefined( data.modifiers["revenge"] ) )
				player processChallenge( "ch_atm" );			

			if ( time < player.flashEndTime || time < player.concussionEndTime )
				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" );
		}
		
		weaponAttachments = getWeaponAttachments( data.sWeapon );
		baseWeapon = getBaseWeaponName( data.sWeapon );
		foreach( weaponAttachment in weaponAttachments )
		{
			switch ( weaponAttachment )
			{
			case "gl":
			case "m320":
			case "gp25":
				if( isStrStart( data.sWeapon, "alt_" ) )
				{
					player processChallenge( "ch_" + baseWeapon + "_" + weaponAttachment );
					if( isDefined( level.challengeInfo[ "ch_marksman_" + baseWeapon ] ) )
						player processChallenge( "ch_marksman_" + baseWeapon );
					if( isDefined( level.challengeInfo[ "pr_marksman_" + baseWeapon ] ) )
						player processChallenge( "pr_marksman_" + baseWeapon );
					player processChallenge( "ch_ouch" );
				}
				continue;
			}
		}
	}
	else if ( data.sMeansOfDeath == "MOD_HEAD_SHOT" )
	{
		weaponClass = getWeaponClass( data.sWeapon );
		
		baseWeapon = getBaseWeaponName( data.sWeapon );
		if( isLootWeapon( baseWeapon ) )
			baseWeapon = maps\mp\gametypes\_class::getBaseFromLootVersion( baseWeapon );
		
		ch_bulletDamageCommon( data, player, time, weaponClass );

		//daily weapon challenge tracking for headshot kills.
		switch ( weaponClass )
		{
			case "weapon_smg":
				player processChallenge( "ch_smg_kill" );
				player processChallenge( "ch_smg_headshot" );
				player processChallenge( "ch_attach_unlock_smg_kills" );
				if ( ! (player PlayerAds()) )
					player processChallenge( "ch_attach_unlock_smg_hipfire" );
				break;
			case "weapon_assault":
				player processChallenge( "ch_ar_kill" );
				player processChallenge( "ch_ar_headshot" );
				player processChallenge( "ch_attach_unlock_ar_kills" );
				if ( ! (player PlayerAds()) )
					player processChallenge( "ch_attach_unlock_ar_hipfire" );
				break;
			case "weapon_shotgun":
				player processChallenge( "ch_shotgun_kill" );
				player processChallenge( "ch_shotgun_headshot" );
				player processChallenge( "ch_attach_unlock_shotgun_kills" );
				if ( ! (player PlayerAds()) )
					player processChallenge( "ch_attach_unlock_shotgun_hipfire" );	
				break;
			case "weapon_sniper":
				player processChallenge( "ch_sniper_kill" );
				player processChallenge( "ch_sniper_headshot" );
				player processChallenge( "ch_attach_unlock_sniper_kills" );
				if ( ! (player PlayerAds()) )
					player processChallenge( "ch_attach_unlock_sniper_hipfire" );
				if (player PlayerAds())
					player processChallenge( "ch_attach_unlock_sniper_ads" );
				break;
			case "weapon_pistol":
				player processChallenge( "ch_pistol_kill" );
				player processChallenge( "ch_pistol_headshot" );
				player processChallenge( "ch_attach_unlock_pistol_kills" );
				if ( ! (player PlayerAds()) )
					player processChallenge( "ch_attach_unlock_pistol_hipfire" );
				break;
			case "weapon_heavy":
				player processChallenge( "ch_heavy_kill" );
				player processChallenge( "ch_heavy_headshot" );			
				player processChallenge( "ch_attach_unlock_heavy_kills" );
				if ( ! (player PlayerAds()) )
					player processChallenge( "ch_attach_unlock_heavy_hipfire" );
				if (player PlayerAds())
					player processChallenge( "ch_attach_unlock_heavy_ads" );
				break;
			case "weapon_special":
				player processChallenge( "ch_special_kill" );
				player processChallenge( "ch_attach_unlock_special_kills" );
				if (player PlayerAds())
					player processChallenge( "ch_attach_unlock_special_ads" );
				break;
			case "weapon_sec_special": // should only be crossbow
				if ( baseWeapon == "iw5_exocrossbow" || baseWeapon == "iw5_exocrossbowblops2")
				{
					player processChallenge( "ch_special_kill" );
					player processChallenge( "ch_attach_unlock_special_kills" );
					if (player PlayerAds())
						player processChallenge( "ch_attach_unlock_special_ads" );
				}
				break;
			default:
		}
	
		if ( isDefined( data.modifiers["revenge"] ) )
			player processChallenge( "ch_colorofmoney" );

		if ( isEnvironmentWeapon( 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 ) && 
		!isEnvironmentWeapon( data.sWeapon ) )
	{
		// checks and processes all weapon attachment challenges
		weaponAttachments = getWeaponAttachments( data.sWeapon );
		baseWeapon = getBaseWeaponName( data.sWeapon );
		foreach( weaponAttachment in weaponAttachments )
		{		
			switch ( weaponAttachment )
			{
			case "acog":
			case "acogsmg":
				weaponAttachment = "acog";
				if ( player PlayerAds() )
					player processChallenge( "ch_" + baseWeapon + "_" + weaponAttachment );
				continue;
			case "akimbo":
				player processChallenge( "ch_" + baseWeapon + "_" + weaponAttachment );
				continue;
			case "eotech":
			case "eotechsmg":
			case "eotechlmg":
				weaponAttachment = "eotech";
				if ( player PlayerAds() )
					player processChallenge( "ch_" + baseWeapon + "_" + weaponAttachment );
				continue;
			case "grip":
				player processChallenge( "ch_" + baseWeapon + "_" + weaponAttachment );
				continue;
			case "heartbeat":
				player processChallenge( "ch_" + baseWeapon + "_" + weaponAttachment );
				continue;
			case "hamrhybrid":
				if ( player PlayerAds() )
					player processChallenge( "ch_" + baseWeapon + "_" + weaponAttachment );
				continue;
			case "hybrid":
				if ( player PlayerAds() )
					player processChallenge( "ch_" + baseWeapon + "_" + weaponAttachment );
				continue;
			case "reflex":
			case "reflexsmg":
			case "reflexlmg":
				weaponAttachment = "reflex";
				if ( player PlayerAds() )
					player processChallenge( "ch_" + baseWeapon + "_" + weaponAttachment );
				continue;
			case "rof":
				player processChallenge( "ch_" + baseWeapon + "_" + weaponAttachment );
				continue;
			case "shotgun":
				if( isStrStart( data.sWeapon, "alt_" ) )
					player processChallenge( "ch_" + baseWeapon + "_" + weaponAttachment );
				continue;
			case "silencer":
			case "silencer01":
			case "silencer02":
			case "silencer03":
			case "silencerpistol":
			case "silencersniper":
				player processChallenge( "ch_" + baseWeapon + "_" + weaponAttachment );
				continue;
			case "thermal":
			case "thermalsmg":
				weaponAttachment = "thermal";
				if ( player PlayerAds() )
					player processChallenge( "ch_" + baseWeapon + "_" + weaponAttachment );
				continue;
			case "vzscope":
				if ( player PlayerAds() )
					player processChallenge( "ch_" + baseWeapon + "_" + weaponAttachment );
				continue;
			case "xmags":
				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 IsItemUnlocked( "specialty_autospot" ) && player _hasPerk( "specialty_autospot" ) && ( player.holdingBreath && player PlayerAds() ) )
			player processChallenge( "ch_autospot_pro" );

//		if ( player IsItemUnlocked( "specialty_bulletaccuracy" ) && player _hasPerk( "specialty_bulletaccuracy" ) && !player PlayerAds() )
//			player processChallenge( "ch_bulletaccuracy_pro" );
		
		if ( player IsItemUnlocked( "specialty_stalker" ) && player _hasPerk( "specialty_stalker" ) && player PlayerAds() )
			player processChallenge( "ch_stalker_pro" );

		if ( distanceSquared( player.origin, data.victim.origin ) < 65536 )// 256^2 
		{
			if ( player IsItemUnlocked( "specialty_quieter" ) && player _hasPerk( "specialty_quieter" ) )
				player processChallenge( "ch_deadsilence_pro" );
		}
		
		if ( player IsItemUnlocked( "specialty_fastreload" ) && player _hasPerk( "specialty_fastreload" ) )
			player processChallenge( "ch_sleightofhand_pro" );
	}
	

	if( player IsItemUnlocked( "specialty_quickdraw" ) && player _hasPerk( "specialty_quickdraw" ) && ( player.adsTime > 0 && player.adsTime < 3 ) )
		player processChallenge( "ch_quickdraw_pro" );		

	if( player IsItemUnlocked( "specialty_coldblooded" ) && player _hasPerk( "specialty_coldblooded" ) )
	{
		if( level.teambased )
		{
			uavUp = false;
			foreach( uav in level.uavmodels[ getOtherTeam( player.team ) ] )
			{
				switch( uav.uavType )
				{
				case "counter":
					continue;
				}

				uavUp = true;
				break;
			}	
			if( uavUp )
				player processChallenge( "ch_coldblooded_pro" );		
		}
		else
		{
			activeUAVs = 0;
			foreach( otherPlayer in level.players )
			{
				if( otherPlayer == player )
					continue;
				activeUAVs += level.activeUAVs[ otherPlayer.guid ];
			}
			// enemy uav is up
			if( activeUAVs > 0 )
				player processChallenge( "ch_coldblooded_pro" );
		}
	}

	if( player IsItemUnlocked( "specialty_empimmune" ) && player _hasPerk( "specialty_empimmune" ) )
	{
		if( level.teambased )
		{
			cuavUp = false;
			foreach( cuav in level.uavmodels[ getOtherTeam( player.team ) ] )
			{
				if( cuav.uavType != "counter" )
					continue;

				cuavUp = true;
				break;
			}	
			if( cuavUp || player isEMPed() )
				player processChallenge( "ch_spygame" );		
		}
		else
		{
			// enemy cuav or emp is up
			if( player.isRadarBlocked || player isEMPed() )
			{
				player processChallenge( "ch_spygame" );
			}
		}
	}

	if ( isDefined( data.victim.isPlanting ) && data.victim.isPlanting )
		player processChallenge( "ch_bombplanter" );		

	if ( isDefined( data.victim.isDefusing ) && data.victim.isDefusing )
		player processChallenge( "ch_bombdefender" );

	if ( isDefined( data.victim.isBombCarrier ) && data.victim.isBombCarrier )
		player processChallenge( "ch_bombdown" );

//	if ( isDefined( data.victim.wasTI ) && data.victim.wasTI )
//		player processChallenge( "ch_tacticaldeletion" );
		
	if( player IsItemUnlocked( "specialty_quickswap" ) && player _hasPerk( "specialty_quickswap" ) )
	{
		if( IsDefined( player.lastPrimaryWeaponSwapTime ) && ( GetTime() - player.lastPrimaryWeaponSwapTime ) < 3000 )
			player processChallenge( "ch_quickswap" );
	}

	if( player IsItemUnlocked( "specialty_extraammo" ) && player _hasPerk( "specialty_extraammo" ) )
		player processChallenge( "ch_extraammo" );

	if( IsExplosiveDamageMOD( data.sMeansOfDeath ) )
	{
		shortWeapon = maps\mp\_utility::strip_suffix( data.sWeapon, "_lefthand" );

		switch( shortWeapon )
		{
		case "frag_grenade_mp":
		case "semtex_mp":
		case "flash_grenade_mp":
		case "stun_grenade_mp":
		case "concussion_grenade_mp":
		case "emp_grenade_mp":
		case "thermobaric_grenade_mp":
		case "stun_grenade_var_mp":
		case "emp_grenade_var_mp":
		case "paint_grenade_var_mp":
		case "smoke_grenade_var_mp":
			if( player IsItemUnlocked( "specialty_fastoffhand" ) && player _hasPerk( "specialty_fastoffhand" ) )
				player processChallenge( "ch_fastoffhand" );
			break;
		}
	}

	if ( player IsItemUnlocked( "specialty_overkillpro" ) && player _hasPerk( "specialty_overkillpro" ) )
	{
		if( player.secondaryWeapon == data.sWeapon )
		{
			weaponAttachments = getWeaponAttachments( data.sWeapon );
			if( weaponAttachments.size > 0 )
				player processChallenge( "ch_secondprimary" );
		}
	}

	if( player IsItemUnlocked( "specialty_stun_resistance" ) && player _hasPerk( "specialty_stun_resistance" ) )
	{
		if( IsDefined( player.lastFlashedTime ) && ( GetTime() - player.lastFlashedTime ) < 5000 )
			player processChallenge( "ch_stunresistance" );
		else if( IsDefined( player.lastConcussedTime ) && ( GetTime() - player.lastConcussedTime ) < 5000 )
			player processChallenge( "ch_stunresistance" );
	}

	if( player IsItemUnlocked( "specialty_selectivehearing" ) && player _hasPerk( "specialty_selectivehearing" ) )
		player processChallenge( "ch_selectivehearing" );

	if( player IsItemUnlocked( "specialty_fastsprintrecovery" ) && player _hasPerk( "specialty_fastsprintrecovery" ) )
	{
		if( IsDefined( player.lastSprintEndTime ) && ( GetTime() - player.lastSprintEndTime ) < 3000 )
			player processChallenge( "ch_fastsprintrecovery" );
	}
}
*/

/*
	Daily Challenge Function
*/
processChallengeDaily( daily_index_arg, arg1, arg2 )
{
	if( !( self rankingEnabled() ) || privateMatch() )
		return;
	
	// Bounce out if daily challenge killswitch dvar is active
	if( !( GetDvarInt( "dailychallenge_killswitch", 0 ) > 0 ) )
		return;
	
	daily_index_actual = self getPlayerData( "dailyChallengeId", 0 );
	current_playlist = GetDvarInt( "scr_current_playlist", 0 );
	
	if( !isdefined( daily_index_actual ) || !isdefined( daily_index_arg ) || daily_index_actual != daily_index_arg )
		return;
	
	switch( daily_index_actual )
	{
		case 1:// "Get &&1 Kills with Shotguns in Team Deathmatch"
			weapon_class = arg1;
			if( IsDefined( weapon_class ) && level.gametype == "war" && weapon_class == "weapon_shotgun" )
				self processChallenge( "ch_daily_01" );
			break;
		
		case 2: // "With Overkill equipped, get a kill with both Primary weapons in one life &&1 times in Team Deathmatch"
			if( level.gametype == "war" )
				self processChallenge( "ch_daily_02" );
			break;			
		
		case 3: // "Get &&1 Kills with Sniper Rifles in Kill Confirmed"
			weapon_class = arg1;
			if( IsDefined( weapon_class ) && level.gametype == "conf" && weapon_class == "weapon_sniper" )
				self processChallenge( "ch_daily_03" );
			break;
		
		case 4: // "Collect &&1 Enemy tags in Kill Confirmed"
			if( level.gametype == "conf" )
				self processChallenge( "ch_daily_04" );
			break;
		
		case 5: // "Get &&1 Kills with Heavy weapons in Domination"
			weapon_class = arg1;
			if( IsDefined( weapon_class ) && level.gametype == "dom" && weapon_class == "weapon_heavy" )
				self processChallenge( "ch_daily_05" );
			break;
		
		case 6: // "Capture &&1 Control Points in Domination"
			if( level.gametype == "dom" )
				self processChallenge( "ch_daily_06" );
			break;
		
		case 7: // "Get &&1 Kills with SMGs in Domination"
			weapon_class = arg1;
			if( IsDefined( weapon_class ) && level.gametype == "dom" && weapon_class == "weapon_smg" )
				self processChallenge( "ch_daily_07" );
			break;
		
		case 8: // "Get &&1 Kills with SMGs in the Mosh Pit Playlist"
			weapon_class = arg1;
			if( IsDefined( weapon_class ) && current_playlist == 1 && weapon_class == "weapon_smg" )
				self processChallenge( "ch_daily_08" );
			break;
		
		case 9: // "Get &&1 Kills with Heavy weapons in the Mosh Pit Playlist"
			weapon_class = arg1;
			if( IsDefined( weapon_class ) && current_playlist == 1 && weapon_class == "weapon_heavy" )
				self processChallenge( "ch_daily_09" );
			break;
		
		case 10: // "Get &&1 Kills with Launchers in the Mosh Pit Playlist"
			weapon_class = arg1;
			if( IsDefined( weapon_class ) && current_playlist == 1 && weapon_class == "weapon_launcher" )
				self processChallenge( "ch_daily_10" );
			break;
		
		case 11: // "Get &&1 Kills with Scorestreaks in the Mosh Pit Playlist"
			sWeapon = arg1;
			if( IsDefined( sWeapon ) && current_playlist == 1 && isKillstreakWeapon( sWeapon ) )
				self processChallenge( "ch_daily_11" );
			break;
		
		case 12: // "Get &&1 Kills with Shotguns in Uplink"
			weapon_class = arg1;
			if( IsDefined( weapon_class ) && level.gametype == "ball" && weapon_class == "weapon_shotgun" )
				self processChallenge( "ch_daily_12" );
			break;
		
		case 13: // "Score &&1 Points in Uplink"
			score = arg1;
			if( IsDefined( score ) && level.gametype == "ball" )
				self processChallenge( "ch_daily_13", score );
			break;
		
		case 14: // "Get &&1 Kills with SMGs in Hardpoint"
			weapon_class = arg1;
			if( IsDefined( weapon_class ) && level.gametype == "hp" && weapon_class == "weapon_smg" )
				self processChallenge( "ch_daily_14" );
			break;
		
		case 15: // "Get &&1 Kills with Heavy weapons in Hardpoint"
			weapon_class = arg1;
			if( IsDefined( weapon_class ) && level.gametype == "hp" && weapon_class == "weapon_heavy" )
				self processChallenge( "ch_daily_15" );
			break;
		
		case 16: // "Capture the Flag in Capture the Flag"
			if( level.gametype == "ctf" )
				self processChallenge( "ch_daily_16" );
			break;
		
		case 17: // "Return the Flag in Capture the Flag"
			if( level.gametype == "ctf" )
				self processChallenge( "ch_daily_17" );
			break;
		
		case 18: // "Get &&1 Kills with SMGs in Capture the Flag"
			weapon_class = arg1;
			if( IsDefined( weapon_class ) && level.gametype == "ctf" && weapon_class == "weapon_smg" )
				self processChallenge( "ch_daily_18" );
			break;
		
		case 19: // "Get &&1 Kills with Heavy weapons in Capture the Flag"
			weapon_class = arg1;
			if( IsDefined( weapon_class ) && level.gametype == "ctf" && weapon_class == "weapon_heavy" )
				self processChallenge( "ch_daily_19" );
			break;
		
		case 20: // "Get &&1 Kills with lethal Exo Launcher equipment in Hardpoint"
			sWeapon = arg1;
			if( isDefined( sWeapon ) && IsSubStr( sWeapon, "_lefthand" ) )
				sWeapon = maps\mp\_utility::strip_suffix( sWeapon, "_lefthand" );
				
			if( isDefined( sWeapon ) && level.gametype == "hp" && is_lethal_equipment( sWeapon ) )
				self processChallenge( "ch_daily_20" );
			break;
		
		case 21: // "Collect &&1 Friendly tags in Kill Confirmed"
			if( level.gametype == "conf" )
				self processChallenge( "ch_daily_21" );
			break;
		
		case 22: // "Earn 4 different Killstreaks in a match"
			streakName = arg1;
			streak_found = false;
			foreach( earned_streak in self.ch_unique_earned_streaks )
			{
				if( earned_streak == streakName )
				{
					streak_found = true;
					break;
				}
			}
			if( streak_found == false )
				self.ch_unique_earned_streaks[self.ch_unique_earned_streaks.size] = streakName;
			
			if( self.ch_unique_earned_streaks.size == 4 )
				self processChallenge( "ch_daily_22" );
			
			break;
		
		case 23: // "Earn the Orbital Care Package Scorestreak &&1 times"
			streakName = arg1;
			if( streakName == "orbital_carepackage" )
				self processChallenge( "ch_daily_23" );
			break;
		
		case 24: // "Get &&1 Kills with a Primary weapon with 3 attachments in Team Deathmatch"
			sWeapon = arg1;
			weaponAttachments = getWeaponAttachments( sWeapon );
			if( level.gameType == "war" && isCACPrimaryWeapon( sWeapon ) && weaponAttachments.size == 3 )
				self processChallenge( "ch_daily_24" );
			break;
		
		case 25: // "Win &&1 CTF matches"
			if( level.gametype == "ctf" )
				self processChallenge( "ch_daily_25" );
			break;
		
		case 26: // "Win &&1 Domination matches"
			if( level.gametype == "dom" )
				self processChallenge( "ch_daily_26" );
			break;
		
		case 27: // "Win &&1 Kill Confirmed matches"
			if( level.gametype == "conf" )
				self processChallenge( "ch_daily_27" );
			break;
		
		case 28: // "Win &&1 Uplink matches"
			if( level.gametype == "ball" )
				self processChallenge( "ch_daily_28" );
			break;
		
		case 29: // "Win &&1 Momentum matches"
			if( level.gametype == "twar" )
				self processChallenge( "ch_daily_29" );
			break;
		
		case 30: // "Win &&1 Hardpoint matches"
			if( level.gametype == "hp" )
				self processChallenge( "ch_daily_30" );
			break;
		
		default:
			break;
	}
}


ch_kills( data )
{
	data.victim playerDied();
	
	if ( !isDefined( data.attacker ) || !isPlayer( data.attacker ) )
		return;
	else
		player = data.attacker;
	
	// don't run challenges on bots for now, no need and causes issues
	if ( IsBot(player) )
		return;
	
	
	//========================================================
	//				Setup Challenge Variables
	//========================================================
	
	
	oneLeftCount = 0;
	secondaryCount = 0;
	killsLast10s = 1;
	killedPlayers[data.victim.name] = data.victim.name;
	usedWeapons[data.sWeapon] = data.sWeapon;
	uniqueKills = 1;
	killstreakKills = [];
	MoD = data.sMeansOfDeath;
	time = data.time;
	
	killswithstolenweapon = 0;
	if( IsDefined( player.pickedUpWeaponFrom[data.sweapon] ) && !isMeleeMOD( MoD ) )
		killswithstolenweapon++;
	
	was_killstreak_weap = isKillstreakWeapon( data.sWeapon ); 
	was_environment_weap = isEnvironmentWeapon( data.sWeapon ); 
	
	was_headshot = false;
	if( MoD == "MOD_HEAD_SHOT" )
		was_headshot = true;
	
	was_longshot = false;
	longshotCount = 0;
	if( IsDefined( data.modifiers["longshot"] ) )
	{
		was_longshot = true;
		longshotCount++;
	}
	
	was_ads = data.was_ads;
	
	was_doublekill = false;
	if( player.recentKillCount == 2 )
		was_doublekill = true;
	
	was_triplekill = false;
	if( player.recentKillCount == 3 )
		was_triplekill = true;
	
	stance = "";
	if( IsDefined( data.attackerstance ) )
		stance = data.attackerstance;
	
	was_streaking_5 = false;
	was_streaking_10 = false;
	was_streaking_15 = false;
	was_streaking_20 = false;
	was_streaking_25 = false;
	was_streaking_30 = false;
	switch ( player.killsthislife.size + 1 )
	{
		case 5:
			was_streaking_5 = true;
			break;
		case 10:
			was_streaking_10 = true;
			break;	
		case 15:
			was_streaking_15 = true;
			break;	
		case 20:
			was_streaking_20 = true;
			break;	
		case 25:
			was_streaking_25 = true;
			break;	
		case 30:
			was_streaking_30 = true;
			break;	
		default:
			break;
	}
	
	foreach ( killData in player.killsThisLife )
	{
		if ( isCACSecondaryWeapon( killData.sWeapon ) && !isMeleeMOD(killData.sMeansOfDeath) )
			secondaryCount++;
		
		if ( isDefined( killData.modifiers["longshot"] ) )
			longshotCount++;
		
		if( longshotCount == 3 )
			player processChallenge( "ch_precision_farsight" );

		if ( time - killData.time < 10000 )
			killsLast10s++;	
		
		if( IsDefined( player.pickedUpWeaponFrom[killdata.sWeapon] ) && !isMeleeMOD( killdata.sMeansOfDeath ) )
		{
			killswithstolenweapon++;
			if( killswithstolenweapon == 5 )
				player processChallenge( "ch_humiliation_finders" );
		}
		
		
		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;
		}		
	}
	
	baseWeapon = getBaseWeaponName( data.sWeapon );
	if( isLootWeapon( baseWeapon ) )
		baseWeapon = maps\mp\gametypes\_class::getBaseFromLootVersion( baseWeapon );
	
	shortWeaponName = baseWeapon;
	
	if(string_starts_with(baseWeapon, "iw5_") )
	{
		shortWeaponName = GetSubStr(baseWeapon, 4);	   	
   	}
	
	weaponClass = get_challenge_weapon_class( data.sWeapon, baseWeapon );
	
	if( level.teamBased )
	{
		if ( level.teamCount[data.victim.pers["team"]] > 3 && player.killedPlayers.size >= level.teamCount[data.victim.pers["team"]] )
		{
			player processChallenge( "ch_precision_cleanhouse" );
		}
	}
	
	if( IsDefined( player.explosive_drone_owner ) && data.victim == player.explosive_drone_owner )
		player processChallenge( "ch_precision_protected" );
	
	//========================================================
	//					Boot Camp Challenges
	//========================================================
	
	if( isdefined( player.powerslideTime ) && ( time - player.powerslideTime < 3000 ) )
		player processChallenge( "ch_boot_hero" );
	
	if( isdefined( player.pickedUpWeaponFrom[ data.sWeapon ] ) )
	{
		if( !isMeleeMOD( MoD ) )
			player processChallenge( "ch_boot_stolen" );
	}
	
	if( stance == "crouch" )
		player processChallenge( "ch_boot_crouch" );
	
	if( stance == "prone" )
		player processChallenge( "ch_boot_prone" );
	
	//========================================================
	//					Wildcard Challenges
	//========================================================

	if(data.victim != data.attacker)
	{
		foreach(wildcard in player.loadoutWildcards)
		{
			//perk1 - kill with 2 perks from group 1
			//perk2 - kill with 2 perks from group 2
			//perk3 - kill with 2 perks from group 3
			perk_group = undefined;
			perk_group_id = 0;
			if(wildcard == "specialty_wildcard_perkslot1")
			{
				perk_group_id = 1;
				perk_group = ["specialty_extended_battery", "specialty_class_lowprofile", "specialty_class_flakjacket", "specialty_class_lightweight", "specialty_class_dangerclose"];
			}
			if(wildcard == "specialty_wildcard_perkslot2")			
			{
				perk_group_id = 2;
				perk_group = ["specialty_class_blindeye", "specialty_class_coldblooded", "specialty_class_peripherals", "specialty_class_fasthands", "specialty_class_dexterity"];
			}
			if(wildcard == "specialty_wildcard_perkslot3")			
			{
				perk_group_id = 3;
				perk_group = ["specialty_class_hardwired", "specialty_class_toughness", "specialty_class_scavenger", "specialty_class_hardline", "specialty_exo_blastsuppressor"];
			}
			
			if(IsDefined(perk_group) && (perk_group_id > 0))
			{
				count = 0; 
				foreach(perk in player.loadoutperks)
					if(array_contains(perk_group, perk))
						count++;
					
				if((count >= 2) && (perk_group_id == 1) && (wildcard == "specialty_wildcard_perkslot1"))
					player processChallenge( "ch_wild_perk1" );
				if((count >= 2) && (perk_group_id == 2) && (wildcard == "specialty_wildcard_perkslot2"))
					player processChallenge( "ch_wild_perk2" );
				if((count >= 2) && (perk_group_id == 3) &&  (wildcard == "specialty_wildcard_perkslot3"))
					player processChallenge( "ch_wild_perk3" );
			}
			
			//primary - with with primary weapon with 3 attachments
			//secondary - kill with secondary weapon with 2 attachments
			if((wildcard == "specialty_wildcard_primaryattachment") || (wildcard == "specialty_wildcard_secondaryattachment"))
			{
				weapon = data.sWeapon;
				attachments = GetWeaponAttachments(weapon);
							
				if((wildcard == "specialty_wildcard_primaryattachment") && isCACPrimaryWeapon(weapon) && (attachments.size >= 3))
					player processChallenge( "ch_wild_primary" );
				if((wildcard == "specialty_wildcard_secondaryattachment") && isCACSecondaryWeapon(weapon) && (attachments.size >= 2))
					player processChallenge( "ch_wild_secondary" );
			}
			
			//overkill - kill with both primaries in 1 life X times
			if(wildcard == "specialty_wildcard_dualprimaries")
			{
				//first make sure we had 2 primiaries in the load out
				gun1 = player.loadoutprimary;
				gun2 = player.loadoutsecondary;
				if(!isCACPrimaryWeapon(gun1) || !isCACPrimaryWeapon(gun2))
					continue;
				
				//get the weapon we just used
				weapon = getBaseWeaponName(data.sWeapon);
				
				//figure out which gun we just used
				looking_for = undefined;
				if(weapon == gun1)
					looking_for = gun2;
				else if(weapon == gun2)
					looking_for = gun1;
				else
					continue; //not using one of our primaries
				
				//look at the past kills for the other gun
				kills = player.killsthislife;
				passed = false;
				found_self = false;
				foreach(lastkill in kills)
				{
					if(getBaseWeaponName(lastkill.sweapon) == looking_for)
						passed = true;
					if(getBaseWeaponName(lastkill.sweapon) == weapon)
						found_self = true;
				}
				
				if(passed && !found_self)
				{
					player processChallenge( "ch_wild_overkill" );
					player processChallengeDaily( 2, baseWeapon, weaponClass );
				}
			}
			
			//exotac - kill with 2 different exo abilities
			if(wildcard == "specialty_wildcard_dualtacticals")		
			{
				if(is_exo_ability_weapon(player.loadoutequipment) && is_exo_ability_weapon(player.loadoutoffhand))
					player processChallenge( "ch_wild_exotac" );
			}
			
			//exobomb - kill with 2 types of grenades
			if(wildcard == "specialty_wildcard_duallethals")
			{
				if( maps\mp\gametypes\_weapons::isgrenade( data.sWeapon ) && !IsSubStr( data.sWeapon, "exoknife_mp" ) )
					if((player.loadoutequipment != "specialty_null") && maps\mp\gametypes\_class::isValidEquipment(player.loadoutequipment, false) 
					   && (player.loadoutoffhand != "specialty_null") && maps\mp\gametypes\_class::isValidEquipment(player.loadoutoffhand, false))
						player processChallenge( "ch_wild_exobomb" );
			}
			
			//exostreak - kill from score streak
			if(wildcard == "specialty_wildcard_extrastreak")		
			{
				//get the weapon we just used
				if(!was_killstreak_weap)
					continue;
				
				//make sure we have 4 killstreaks selected
				if(player.killstreaks.size < 4)
					continue;
				
				player processChallenge( "ch_wild_fourthscore" );
			}
		}
	}
	
	
	//========================================================
	//					Perk Challenges
	//========================================================
	
	if( data.victim != data.attacker )
	{
		is_on_enemy_team = !level.teambased || data.victim.team != data.attacker.team;
		
		//Low Profile Perk Challenge
		if( is_on_enemy_team && player _hasPerk( "specialty_class_lowprofile" ) )
		{
			numuav = 0;
			if( IsDefined( level.UAVModels ) )
			{
				if( level.teamBased )
				{
					numuav = level.UAVModels[ getOtherTeam( data.attacker.team ) ].size;
				}
				else
				{
					if( level.UAVModels.size > 0 )
					{
						player_uavs = 0;
						foreach( uav in level.UAVModels )
						{
							if( uav.owner == player )
							{
								player_uavs++;
							}
						}
						
						if( player_uavs > 0 )
							numuav = level.UAVModels.size - player_uavs;
						else
							numuav = level.UAVModels.size;
					}
				}
			}
			if( numuav > 0 )
				player processChallenge( "ch_perk_lowprofile" );
		}
		
		//Quickdraw Perk Challenge
		if ( is_on_enemy_team && player _hasPerk( "specialty_class_quickdraw" ) && player AdsButtonPressed( ) )
			player processChallenge( "ch_perk_quickdraw" );
		
		//Lightweight Perk Challenge
		if ( is_on_enemy_team && player _hasPerk( "specialty_class_lightweight" ) )
			player processChallenge( "ch_perk_lightweight" );
		
		//Cold Blooded Perk Challenge
		if ( is_on_enemy_team && player _hasPerk( "specialty_class_coldblooded" ) )
			player processChallenge( "ch_perk_coldblood" );
		
		//Peripherals Perk Challenge
		if ( is_on_enemy_team && player _hasPerk( "specialty_class_peripherals" ) )
			player processChallenge( "ch_perk_peripheral" );
		
		//Gungho Perk Challenge
		if( is_on_enemy_team && player _hasPerk( "specialty_class_dexterity" ) && ( player IsSprinting() || player IsPowerSliding() ) )
			player processChallenge( "ch_perk_gungho" );
		
		//Blastsuppress Perk Challenge
		if ( is_on_enemy_team && player _hasPerk( "specialty_exo_blastsuppressor" ) )
			player processChallenge( "ch_perk_blast" );
		
		//Hardwire Perk Challenge
		if ( is_on_enemy_team && player _hasPerk( "specialty_class_hardwired" ) )
		{
			uavEmpUp = false;
			if( IsDefined( level.UAVModels ) )
			{
				if( level.teamBased )
				{
					foreach( uav in level.UAVModels[ getOtherTeam( player.team ) ] )
					{
						if( uav.uavType == "counter" )
						{
							uavEmpUp = true;
							break;
						}
					}
					
					if( IsDefined( level.empOwner ) && level.empOwner.team != player.team )
					{
						uavEmpUp = true;
					}
				}
				else
				{
					foreach( uav in level.UAVModels )
					{
						if( uav.uavType == "counter" && !( uav.owner == player ) )
						{
							uavEmpUp = true;
							break;
						}
					}
				}
			}
			if( uavEmpUp )
				player processChallenge( "ch_perk_hardwire" );
		}
		
		//Fast Hands Perk Challenge
		if ( is_on_enemy_team && player _hasPerk( "specialty_class_fasthands" ) )
		{
			if ( IsDefined( player.lastPrimaryWeaponSwapTime ) && ( GetTime() - player.lastPrimaryWeaponSwapTime ) < 5000 )
				player processChallenge( "ch_perk_fasthand" );
		}
		
		//Toughness Perk Challenge
		if ( is_on_enemy_team && player _hasPerk( "specialty_class_toughness" ) )
		{
			if( IsDefined( player.lastDamageFromEnemyTargetTime ) && ( GetTime() - player.lastDamageFromEnemyTargetTime ) < 2000 )
				player processChallenge( "ch_perk_tough" );
		}
		
		//Scavenger Perk Challenge
		if ( is_on_enemy_team && player _hasPerk( "specialty_class_scavenger" ) && IsDefined( player.hasScavengedAmmoThisLife ) && player.hasScavengedAmmoThisLife == 1 )
			player processChallenge( "ch_perk_scavenge" );
	}
	
	//========================================================
	//					Exo Launcher Challenges (non-explosive/knife)
	//========================================================
	
	//stun - kill a player stunned by the stun grenade
	if( time < data.victim.concussionEndTime )
		player processChallenge( "ch_exolauncher_stun" );
	
	//smoke - kill a player in a smoke cloud
	if( IsDefined( data.victim.inPlayerSmokeScreen ))
		player processChallenge( "ch_exolauncher_smoke" );
	
	//threat - kill a player who is painted
	foreach(threat in data.victim._threatdetection.showlist)
	{
		if((threat.eventtype == "PAINT_GRENADE") && (time < threat.endTime))
		{
			player processChallenge( "ch_exolauncher_threat" );
			break; //only give 1
		}
	}

	//emp - kill an player who is empd
	if(IsDefined(data.victim.empGrenaded) && ((data.victim.empGrenaded == true) || (time < data.victim.empendtime)))
		player processChallenge( "ch_exolauncher_emp" );
		
	//tracker drone - kill a player being tracked
	if(IsDefined(data.victim.died_being_tracked) && (data.victim.died_being_tracked == true))
	{
		data.victim.died_being_tracked = undefined;
		player processChallenge( "ch_exolauncher_tracking" );
	}
	
	//========================================================
	//				Exo Ability Challenges
	//========================================================
	
	//overclock - kill while over clocking
	if(((player.loadoutequipment == "adrenaline_mp") || (player.loadoutoffhand == "adrenaline_mp" )) && (player.overclock_on == true))
	{
		player processChallenge( "ch_exoability_overclock" );
	}
	//cloak - kill after just clocking
	if((player.loadoutequipment == "exocloak_equipment_mp") || (player.loadoutoffhand == "exocloak_equipment_mp" ))
	{
		if((player.exo_cloak_on == true) || (IsDefined(player.exo_cloak_off_time) && (data.time < (player.exo_cloak_off_time + 3000))))
			player processChallenge( "ch_exoability_cloak" );
	}
	
	//hover - kill while hovering
	if(((player.loadoutequipment == "exohover_equipment_mp") || (player.loadoutoffhand == "exohover_equipment_mp" )) && (player.exo_hover_on == true))
		player processChallenge( "ch_exoability_hover" );
	
	//mute - kill while muted
	if(((player.loadoutequipment == "exomute_equipment_mp") || (player.loadoutoffhand == "exomute_equipment_mp" )) && (player.mute_on == true))
		player processChallenge( "ch_exoability_mute" );
	
	//health - kill while healing and damage
	if(((player.loadoutequipment == "ch_exoability_health") || (player.loadoutoffhand == "extra_health_mp" )) && player.exo_health_on == true)
	{
		foreach(attacker in player.attackerdata)
		{
			if(data.time < (attacker.lasttimedamaged + 4000))
			{
				player processChallenge( "ch_exoability_health" );
				break;
			}
		}
	}
	
	
	//========================================================
	//				Weapon Attachment Challenges
	//========================================================
	
	
	attachment = "";
	weaponAttachments = getWeaponAttachments( data.sWeapon );
	foreach( weaponAttachment in weaponAttachments )
	{		
		switch( weaponAttachment )
		{
			// Sight Attachments
			case "opticsacog2":
			case "opticsacog2ar":
				if( was_ads )
					player processChallenge( "ch_attach_kill_opticsacog2" );
				break;
			case "opticseotech":
			case "opticsreddot":
			case "opticstargetenhancer":
			case "variablereddot":
			case "ironsights":
				if( was_ads )
					player processChallenge( "ch_attach_kill_" + weaponAttachment );
				break;
				
			// Weapon Attachments
			case "akimbo":
			case "firerate":
			case "foregrip":
			case "heatsink":
			case "lasersight":
			case "longrange":
			case "parabolicmicrophone":
			case "quickdraw":
			case "stock":
			case "trackrounds":
			case "xmags":
			case "dualmag":
				player processChallenge( "ch_attach_kill_" + weaponAttachment );
				break;
			
				// Thermal
			case "opticsthermal":
			case "opticsthermalar":
				player processChallenge( "ch_attach_kill_opticsthermal" );
				break;
				
			// Silencer
			case "silencer01":
			case "silencerpistol":			
			case "silencersniper":
				player processChallenge( "ch_attach_kill_silencer01" );
				break;
			
			// Stabilizer
			case "stabilizer":
			case "morsstabilizer":
			case "gm6stabilizer":
			case "m990stabilizer":
			case "thorstabilizer":
				player processChallenge( "ch_attach_kill_stabilizer" );
				break;
			
			// Variable zoom
			case "scopevz":
			case "morsscopevz":
			case "m990scopevz":
			case "thorscopevz":
			case "gm6scopevz":
				player processChallenge( "ch_attach_kill_scopevz" );
				break;
		}	
	}
	
	
	//========================================================
	//				Weapon Class Challenges
	//========================================================

	
	// Bullet weapon kills
	if( ( MoD == "MOD_PISTOL_BULLET" || MoD == "MOD_RIFLE_BULLET" || MoD == "MOD_HEAD_SHOT" || baseWeapon == "iw5_m990" ) && !was_environment_weap && !was_killstreak_weap )
	{
		switch( weaponClass )
		{
			case "weapon_smg":
				player processChallenge( "ch_smg_kill" );
				if( was_headshot )
					player processChallenge( "ch_smg_headshot" );
				break;
			case "weapon_assault":
				player processChallenge( "ch_ar_kill" );
				if( was_headshot )
					player processChallenge( "ch_ar_headshot" );
				break;
			case "weapon_shotgun":
				player processChallenge( "ch_shotgun_kill" );
				if( was_headshot )
					player processChallenge( "ch_shotgun_headshot" );
				break;
			case "weapon_sniper":
				player processChallenge( "ch_sniper_kill" );
				if( was_headshot )
					player processChallenge( "ch_sniper_headshot" );
				break;
			case "weapon_pistol":
				player processChallenge( "ch_pistol_kill" );
				if( was_headshot )
					player processChallenge( "ch_pistol_headshot" );
				break;
			case "weapon_heavy":
				player processChallenge( "ch_heavy_kill" );
				if( was_headshot )
					player processChallenge( "ch_heavy_headshot" );			
				break;
			default:
		}
		
		if( MoD == "MOD_HEAD_SHOT" )
		{
			if( weaponClass == "weapon_pistol" )
				player notify( "increment_pistol_headshots" );
			else if( weaponClass == "weapon_assault" )
				player notify( "increment_ar_headshots" );
		}
		
		
		//========================================================
		//				Weapon Marksman Challenges
		//========================================================
				
		
		if( isDefined( level.challengeInfo["ch_marksman_" + baseWeapon] ) && player getChallengeStatus( "ch_marksman_" + baseWeapon ) > 0 )
			player processChallenge( "ch_marksman_" + baseWeapon );
		
		player processChallengeDaily( 1, weaponClass, undefined );
		player processChallengeDaily( 3, weaponClass, undefined );
		player processChallengeDaily( 5, weaponClass, undefined );
		player processChallengeDaily( 7, weaponClass, undefined );
		player processChallengeDaily( 8, weaponClass, undefined );
		player processChallengeDaily( 9, weaponClass, undefined );
		player processChallengeDaily( 12, weaponClass, undefined );
		player processChallengeDaily( 14, weaponClass, undefined );
		player processChallengeDaily( 15, weaponClass, undefined );
		player processChallengeDaily( 18, weaponClass, undefined );
		player processChallengeDaily( 19, weaponClass, undefined );
		player processChallengeDaily( 24, data.sWeapon, undefined );
	}
	else if( IsSubStr( baseWeapon, "microdronelauncher" ) ) // Microdrone launcher
	{
		if( isDefined( level.challengeInfo["ch_marksman_" + baseWeapon] ) && player getChallengeStatus( "ch_marksman_" + baseWeapon ) > 0 )
			player processChallenge( "ch_marksman_" + baseWeapon );
	}
	else if( IsSubStr( baseWeapon, "exocrossbow" ) ) // Crossbow
	{
		if( isDefined( level.challengeInfo["ch_marksman_" + baseWeapon] ) && player getChallengeStatus( "ch_marksman_" + baseWeapon ) > 0 )
			player processChallenge( "ch_marksman_" + baseWeapon );
	}
	
	player processChallengeDaily( 20, data.sWeapon, undefined );
	player processChallengeDaily( 11, data.sWeapon, undefined );
		
	//========================================================
	//  Attachment Unlock Challenges
	//========================================================
	
	// Bullet weapon kills
	if( ( MoD == "MOD_PISTOL_BULLET" || MoD == "MOD_RIFLE_BULLET" || MoD == "MOD_HEAD_SHOT" || baseWeapon == "iw5_m990" ) && !was_environment_weap && !was_killstreak_weap )
	{
		switch( weaponClass )
		{
			case "weapon_smg":
			case "weapon_shotgun":
			case "weapon_sniper":
			case "weapon_heavy":
			case "weapon_pistol":
			case "weapon_assault":
				if( IsSubStr( baseWeapon, "dlcgun" ) && weaponClass == "weapon_assault" )
					player processChallenge( "ch_attach_unlock_type1_" + shortWeaponName );
				else
					player processChallenge( "ch_attach_unlock_kills_" + shortWeaponName );
				
				if( was_ads )
				{
					if( IsSubStr( baseWeapon, "dlcgun" ) && weaponClass == "weapon_assault" )
						player processChallenge( "ch_attach_unlock_type3_" + shortWeaponName );
					else
						player processChallenge( "ch_attach_unlock_ads_" + shortWeaponName );
				}
				else
				{
					if( IsSubStr( baseWeapon, "dlcgun" ) && weaponClass == "weapon_assault" )
						player processChallenge( "ch_attach_unlock_type2_" + shortWeaponName );
					else
						player processChallenge( "ch_attach_unlock_hipfirekills_" + shortWeaponName );
				}
				break;
			default:
				break;
		}
		
		if ( MoD == "MOD_HEAD_SHOT")
			if( IsDefined( level.challengeInfo[ "ch_attach_unlock_headShots_" + shortWeaponName ] ) )
				player processChallenge( "ch_attach_unlock_headShots_" + shortWeaponName );
	}

	if( IsDefined( player.riotshieldEntity ) )
	{
		// if kill happens within 3 seconds
		if( time - player.riotshieldEntity.birthtime < 3000 )
			player processChallenge("ch_attach_unlock_postplant_riotshieldt6");
	}
	
	//========================================================
	// Melee kills
	//========================================================
	if( isMeleeMOD( MoD ) && !was_environment_weap && !was_killstreak_weap )
	{
		// REGULAR MELEE
		if( !isSubStr( baseWeapon, "riotshield" ) )
		{
			player.pers["meleeKillStreak"]++;
			
			weaponAttachments = getWeaponAttachments( data.sWeapon );
			foreach( weaponAttachment in weaponAttachments )
			{
				if( weaponAttachment == "tactical" )
					player processChallenge( "ch_attach_kill_tactical" );
			}
		}
		
		// RIOTSHIELD MELEE
		else if( isSubStr( baseWeapon, "riotshield" ) )
		{
			if( baseWeapon == "iw5_riotshieldt6" )
			{
				player processChallenge("ch_attach_unlock_meleekills_riotshieldt6");
				player processChallenge( "ch_marksman_iw5_riotshieldt6" );
				player processChallenge( "ch_special_kill" );
				player.pers["shieldKillStreak"]++;
			}
		}
		
		//EXOSHIELD MELEE
		if( isSubStr( baseWeapon, "exoshield" ) )
			player processChallenge( "ch_exoability_shield" );
		
		//TRACK COMBAT KNIFE AND EXO KNIFE KILLS PER MATCH
		if( isSubstr( baseWeapon, "combatknife" ) )
		{
			player notify( "increment_knife_kill" );
		}
		
	}
	
	
	//========================================================
	// MoD Impact
	//========================================================
	if( isSubStr( MoD, "MOD_IMPACT" )  && !was_environment_weap && !was_killstreak_weap )
	{
		if( IsSubStr( data.sWeapon, "exoknife_mp" ) )
		{
			player notify( "increment_knife_kill" );
			
			player processChallenge( "ch_exolauncher_knife" );
			if( was_longshot )
				player processChallenge( "ch_humiliation_hailmary" );
			
			foreach( wildcard in player.loadoutWildcards )
			{
				if( wildcard == "specialty_wildcard_duallethals" )		
				{
					equipment = maps\mp\_utility::strip_suffix( player.loadoutequipment, "_lefthand" );
					offhand = maps\mp\_utility::strip_suffix( player.loadoutoffhand, "_lefthand" );
					
					if( is_lethal_equipment( equipment ) && is_lethal_equipment( offhand ) )
					{
						player notify( "increment_duallethal_kills" );
					}
					
					break;
				}
			}
		}
		// Camo
		if ( baseWeapon == "iw5_microdronelauncher" && IsDefined( level.challengeInfo["ch_impact_iw5_microdronelauncher" ] ) )
		{
			player processChallenge( "ch_impact_iw5_microdronelauncher" );
		}
		
		//track crossbow impact kills
		if ( baseWeapon == "iw5_exocrossbow" )
		{
			if( isDefined( level.challengeInfo["ch_attach_unlock_kills_" + shortWeaponName] ) )
				player processChallenge( "ch_attach_unlock_kills_" + shortWeaponName );
			if( was_ads )
				if( isDefined( level.challengeInfo["ch_attach_unlock_ads_" + shortWeaponName] ) )
					player processChallenge( "ch_attach_unlock_ads_" + shortWeaponName );
		}
	}
	
	
	//========================================================
	// Explosive kills
	//========================================================
	if( isSubStr( MoD, "MOD_GRENADE" ) || isSubStr( MoD, "MOD_PROJECTILE" ) || isSubStr( MoD, "MOD_EXPLOSIVE" )  && !was_environment_weap && !was_killstreak_weap )
	{
		// WEAPON CLASS AND ATTACHMENT UNLOCK
		switch( weaponClass )
		{
			case "weapon_special":
				player processChallenge( "ch_special_kill" );
				break;
			default:
		}
			
		if ( baseWeapon == "iw5_exocrossbow" )
		{
			if( isDefined( level.challengeInfo["ch_attach_unlock_kills_" + shortWeaponName] ) )
				player processChallenge( "ch_attach_unlock_kills_" + shortWeaponName );
			
			if( was_ads )
				if( isDefined( level.challengeInfo["ch_attach_unlock_ads_" + shortWeaponName] ) )
					player processChallenge( "ch_attach_unlock_ads_" + shortWeaponName );
		}
		
		// FRAG GRENADE
		if( isStrStart( data.sWeapon, "frag_" ) )
		{
			player processChallenge( "ch_exolauncher_frag" );
			
			if( data.victim.explosiveInfo["cookedKill"] )
				player processChallenge( "ch_precision_masterchef" );
			
			if( data.victim.explosiveInfo["throwbackKill"] )
			{
				player processChallenge( "ch_precision_return" );
			}
		}
		
		// SEMTEX
		if( isStrStart(data.sWeapon, "semtex_" ))
		{
			player processChallenge( "ch_exolauncher_semtex" );
		}
		
		// EXPLOSIVE DRONE
		if( isStrStart(data.sWeapon, "explosive_drone" ))
		{
			player processChallenge( "ch_exolauncher_explosive_drone" );
		}
		
		
		// DESTRUCTIBLE
		if( isdefined( data.einflictor.classname ) && data.einflictor.classname == "scriptable" )
		{
			player processChallenge( "ch_boot_vandalism" );
			player processChallenge( "ch_precision_sitaware" );
		}
			// catch specifically for the BioLab/lab2 destructible containers
		if( IsDefined(data.sWeapon) && data.sWeapon == "mp_lab_gas_explosion")
			player processChallenge( "ch_precision_sitaware" );
		
		if( IsSubStr( data.sWeapon, "frag" ) || IsSubStr( data.sWeapon, "semtex" ) || IsSubStr( data.sWeapon, "explosive_drone" ) )
		{
			foreach( wildcard in player.loadoutWildcards )
			{
				if( wildcard == "specialty_wildcard_duallethals" )		
				{
					equipment = maps\mp\_utility::strip_suffix( player.loadoutequipment, "_lefthand" );
					offhand = maps\mp\_utility::strip_suffix( player.loadoutoffhand, "_lefthand" );
					
					if( is_lethal_equipment( equipment ) && is_lethal_equipment( offhand ) )
					{
						player notify( "increment_duallethal_kills" );
					}
					break;
				}
			}
		}
		
		//DANGER CLOSE
		if( isPlayerOnEnemyTeam( player ) && player _hasPerk( "specialty_class_dangerclose" ) )
			player processChallenge( "ch_perk_dangerclose" );
	}
	
	//GRENADE LAUNCHER ATTACHMENT EXPLOSION
	attachment = "";
	weaponAttachments = getWeaponAttachments( data.sWeapon );
	foreach( weaponAttachment in weaponAttachments )
	{		
		switch( weaponAttachment )
		{
			// CL Weapon Attachment
			case "gl":
				if( IsDefined( level.challengeInfo["ch_attach_kill_" + weaponAttachment] ) )
					player processChallenge( "ch_attach_kill_" + weaponAttachment );
				break;
		}	
	}
	
	// CARE PACKAGE TRAP CHALLENGE
	if( IsSubStr( MoD, "MOD_EXPLOSIVE" ) && data.sWeapon == "airdrop_trap_explosive_mp" )
		player maps\mp\gametypes\_missions::processChallenge( "ch_precision_surprise" );
	// End Explosive kills
	//	========================================================
	//					Weapon Camo Challenges
	//	========================================================
	if( MoD == "MOD_PISTOL_BULLET" || MoD == "MOD_RIFLE_BULLET" || MoD == "MOD_HEAD_SHOT" || baseWeapon == "iw5_microdronelauncher" || baseWeapon == "iw5_exocrossbow" || baseWeapon == "iw5_m990" )
	{
		if( was_longshot )
		{
			// only weapon_assault, weapon_sniper, MICRODRONE, crossbow
			if( WeaponClass == "weapon_assault" || WeaponClass == "weapon_sniper" || baseWeapon == "iw5_exocrossbow" || WeaponClass== "weapon_heavy" )
			{
				if( IsSubStr( baseWeapon, "dlcgun" ) && weaponClass == "weapon_assault" )
					player processChallenge( "ch_tier1_1_" + baseWeapon );
				else
					player processChallenge( "ch_longshot_" + baseWeapon );
			}
		}
		// hipfire, account only weapon_smg, weapon_shotgun, weapon_heavy, crossbow
		if(!was_ads)
		{
			if(WeaponClass == "weapon_shotgun" || WeaponClass == "weapon_smg" || WeaponClass == "weapon_heavy" || baseweapon == "iw5_microdronelauncher" )
			{
				if( IsDefined( level.challengeInfo["ch_hip_" + baseWeapon ] ) )
					player processChallenge( "ch_hip_" + baseWeapon );
			}
		}
		// hipfire for crossbow
		if( IsSubStr( baseWeapon, "iw5_exocrossbow" ) )
		{
			if( !was_ads )
				player processChallenge( "ch_hip_iw5_exocrossbow" );
		}
		// headshot, account only for pistols, sniper, shotgun, smg, assault
		if( was_headshot )
		{
			switch( weaponClass )
			{
				case "weapon_pistol":
				case "weapon_sniper":
				case "weapon_shotgun":
				case "weapon_smg":
				case "weapon_assault":
					if( IsSubStr( baseWeapon, "dlcgun" ) && weaponClass == "weapon_assault" )
						player processChallenge( "ch_tier1_2_" + baseWeapon );
					else
						player processChallenge( "ch_headshot_" + baseWeapon );
					break;
			}
		}
		// microdronelauncher kills
		if( baseWeapon == "iw5_microdronelauncher" )
		{
			if( IsDefined( level.challengeInfo["ch_kills_iw5_microdronelauncher"] ) )
				player processChallenge( "ch_kills_iw5_microdronelauncher" );
		}
	}
	if( weaponClass == "weapon_launcher" )
	{
		if( IsDefined( level.challengeInfo["ch_kills_" + baseWeapon] ) )
			player processChallenge( "ch_kills_" + baseWeapon );
		
		player processChallengeDaily( 10, weaponClass, undefined );
		
		if( IsSubStr( data.victim.model, "npc_exo_armor_mp_base" ) ) // if goliath
		{
			player processChallenge( "ch_launcher_kill" );
			
			if( IsDefined( level.challengeInfo["ch_vehicle_" + baseWeapon] ) )
				player processChallenge( "ch_vehicle_" + baseWeapon );
			if( IsDefined( level.challengeInfo["ch_goliath_" + baseWeapon] ) )
				player processChallenge( "ch_goliath_" + baseWeapon );
		}
	}
	
	if( was_streaking_5 || was_streaking_10 || was_streaking_15 || was_streaking_20 || was_streaking_25 || was_streaking_30 )
	{
		if( WeaponClass == "weapon_sniper" )
		{
			if( IsDefined( level.challengeInfo["ch_blood_" + baseWeapon] ))
	  			player processChallenge( "ch_blood_" + baseWeapon );
		}
		
		if( WeaponClass == "weapon_assault" || WeaponClass == "weapon_heavy" || baseWeapon == "iw5_microdronelauncher" )
		{
			if( IsSubStr( baseWeapon, "dlcgun" ) && weaponClass == "weapon_assault" )
				player processChallenge( "ch_tier2_2_" + baseWeapon );
			else
				player processChallenge( "ch_triple_" + baseWeapon );
		}
	}
	// "BareBones" no attachments
	weaponAttachments = getWeaponAttachments( data.sWeapon );
	if( weaponAttachments.size == 0 )
	{
		if( !IsSubStr( MoD, "MOD_MELEE" ) ) // no credit for melee kills
		{
			switch( WeaponClass )
			{
				case "weapon_smg":
				case "weapon_assault":
				case "weapon_shotgun":
				case "weapon_sniper":
				case "weapon_pistol":
				case "weapon_heavy":
					if( IsSubStr( baseWeapon, "dlcgun" ) && weaponClass == "weapon_assault" )
						player processChallenge( "ch_tier2_4_" + baseWeapon );
					else
						player processChallenge( "ch_barebone_" + baseWeapon );	
					break;
			}
			if( baseWeapon == "iw5_exocrossbow" )
			{
				if( IsDefined( level.challengeInfo["ch_barebone_" + baseWeapon] ) )
					player processChallenge( "ch_barebone_" + baseWeapon );
			}
		}
	}
	// "Unspecial Forces" (no perks)
	numNull = 0;
	foreach(perkslot in player.loadoutperks)
	{		
		if(perkslot == "specialty_null")
			numNull++;
		else
			break;
	}
	if(numNull == 6)
	{
		switch(WeaponClass)
		{
			case "weapon_assault":
			case "weapon_smg":
			case "weapon_shotgun":
			case "weapon_sniper":
			case "weapon_pistol":
			case "weapon_heavy":
			case "weapon_special":
				if( IsSubStr( baseWeapon, "dlcgun" ) && weaponClass == "weapon_assault" )
					player processChallenge( "ch_tier2_5_" + baseWeapon );
				else
					player processChallenge( "ch_noperk_" + baseWeapon );	
				break;
		}
	}
	
	// check Patient Zero here
	if( IsDefined( player.patient_zero ) )
	{
		player.patient_zero++; // increment it
	
		if( player.patient_zero == 3 ) // player must kill 3 survivors as an infected
			player maps\mp\gametypes\_missions::processChallenge( "ch_infect_patientzero" ); // award challenge
	}
}

get_challenge_weapon_class( weaponRef, baseWeapon )
{
	weaponClass = getWeaponClass( weaponRef );
	
	if( !IsDefined( baseWeapon ) )
	{
		baseWeapon = getBaseWeaponName( weaponRef );
		if( isLootWeapon( baseWeapon ) )
			baseWeapon = maps\mp\gametypes\_class::getBaseFromLootVersion( baseWeapon );
	}
	
	// Overrides
	if( baseWeapon == "iw5_exocrossbow" || baseWeapon == "iw5_exocrossbowblops2" )
		return "weapon_special";
	
	if( baseWeapon == "iw5_maaws" || baseWeapon == "iw5_mahem" || baseWeapon == "iw5_stingerm7" )
		return "weapon_launcher";
	
	return weaponClass;
}


ch_bulletDamageCommon( data, player, time, weaponClass )
{
	if( !isEnvironmentWeapon( data.sWeapon ) )
		player endMGStreak();
		
	if( isKillstreakWeapon( data.sWeapon ) )
		return;
	
	// don't run challenges on bots for now, no need and causes issues
	if( IsBot(player) )
		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" );
	
	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" );
		}
	}
	
	if( !IsDefined( player.inFinalStand ) || !player.inFinalStand )
	{
		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_stealthvet" ); 
}

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" );
		
		//not enough players to count as winning
		if ( level.placement["all"].size < 3 )
			return;
			
		if ( player.score > 0 )
		{
			switch ( level.gameType )
			{
				case "dm":
					if ( data.place < 3 )
					{
						player processChallenge( "ch_victor_dm" );
						player processChallenge( "ch_ffa_win" );
					}
					player processChallenge( "ch_ffa_participate" );
					break;
				case "war":
					if ( data.winner )
					{
						player processChallenge( "ch_war_win" );
					}
					player processChallenge( "ch_war_participate" );
					break;
				case "kc":
					if ( data.winner )
					{
						player processChallenge( "ch_kc_win" );
					}
					player processChallenge( "ch_kc_participate" );
					break;
				case "dd":
					if ( data.winner )
					{
						player processChallenge( "ch_dd_win" );
					}
					player processChallenge( "ch_dd_participate" );
					break;
				case "koth":
					if ( data.winner )
					{
						player processChallenge( "ch_koth_win" );
					}
					player processChallenge( "ch_koth_participate" );
					break;
				case "sab":
					if ( data.winner )
					{
						player processChallenge( "ch_sab_win" );
					}
					player processChallenge( "ch_sab_participate" );
					break;
				case "sd":
					if ( data.winner )
					{
						player processChallenge( "ch_sd_win" );
					}
					player processChallenge( "ch_sd_participate" );
					break;
				case "dom":
					if ( data.winner )
					{
						player processChallenge( "ch_dom_win" );
					}
					player processChallenge( "ch_dom_participate" );
					break;
				case "ctf":
					if ( data.winner )
					{
						player processChallenge( "ch_ctf_win" );
					}
					player processChallenge( "ch_ctf_participate" );
					break;
				case "tdef":
					if ( data.winner )
					{
						player processChallenge( "ch_tdef_win" );
					}
					player processChallenge( "ch_tdef_participate" );
					break;
				case "hp":
					if ( data.winner )
					{
						player processChallenge( "ch_hp_win" );
					}
					player processChallenge( "ch_hp_participate" );
			}
		}
	}
}


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":
			case "hp":
				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 )
{
	if ( !isPlayer( self ) )
		return;

	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";
		
	}
	
	if ( IsDefined( self ) && IsDefined( attacker ) && IsDefined( self.team ) && IsDefined( attacker.team ) )
	{
		//Flak Jacket Perk Challenge
		if( ( self.team != attacker.team ) && self _hasPerk( "specialty_class_flakjacket" ) && IsExplosiveDamageMOD( data.sMeansOfDeath ) && isReallyAlive( self ) && sWeapon != "killstreak_solar_mp" )
		{
			self processChallenge( "ch_perk_flakjack" );
		}
		//Toughness Perk Challenge
		if( ( self.team != attacker.team ) && self _hasPerk( "specialty_class_toughness" ) )
			self.lastDamageFromEnemyTargetTime = GetTime();
	}

	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";
	}
	
	// inflictor not guaranteed to be around after waiting
	ads_ratio = 0;
	if( IsDefined( data.eInflictor ) && IsDefined( data.eInflictor.firedAds ) )
		ads_ratio = data.eInflictor.firedAds;
	else if( IsDefined( attacker ) && IsPlayer( attacker ) )
		ads_ratio = attacker PlayerAds();
	
	data.was_ads = false;
	if( ads_ratio >= 0.2 )
		data.was_ads = true;
	
	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();
	
	doMissionCallback( "vehicleKilled", data );

//	if ( IsDefined( attacker ) && isPlayer( attacker ) && ( !IsDefined( owner ) || attacker != owner ) && !isKillstreakWeapon( sWeapon ) )
//	{
//		
//				
//		switch( sWeapon )
//		{
//		case "stinger_mp":
//			// stinger marksman is kill n vehicles, stinger expert is kill n helicopters
//			if( IsDefined( vehicle.helitype ) && ( vehicle.helitype == "littlebird" || vehicle.heliType == "airdrop" ) )
//			{
//				attacker processChallenge( "pr_expert_stinger" );
//			}
//			attacker processChallenge( "ch_marksman_stinger" );
//			attacker processChallenge( "pr_marksman_stinger" );
//			break;
//
//		// all other weapons can process this vehicle kill as a kill
//		default:
//			baseWeapon = getBaseWeaponName( sWeapon );
//
//			if( IsDefined( level.challengeInfo["ch_marksman_" + baseWeapon] ) )
//				attacker processChallenge( "ch_marksman_" + baseWeapon );
//
//			if( IsDefined( level.challengeInfo["pr_marksman_" + baseWeapon] ) )
//				attacker processChallenge( "pr_marksman_" + baseWeapon );
//			break;
//		}
//
//		// only check lock-on weapon for the faster lock-on challenge
//		switch( sWeapon )
//		{
//		case "stinger_mp":
//			if( attacker IsItemUnlocked( "specialty_fasterlockon" ) && attacker _hasPerk( "specialty_fasterlockon" ) )
//				attacker processChallenge( "ch_fasterlockon" );
//			break;
//		}
//		
//		if ( attacker IsItemUnlocked( "specialty_blindeye" ) && attacker _hasPerk( "specialty_blindeye" ) )
//		{
//			// we give blind eye when you spawn and if you're in a scrambler, so make sure that isn't the case now
//			process = true;
//			if( IsDefined( attacker.scramProxyPerk ) && attacker.scramProxyPerk )
//				process = false;
//			if( IsDefined( attacker.spawnPerk ) && attacker.spawnPerk )
//				process = false;
//
//			if( process )
//				attacker processChallenge( "ch_blindeye_pro" );
//		}
//	}
}

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 )
{
	// since this is threaded we need this endon in case they player disconnects, this fixes an sre when calling the ac130 and leaving the game as you are entering it
	self endon( "disconnect" );

	wait .05;
	WaitTillSlowProcessAllowed();

	data = spawnstruct();

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

	doMissionCallback( "playerHardpoint", data );
}


roundBegin()
{
	/#
	if( GetDvarInt( "debug_challenges", 0 ) == 1 )
		if( inVirtualLobby() )
			return;
	#/
	
	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 ) && isPlayer( 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 IsItemUnlocked( "specialty_longersprint" ) && self _hasPerk( "specialty_longersprint" ) )
			self processChallenge( "ch_longersprint_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" );
	
	self waittill( "sprint_end" );
	
	self.lastSprintEndTime = GetTime();
}	

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

//	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 isEMPed() )
				self processChallenge( "ch_boot_shortcut" );

			if( falldist / 12.0 > 30 && !isAlive( self ) && self isEMPed() )
				self processChallenge( "ch_boot_gravity" );
		}
		wait .05;
	}
}

//Track boost jump total height for challenge
monitorBoostJumpDistance()
{
	//ch_exomech_frontier
	//327360 feet to space
	level endon( "game_ended" );
	self endon( "disconnect" );
	
	while(1)
	{
		if ( !isAlive( self ) )
		{
			self waittill("spawned_player");
			continue;
		}
		
		self waittill( "exo_boost" );
		
		if ( !self isOnGround() )
		{
			highestPoint = self.origin[2];
			startingPoint = self.origin[2];
			while( !self isOnGround() && isAlive( self ) )
			{
				if ( self.origin[2] > highestPoint )
					highestPoint = self.origin[2];
				wait .05;
			}

			jumpHeight = highestPoint - startingPoint;
			
			if ( jumpHeight < 0 )
				jumpHeight = 0;
			
			self processChallenge( "ch_exomech_frontier", int( ceil( jumpHeight/12/10 )) ); //stored in increments of 10 feet
		}
		wait .05;
	}
}

monitorPlayerMatchChallenges()
{
	//combatknife or exoknife kills
	self thread monitorMatchChallenges( "increment_knife_kill", 15, "ch_precision_slice" );
	//stuck kills
	self thread monitorMatchChallenges( "increment_stuck_kills", 5, "ch_precision_ticktick" );
	//pistol headshots
	self thread monitorMatchChallenges( "increment_pistol_headshots", 10, "ch_precision_pistoleer" );
	//ar headshots
	self thread monitorMatchChallenges( "increment_ar_headshots", 5, "ch_precision_headhunt" );
	//one shot one kill sniper
	self thread monitorMatchChallenges( "increment_sharpshooter_kills", 10, "ch_precision_sharpshoot" );
	//one shot one kill shotgun
	self thread monitorMatchChallenges( "increment_oneshotgun_kills", 10, "ch_precision_cqexpert" );
	//dual lethal kills
	self thread monitorMatchChallenges( "increment_duallethal_kills", 5, "ch_precision_dangerclose" );
}

monitorMatchChallenges( sNotify, target_value, challenge )
{
	level endon( "game_ended" );
	self endon( "disconnect" );
	
	if( !IsDefined( game[ challenge ] ) )
	   game[ challenge ] = [];
	
	if( !IsDefined( game[ challenge ][ self.guid ] ) )
		game[ challenge ][ self.guid ] = 0;
	
	self thread remove_tracking_on_disconnect( challenge );
		
	
	while( true )
	{
		self waittill( sNotify );
		
		counter = game[ challenge ][ self.guid ];
		counter++;
		game[ challenge ][ self.guid ] = counter;
		if( counter == target_value )
			self processChallenge( challenge );
	}
}

remove_tracking_on_disconnect( challenge )
{
	level endon( "game_ended" );
	
	self waittill( "disconnect" );
	
	if( IsDefined( game[ challenge ][ self.guid ] ) )
		game[ challenge ][ self.guid ] = undefined;
}

// 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;
	
	if ( isDefined(self) )
		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 endon("disconnect");
	
	while( true )
	{
		waittillString = self waittill_any_return_no_endon_death( "destroyed_explosive", "begin_airstrike", "destroyed_car", "destroyed_car" );
		
		monitorMiscCallback( waittillString );
	}
}

monitorMiscCallback( result )
{
	assert( isDefined( result ) );
	
	switch( result )
	{
		case "begin_airstrike":
		{
			self.pers["airstrikeStreak"] = 0;
			break;
		}
		case "destroyed_explosive":
		{
			self processChallenge( "ch_backdraft" );
			
			if( self IsItemUnlocked( "specialty_detectexplosive" ) && self _hasPerk( "specialty_detectexplosive" ) )
			{
				self processChallenge( "ch_detectexplosives_pro" );
			}

			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 ) // self == player
{
	if ( !mayProcessChallenges() )
		return;
	
	if( (level.players.size < 2) && !getdvarint( "force_ranking" ) )
	{
		debug_challenges = undefined;
		/# debug_challenges = getDvarInt( "debug_challenges", 0 ); #/
		if( IsDefined( debug_challenges ) )
		{
			if( debug_challenges == 0 )
				return;
		}
		else
			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;

	// get the current progress from the player data
	currentProgress = ch_getProgress( baseName );
	
	if ( isDefined( forceSetProgress ) && forceSetProgress )
	{
		newProgress = progressInc;
		assertex( newProgress >= currentProgress, "Attempted progress regression (forceSet) for challenge '" + baseName + "' - from " + currentProgress + " to " + newProgress + " for player " + self.name );
	}
	else if ( isWeaponClassChallenge( baseName ) )
	{
		// since the self.trackingweapon<kills> already includes the progressInc
		newProgress = currentProgress;
	}
	else
	{
		newProgress = currentProgress + progressInc;
		assertex( newProgress >= currentProgress, "Attempted progress regression (inc) for challenge '" + baseName + "' - from " + currentProgress + " to " + newProgress + " for player " + self.name );
	}
	
	// check whether we've completed any tiers
	tiersReached = 0;
	targetProgress = level.challengeInfo[baseName]["targetval"][missionStatus];
	while ( isDefined( targetProgress ) && newProgress >= targetProgress )
	{
		tiersReached++;
		targetProgress = level.challengeInfo[baseName]["targetval"][missionStatus + tiersReached];
	}

	// defensive, don't set the progress if it would be a regression
	if ( currentProgress < newProgress )
		self ch_setProgress( baseName, newProgress );
	
	if ( tiersReached > 0 )
	{
		originalMissionStatus = missionStatus;

		while ( tiersReached )
		{
			self thread giveRankXpAfterWait( baseName, missionStatus );

			challengeID = getchallengeid( baseName, missionStatus );
			self ChallengeNotification( challengeID );
			challengeID_string = toString( challengeID );
			ch_uid = int( GetSubStr( challengeID_string, 0, ( challengeID_string.size - 2 ) ) );

			if ( !isDefined( game["challengeStruct"]["challengesCompleted"][self.guid] ) )
			{
				game["challengeStruct"]["challengesCompleted"][self.guid] = [];
			}

			//remove duplicate entries.
			chFound = false;
			foreach( challenge in game["challengeStruct"]["challengesCompleted"][self.guid] )
			{
				if( challenge == ch_uid )
					chFound = true;
			}
			if ( !chFound )
				game["challengeStruct"]["challengesCompleted"][self.guid][( game["challengeStruct"]["challengesCompleted"][self.guid].size )] = ch_uid;

			//Check for masterychallenges
			if( ( missionStatus >= level.challengeInfo[baseName]["targetval"].size ) && level.challengeInfo[baseName]["parent_challenge"] != "" )
				self maps\mp\gametypes\_missions::processChallenge( level.challengeInfo[baseName]["parent_challenge"] );

			missionStatus++;
			tiersReached--;

			rewardItemID = getchallengerewarditem( challengeID );
			// only log completed challenges to the match summary for challenges that actually award an item 
			// (some challenges are just awarding XP and we do not validate those)
			if( rewardItemID != 0 )
			{
				self maps\mp\_matchdata::logCompletedChallenge( challengeID );
			}
		}

		if( !( IsSubStr( baseName, "ch_limited_bloodshed" ) ) )
			self thread maps\mp\gametypes\_hud_message::challengeSplashNotify( baseName, originalMissionStatus, missionStatus );

		self ch_setState( baseName, missionStatus );
		self.challengeData[baseName] = missionStatus;
	}
}

//checkGameModeMastery( player )
//{
//	player endon( "disconnect" );
//	
//	gameModeChallenges = [];
//	gameModeChallenges[0] 	= "ch_" + level.gametype + "_wins";
//	gameModeChallenges[1] 	= "ch_" + level.gametype + "_veteran";
//	gameModeChallenges[2] 	= "ch_" + level.gametype + "_superior";
//	gameModeChallenges[3] 	= "ch_" + level.gametype + "_mvp";
//
//	switch( level.gameType )
//	{
//		case "war":
//			gameModeChallenges[0] 	= "ch_war_triple";
//			gameModeChallenges[5] 	= "ch_war_avenger";
//			gameModeChallenges[6] 	= "ch_war_savior";
//			gameModeChallenges[7] 	= "ch_war_star";
//			gameModeChallenges[8] 	= "ch_war_crushing";
//			break;
//		case "dm":
//			gameModeChallenges[4] 	= "ch_dm_merciless";
//			gameModeChallenges[5] 	= "ch_dm_crushing";
//			gameModeChallenges[6] 	= "ch_dm_star";
//			gameModeChallenges[7] 	= "ch_dm_double";
//			gameModeChallenges[8] 	= "ch_dm_triple";
//			break;
//		case "dom":
//			gameModeChallenges[4] 	= "ch_dom_aggression";
//			gameModeChallenges[5] 	= "ch_dom_alphalock";
//			gameModeChallenges[6] 	= "ch_dom_bravolock";
//			gameModeChallenges[7] 	= "ch_dom_charlielock";
//			gameModeChallenges[8] 	= "ch_dom_tripcap";
//			break;
//		case "ctf":
//			gameModeChallenges[4] 	= "ch_ctf_crushing";
//			gameModeChallenges[5] 	= "ch_ctf_flag_defend";
//			gameModeChallenges[6] 	= "ch_ctf_flag_carry";
//			gameModeChallenges[7] 	= "ch_ctf_flag_return";
//			gameModeChallenges[8] 	= "ch_ctf_flag_capture";
//			break;
//		case "sd":
//			gameModeChallenges[4] 	= "ch_sd_destroyer";
//			gameModeChallenges[5] 	= "ch_sd_bombdefuse";
//			gameModeChallenges[6] 	= "ch_sd_ninja";
//			gameModeChallenges[7] 	= "ch_sd_protector";
//			gameModeChallenges[8] 	= "ch_sd_interrupt";
//			break;
//		case "sr":
//			gameModeChallenges[4] 	= "ch_sr_destroyer";
//			gameModeChallenges[5] 	= "ch_sr_bombdefuse";
//			gameModeChallenges[6] 	= "ch_sr_medic";
//			gameModeChallenges[7] 	= "ch_sr_protector";
//			gameModeChallenges[8] 	= "ch_sr_interrupt";
//			break;
//		case "hp":
//			gameModeChallenges[4] 	= "ch_hp_aggression";
//			gameModeChallenges[5] 	= "ch_hp_attacker";
//			gameModeChallenges[6] 	= "ch_hp_defender";
//			gameModeChallenges[7] 	= "ch_hp_crushing";
//			gameModeChallenges[8] 	= "ch_hp_star";
//			break;
//		case "conf":
//			gameModeChallenges[4] 	= "ch_conf_collector";
//			gameModeChallenges[5] 	= "ch_conf_denial";
//			gameModeChallenges[6] 	= "ch_conf_save_yourself";
//			gameModeChallenges[7] 	= "ch_conf_crushing";
//			gameModeChallenges[8] 	= "ch_conf_savior";
//			break;
//		case "twar":
//			gameModeChallenges[4] 	= "ch_twar_aggression";
//			gameModeChallenges[5] 	= "ch_twar_attacker";
//			gameModeChallenges[6] 	= "ch_twar_defender";
//			gameModeChallenges[7] 	= "ch_twar_crushing";
//			gameModeChallenges[8] 	= "ch_twar_blitzkrieg";
//			break;
//		case "ball":
//			gameModeChallenges[4] 	= "ch_ball_touchdown";
//			gameModeChallenges[5] 	= "ch_ball_fieldgoal";
//			gameModeChallenges[6] 	= "ch_ball_crushing";
//			gameModeChallenges[7] 	= "ch_ball_assist";
//			gameModeChallenges[8] 	= "ch_ball_alleyoop";
//			break;
//		case "infect":
//			gameModeChallenges[4] 	= "ch_infect_crushing";
//			gameModeChallenges[5] 	= "ch_infect_survivor";
//			gameModeChallenges[6] 	= "ch_infect_patientzero";
//			gameModeChallenges[7] 	= "ch_infect_cleanup";
//			gameModeChallenges[8] 	= "ch_infect_tooclose";
//			break;
//		case "gun":
//			gameModeChallenges[4] 	= "ch_gun_crushing";
//			gameModeChallenges[5] 	= "ch_gun_regression";
//			gameModeChallenges[6] 	= "ch_gun_double";
//			gameModeChallenges[7] 	= "ch_gun_triple";
//			gameModeChallenges[8] 	= "ch_gun_star";
//			break;
//		default:
//			return;
//	}
//	
//	allChallengesComplete = true;
//	
//	foreach( challengeName in gameModeChallenges )
//	{
//		missionStatus = player getChallengeStatus( challengeName );
//	
//		if( missionStatus == 0 )
//			continue;
//		
//		// challenge not completed
//		if( missionStatus < level.challengeInfo[challengeName]["targetval"].size )
//		{
//			allChallengesComplete = false;
//			break;
//		}
//	}
//	
//	if( allChallengesComplete )
//		player maps\mp\gametypes\_missions::processChallenge( "ch_" + level.gametype + "_mastery" ); 
//	
//}

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

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


getMarksmanUnlockAttachment( baseName, index )
{
	return ( tableLookup( UNLOCK_TABLE_REF, 0, baseName, 4 + index ) );
}


masteryChallengeProcess( baseWeapon )
{
	if ( tableLookup( ALL_CHALLENGES_TABLE_REF, 0 , "ch_" + baseWeapon + "_mastery", 1 ) == "" )
		return;

	progress = 0;	
	all_attachments = getWeaponAttachmentFromStats( baseWeapon );
	foreach( attachmentName in all_attachments )
	{
		if ( attachmentName == "" )
			continue;
			
		if ( self maps\mp\gametypes\_class::isAttachmentUnlocked( baseWeapon, attachmentName ) )
			progress++;
	}
			
	processChallenge( "ch_" + baseWeapon + "_mastery", progress, true );
}


isChallengeUnlocked( challengeRef, challengeIndex )
{
	prestigeShopString = tableLookupByRow( ALL_CHALLENGES_TABLE_REF, challengeIndex, CH_PRESTIGE_COL );
	if ( prestigeShopString != "" )
	{
		prestigeShopStatus = self getChallengeStatus( prestigeShopString );

		if ( prestigeShopStatus > 1 )
			return true;
	}

	rankString = tableLookupByRow( ALL_CHALLENGES_TABLE_REF, challengeIndex, CH_LEVEL_COL );
	if ( rankString != "" )
	{
		rank = self maps\mp\gametypes\_rank::getRank();
		if ( rank < int( rankString ) )
			return false;
	}
	
	challengeString = tableLookupByRow( ALL_CHALLENGES_TABLE_REF, challengeIndex, CH_CHALLENGE_COL );
	if ( challengeString != "" )
	{
		challengeStatus = self getChallengeStatus( challengeString );
	
		if ( challengeStatus <= 1 )
			return false;
	}

	return true;
}


updateChallenges()
{
	self.challengeData = [];
	
	if( !isDefined( self.ch_unique_earned_streaks ) )
		self.ch_unique_earned_streaks = []; // for daily challenge tracking "ch_daily_22"
	
	if( !IsDefined( game["challengeStruct"] ) )
		game["challengeStruct"] = [];
	
	if( !IsDefined( game["challengeStruct"]["limitedChallengesReset"] ) )
		game["challengeStruct"]["limitedChallengesReset"] = [];
	
	if( !IsDefined( game["challengeStruct"]["challengesCompleted"] ) )
		game["challengeStruct"]["challengesCompleted"] = [];

	self endon( "disconnect" );
	
	if ( !mayProcessChallenges() )
		return;

	if ( !self IsItemUnlocked( "challenges" ) )
		return;
	
	challengeCount = 0;

	foreach ( challengeRef, levelChallengeData in level.challengeInfo )
	{
		// limit the number of these we do in a single server frame so that it doesn't cause jitter as clients join
		// with ~2500 challenges at 20Hz, processing 40 per frame should take 3.125 total seconds to complete.
		challengeCount++;
		if ( (challengeCount % 40) == 0 )
			wait( 0.05 );

		self.challengeData[challengeRef] = 0;
		
		// The order of operations here is significant. IsItemUnlocked is *very* expensive and so things are arranged to keep the number of calls
		// to IsItemUnlocked to a minimum. updateChallenges must be sliced across multiple frames (see the challengeCount code above) to avoid
		// stalling the server when clients join. TableLookup is also generally a slow function (though not as bad as IsItemUnlocked) and should
		// be avoided here. TableLookupByRow is best. Be mindful of this when adding new challenges and types of challenges to this processing loop.
		// The best strategy is usually to do the heavy processing of challenge information in buildChallengeTableInfo (below) and store the
		// information in level.challengeInfo. This is done for example with weapon and killstreak challenges.
		
			assertEx( isDefined( levelChallengeData["index"] ), "Challenge index not defined: " + challengeRef + " for player " + self.name + " from " + level.challengeInfo.size + " total challenges" );
			challengeIndex = levelChallengeData["index"];

//			/# //========================================
//			// Debug - bypass isItemUnlocked as setting the player rank can be toublesome when debugging/testing challenges.
//			if( GetDvarInt( "debug_challenges", 0 ) == 1 )
//			{
//				status = ch_getState( challengeRef );
//				if ( status == 0 )
//				{
//					ch_setState( challengeRef, 1 );
//					status = 1;
//				}
//				
//			if( isTimeLimitedChallenge( challengeRef ) && !IsDefined( game["challengeStruct"]["limitedChallengesReset"][self.guid] ) )
//				{
//					self ch_setState( challengeRef, 1 );
//					self ch_setProgress( challengeRef, 0 );
//					status = 1;
//				}
//				
//				self.challengeData[challengeRef] = status;
//				continue;
//			}
//			#/ //========================================
				
//			if ( IsDefined( levelChallengeData["weapon"] ) )
//			{
//				if ( !self isChallengeUnlocked( challengeRef, challengeIndex ) )		// make sure the challenge is unlocked
//					continue;
//
//				baseWeapon = levelChallengeData["weapon"];
//				if( !self IsItemUnlocked( baseWeapon ) )								// make sure the weapon is unlocked
//					continue;
//
//				if ( IsDefined( levelChallengeData["attachment"] ) )
//				{
//					if( !maps\mp\gametypes\_class::isAttachmentUnlocked( baseWeapon, levelChallengeData["attachment"] ) )
//						continue;
//				}
//			}
//			else if( IsDefined( levelChallengeData["killstreak"] ) )
//			{
//				if ( !self isChallengeUnlocked( challengeRef, challengeIndex ) )		// make sure the challenge is unlocked
//					continue;
//
//				killstreakName = levelChallengeData["killstreak"];
//				if( IsDefined( killstreakName ) )
//				{
//					if( !self GetCommonPlayerData( "killstreakUnlocked", killstreakName ) )	// make sure the killstreak is unlocked
//						continue;
//				}
//			}
//			else
//			{
//				if ( !self isChallengeUnlocked( challengeRef, challengeIndex ) )		// make sure the challenge is unlocked
//					continue;
//				
//				if ( isDefined( levelChallengeData["requirement"] ) && !self IsItemUnlocked( levelChallengeData["requirement"] ) )
//					continue;
//			}
		
		status = ch_getState( challengeRef );
		if( isTimeLimitedChallenge( challengeRef ) && !IsDefined( game["challengeStruct"]["limitedChallengesReset"][self.guid] ) )
		{
			self ch_setProgress( challengeRef, 0 );
			status = 0;
		}
		
		if ( status == 0 )
		{
			ch_setState( challengeRef, 1 );
			status = 1;
		}
		
		self.challengeData[challengeRef] = status;
	}

	game["challengeStruct"]["limitedChallengesReset"][self.guid] = true;
}

isInUnlockTable( challengeName )
{
	return ( TableLookup( UNLOCK_TABLE_REF, 0, challengeName, 0 ) != "" );
}

getChallengeFilter( challengeName )
{
	return TableLookup( ALL_CHALLENGES_TABLE_REF, 0, challengeName, 5 );
}

getChallengeTable( challengeFilter )
{
	return TableLookup( CHALLENGE_TABLE_REF, 8, challengeFilter, 4 );
}

getTierFromTable( challengeTable, challengeName )
{
	return TableLookup( challengeTable, 0, challengeName, 1 );
}

isWeaponChallenge( challengeName )
{
	if( !IsDefined( challengeName ) )
		return false;

	// riot shield has different named challenges than something like ch_marksman_m4 where the weapon name is part of the challenge
	tableValue = getChallengeFilter( challengeName );
	if( IsDefined( tableValue ) && tableValue == "riotshield" )
		return true;

	// is this a weapon challenge? search the challenge name for the weapon name
	// we could do a table lookup but this is probably just as effecient for now
	tokens = getWeaponNameTokens( challengeName );
	for( i = 0; i < tokens.size; i++ )
	{
		concatName = tokens[i];
		if( concatName == "iw5" || concatName == "iw6" )
		{
			concatName = tokens[i] + "_" + tokens[ i + 1 ];
		}

		if( maps\mp\gametypes\_class::isValidPrimary( concatName ) || maps\mp\gametypes\_class::isValidSecondary( concatName, false ) )
			return true;
	}

	return false;
}

getWeaponFromChallenge( challengeRef )
{
	prefix = "ch_";
	if ( isSubStr( challengeRef, "ch_marksman_" ) )
		prefix = "ch_marksman_";
	else if ( isSubStr( challengeRef, "ch_expert_" ) )
		prefix = "ch_expert_";
	else if ( isSubStr( challengeRef, "pr_marksman_" ) )
		prefix = "pr_marksman_";
	else if ( isSubStr( challengeRef, "pr_expert_" ) )
		prefix = "pr_expert_";

	weaponName = GetSubStr( challengeRef, prefix.size, challengeRef.size );
	// now strip off the attachment
	weaponTokens = getWeaponNameTokens( weaponName );
	weaponName = undefined;
	if( weaponTokens[0] == "iw5" || weaponTokens[0] == "iw6" )
		weaponName = weaponTokens[0] + "_" + weaponTokens[1];
	else
		weaponName = weaponTokens[0];

	return weaponName;
}

getWeaponAttachmentFromChallenge( challengeRef )
{
	prefix = "ch_";
	if ( isSubStr( challengeRef, "ch_marksman_" ) )
		prefix = "ch_marksman_";
	else if ( isSubStr( challengeRef, "ch_expert_" ) )
		prefix = "ch_expert_";
	else if ( isSubStr( challengeRef, "pr_marksman_" ) )
		prefix = "pr_marksman_";
	else if ( isSubStr( challengeRef, "pr_expert_" ) )
		prefix = "pr_expert_";

	baseWeapon = GetSubStr( challengeRef, prefix.size, challengeRef.size );
	// now strip off the attachment
	weaponTokens = getWeaponNameTokens( baseWeapon );
	attachment = undefined;
	if( IsDefined( weaponTokens[2] ) && isAttachment( weaponTokens[2] ) )
		attachment = weaponTokens[2];

	return attachment;
}

isKillstreakChallenge( challengeName )
{
	if( !IsDefined( challengeName ) )
		return false;

	// look at the filter column to see if this is a killstreak challenge
	tableValue = getChallengeFilter( challengeName );
	if( IsDefined( tableValue ) && ( tableValue == "killstreaks_assault" || tableValue == "killstreaks_support" ) )
		return true;

	return false;
}

getKillstreakFromChallenge( challengeRef )
{
	prefix = "ch_";
	killstreakName = GetSubStr( challengeRef, prefix.size, challengeRef.size );

	if( killstreakName == "assault_streaks" || killstreakName == "support_streaks" )
		killstreakName = undefined;

	return killstreakName;
}

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


challenge_rewardVal( tableName, refString, tierId )
{
	value = tableLookup( tableName, CH_REF_COL, refString, CH_REWARD_COL + ((tierId-1)*2) );
	return int( value );
}

challenge_parentChallenge( tableName, stringRef )
{
	value = tableLookup( tableName, CH_REF_COL, stringRef, CH_PARENTCHALLENGE_COL );
	if( !isdefined( value ) )
		value = "";
	return value;
}

buildChallengeTableInfo( tableName, typeId )
{
	totalRewardXP = 0;
	index = 0;
	while ( true )
	{
		// index starts at 1 after increment
		index++;
		refString = tableLookupByRow( tableName, index, CH_REF_COL );
		if ( refString == "" )
			break;
		
		skipValidation = TableLookupByRow( tableName, index, CH_SKIPVALIDATION_COL );
		if ( skipValidation == "1" )
			continue;
	
		assertEx( isSubStr( refString, "ch_" ) || isSubStr( refString, "pr_" ), "Invalid challenge name: " + refString + " found in " + tableName );

		level.challengeInfo[refString] = [];
		level.challengeInfo[refString]["index"] = index;
		level.challengeInfo[refString]["type"] = typeId;
		level.challengeInfo[refString]["targetval"] = [];
		level.challengeInfo[refString]["reward"] = [];
		level.challengeInfo[refString]["parent_challenge"] = "";

		if ( isWeaponChallenge( refString ) )
		{
			baseWeapon = getWeaponFromChallenge( refString );
			attachment = getWeaponAttachmentFromChallenge( refString );

			if ( IsDefined( baseWeapon ) )
				level.challengeInfo[refString]["weapon"] = baseWeapon;
			if ( IsDefined( attachment ) )
				level.challengeInfo[refString]["attachment"] = attachment;
		}
		else if( isKillstreakChallenge( refString ) ) 	// make sure the killstreak is unlocked
		{
			killstreakName = getKillstreakFromChallenge( refString );
			if( IsDefined( killstreakName ) )
				level.challengeInfo[refString]["killstreak"] = killstreakName;
		}

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

			if ( targetVal == 0 )
				break;

			level.challengeInfo[refString]["targetval"][tierId] = targetVal;
			level.challengeInfo[refString]["reward"][tierId] = rewardVal;
			
			totalRewardXP += rewardVal;
		}
		
		level.challengeInfo[refString]["parent_challenge"] = challenge_parentChallenge( tableName, refstring);
		
		assert( isDefined( level.challengeInfo[refString]["targetval"][1] ) );
	}

	/#
	printLn( "Added " + (index - 1) + " challenges from " + tableName );
	#/
	
	return int( totalRewardXP );
}

buildChallegeInfo()
{
	level.challengeInfo = [];
	if ( GetDvar( "virtualLobbyActive" ) == "1" )
		return;
	
	totalRewardXP = 0;
	
	totalRewardXP += buildChallengeTableInfo( ALL_CHALLENGES_TABLE_REF, CH_REGULAR );
//	totalRewardXP += buildChallengeTableInfo( DAILY_CHALLENGES_TABLE_REF, CH_DAILY );
//	totalRewardXP += buildChallengeTableInfo( WEEKLY_CHALLENGES_TABLE_REF, CH_WEEKLY );
	
	/#
	printLn( "TOTAL CHALLENGE REWARD XP: " + totalRewardXP );
	#/
	
//	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 );	
//	}
}

/#
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 maps\mp\gametypes\_hud_util::ch_setProgress( challengeRef, finalTarget );
			self maps\mp\gametypes\_hud_util::ch_setState( 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 == 9) && isDefined(self.killstreaks[7]) && isDefined(self.killstreaks[8]) && isDefined(self.killstreaks[9]) )
			self processChallenge( "ch_6fears7" );
		
		if( streakCount == 10 && self.killstreaks.size == 0 )
			self processChallenge( "ch_theloner" );		
	}
}


monitorKilledKillstreak()
{
	self endon( "disconnect" );
	level endon( "game_end" );
	
	for( ;; )
	{
		self waittill( "destroyed_killstreak", weapon );
		
		if ( self IsItemUnlocked( "specialty_blindeye" ) && self _hasPerk( "specialty_blindeye" ) )
			self processChallenge( "ch_blindeye_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 "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_dmr":
//			self processChallenge( "ch_dmr_sniper" );
			break;
		case "weapon_sniper":
			self processChallenge( "ch_surgical_sniper" );
			break;
		case "shield_damage":
			if( !self isJuggernaut() )
				self processChallenge( "ch_shield_damage", value );
			break;
		case "shield_bullet_hits":
			if( !self isJuggernaut() )
				self processChallenge( "ch_shield_bullet", value );
			break;
		case "shield_explosive_hits":
			if( !self isJuggernaut() )
				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;
}

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

	self.adsTime = 0.0;
	while( true )
	{
		if( self PlayerAds() == 1 )
		{
			self.adsTime += 0.05;
		}
		else
		{
			self.adsTime = 0.0;
		}

		wait( 0.05 );
	}
}

monitorProneTime()
{
	self endon ( "disconnect" );
	level endon( "game_ended" );

	self.proneTime = undefined;
	was_prev_prone = false;
	while( true )
	{
		stance = self GetStance();
		if( stance == "prone" && was_prev_prone == false )
		{
			self.proneTime = getTime();
			was_prev_prone = true;
		}
		else if( stance != "prone" )
		{
			self.proneTime = undefined;
			was_prev_prone = false;
		}

		wait( 0.05 );
	}
}

monitorPowerSlideTime()
{
	self endon ( "disconnect" );
	level endon( "game_ended" );

	self.powerslideTime = undefined;
	
	while( true )
	{
		while( !self IsPowerSliding() )
		{
			wait 0.05;
		}
		
		self.powerslideTime = getTime();

		wait( 0.05 );
	}
}

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

	self.holdingBreath = false;
	while( true )
	{
		self waittill( "hold_breath" );
		self.holdingBreath = true;
		self waittill( "release_breath" );
		self.holdingBreath = false;
	}
}

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

	self.mantling = false;
	while( true )
	{
		self waittill( "jumped" );
		prevWeaponName = self GetCurrentWeapon();
		self waittill_notify_or_timeout( "weapon_change", 1 );
		currWeaponName = self GetCurrentWeapon();
		if( currWeaponName == "none" )
			self.mantling = true;
		else
			self.mantling = false;
		
		if( self.mantling )
		{
			if( self IsItemUnlocked( "specialty_fastmantle" ) && self _hasPerk( "specialty_fastmantle" ) )
				self processChallenge( "ch_fastmantle" );

			self waittill_notify_or_timeout( "weapon_change", 1 );
			currWeaponName = self GetCurrentWeapon();
			if( currWeaponName == prevWeaponName )
				self.mantling = false;
		}
	}
}

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

	prevWeaponName = self GetCurrentWeapon();
	while( true )
	{
		self waittill( "weapon_change", newWeaponName );

		if( newWeaponName == "none" )
			continue;

		if( newWeaponName == prevWeaponName )
			continue;

		if( isKillstreakWeapon( newWeaponName ) )
			continue;

		if( isBombSiteWeapon( newWeaponName ) )
			continue;

		weaponInvType = WeaponInventoryType( newWeaponName );
		if( weaponInvType != "primary" )
			continue;

		self.lastPrimaryWeaponSwapTime = GetTime();
	}
}

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

	while( true )
	{
		self waittill( "flashbang", origin, amount_distance, amount_angle, attacker );
		
		if( isdefined( attacker ) && ( self == attacker ) )
			continue;

		self.lastFlashedTime = GetTime();
	}
}

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

	while( true )
	{
		self waittill( "concussed", attacker );

		if( self == attacker )
			continue;

		self.lastConcussedTime = GetTime();
	}
}

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

	while( true )
	{
		// made this very simple, if they trip a mine or claymore and live the delay mine amount of time then process challenge
		self waittill_any( "triggered_mine", "triggered_claymore" );

		self thread waitDelayMineTime();
	}
}

waitDelayMineTime()
{
	self endon( "death" );
	self endon( "disconnect" );
	level endon( "game_ended" );

	// add more time to give the mine a chance to detonate and kill
	wait( level.delayMineTime + 2 );

	self processChallenge( "ch_delaymine" );
}

is_lethal_equipment( equipment )
{
	if( !IsDefined( equipment ) )
		return false;
	
	switch( equipment )
	{
		case "frag_grenade_mp":
		case "semtex_mp":
		case "exoknife_mp":
		case "explosive_drone_mp":
			return true;
		default:
			return false;
	}
}
