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

init()
{

	//
	// It seems to me that we should not do all this init until we are actually in a match
	// i.e. not in the lobby.
	// However, we get a variety of runtime script errors in _playerlogic::Callback_PlayerConnect()
	// if we don't allow the stuff below to happen even in lobby... ?
	//

	PrintLn( "---- _matchdata::init() ----" );
	
	if ( !isDefined( game["gamestarted"] ) )
	{
		//setMatchDataDef( "mp/matchdata_" + level.gametype + ".def" );
		setMatchDataDef( "mp/matchdata.def" );
		setMatchData( "map", level.script );
		if( level.hardcoremode )
		{
			tmp = level.gametype + " hc";
			setMatchData( "gametype", tmp );
		}
		else
		{
			setMatchData( "gametype", level.gametype );
		}
		setMatchData( "buildVersion", getBuildVersion() );
		setMatchData( "buildNumber", getBuildNumber() );
		
		setMatchDataID();
	}

	level.MaxLives = 490; // must match MaxLives in matchdata definition	

	level.MaxEvents = 150;
	level.MaxKillstreaks = 64;
	level.MaxLogClients = 30;
	level.MaxNumChallengesPerPlayer = 5;		// must match MaxChallengesPerPlayer in matchdata.def
	level.MaxNumAwardsPerPlayer = 10;
	level.MaxLoadouts = 10; 					// must match MaxLoadouts in matchdata.def
	
	level thread gameEndListener();
	
//	/#
	if( GetDvar( "virtualLobbyActive" ) != "1" )
	{
		level thread reconLogPlayerInfo();
	}
//	#/
	
}

matchStarted()
{
	// both of these tests seems to work
	// not sure if one is more reliable?
	if ( GetDvar( "virtualLobbyActive" ) == "1" )
	{
		PrintLn( "_matchdata::matchStarted() -- skipping because GetDvar('virtualLobbyActive') == '1'" );
		return;
	}
	if( GetDvar( "mapname" ) == getdvar( "virtualLobbyMap") )
	{
		PrintLn( "_matchdata::matchStarted() -- skipping because GetDvar('mapname') == getdvar('virtualLobbyMap')" );
		return;
	}
	//if ( getdvar( "mapname" ) == "mp_virtual_lobby" )	<-- does not work
	
	PrintLn( "---- _matchdata::matchStarted() ----" );

	SysPrint( "MatchStarted: Completed" );
	
	playlistName = GetPlaylistName();
	setMatchData( "playlistName", playlistName );

	localTimeString = GetLocalTimeString();
	setMatchData( "localTimeStringAtMatchStart", localTimeString );

	if ( getMatchStartTimeUTC() == 0 )
	{
		setMatchData( "startTimeUTC", getSystemTime() );
	}

	setMatchData( "iseSports", isMLGMatch() );

	if( privateMatch() )
		setMatchData( "privateMatch", true );

	if( practiceRoundGame() )
		setMatchData( "practiceRound", true );

	if( !isAugmentedGameMode() )
		setMatchData( "classicMode", true );

	if( isDivisionMode() )
		setMatchData( "divisionMode", true );

	thread logBreadcrumbData();
	thread accumulatePlayerPingData();
}


logBreadcrumbData()
{	
	level endon ( "game_ended" );
	while(1)
	{
		timePassedSecondsIncludingRounds = getGameTimePassedSeconds();
		//PrintLn("logBreadcrumbData: timePassedIncludingRoundsMs: ", timePassedIncludingRoundsMs, " timePassedSecondsIncludingRounds: ", timePassedSecondsIncludingRounds );

		foreach( player in level.players )
		{
			if ( IsBot( player ) || IsTestClient( player ) )
				continue;

			if( isReallyAlive( player ) && isDefined( player.lifeId ) && canLogLife( player.lifeId) )
			{
				// use 31 for undefined value, since [0,29] are valid player indexes
				// and in the breadcrumbing in code we reserve 5 bits for this value				
				lastShotBy = 31;
				if( isDefined( player.lastShotBy ) )
					lastShotBy = player.lastShotBy;
				
				RecordBreadCrumbDataForPlayer( player, player.lifeId, timePassedSecondsIncludingRounds, lastShotBy );			
				player.lastShotBy = undefined;	 // clear after each time that we log
			}			
		}
		wait 2;		// must match #define MATCH_SAMPLE_SECS in code
	}	
}


accumulatePlayerPingData()
{	
	level endon ( "game_ended" );
	while(1)
	{
		foreach( player in level.players )
		{
			if ( IsBot( player ) || IsTestClient( player ) )
				continue;

			if( !isDefined( player.pers["pingAccumulation"] ) ||
			   	!isDefined( player.pers["minPing"] ) ||
			   	!isDefined( player.pers["maxPing"] ) ||
				!isDefined( player.pers["pingSampleCount"]) )
				continue;

			currentPing = player GetCurrentPing();
			player.pers["pingAccumulation"] += currentPing;
			player.pers["pingSampleCount"]++;
			
			// Skip the first few pings
			if ( player.pers["pingSampleCount"] > 5 && currentPing > 0 )
			{
				if ( currentPing > player.pers["maxPing"] )
				{
					player.pers["maxPing"] = currentPing;
				}
				
				if ( currentPing < player.pers["minPing"] )
				{
					player.pers["minPing"] = currentPing;
				}
			}
		}
		wait 2;		
	}	
}


getMatchStartTimeUTC()
{
	return GetMatchData( "startTimeUTC" );
}

logKillstreakEvent( event, position )
{
	assertEx( IsGameParticipant( self ), "self is not a player: " + self.code_classname );
	
	if ( !canLogClient( self ) || !canLogKillstreak() )
		return;

	eventId = getMatchData( "killstreakCount" );
	setMatchData( "killstreakCount", eventId+1 );
	
	setMatchData( "killstreaks", eventId, "eventType", event );
	setMatchData( "killstreaks", eventId, "player", self.clientid );
	setMatchData( "killstreaks", eventId, "eventStartTimeDeciSecondsFromMatchStart", getTimePassedDeciSeconds() );	
	setMatchData( "killstreaks", eventId, "eventPos", 0, int( position[0] ) );	
	setMatchData( "killstreaks", eventId, "eventPos", 1, int( position[1] ) );	
	setMatchData( "killstreaks", eventId, "eventPos", 2, int( position[2] ) );	
	setMatchData( "killstreaks", eventId, "index", eventId );	
	setMatchData( "killstreaks", eventId, "coopPlayerIndex", 255 );	

	self.currentKillStreakIndex = eventId;

//	/#
	ReconSpatialEvent( position, "script_mp_killstreak: eventType %s, player_name %s, player %d, gameTime %d", event, self.name, self.clientid, getTime() );
//	#/
}


logGameEvent( event, position )
{
	assertEx( IsGameParticipant( self ), "self is not a player: " + self.code_classname );

	if ( !canLogClient( self ) || !canLogEvent() )
		return;
		
	eventId = getMatchData( "eventCount" );
	setMatchData( "eventCount", eventId+1 );
	
	setMatchData( "events", eventId, "eventType", event );
	setMatchData( "events", eventId, "player", self.clientid );
	setMatchData( "events", eventId, "eventTimeDeciSecondsFromMatchStart", getTimePassedDeciSeconds() );	
	setMatchData( "events", eventId, "eventPos", 0, int( position[0] ) );	
	setMatchData( "events", eventId, "eventPos", 1, int( position[1] ) );	
	setMatchData( "events", eventId, "eventPos", 2, int( position[2] ) );	
	
//	/#
	ReconSpatialEvent( position, "script_mp_event: event_type %s, player_name %s, player %d, gameTime %d", event, self.name, self.clientid, getTime() );
//	#/
}


logKillEvent( lifeId, eventRef )
{
	if ( !canLogLife( lifeId ) )
		return;

	setMatchData( "lives", lifeId, "modifiers", eventRef, true );
}


logMultiKill( lifeId, multikillCount )
{
	if ( !canLogLife( lifeId ) )
		return;

	setMatchData( "lives", lifeId, "multikill", multikillCount );
}


logPlayerLife( died )
{
	if ( !canLogClient( self ) || !canLogLife( self.lifeId ) )
		return;

	lifeDuration = getTime() - self.spawnTime;
	self.totalLifeTime += lifeDuration;	
	lifeDurationDeciSeconds = convertMillisecondsToDeciSecondsAndClampToShort( lifeDuration );
	
	setMatchData( "lives", self.lifeId, "player", self.clientid );
	setMatchData( "lives", self.lifeId, "spawnPos", 0,  int( self.spawnPos[0] ) );
	setMatchData( "lives", self.lifeId, "spawnPos", 1,  int( self.spawnPos[1] ) );
	setMatchData( "lives", self.lifeId, "spawnPos", 2,  int( self.spawnPos[2] ) );
	setMatchData( "lives", self.lifeId, "wasTacticalInsertion", self.wasTI );
	setMatchData( "lives", self.lifeId, "team", self.team );

	if( isDefined( self.spawnTimeDeciSecondsFromMatchStart) )
	{
		setMatchData( "lives", self.lifeId, "spawnTimeDeciSecondsFromMatchStart", self.spawnTimeDeciSecondsFromMatchStart );	
	}
	else
	{
		setMatchData( "lives", self.lifeId, "spawnTimeDeciSecondsFromMatchStart", -1 );			
	}
	
	setMatchData( "lives", self.lifeId, "durationDeciSeconds", lifeDurationDeciSeconds );

	loadoutIndex = self logLoadout();
	setMatchData( "lives", self.lifeId, "loadoutIndex", loadoutIndex );

	score = clampToShort( self.pers["score"] - self.scoreAtLifeStart );
	setMatchData( "lives", self.lifeId, "scoreEarnedDuringThisLife", score );

	if( isDefined( self.pers["summary"]) && isDefined( self.pers["summary"]["xp"] ) )
	{
		if( isDefined( self.xpAtLifeStart ) ) 	// should be covered by the above tests, but doesn't hurt to be safe
		{
			xp = clampToShort( self.pers["summary"]["xp"] - self.xpAtLifeStart );
			setMatchData( "lives", self.lifeId, "xpEarnedDuringThisLife", xp );					
		}
		
	}

}

logPlayerXP( xp, xpName  )
{
	if ( !canLogClient( self ) )
		return;
	setMatchData( "players", self.clientid, xpName, xp );
}

logCompletedChallenge( challengeID )
{
	if( !isPlayer( self ) || !canLogClient( self ) || isBot( self ) )
		return;

	numCompletedChallenges = getMatchData( "players", self.clientid, "challengeCount" );
	if( numCompletedChallenges < level.MaxNumChallengesPerPlayer )
	{
		setMatchData( "players", self.clientid, "challenges", numCompletedChallenges, challengeID );
		setMatchData( "players", self.clientid, "challengeCount", numCompletedChallenges + 1 );
	}
}


logLoadout()
{
	noSlotValue = 255;	// not sure if it's safe to try and pack -1 into a byte
	if ( !canLogClient( self ) || !canLogLife( self.lifeId ) || self.curClass == "gamemode" )
		return noSlotValue;

	class = self.curClass;

	//
	// before we do anything else here, figure out if we've already logged the info for this loadout
	//
	slot = 0;	
	for( slot = 0; slot < level.MaxLoadouts; slot++ )
	{
		slotUsed = getMatchData( "players", self.clientid, "loadouts", slot, "slotUsed" );
		if( !slotUsed )
		{
			break;	// found a new place to log a loadout
		}
		else
		{
			// check loadout name
			// assumption here that 'class' string uniquely identifies the loadout; certainly appears to be true
			className = getMatchData( "players", self.clientid, "loadouts", slot, "className" );
			if( class == className )
			{
				// already logged!
				// PrintLn("_matchdata::logLoadout -- player index: ", self.clientid, " already logged loadout with className: ", className, " in slot: ", slot, "\n");
				return slot;
			}
		}
	}
	if( slot == level.MaxLoadouts )
	{
		PrintLn(" WARNING: trying to log loadout information and no more Player.loadouts slots available! ");
		return noSlotValue;
	}

	//PrintLn("_matchdata::logLoadout -- player index: ", self.clientid, " we will log loadout with className: ", class, " in slot: ", slot, "\n");
	setMatchData( "players", self.clientid, "loadouts", slot, "slotUsed", true );
	setMatchData( "players", self.clientid, "loadouts", slot, "className", class );

	loadoutKillstreak1 = "";
	loadoutKillstreak2 = "";
	loadoutKillstreak3 = "";
	loadoutKillstreak4 = "";

	loadoutPerks = [];
	loadoutWildcards = [];

	// not sure how the script compiled with these not defined outside the blocks below
	// but referred to after them in the setMatchData calls?
	loadoutEquipment = "";	
	loadoutEquipmentExtra = false;	
	loadoutOffhand = "";
	loadoutOffhand2 = "";				// there is a wildcard that can give you a second offhand (exo tactical) ability
	loadoutOffhandExtra = false;

	
	if ( class == "copycat" )
	{
		clonedLoadout = self.pers["copyCatLoadout"];

		// Primary
		loadoutPrimary 					= clonedLoadout["loadoutPrimary"];
		loadoutPrimaryAttachment 		= clonedLoadout["loadoutPrimaryAttachment"];
		loadoutPrimaryAttachment2 		= clonedLoadout["loadoutPrimaryAttachment2"] ;
		loadoutPrimaryAttachment3 		= clonedLoadout["loadoutPrimaryAttachment3"];
		loadoutPrimaryCamo 				= clonedLoadout["loadoutPrimaryCamo"];
		loadoutPrimaryReticle 			= clonedLoadout["loadoutPrimaryReticle"];
		
		// Secondary
		loadoutSecondary 				= clonedLoadout["loadoutSecondary"];
		loadoutSecondaryAttachment 		= clonedLoadout["loadoutSecondaryAttachment"];
		loadoutSecondaryAttachment2 	= clonedLoadout["loadoutSecondaryAttachment2"];
		loadoutSecondaryCamo 			= clonedLoadout["loadoutSecondaryCamo"];
		loadoutSecondaryReticle 		= clonedLoadout["loadoutSecondaryReticle"];

		// Equipment
		loadoutEquipment 				= clonedLoadout["loadoutEquipment"];
		loadoutEquipmentExtra			= clonedLoadout["loadoutEquipmentExtra"];
		loadoutOffhand 					= clonedLoadout["loadoutOffhand"];
		loadoutOffhandExtra 			= clonedLoadout["loadoutOffhandExtra"];
		
		// Perks
		for ( i = 0; i < 6; i++ )
		{
			loadoutPerks[i] 			= clonedLoadout["loadoutPerks"][i];
		}
		
		// Wildcards
		for ( i = 0; i < 3; i++ )
		{
			loadoutWildcards[i] 		= clonedLoadout["loadoutWildcards"][i];
		}

		// Streaks
		loadoutKillstreak1 				= clonedLoadout["loadoutKillstreaks"][0];
		loadoutKillstreak1ModuleA		= clonedLoadout["loadoutKillstreakModules"][0][0];
		loadoutKillstreak1ModuleB		= clonedLoadout["loadoutKillstreakModules"][0][1];
		loadoutKillstreak1ModuleC		= clonedLoadout["loadoutKillstreakModules"][0][2];
		loadoutKillstreak2 				= clonedLoadout["loadoutKillstreaks"][1];
		loadoutKillstreak2ModuleA		= clonedLoadout["loadoutKillstreakModules"][1][0];
		loadoutKillstreak2ModuleB		= clonedLoadout["loadoutKillstreakModules"][1][1];
		loadoutKillstreak2ModuleC		= clonedLoadout["loadoutKillstreakModules"][1][2];
		loadoutKillstreak3 				= clonedLoadout["loadoutKillstreaks"][2];
		loadoutKillstreak3ModuleA		= clonedLoadout["loadoutKillstreakModules"][2][0];
		loadoutKillstreak3ModuleB		= clonedLoadout["loadoutKillstreakModules"][2][1];
		loadoutKillstreak3ModuleC		= clonedLoadout["loadoutKillstreakModules"][2][2];
		loadoutKillstreak4 				= clonedLoadout["loadoutKillstreaks"][3];
		loadoutKillstreak4ModuleA		= clonedLoadout["loadoutKillstreakModules"][3][0];
		loadoutKillstreak4ModuleB		= clonedLoadout["loadoutKillstreakModules"][3][1];
		loadoutKillstreak4ModuleC		= clonedLoadout["loadoutKillstreakModules"][3][2];
		
		assert( IsDefined( loadoutKillstreak1 ) );
		assert( IsDefined( loadoutKillstreak2 ) );
		assert( IsDefined( loadoutKillstreak3 ) );
		assert( IsDefined( loadoutKillstreak4 ) );
	}
	else if( isSubstr( class, "custom" ) ) // Custom CaC Class
	{
		class_num = getClassIndex( class );

		loadoutPrimary 					= maps\mp\gametypes\_class::cac_getWeapon( class_num, 0 );
		loadoutPrimaryAttachment 		= maps\mp\gametypes\_class::cac_getWeaponAttachment( class_num, 0 );
		loadoutPrimaryAttachment2 		= maps\mp\gametypes\_class::cac_getWeaponAttachmentTwo( class_num, 0 );
		loadoutPrimaryAttachment3 		= maps\mp\gametypes\_class::cac_getWeaponAttachmentThree( class_num, 0 );
		loadoutPrimaryCamo 				= maps\mp\gametypes\_class::cac_getWeaponCamo( class_num, 0 );
		
		for (i=0; i<6; i++)
			loadoutPerks[i] 			= maps\mp\gametypes\_class::cac_getPerk( class_num, i );
		
		for (i=0; i<3; i++)
			loadoutWildcards[i] 		= maps\mp\gametypes\_class::cac_getWildcard( class_num, i );

		loadoutSecondary 				= maps\mp\gametypes\_class::cac_getWeapon( class_num, 1 );
		loadoutSecondaryAttachment 		= maps\mp\gametypes\_class::cac_getWeaponAttachment( class_num, 1 );
		loadoutSecondaryAttachment2 	= maps\mp\gametypes\_class::cac_getWeaponAttachmentTwo( class_num, 1 );
		loadoutSecondaryCamo 			= maps\mp\gametypes\_class::cac_getWeaponCamo( class_num, 1 );

		loadoutEquipment 				= maps\mp\gametypes\_class::cac_getEquipment( class_num, 0 );
		loadoutEquipmentExtra			= maps\mp\gametypes\_class::cac_getEquipmentExtra( class_num, 0 );
		loadoutOffhand 					= maps\mp\gametypes\_class::cac_getOffhand( class_num );

		loadoutKillstreak1 				= maps\mp\gametypes\_class::cac_getKillstreak( class_num, 0 );
		loadoutKillstreak1ModuleA		= maps\mp\gametypes\_class::cac_getKillstreakModule( class_num, 0, 0 );
		loadoutKillstreak1ModuleB		= maps\mp\gametypes\_class::cac_getKillstreakModule( class_num, 0, 1 );
		loadoutKillstreak1ModuleC		= maps\mp\gametypes\_class::cac_getKillstreakModule( class_num, 0, 2 );
		loadoutKillstreak2 				= maps\mp\gametypes\_class::cac_getKillstreak( class_num, 1 );
		loadoutKillstreak2ModuleA		= maps\mp\gametypes\_class::cac_getKillstreakModule( class_num, 1, 0 );
		loadoutKillstreak2ModuleB		= maps\mp\gametypes\_class::cac_getKillstreakModule( class_num, 1, 1 );
		loadoutKillstreak2ModuleC		= maps\mp\gametypes\_class::cac_getKillstreakModule( class_num, 1, 2 );
		loadoutKillstreak3 				= maps\mp\gametypes\_class::cac_getKillstreak( class_num, 2 );
		loadoutKillstreak3ModuleA		= maps\mp\gametypes\_class::cac_getKillstreakModule( class_num, 2, 0 );
		loadoutKillstreak3ModuleB		= maps\mp\gametypes\_class::cac_getKillstreakModule( class_num, 2, 1 );
		loadoutKillstreak3ModuleC		= maps\mp\gametypes\_class::cac_getKillstreakModule( class_num, 2, 2 );
		loadoutKillstreak4 				= maps\mp\gametypes\_class::cac_getKillstreak( class_num, 3 );
		loadoutKillstreak4ModuleA		= maps\mp\gametypes\_class::cac_getKillstreakModule( class_num, 3, 0 );
		loadoutKillstreak4ModuleB		= maps\mp\gametypes\_class::cac_getKillstreakModule( class_num, 3, 1 );
		loadoutKillstreak4ModuleC		= maps\mp\gametypes\_class::cac_getKillstreakModule( class_num, 3, 2 );
		
		assert( IsDefined( loadoutKillstreak1 ) );
		assert( IsDefined( loadoutKillstreak2 ) );
		assert( IsDefined( loadoutKillstreak3 ) );
		assert( IsDefined( loadoutKillstreak4 ) );
	}
	else if ( IsSubStr( class, "practice" ) ) // Practice Round Class
	{
		class_num = getClassIndex( class );
		
		// Map it to the absolute class num (chosen randomly per player)
		class_num = self.pers["practiceRoundClasses"][class_num];
				
		// Primary
		loadoutPrimary 					= maps\mp\gametypes\_class::table_getWeapon( level.practiceRoundClassTableName, class_num, 0 );
		loadoutPrimaryAttachment 		= maps\mp\gametypes\_class::table_getWeaponAttachment( level.practiceRoundClassTableName, class_num, 0, 0 );
		loadoutPrimaryAttachment2 		= maps\mp\gametypes\_class::table_getWeaponAttachment( level.practiceRoundClassTableName, class_num, 0, 1 );
		loadoutPrimaryAttachment3 		= maps\mp\gametypes\_class::table_getWeaponAttachment( level.practiceRoundClassTableName, class_num, 0, 2 );
		loadoutPrimaryCamo 				= maps\mp\gametypes\_class::table_getWeaponCamo( level.practiceRoundClassTableName, class_num, 0 );
		
		// Secondary
		loadoutSecondary 				= maps\mp\gametypes\_class::table_getWeapon( level.practiceRoundClassTableName, class_num, 1 );
		loadoutSecondaryAttachment 		= maps\mp\gametypes\_class::table_getWeaponAttachment( level.practiceRoundClassTableName, class_num, 1, 0 );
		loadoutSecondaryAttachment2 	= maps\mp\gametypes\_class::table_getWeaponAttachment( level.practiceRoundClassTableName, class_num, 1, 1 );
		loadoutSecondaryCamo 			= maps\mp\gametypes\_class::table_getWeaponCamo( level.practiceRoundClassTableName, class_num, 1 );
		
		// Equipment
		loadoutEquipment				= maps\mp\gametypes\_class::table_getEquipment( level.practiceRoundClassTableName, class_num );
		loadoutEquipmentExtra			= maps\mp\gametypes\_class::table_getEquipmentExtra( level.practiceRoundClassTableName, class_num );
		loadoutOffhand 					= maps\mp\gametypes\_class::table_getOffhand( level.practiceRoundClassTableName, class_num );
		
		// Perks
		for ( i = 0; i < 6; i++ )
			loadoutPerks[i] 			= maps\mp\gametypes\_class::table_getPerk( level.practiceRoundClassTableName, class_num, i );
		
		// Wildcards
		for ( i = 0; i < 3; i++ )
			loadoutWildcards[i] 		= maps\mp\gametypes\_class::table_getWildcard( level.practiceRoundClassTableName, class_num, i );
		
		// Streaks
		loadoutKillstreak1 				= maps\mp\gametypes\_class::table_getKillstreak( level.practiceRoundClassTableName, class_num, 0 );
		loadoutKillstreak1ModuleA		= maps\mp\gametypes\_class::table_getKillstreakModule( level.practiceRoundClassTableName, class_num, 0, 0 );
		loadoutKillstreak1ModuleB		= maps\mp\gametypes\_class::table_getKillstreakModule( level.practiceRoundClassTableName, class_num, 0, 1 );
		loadoutKillstreak1ModuleC		= maps\mp\gametypes\_class::table_getKillstreakModule( level.practiceRoundClassTableName, class_num, 0, 2 );
		loadoutKillstreak2 				= maps\mp\gametypes\_class::table_getKillstreak( level.practiceRoundClassTableName, class_num, 1 );
		loadoutKillstreak2ModuleA		= maps\mp\gametypes\_class::table_getKillstreakModule( level.practiceRoundClassTableName, class_num, 1, 0 );
		loadoutKillstreak2ModuleB		= maps\mp\gametypes\_class::table_getKillstreakModule( level.practiceRoundClassTableName, class_num, 1, 1 );
		loadoutKillstreak2ModuleC		= maps\mp\gametypes\_class::table_getKillstreakModule( level.practiceRoundClassTableName, class_num, 1, 2 );
		loadoutKillstreak3 				= maps\mp\gametypes\_class::table_getKillstreak( level.practiceRoundClassTableName, class_num, 2 );
		loadoutKillstreak3ModuleA		= maps\mp\gametypes\_class::table_getKillstreakModule( level.practiceRoundClassTableName, class_num, 2, 0 );
		loadoutKillstreak3ModuleB		= maps\mp\gametypes\_class::table_getKillstreakModule( level.practiceRoundClassTableName, class_num, 2, 1 );
		loadoutKillstreak3ModuleC		= maps\mp\gametypes\_class::table_getKillstreakModule( level.practiceRoundClassTableName, class_num, 2, 2 );		
		loadoutKillstreak4 				= maps\mp\gametypes\_class::table_getKillstreak( level.practiceRoundClassTableName, class_num, 3 );
		loadoutKillstreak4ModuleA		= maps\mp\gametypes\_class::table_getKillstreakModule( level.practiceRoundClassTableName, class_num, 3, 0 );
		loadoutKillstreak4ModuleB		= maps\mp\gametypes\_class::table_getKillstreakModule( level.practiceRoundClassTableName, class_num, 3, 1 );
		loadoutKillstreak4ModuleC		= maps\mp\gametypes\_class::table_getKillstreakModule( level.practiceRoundClassTableName, class_num, 3, 2 );	
		
		assert( IsDefined( loadoutKillstreak1 ) );
		assert( IsDefined( loadoutKillstreak2 ) );
		assert( IsDefined( loadoutKillstreak3 ) );
		assert( IsDefined( loadoutKillstreak4 ) );
	}
	else // Default Class
	{
		class_num = getClassIndex( class );
		
		loadoutPrimary 					= maps\mp\gametypes\_class::table_getWeapon( level.classTableName, class_num, 0 );
		loadoutPrimaryAttachment 		= maps\mp\gametypes\_class::table_getWeaponAttachment( level.classTableName, class_num, 0 , 0);
		loadoutPrimaryAttachment2 		= maps\mp\gametypes\_class::table_getWeaponAttachment( level.classTableName, class_num, 0, 1 );
		loadoutPrimaryAttachment3 		= maps\mp\gametypes\_class::table_getWeaponAttachment( level.classTableName, class_num, 0, 2 );
		loadoutPrimaryCamo 				= maps\mp\gametypes\_class::table_getWeaponCamo( level.classTableName, class_num, 0 );

		for (i=0; i<6; i++)
			loadoutPerks[i] 			= maps\mp\gametypes\_class::table_getPerk( level.classTableName, class_num, i );

		for (i=0; i<3; i++)
			loadoutWildcards[i] 		= maps\mp\gametypes\_class::table_getWildcard( level.classTableName, class_num, i );

		loadoutSecondary 				= maps\mp\gametypes\_class::table_getWeapon( level.classTableName, class_num, 1 );
		loadoutSecondaryAttachment 		= maps\mp\gametypes\_class::table_getWeaponAttachment( level.classTableName, class_num, 1 , 0);
		loadoutSecondaryAttachment2 	= maps\mp\gametypes\_class::table_getWeaponAttachment( level.classTableName, class_num, 1, 1 );;
		loadoutSecondaryCamo 			= maps\mp\gametypes\_class::table_getWeaponCamo( level.classTableName, class_num, 1 );

		loadoutEquipment 				= maps\mp\gametypes\_class::table_getEquipment( level.classTableName, class_num );
		loadoutEquipmentExtra			= maps\mp\gametypes\_class::table_getEquipmentExtra( level.classTableName, class_num );
		loadoutOffhand 					= maps\mp\gametypes\_class::table_getOffhand( level.classTableName, class_num );

		loadoutKillstreak1 				= maps\mp\gametypes\_class::table_getKillstreak( level.classTableName, class_num, 0 );
		loadoutKillstreak1ModuleA		= maps\mp\gametypes\_class::table_getKillstreakModule( level.classTableName, class_num, 0, 0 );
		loadoutKillstreak1ModuleB		= maps\mp\gametypes\_class::table_getKillstreakModule( level.classTableName, class_num, 0, 1 );
		loadoutKillstreak1ModuleC		= maps\mp\gametypes\_class::table_getKillstreakModule( level.classTableName, class_num, 0, 2 );
		loadoutKillstreak2 				= maps\mp\gametypes\_class::table_getKillstreak( level.classTableName, class_num, 1 );
		loadoutKillstreak2ModuleA		= maps\mp\gametypes\_class::table_getKillstreakModule( level.classTableName, class_num, 1, 0 );
		loadoutKillstreak2ModuleB		= maps\mp\gametypes\_class::table_getKillstreakModule( level.classTableName, class_num, 1, 1 );
		loadoutKillstreak2ModuleC		= maps\mp\gametypes\_class::table_getKillstreakModule( level.classTableName, class_num, 1, 2 );
		loadoutKillstreak3 				= maps\mp\gametypes\_class::table_getKillstreak( level.classTableName, class_num, 2 );
		loadoutKillstreak3ModuleA		= maps\mp\gametypes\_class::table_getKillstreakModule( level.classTableName, class_num, 2, 0 );
		loadoutKillstreak3ModuleB		= maps\mp\gametypes\_class::table_getKillstreakModule( level.classTableName, class_num, 2, 1 );
		loadoutKillstreak3ModuleC		= maps\mp\gametypes\_class::table_getKillstreakModule( level.classTableName, class_num, 2, 2 );		
		loadoutKillstreak4 				= maps\mp\gametypes\_class::table_getKillstreak( level.classTableName, class_num, 3 );
		loadoutKillstreak4ModuleA		= maps\mp\gametypes\_class::table_getKillstreakModule( level.classTableName, class_num, 3, 0 );
		loadoutKillstreak4ModuleB		= maps\mp\gametypes\_class::table_getKillstreakModule( level.classTableName, class_num, 3, 1 );
		loadoutKillstreak4ModuleC		= maps\mp\gametypes\_class::table_getKillstreakModule( level.classTableName, class_num, 3, 2 );
		
		assert( IsDefined( loadoutKillstreak1 ) );
		assert( IsDefined( loadoutKillstreak2 ) );
		assert( IsDefined( loadoutKillstreak3 ) );
		assert( IsDefined( loadoutKillstreak4 ) );
	}
	
	loadoutPrimaryAttachment = attachmentMap_toBase( loadoutPrimaryAttachment );
	loadoutPrimaryAttachment2 = attachmentMap_toBase( loadoutPrimaryAttachment2 );
	loadoutPrimaryAttachment3 = attachmentMap_toBase( loadoutPrimaryAttachment3 );
	loadoutSecondaryAttachment = attachmentMap_toBase( loadoutSecondaryAttachment );
	loadoutSecondaryAttachment2 = attachmentMap_toBase( loadoutSecondaryAttachment2 );

	setMatchData( "players", self.clientid, "loadouts", slot, "primaryWeapon", loadoutPrimary );	
	setMatchData( "players", self.clientid, "loadouts", slot, "primaryAttachments", 0, loadoutPrimaryAttachment );
	setMatchData( "players", self.clientid, "loadouts", slot, "primaryAttachments", 1, loadoutPrimaryAttachment2 );
	setMatchData( "players", self.clientid, "loadouts", slot, "primaryAttachments", 2, loadoutPrimaryAttachment3 );

	for (i=0; i<6; i++)
		setMatchData( "players", self.clientid, "loadouts", slot, "perkSlots", i, loadoutPerks[i] );
	
	for (i=0; i<3; i++)
		setMatchData( "players", self.clientid, "loadouts", slot, "wildcardSlots", i, loadoutWildcards[i] );

	setMatchData( "players", self.clientid, "loadouts", slot, "secondaryWeapon", loadoutSecondary );
	setMatchData( "players", self.clientid, "loadouts", slot, "secondaryAttachments", 0,  loadoutSecondaryAttachment );
	setMatchData( "players", self.clientid, "loadouts", slot, "secondaryAttachments", 1,  loadoutSecondaryAttachment2 );

	setMatchData( "players", self.clientid, "loadouts", slot, "offhandWeapons", 0, loadoutOffhand );
	// TODO: fetch and write loadoutOffhand2
	setMatchData( "players", self.clientid, "loadouts", slot, "equipment", 0, loadoutEquipment );
	// TODO: fetch and write equipment2
	setMatchData( "players", self.clientid, "loadouts", slot, "equipmentWeaponExtra", loadoutEquipmentExtra );

	SetMatchData( "players", self.clientid, "loadouts", slot, "assaultStreaks", 0, "streak", loadoutKillstreak1 );
	SetMatchData( "players", self.clientid, "loadouts", slot, "assaultStreaks", 0, "modules", 0, loadoutKillstreak1ModuleA );
	SetMatchData( "players", self.clientid, "loadouts", slot, "assaultStreaks", 0, "modules", 1, loadoutKillstreak1ModuleB );
	SetMatchData( "players", self.clientid, "loadouts", slot, "assaultStreaks", 0, "modules", 2, loadoutKillstreak1ModuleC );

	SetMatchData( "players", self.clientid, "loadouts", slot, "assaultStreaks", 1, "streak", loadoutKillstreak2 );
	SetMatchData( "players", self.clientid, "loadouts", slot, "assaultStreaks", 1, "modules", 0, loadoutKillstreak2ModuleA );
	SetMatchData( "players", self.clientid, "loadouts", slot, "assaultStreaks", 1, "modules", 1, loadoutKillstreak2ModuleB );
	SetMatchData( "players", self.clientid, "loadouts", slot, "assaultStreaks", 1, "modules", 2, loadoutKillstreak2ModuleC );

	SetMatchData( "players", self.clientid, "loadouts", slot, "assaultStreaks", 2, "streak", loadoutKillstreak3 );
	SetMatchData( "players", self.clientid, "loadouts", slot, "assaultStreaks", 2, "modules", 0, loadoutKillstreak3ModuleA );
	SetMatchData( "players", self.clientid, "loadouts", slot, "assaultStreaks", 2, "modules", 1, loadoutKillstreak3ModuleB );
	SetMatchData( "players", self.clientid, "loadouts", slot, "assaultStreaks", 2, "modules", 2, loadoutKillstreak3ModuleC );

	SetMatchData( "players", self.clientid, "loadouts", slot, "assaultStreaks", 3, "streak", loadoutKillstreak4 );
	SetMatchData( "players", self.clientid, "loadouts", slot, "assaultStreaks", 3, "modules", 0, loadoutKillstreak4ModuleA );
	SetMatchData( "players", self.clientid, "loadouts", slot, "assaultStreaks", 3, "modules", 1, loadoutKillstreak4ModuleB );
	SetMatchData( "players", self.clientid, "loadouts", slot, "assaultStreaks", 3, "modules", 2, loadoutKillstreak4ModuleC );
	
//	/#
	thread recon_log_loadout( self,
		loadoutPrimary, loadoutPrimaryAttachment, loadoutPrimaryAttachment2, loadoutPrimaryAttachment3, loadoutPrimaryCamo,
		loadoutSecondary, loadoutSecondaryAttachment, loadoutSecondaryAttachment2, loadoutSecondaryCamo,
		loadoutEquipment, loadoutEquipmentExtra, loadoutOffhand,
		loadoutPerks[0], loadoutPerks[1], loadoutPerks[2], loadoutPerks[3], loadoutPerks[4], loadoutPerks[5],
		loadoutWildcards[0], loadoutWildcards[1], loadoutWildcards[2],
		loadoutKillstreak1, loadoutKillstreak1ModuleA, loadoutKillstreak1ModuleB, loadoutKillstreak1ModuleC,
		loadoutKillstreak2, loadoutKillstreak2ModuleA, loadoutKillstreak2ModuleB, loadoutKillstreak2ModuleC,
		loadoutKillstreak3, loadoutKillstreak3ModuleA, loadoutKillstreak3ModuleB, loadoutKillstreak3ModuleC,
		loadoutKillstreak4, loadoutKillstreak4ModuleA, loadoutKillstreak4ModuleB, loadoutKillstreak4ModuleC );
//	#/
	
	return slot;
}

///#
recon_log_loadout( player,
		primary_weapon, primary_attach_1, primary_attach_2, primary_attach_3, primary_camo,
		secondary_weapon, secondary_attach_1, secondary_attach_2, secondary_camo,
		equipment, equipment_extra, exo_ability,
		perk_1, perk_2, perk_3, perk_4, perk_5, perk_6, wildcard_1, wildcard_2, wildcard_3,
		killstreak_1, killstreak_1_module_a, killstreak_1_module_b, killstreak_1_module_c,
		killstreak_2, killstreak_2_module_a, killstreak_2_module_b, killstreak_2_module_c,
		killstreak_3, killstreak_3_module_a, killstreak_3_module_b, killstreak_3_module_c,
		killstreak_4, killstreak_4_module_a, killstreak_4_module_b, killstreak_4_module_c )
{
	is_bot = isBot( player ) || IsTestClient( player );
	script_file = "_matchdata.gsc";
	gameTime = player.spawnTime;
	class = player.curClass;
	player_name = player.name;

	ReconEvent( "script_mp_loadout_gear: script_file %s, gameTime %d, player_name %s, is_bot %b, class %s, primary_weapon %s, primary_attach_1 %s, primary_attach_2 %s, primary_attach_3 %s, primary_camo %s, secondary_weapon %s, secondary_attach_1 %s, secondary_attach_2 %s, secondary_camo %s, equipment %s, equipment_extra %b, exo_ability %s",
										 script_file,    gameTime,    player_name,    is_bot,    class,    primary_weapon,    primary_attach_1,    primary_attach_2,    primary_attach_3,    primary_camo,    secondary_weapon,    secondary_attach_1,    secondary_attach_2,    secondary_camo,    equipment,    equipment_extra,    exo_ability );

	ReconEvent( "script_mp_loadout_perks: script_file %s, gameTime %d, player_name %s, perk_1 %s, perk_2 %s, perk_3 %s, perk_4 %s, perk_5 %s, perk_6 %s, wildcard_1 %s, wildcard_2 %s, wildcard_3 %s",
										  script_file,    gameTime,    player_name,    perk_1,    perk_2,    perk_3,    perk_4,    perk_5,    perk_6,    wildcard_1,    wildcard_2,    wildcard_3 );

	ReconEvent( "script_mp_loadout_killstreaks: script_file %s, gameTime %d, player_name %s, killstreak_1 %s, killstreak_1_module_a %s, killstreak_1_module_b %s, killstreak_1_module_c %s, killstreak_2 %s, killstreak_2_module_a %s, killstreak_2_module_b %s, killstreak_2_module_c %s, killstreak_3 %s, killstreak_3_module_a %s, killstreak_3_module_b %s, killstreak_3_module_c %s, killstreak_4 %s, killstreak_4_module_a %s, killstreak_4_module_b %s, killstreak_4_module_c %s",
												script_file,    gameTime,    player_name,    killstreak_1,    killstreak_1_module_a,    killstreak_1_module_b,    killstreak_1_module_c,    killstreak_2,    killstreak_2_module_a,    killstreak_2_module_b,    killstreak_2_module_c,    killstreak_3,    killstreak_3_module_a,    killstreak_3_module_b,    killstreak_3_module_c,    killstreak_4,    killstreak_4_module_a,    killstreak_4_module_b,    killstreak_4_module_c );

}
//#/

logPlayerAndKillerExoMoveData( lifeId, killer )
{
	if ( !canLogClient( self ) || ( isPlayer( killer ) && !canLogClient( killer ) ) || !canLogLife( lifeId ) )
		return;
	
	if ( lifeId >= level.MaxLives )
		return;
	
	//
	// self
	// 
	currentDeciSeconds = getTimePassedDeciSecondsIncludingRounds();

	if( isDefined( self.exoCount["exo_boost"] ) && isDefined( self.exoMostRecentTimeDeciSeconds["exo_boost"] ) )
	{		
		setMatchData( "lives", lifeId, "numberOfBoosts", self.exoCount["exo_boost"] );
		diffTime = clampToByte(currentDeciSeconds - self.exoMostRecentTimeDeciSeconds["exo_boost"]);
		setMatchData( "lives", lifeId, "victimDeciSecondsSinceLastBoost", diffTime );
	}
	if( isDefined( self.exoCount["ground_slam"] ) && isDefined( self.exoMostRecentTimeDeciSeconds["ground_slam"] ) )
	{	
		setMatchData( "lives", lifeId, "numberOfBoostsSlams", self.exoCount["ground_slam"] );
		diffTime = clampToByte(currentDeciSeconds - self.exoMostRecentTimeDeciSeconds["ground_slam"]);
		setMatchData( "lives", lifeId, "victimDeciSecondsSinceLastBoostSlam", diffTime );		
	}
	if( isDefined( self.exoCount["exo_dodge"] ) && isDefined( self.exoMostRecentTimeDeciSeconds["exo_dodge"] ) )
	{
		setMatchData( "lives", lifeId, "numberOfDodges", self.exoCount["exo_dodge"] );
		diffTime = clampToByte(currentDeciSeconds - self.exoMostRecentTimeDeciSeconds["exo_dodge"]);
		setMatchData( "lives", lifeId, "victimDeciSecondsSinceLastDodge", diffTime );			
	}
	if( isDefined( self.exoCount["exo_slide"] ) )
	{
		// they only want the count for knee slides
		setMatchData( "lives", lifeId, "numberOfKneeSlides", self.exoCount["exo_slide"] );
	}

	//
	// killer
	//
	if( isPlayer( killer ) )
	{
		if( !isDefined( killer.exoMostRecentTimeDeciSeconds ) )
			return;
		
		if( IsDefined( killer.exoMostRecentTimeDeciSeconds["exo_boost"] ) )
		{
			diffTime = clampToByte(currentDeciSeconds - killer.exoMostRecentTimeDeciSeconds["exo_boost"]);
			setMatchData( "lives", lifeId, "killerDeciSecondsSinceLastBoost", diffTime );
		}
		if( isDefined( killer.exoMostRecentTimeDeciSeconds["ground_slam"] ) )
		{
			diffTime = clampToByte(currentDeciSeconds - killer.exoMostRecentTimeDeciSeconds["ground_slam"]);
			setMatchData( "lives", lifeId, "killerDeciSecondsSinceLastBoostSlam", diffTime );	
		}
		if( isDefined( killer.exoMostRecentTimeDeciSeconds["exo_dodge"] ) )
		{
			diffTime = clampToByte(currentDeciSeconds - killer.exoMostRecentTimeDeciSeconds["exo_dodge"]);
			setMatchData( "lives", lifeId, "killerDeciSecondsSinceLastDodge", diffTime );		
		}
	}
}


logPlayerAndKillerADSandFOV( lifeId, killer )
{
	if ( !canLogClient( self ) || ( isPlayer( killer ) && !canLogClient( killer ) ) || !canLogLife( lifeId ) )
		return;
	
	if ( lifeId >= level.MaxLives )
		return;
	
	if( isPlayer(killer) )
	{
		//
		// ADS and fov		
		//
		
		if( killer PlayerAds() > 0.5 )  	
		{
			//PrintLn("killer: ", attacker.name, " was ADS at time of death!!!");	
			setMatchData( "lives", lifeId, "killerWasADS", true);
		}
		// else setMatchData( "lives", lifeId, "killerWasADS", false); <-- not necessary - defaults to false (i.e. 0)
		
		killerEye = killer GetEye();
		if( within_fov( killerEye, killer.angles, self.origin, cos( GetDvarFloat( "cg_fov" ) ) ) )
		{
			//PrintLn("for killer: ", attacker.name, " victim was in killer's FOV!!");	
			setMatchData( "lives", lifeId, "victimWasInKillersFOV", true);
		}			
		victimEye = self GetEye();
		if( within_fov( victimEye, self.angles, killer.origin, cos( GetDvarFloat( "cg_fov" ) ) ) )
		{
			//PrintLn("for victim: ", self.name, " killer was in victim's FOV!!");	
			setMatchData( "lives", lifeId, "killerWasInVictimsFOV", true);
		}	
	}

	if( self PlayerAds() > 0.5 ) 		
	{
		//PrintLn("dude: ", self.name, " was ADS at time of death!!!");
		setMatchData( "lives", lifeId, "victimWasADS", true);
	}

}


logPlayerAndKillerShieldCloakHoverActive( lifeId, killer )
{
	if ( !canLogClient( self ) || ( isPlayer( killer ) && !canLogClient( killer ) ) || !canLogLife( lifeId ) )
		return;
	
	if ( lifeId >= level.MaxLives )
		return;
	
	// self:	
	if( isDefined( self.exo_shield_on ) && self.exo_shield_on )
	{
		setMatchData( "lives", lifeId, "victimShieldActive", true );

	}
	if( isDefined( self.exo_hover_on ) && self.exo_hover_on )	
	{
		setMatchData( "lives", lifeId, "victimHoverActive", true );
	}
	if( self IsCloaked() )
		setMatchData( "lives", lifeId, "victimCloakingActive", true );

	// killer	
	if( isPlayer(killer) )
	{		
		if( isDefined( killer.exo_shield_on ) && killer.exo_shield_on )
		{
			setMatchData( "lives", lifeId, "killerShieldActive", true );

		}
		if( isDefined( killer.exo_hover_on ) && killer.exo_hover_on )	
		{
			setMatchData( "lives", lifeId, "killerHoverActive", true );
		}
		if( killer IsCloaked() )
			setMatchData( "lives", lifeId, "killerCloakingActive", true );
	}	
}


determineWeaponNameAndAttachments( weaponNameFull, primaryWeapon ) 
{
	weaponType = undefined;
	weaponClass = undefined;

	if ( weaponNameFull == "none" )
	{
		weaponType = "none";
		weaponClass = "none";
	}
	else
	{
		weaponType = weaponInventoryType( weaponNameFull );
		weaponClass = weaponClass( weaponNameFull );
	}
	
	if ( isSubstr( weaponNameFull, "destructible" ) )
		weaponNameFull = "destructible";

	attachments = [];	
	attachments[0] = "None";
	attachments[1] = "None";
	attachments[2] = "None";
	
	weaponName = "";

	if ( isDefined( weaponType ) && (weaponType == "primary" || weaponType == "altmode") && (weaponClass == "pistol" || 
					weaponClass == "smg" || weaponClass == "rifle" || weaponClass == "spread" || weaponClass == "mg" || 
					weaponClass == "grenade" || weaponClass == "rocketlauncher" || weaponClass == "sniper" || 
					weaponClass == "cone" || weaponClass == "beam" || weaponClass == "shield" ) )
	{				
		if ( weaponType == "altmode" )
		{
			if ( isDefined(primaryWeapon) )
				weaponNameFull = primaryWeapon;
		}
		
		assertEx( isDefined(weaponNameFull), "No weapon defined in _matchdata::determineWeaponEnumAndAttachments" );
		
		weaponTokens = getWeaponNameTokens( weaponNameFull );
		weaponName = getBaseWeaponName( weaponNameFull );
	
		if ( weaponTokens[0] == "iw5" || weaponTokens[0] == "iw6" )
		{
			assert( weaponTokens.size > 1 );
			
			weaponAttachments = GetWeaponAttachments( weaponNameFull );
			
			i = 0;
			foreach ( weaponAtt in weaponAttachments )
			{
				if ( !isAttachment( weaponAtt ) )
					continue;
				
				weaponAttachment = attachmentMap_toBase( weaponAtt );
				assert( i <= 2 );
				if( i <= 2 )		// not sure if script asserts go away in Release...
				{
					attachments[i] = weaponAttachment;
					i++;
				}
				else
				{
					break;
				}
			}
		}
		else if( weaponTokens[0] == "alt" )	
		{
			assert( weaponTokens.size > 1 );
			
			baseMW5WeaponName = weaponTokens[1] + "_" + weaponTokens[2];
			
			weaponName = baseMW5WeaponName;
			
			if ( isDefined( weaponTokens[4] ) && isAttachment( weaponTokens[4] ) )	
			{
				weaponAttachment = attachmentMap_toBase( weaponTokens[4] );
				attachments[0] = weaponAttachment;
			}
				
			if ( isDefined( weaponTokens[5] ) && isAttachment( weaponTokens[5] ) )
			{
				weaponAttachment = attachmentMap_toBase( weaponTokens[5] );
				attachments[1] = weaponAttachment;	
			}
		}
		else
		{
			assert( weaponTokens.size > 1 && weaponTokens.size < 4 );
	
			assertEx( weaponTokens[weaponTokens.size - 1] == "mp", "weaponTokens[weaponTokens.size - 1]: " + weaponTokens[weaponTokens.size - 1] );
			weaponTokens[weaponTokens.size - 1] = undefined; // remove the trailing "mp"
					
	
			if ( isDefined( weaponTokens[1] ) && weaponType != "altmode" )// && !isLootWeapon( weaponTokens ) )
			{
				weaponAttachment = attachmentMap_toBase( weaponTokens[1] );
				attachments[0] = weaponAttachment;
			}
		
			if ( isDefined( weaponTokens[2] ) && weaponType != "altmode" )
			{
				weaponAttachment = attachmentMap_toBase( weaponTokens[2] );
				attachments[1] = weaponAttachment;	
			}
		}
	}
	else if ( weaponType == "item" || weaponType == "offhand" )
	{
		weaponName = maps\mp\_utility::strip_suffix( weaponNameFull, "_lefthand" );
		weaponName = maps\mp\_utility::strip_suffix( weaponName, "_mp" );
	}
	else
	{
		weaponName = weaponNameFull;
	}

	returnStruct = spawnStruct();
	returnStruct.weaponName = weaponName;
	returnStruct.attachments = attachments;
	returnStruct.weaponType = weaponType;
	returnStruct.weaponClass = weaponClass;
	returnStruct.weaponNameFull = weaponNameFull;  	// presevering the old behaviour, it's possible for the code in this function to modify the incoming weaponNameFull
	
	return returnStruct;
}


logFirefightShotsHits( lifeId, killer )
{
	if ( !canLogClient( self ) || ( isPlayer( killer ) && !canLogClient( killer ) ) || !canLogLife( lifeId ) )
		return;
	
	if( !isPlayer( killer ) ) //BS-50946
		return;
	
	if ( lifeId >= level.MaxLives )
		return;
	
	//
	// self (i.e. victim):
	//
	if( self.currentFirefightShots > 0 )
	{
		setMatchData( "lives", lifeId, "shots", clampToByte( self.currentFirefightShots ) );
		//PrintLn("######## dude: ", self.name, " fired ", self.currentFirefightShots, " shots!");
	}
	if( isDefined( killer.enemyHitCounts) && isDefined( killer.enemyHitCounts[self.guid] ) && killer.enemyHitCounts[self.guid] > 0 )
	{
		setMatchData( "lives", lifeId, "hits", clampToByte( killer.enemyHitCounts[self.guid] ) );	// number of times self hit killer
		//PrintLn("######## dude: ", self.name, " had ", killer.enemyHitCounts[self.guid], " hits!");
	}

	//
	// killer:
	//
	if( killer.currentFirefightShots > 0 )
	{
		setMatchData( "lives", lifeId, "killerShots", clampToByte( killer.currentFirefightShots ) );
		//PrintLn("######## killer: ", killer.name, " fired ", killer.currentFirefightShots, " shots!");
	}

	if( isDefined( self.enemyHitCounts) && isDefined( self.enemyHitCounts[killer.guid] ) && self.enemyHitCounts[killer.guid] > 0 )
	{
		setMatchData( "lives", lifeId, "killerHits", clampToByte( self.enemyHitCounts[killer.guid] ) ); // number of times killer hit self
		//PrintLn("######## killer: ", killer.name, " had ", self.enemyHitCounts[killer.guid], " hits!");
	}


}


logPlayerAndKillerStanceAndMotionState( lifeId, attacker )
{
	if( !canLogLife( lifeId ) )
		return;
	
	if( isPlayer( self ) && canLogClient( self ) )
	{
		stanceAndMotionState = GetStanceAndMotionStateForPlayer( self );
		setMatchData( "lives", lifeId, "victimStanceAndMotionState", stanceAndMotionState );
	}
	if( isPlayer( attacker ) && canLogClient( attacker ) )
	{
		stanceAndMotionState = GetStanceAndMotionStateForPlayer( attacker );
		setMatchData( "lives", lifeId, "killerStanceAndMotionState", stanceAndMotionState );
	}
	
}


logAssists( lifeId, attacker )
{
	if( !canLogLife( lifeId ) )
		return;
	
	if( isPlayer( self ) && canLogClient( self ) )
	{
		if( isDefined( self.attackerData ) )
		{
			slot = 0;			
			foreach( attackerStruct in self.attackerData )
			{						
				if( isPlayer( attackerStruct.attackerEnt ) )
				{
					if( attackerStruct.attackerEnt != attacker )
					{
						setMatchData( "lives", lifeId, "assists", slot, "assistingPlayerIndex", attackerStruct.attackerEnt.clientid );
						setMatchData( "lives", lifeId, "assists", slot, "damage", clampToByte( attackerStruct.damage ) );
						slot++;
						if( slot == 5 )		// capped by Life.assists[5]
							break;
					}
				}
			}
			if( slot < 5)
			{
				for( i = slot; i < 5; i++ )
				{
					// set assitingPlayerIndex to 255 for unused slots
					setMatchData( "lives", lifeId, "assists", i, "assistingPlayerIndex", 255 );
				}
			}
		}
	}
}


logSpecialAssists( attacker, xpevent )
{
	if( !isPlayer( self ) || !canLogClient( self ) )
		return;	
	
	if( !isPlayer( attacker ) || !canLogClient( attacker ) )
		return;	
	
	//
	// called from processAssistEvent, called from handleNormalDeath, which is called after
	// logPlayerDeath and logAssists
	//
	// I believe it's safe to assume that we (the victim) still have the same lifeId to use for logging these assists
	//

	lifeId = self.lifeId;

	if( !canLogLife( lifeId ) )
		return;
	
	// xpevent possible values:
	// "assist_riot_shield"
	// "assist_uav_plus"
	// "assist_uav"
	// "assist_emp"

	if( xpevent == "assist_emp" ||
		xpevent == "assist_uav" ||
		xpevent == "assist_uav_plus" ||
		xpevent == "assist_riot_shield" )
	{
		//
		// see if attacker id is already in assists array
		// even if the attacker here was the killer, we still want to log this assists, as it's "special"
		//

		for( i = 0; i < 5; i++ )	// capped by Life.assists[5]
		{
			assistingPlayerIndex = getMatchData( "lives", lifeId, "assists", i, "assistingPlayerIndex");
			if( assistingPlayerIndex == attacker.clientid || assistingPlayerIndex == 255 )
			{
				if( assistingPlayerIndex == 255 )
					setMatchData( "lives", lifeId, "assists", i, "assistingPlayerIndex", attacker.clientid );

				if( xpevent == "assist_emp" )				
					setMatchData( "lives", lifeId, "assists", i, "assistEMP", true );														
				else if( xpevent == "assist_uav" )
					setMatchData( "lives", lifeId, "assists", i, "assistUAV", true );					
				else if( xpevent == "assist_uav_plus" )
					setMatchData( "lives", lifeId, "assists", i, "assistUAVPlus", true );					
				else if( xpevent == "assist_riot_shield" )
					setMatchData( "lives", lifeId, "assists", i, "assistRiotShield", true );					
				else
					PrintLn( "^1warning: assist type: ", xpevent, " not currently handled by matchdata Life.assists[] ");
			
				break;
			}
		}
	}
}


logPlayerDeath( lifeId, attacker, iDamage, sMeansOfDeath, sKillersWeapon, sKillersPrimaryWeapon, sHitLoc, victimCurrentWeapon )
{	
	if ( !canLogClient( self ) || ( isPlayer( attacker ) && !canLogClient( attacker ) ) || !canLogLife( lifeId ) )
		return;
	
	if ( lifeId >= level.MaxLives )
		return;
	
	if( level.isZombieGame )
		return;
	
	self logPlayerAndKillerExoMoveData( lifeId, attacker );
	self logPlayerAndKillerADSandFOV( lifeId, attacker );
	self logPlayerAndKillerShieldCloakHoverActive( lifeId, attacker );
	self logFirefightShotsHits( lifeId, attacker );
	self logPlayerAndKillerStanceAndMotionState( lifeId, attacker );
	self logAssists( lifeId, attacker );

	//
	// kiler weapon and attachments
	//		
	killerWeaponAndAttachments = determineWeaponNameAndAttachments( sKillersWeapon, sKillersPrimaryWeapon );
	assert( isDefined( killerWeaponAndAttachments.weaponName ) );
	assert( isDefined( killerWeaponAndAttachments.attachments ) );
	assert( isDefined( killerWeaponAndAttachments.weaponType ) );
	assert( isDefined( killerWeaponAndAttachments.weaponClass ) );
	assert( isDefined( killerWeaponAndAttachments.weaponNameFull ) );
	for( i = 0; i < 3; i++ )
	{
		if( isDefined( killerWeaponAndAttachments.attachments[i] ) && killerWeaponAndAttachments.attachments[i] != "None" )
			setMatchData( "lives", lifeId, "killersWeaponAttachments", i, killerWeaponAndAttachments.attachments[i] );	
	}
	if( killerWeaponAndAttachments.weaponType != "exclusive" )
	{
		setMatchData( "lives", lifeId, "killersWeapon", killerWeaponAndAttachments.weaponName );
	}

	if(  killerWeaponAndAttachments.weaponNameFull == "altmode" )
		setMatchData( "lives", lifeId, "killersWeaponAltMode", true);
	
	if ( isKillstreakWeapon(  killerWeaponAndAttachments.weaponNameFull ) )
	{
		setMatchData( "lives", lifeId, "modifiers", "killstreak", true );

		if( isDefined( attacker.currentKillStreakIndex ) )
		{
			// I suspect the majority of the time it's a safe assumption that the attacker's 
			// killstreak weapon just used in this kill is his index most recently set.
			
			currKills = getMatchData( "killstreaks", attacker.currentKillStreakIndex, "killsTotal" );
			currKills++;
			setMatchData( "killstreaks", attacker.currentKillStreakIndex, "killsTotal", clampToShort(currKills) );

			setMatchData( "lives", lifeId, "killerKillstreakIndex", attacker.currentKillStreakIndex );
		}
	}
	else
	{
		setMatchData( "lives", lifeId, "killerKillstreakIndex", 255 );
	}

	//
	// victim weapon and attachments
	//	 	
	victimWeaponAndAttachments = determineWeaponNameAndAttachments( victimCurrentWeapon, undefined );
	assert( isDefined( victimWeaponAndAttachments.weaponName ) );
	assert( isDefined( victimWeaponAndAttachments.attachments ) );
	assert( isDefined( victimWeaponAndAttachments.weaponType ) );
	assert( isDefined( victimWeaponAndAttachments.weaponClass ) );
	assert( isDefined( victimWeaponAndAttachments.weaponNameFull ) );	
	for( i = 0; i < 3; i++ )
	{
		if( isDefined( victimWeaponAndAttachments.attachments[i] ) && victimWeaponAndAttachments.attachments[i] != "None" )
			setMatchData( "lives", lifeId, "victimCurrentWeaponAtDeathAttachments", i, victimWeaponAndAttachments.attachments[i] );	
	}

	//
	// we need a safeguard here for the victim's weapon, as the incoming value for a victim using 
	// a killstreak is not in the Weapon enum, and is not the same as the killer's value when using the same killstreak :|
	//
	// BS-63465
	// additionally, the victim can get killed in weird states like defusing a bomb, and that registers as their current weapon
	// so skip this altogether if weaponType == "exclusive"
	//
	if( victimWeaponAndAttachments.weaponType != "exclusive" )
	{
		if( isKillstreakWeapon( victimWeaponAndAttachments.weaponName ) )
		{
			if( isDefined( self.primaryWeapon) )
			{
				baseName = 	getBaseWeaponName( self.primaryWeapon );
				setMatchData( "lives", lifeId, "victimCurrentWeaponAtDeath", baseName );
			}
		}
		else
		{
			setMatchData( "lives", lifeId, "victimCurrentWeaponAtDeath", victimWeaponAndAttachments.weaponName );
		}
	}

	// victim using a weapon he picked up?
	if( isDefined( self.pickedUpWeaponFrom) && isDefined( self.pickedUpWeaponFrom[ victimWeaponAndAttachments.weaponNameFull ] ) )
		setMatchData( "lives", lifeId, "victimCurrentWeaponPickedUp", true );
		
	setMatchData( "lives", lifeId, "meansOfDeath", sMeansOfDeath );
	deathDot = 2;
	if ( isPlayer( attacker ) )
	{
		setMatchData( "lives", lifeId, "killer", attacker.clientid );
		setMatchData( "lives", lifeId, "killerLifeIndex", attacker.lifeId );
		setMatchData( "lives", lifeId, "killerPos", 0, int( attacker.origin[0] ) );
		setMatchData( "lives", lifeId, "killerPos", 1, int( attacker.origin[1] ) );
		setMatchData( "lives", lifeId, "killerPos", 2, int( attacker.origin[2] ) );

		setMatchData( "lives", lifeId, "killerAngles", 0, int( attacker.angles[0] ) );
		setMatchData( "lives", lifeId, "killerAngles", 1, int( attacker.angles[1] ) );
		setMatchData( "lives", lifeId, "killerAngles", 2, int( attacker.angles[2] ) );


		victimForward = anglesToForward( (0,self.angles[1],0) );
		attackDirection = (self.origin - attacker.origin);
		attackDirection = VectorNormalize( (attackDirection[0], attackDirection[1], 0) );
		deathDot = VectorDot( victimForward, attackDirection );
		setMatchData( "lives", lifeId, "dotOfDeath", deathDot );

		if( attacker isJuggernaut() )
			SetMatchData( "lives", lifeId, "killerIsJuggernaut", true );

		// killer using a weapon he picked up?
		if( isDefined( attacker.pickedUpWeaponFrom) && isDefined( attacker.pickedUpWeaponFrom[ killerWeaponAndAttachments.weaponNameFull ] ) )
			setMatchData( "lives", lifeId, "killerCurrentWeaponPickedUp", true );
	}
	else
	{
		// 255 is world
		setMatchData( "lives", lifeId, "killer", 255 );
		setMatchData( "lives", lifeId, "killerLifeIndex", 65535 );
		setMatchData( "lives", lifeId, "killerPos", 0, int( self.origin[0] ) );
		setMatchData( "lives", lifeId, "killerPos", 1, int( self.origin[1] ) );
		setMatchData( "lives", lifeId, "killerPos", 2, int( self.origin[2] ) );

		setMatchData( "lives", lifeId, "killerAngles", 0, int( self.angles[0] ) );
		setMatchData( "lives", lifeId, "killerAngles", 1, int( self.angles[1] ) );
		setMatchData( "lives", lifeId, "killerAngles", 2, int( self.angles[2] ) );

	}
	
	setMatchData( "lives", lifeId, "player", self.clientid );
	setMatchData( "lives", lifeId, "victimPos", 0, int( self.origin[0] ) );
	setMatchData( "lives", lifeId, "victimPos", 1, int( self.origin[1] ) );
	setMatchData( "lives", lifeId, "victimPos", 2, int( self.origin[2] ) );

	setMatchData( "lives", lifeId, "victimAngles", 0, int( self.angles[0] ) );
	setMatchData( "lives", lifeId, "victimAngles", 1, int( self.angles[1] ) );
	setMatchData( "lives", lifeId, "victimAngles", 2, int( self.angles[2] ) );	
	
//	/#
	// Recon Spawn Data Tracking - Start
	attacker_name = "world";
	if( IsPlayer( attacker ) )
		attacker_name = attacker.name;
	
	bSoleAttacker = true;
	numBullets = 0;
	bIsBot = IsAITeamParticipant( self );
	
	bKillerIsbot = false;
	if( IsPlayer( attacker ) )
		bKillerIsbot = IsAITeamParticipant( attacker );
	
	attacker_distance = Length( self.origin - attacker.origin );
	bIsLoot = false;
	adsFrac = 0.0;
	spawnToDeathTime = -1;
	spawnToKillTime = -1;
	gameTime = GetTime();
	
	if( IsPlayer( attacker ) )
		adsFrac = attacker PlayerAds();
	attacker_clientid = attacker.clientid;
	if( !IsDefined(attacker_clientid) )
		attacker_clientid = -1;
	attacker_lifeid = attacker.lifeId;
	if( !IsDefined(attacker_lifeid) )
		attacker_lifeid = -1;
	CONST_weapon_tuning_version = 0.1;
	
	if( self.damage_info.size > 1 )
	{
		bSoleAttacker = false;
	}

	if( isDefined( self.damage_info[ attacker GetEntityNumber() ] ) )
	{
		numBullets = self.damage_info[ attacker GetEntityNumber() ].num_shots;
	}
	
	reconVictimWeapon = self.pers[ "primaryWeapon" ] + "_mp";
	reconVictimWeaponClass = WeaponClass( reconVictimWeapon );
	
	if( IsSubStr( killerWeaponAndAttachments.weaponName, "loot" ) )
	{
		bIsLoot = true;
	}

	if( IsDefined( self.spawnInfo ) && IsDefined( self.spawnInfo.spawnTime ) )
	{
		spawnToDeathTime = ( gameTime - self.spawnInfo.spawnTime ) / 1000.0;
	}
	
	if( IsDefined( attacker.spawnInfo ) && IsDefined( attacker.spawnInfo.spawnTime ) && IsPlayer( attacker ) )
	{
		spawnToKillTime = ( gameTime - attacker.spawnInfo.spawnTime ) / 1000.0;
	}
	
	ReconSpatialEvent( self.origin, "script_mp_playerdeath: player_name %s, life_id %d, angles %v, death_dot %f, is_jugg %b, is_killstreak %b, mod %s, gameTime %d, spawnToDeathTime %f, attackerAliveTime %f, attacker_life_id %d", 
					  										self.name, self.lifeId, self.angles, deathDot, attacker isJuggernaut(), isKillstreakWeapon(killerWeaponAndAttachments.weaponNameFull), sMeansOfDeath, gameTime, spawnToDeathTime, spawnToKillTime, attacker_lifeid );
	ReconSpatialEvent( self.origin, "script_mp_weaponinfo: player_name %s, life_id %d, isbot %b, attacker_name %s, attacker %d, attacker_pos %v, distance %f, ads_fraction %f, is_jugg %b, is_killstreak %b, weapon_type %s, weapon_class %s, weapon_name %s, isLoot %b, attachment0 %s, attachment1 %s, attachment2 %s, numShots %d, soleAttacker %b, gameTime %d", 
					  										self.name, self.lifeId, bIsBot, attacker_name, attacker_clientid, attacker.origin, attacker_distance, adsFrac, attacker isJuggernaut(), isKillstreakWeapon(killerWeaponAndAttachments.weaponNameFull), killerWeaponAndAttachments.weaponType, killerWeaponAndAttachments.weaponClass, killerWeaponAndAttachments.weaponName, bIsLoot, 
					  										killerWeaponAndAttachments.attachments[0], killerWeaponAndAttachments.attachments[1], killerWeaponAndAttachments.attachments[2], numBullets, bSoleAttacker, gameTime );
	ReconSpatialEvent( self.origin, "script_mp_weaponinfo_ext: player_name %s, life_id %d, gametime %d, version %f, victimWeapon %s, victimWeaponClass %s, killerIsBot %b",
					  										self.name, self.lifeId, gameTime, CONST_weapon_tuning_version, reconVictimWeapon, reconVictimWeaponClass, bKillerIsbot );

	if( !isDefined( level.matchData ) )
		level.matchData = [];
	
	if( !isDefined( level.matchData["deathCount"] ) )
		level.matchData["deathCount"] = 1;
	else
		level.matchData["deathCount"]++;
	
	if( spawnToDeathTime <= 3.0 )
	{
		if ( !isDefined( level.matchData["badSpawnDiedTooFastCount"] ) )
			level.matchData["badSpawnDiedTooFastCount"] = 1;
		else
			level.matchData["badSpawnDiedTooFastCount"]++;

		if( self.spawnInfo.badSpawn == false )
		{
			if( !isDefined( level.matchData["badSpawnByAnyMeansCount"] ) )
				level.matchData["badSpawnByAnyMeansCount"] = 1;
			else
				level.matchData["badSpawnByAnyMeansCount"]++;

			self.spawnInfo.badSpawn = true;
		}
	}
	
	if( IsPlayer( attacker ) && spawnToKillTime <= 3.0 && !( killerWeaponAndAttachments.weaponName == "sentry_minigun_mp" ) )
	{
		if( !isDefined( level.matchData["badSpawnKilledTooFastCount"] ) )
			level.matchData["badSpawnKilledTooFastCount"] = 1;
		else
			level.matchData["badSpawnKilledTooFastCount"]++;

		if( attacker.spawnInfo.badSpawn == false )
		{
			if( !isDefined( level.matchData["badSpawnByAnyMeansCount"] ) )
				level.matchData["badSpawnByAnyMeansCount"] = 1;
			else
				level.matchData["badSpawnByAnyMeansCount"]++;

			attacker.spawnInfo.badSpawn = true;
		}
	}
	// Recon Spawn Data Tracking - End
//	#/
}

logPlayerData()
{
	if ( !canLogClient( self ) )
		return;
		
	setMatchData( "players", self.clientid, "score", self getPersStat( "score" ) );
	
	if( self getPersStat( "assists" ) > 255 )
		setMatchData( "players", self.clientid, "assists", 255 );
	else
		setMatchData( "players", self.clientid, "assists", self getPersStat( "assists" ) );
		
	if( self getPersStat( "longestStreak" ) > 255 )
		setMatchData( "players", self.clientid, "longestStreak", 255 );
	else
		setMatchData( "players", self.clientid, "longestStreak", self getPersStat( "longestStreak" ) );
	
	if( IsDefined( self ) && IsDefined( self.pers ) && IsDefined( self.pers[ "validationInfractions" ] ) )
	{	
	   	if( self getPersStat( "validationInfractions" ) > 255 )
			setMatchData( "players", self.clientid, "validationInfractions", 255 );
		else
			setMatchData( "players", self.clientid, "validationInfractions", self getPersStat( "validationInfractions" ) );
	}
}


// log to playerdata.
endOfGameSummaryLogger()
{
	foreach ( player in level.players )
	{
		wait( 0.05 );
		
		//player may disconnect during waits
		if ( !isdefined( player ) )
			continue;
		
		logPlayerPing( player );
		
		if ( isDefined( player.detectedExploit ) && player.detectedExploit && ( player rankingEnabled() ) )
			player setRankedPlayerData( "restXPGoal", player.detectedExploit );
		
		challengesCompleted = undefined;
		challengeDataDefined = false;
		if( isdefined( game["challengeStruct"] ) && IsDefined( game["challengeStruct"]["challengesCompleted"] ) && IsDefined( game["challengeStruct"]["challengesCompleted"][player.guid] ) )
			challengeDataDefined = true;
		
		if( challengeDataDefined )
		{
			challengesCompleted = game["challengeStruct"]["challengesCompleted"][player.guid];
			if( challengesCompleted.size > 0 )
			{
				player setCommonPlayerData( "round", "challengeNumCompleted", challengesCompleted.size );
				cp = clampToByte( challengesCompleted.size );			
				setMatchData( "players", player.clientid, "challengesCompleted", cp );
			}
			else
				player setCommonPlayerData( "round", "challengeNumCompleted", 0 );
		}
		else
			player setCommonPlayerData( "round", "challengeNumCompleted", 0 );
		
		for ( i = 0; i < 20; i++ )
		{
			if ( isDefined( challengesCompleted ) && isDefined( challengesCompleted[i] ) && challengesCompleted[i] != 8000 ) // 8000 = "ch_prestige"
				player setCommonPlayerData( "round", "challengesCompleted", i, challengesCompleted[i] );
			else
				player setCommonPlayerData( "round", "challengesCompleted", i, 0 );
		}
		
		player setCommonPlayerData( "round", "gameMode", level.gametype );
		player setCommonPlayerData( "round", "map", ToLower( GetDvar( "mapname" ) ) );
	}
}

// log ping for player
logPlayerPing( player )
{
	if( !isDefined ( player.pers["maxPing"] ) || 
		!isDefined ( player.pers["minPing"] ) ||
	    !isDefined ( player.pers["pingAccumulation"] ) ||
	    !isDefined ( player.pers["pingSampleCount"] ) )
	{	
		return;
	}
	
	if( player.pers["pingSampleCount"] > 0 && player.pers["maxPing"] > 0 )
	{
		averagePing = clampToShort( player.pers["pingAccumulation"] / player.pers["pingSampleCount"] );
		setMatchData( "players", player.clientid, "averagePing", averagePing );
		setMatchData( "players", player.clientid, "maxPing", clampToShort( player.pers["maxPing"] ) );
		setMatchData( "players", player.clientid, "minPing", clampToShort( player.pers["minPing"] ) );
	}
}


// log the lives of players who are still alive at match end.
gameEndListener()
{
	level waittill ( "game_ended" );
	
	foreach ( player in level.players )
	{		
		player logPlayerData();
		
		if ( !isAlive( player ) )
			continue;
			
		player logPlayerLife( false );
	}

	// compute the players score per minute
	foreach ( player in level.players )
	{
		// report the player spm
		if (player.totalLifeTime > 0)
		{
			scoreperminute = player getPersStat( "score" ) / ( player.totalLifeTime / 60000 );
			tournamentreportplayerspm( player.xuid, scoreperminute, player.team );
		}
		
		// reset the player total life time
		player.totalLifeTime = 0;
	}
}

canLogClient( client )
{
	if ( IsAgent( client ) )
	{
		return false;
	}
	
	classname = client.code_classname;
	if ( !IsDefined(classname) )
		classname = "undefined";
	
	assertEx( isPlayer( client ) , "Client is not a player: " + classname );
	return ( client.clientid < level.MaxLogClients );
}

canLogEvent()
{
	return ( getMatchData( "eventCount" ) < level.MaxEvents );
}

canLogKillstreak()
{
	return ( getMatchData( "killstreakCount" ) < level.MaxKillstreaks );
}

canLogLife( lifeId )
{
	return ( getMatchData( "lifeCount" ) < level.MaxLives );
}

logWeaponStat( weaponName, statName, incValue )
{
	if ( !canLogClient( self ) )
		return;
	
	if( isKillstreakWeapon( weaponName ) )
		return;
		
	if( !isDefined( self.pers["mpWeaponStats"][weaponName]) ) 
	{
		self.pers["mpWeaponStats"][weaponName] = [];
	}

	// statName is one of:
		// shots;
		// headShots;
		// kills;
		// hipfirekills;
		// hits;
		// deaths;
		// XP; (maybe?)
	// i.e. all numeric fields that can be just incremented
	// all the fields in script are essentially 'int', and they
	// will be written to ItemStats in playerdata where all the fields are explicitly 'int'
	// so we can safely skip any size/clamping tests.

	if( !isDefined( self.pers["mpWeaponStats"][weaponName][statName] ) )
	{
		self.pers["mpWeaponStats"][weaponName][statName] = 0;
	}
	
	value = self.pers["mpWeaponStats"][weaponName][statName];
	value += incValue;
	self.pers["mpWeaponStats"][weaponName][statName] = value;

	//PrintLn("logging weapon stat for: ", weaponName, " stat: ", statName, " new value: ", value);

}


buildBaseWeaponList()
{
	baseWeapons = [];
	max_weapon_num = 149;
	for( weaponId = 0; weaponId <= max_weapon_num; weaponId++ )
	{
		weapon_name = tablelookup( "mp/statstable.csv", 0, weaponId, 4 );
		if( weapon_name == "" )
			continue;
		
		if ( !isSubStr( tableLookup( "mp/statsTable.csv", 0, weaponId, 2 ), "weapon_" ) )
			continue;
		
		if ( tableLookup( "mp/statsTable.csv", 0, weaponId, 2 ) == "weapon_other" )
			continue;
		
		if ( TableLookup( "mp/statsTable.csv", 0, weaponId, 51 ) != "" )
			continue;
			 
		baseWeapons[baseWeapons.size] = weapon_name;
	}
	return baseWeapons;
}


logKillsConfirmed()
{
	if ( !canLogClient( self ) )
		return;
	
	setMatchData( "players", self.clientid, "killsConfirmed", self.pers["confirmed"] );
}

logKillsDenied()
{
	if ( !canLogClient( self ) )
		return;
		
	setMatchData( "players", self.clientid, "killsDenied", self.pers["denied"] );
}

logInitialStats()
{
	if ( GetDvarInt( "mdsd" ) > 0 )
	{
		xp = self getRankedPlayerData( "experience" );
		setMatchData( "players", self.clientid, "startXp", xp );

		setMatchData( "players", self.clientid, "startKills", self getRankedPlayerData( "kills" ) );
		setMatchData( "players", self.clientid, "startDeaths", self getRankedPlayerData( "deaths" ) );
		setMatchData( "players", self.clientid, "startWins", self getRankedPlayerData( "wins" ) );
		setMatchData( "players", self.clientid, "startLosses", self getRankedPlayerData( "losses" ) );
		setMatchData( "players", self.clientid, "startHits", self getRankedPlayerData( "hits" ) );
		setMatchData( "players", self.clientid, "startMisses", self getRankedPlayerData( "misses" ) );
		setMatchData( "players", self.clientid, "startGamesPlayed", self getRankedPlayerData( "gamesPlayed" ) );
		//setMatchData( "players", self.clientid, "startTimePlayedTotal", self getRankedPlayerData( "timePlayedTotal" ) );
		setMatchData( "players", self.clientid, "startScore", self getRankedPlayerData( "score" ) );
		setMatchData( "players", self.clientid, "startUnlockPoints", self getRankedPlayerData( "unlockPoints" ) );
		setMatchData( "players", self.clientid, "startPrestige", self getRankedPlayerData( "prestige" ) );
		setMatchData( "players", self.clientid, "startDP", self getRankedPlayerData( "division" ) );
		setMatchData( "players", self.clientid, "startMMR", self getRankedPlayerData( "mmr" ) );
	}
}

logFinalStats()
{
	if ( GetDvarInt( "mdsd" ) > 0 )
	{
		xp = self getRankedPlayerData( "experience" );
		setMatchData( "players", self.clientid, "endXp", xp );
		
		setMatchData( "players", self.clientid, "endKills", self getRankedPlayerData( "kills" ) );
		setMatchData( "players", self.clientid, "endDeaths", self getRankedPlayerData( "deaths" ) );
		setMatchData( "players", self.clientid, "endWins", self getRankedPlayerData( "wins" ) );
		setMatchData( "players", self.clientid, "endLosses", self getRankedPlayerData( "losses" ) );
		setMatchData( "players", self.clientid, "endHits", self getRankedPlayerData( "hits" ) );
		setMatchData( "players", self.clientid, "endMisses", self getRankedPlayerData( "misses" ) );
		setMatchData( "players", self.clientid, "endGamesPlayed", self getRankedPlayerData( "gamesPlayed" ) );
		//setMatchData( "players", self.clientid, "endTimePlayedTotal", self getRankedPlayerData( "timePlayedTotal" ) );
		setMatchData( "players", self.clientid, "endScore", self getRankedPlayerData( "score" ) );
		setMatchData( "players", self.clientid, "endUnlockPoints", self getRankedPlayerData( "unlockPoints" ) );
		setMatchData( "players", self.clientid, "endPrestige", self getRankedPlayerData( "prestige" ) );
		setMatchData( "players", self.clientid, "endMMR", self getRankedPlayerData( "mmr" ) );

		// rankAtStart is logged in mp\gametypes\_rank::onPlayerConnect()
		// it is not yet defined when logInitialStats() is called
		if( isDefined( self.pers["rank"] ) )	
		{
			rank = clampToByte( self maps\mp\gametypes\_rank::getRank() );
			setMatchData( "players", self.clientid, "rankAtEnd", rank );		
		}
	}
}

///#
reconLogPlayerInfo()
{
	while (true)
	{
		if (getdvarint("cl_freemove")==0)
		{
			foreach (player in level.players)
			{
				is_alive = false;
				if( isReallyAlive( player ) )
					is_alive = true;
				
				if ( IsTestClient( player ) )
					continue;	// for now, don't record bots
				
				// Ensure all variables are defined
				if( !isdefined( player.origin ) )
					continue;
				
				player_name = "disconnected?";
				if( isdefined( player.name ) )
					player_name = player.name;
				
				client = -1;
				if( isdefined( player.clientid ) )
					client = player.clientid;
				
				angles = (-999,-999,-999);
				if( isdefined( player.angles ) )
					angles = player.angles;
				
				playerTeam = "undefined";
				if( isdefined( player.team ) )
					playerTeam = player.team;
				
				gameTime = getTime();
				
				ReconSpatialEvent( player.origin, "script_mp_playerpos: player_name %s, angles %v, gameTime %d, playerTeam %s, is_alive %b",
																		player_name,    angles,    gameTime,    playerTeam,    is_alive );
			}
		}
		wait 0.2;
	}
}
//#/
