
#include common_scripts\utility;
#include maps\mp\_utility;
#include maps\mp\bots\_bots_ks;

SCR_CONST_MAX_CUSTOM_DEFAULT_LOADOUTS = 6; // See recipes.raw: CustomGameClass defaultClasses[ PlayerGroup ][ 6 ];
SCR_CONST_BOT_CHANGE_LOADOUT_CHANCE = 0.1;

init()
{
	// Initialize tables of loadouts per personality and per difficulty
	init_template_table();
	init_class_table();
	init_bot_attachmenttable();	// needs to be before init_bot_weap_statstable
	init_bot_weap_statstable();
	init_bot_reticletable();
	init_bot_camotable();
	
	level.bot_min_rank_for_item = [];
		
	// Needed to ensure we're not trying to set bot loadouts before they have been parsed
	level.bot_loadouts_initialized = true;
}

init_class_table()
{
	filename = "mp/botClassTable.csv";
	
	level.botLoadoutSets = [];
	fieldArray = bot_loadout_fields();
	column = 0;

	for(;;)
	{
		column++;
		
		strPers = tableLookup( filename, 0, "botPersonalities", column );
		strDiff = tableLookup( filename, 0, "botDifficulties", column );
		
		if ( !isDefined( strPers ) || (strPers == "") )
			break;
		
		if ( !isDefined( strDiff ) || (strDiff == "") )
			break;
		
		loadoutValues = [];				
		foreach ( field in fieldArray )	
		{
			loadoutValues[field] = tableLookup( filename, 0, field, column );
/#
			assert_field_valid( field, loadoutValues[field], filename, loadoutValues );
#/
		}
		
		personalities = StrTok( strPers, "| " );
		difficulties = StrTok( strDiff, "| " );

		foreach ( personality in personalities )
		{
			foreach ( difficulty in difficulties )
			{
				loadoutSet = bot_loadout_set( personality, difficulty, true );
				loadout = SpawnStruct();
				loadout.loadoutValues = loadoutValues;
				loadoutSet.loadouts[loadoutSet.loadouts.size] = loadout;
			}
		}
	}
}
	
init_template_table()
{
	filename = "mp/botTemplateTable.csv";

	level.botLoadoutTemplates = [];
	fieldArray = bot_loadout_fields();
	column = 0;
	
	for(;;)
	{
		column++;
		
		strTemplateName = tableLookup( filename, 0, "template_", column );
		
		if ( !isDefined( strTemplateName ) || (strTemplateName == "") )
			break;
				
		strTemplateLookupName = "template_" + strTemplateName;
		
		level.botLoadoutTemplates[strTemplateLookupName] = [];
		
		foreach ( field in fieldArray )	
		{
			entry = tableLookup( filename, 0, field, column );
			if ( IsDefined( entry ) && entry != "" )
			{
				level.botLoadoutTemplates[strTemplateLookupName][field] = entry;
/#
				assert_field_valid( field, entry, filename );	
#/
			}
		}
	}	
}

bot_custom_classes_allowed()
{
	if ( IsUsingMatchRulesData() && !GetMatchRulesData( "commonOption", "allowCustomClasses" ) )
		return false;
	
	return true;
}

/* 
=============
///ScriptDocBegin
"Name: bot_loadout_item_allowed( <type>, <item>, <perkClass> )"
"Summary: Checks if a loadout item is restricted in the current match rules (returns true if not restricted)"
"MandatoryArg: <type> : The type of item ("weapon", "attachment", "killstreak", "perk")"
"MandatoryArg: <item> : The specific item"
"OptionalArg: <perkClass> : If type is perk, then the relevant PerkClass enum from recipes.def"
"Example: bot_loadout_item_allowed( "weapon", "iw6_fads", undefined );"
///ScriptDocEnd
============
*/
bot_loadout_item_allowed( type, item, perkClass )
{
	if ( !IsUsingMatchRulesData() )
		return true;
	
	if ( !bot_custom_classes_allowed() )
		return false;
	
	if ( item == "specialty_null" )
		return true;

	if ( item == "none" )
		return true;
	
	itemRuleName = type + "Restricted";
	classRuleName = type + "ClassRestricted";
	class = "";
			
	switch ( type )
	{
		case "weapon":
			class = getWeaponClass( item );
			break;
		case "attachment":
			class = getAttachmentType( item );
			break;
		case "killstreak":
			if ( GetMatchRulesData( "commonOption", "allStreaksRestricted" ) )
				return false;
			break;
		case "module":
			class = maps\mp\killstreaks\_killstreaks::getStreakModuleBaseKillstreak( item );
			break;
		case "perk":
			class = perkClass;
			break;
		default:
			AssertEx( 0, "bot_loadout_item_allowed() - Unsupported loadout type '" + type + "'" );
			return false;
	}

	if ( GetMatchRulesData( "commonOption", itemRuleName, item ) )
		return false;
	
	if ( ( class != "" ) && GetMatchRulesData( "commonOption", classRuleName, class ) )
		return false;
	
	return true;
}

bot_loadout_choose_fallback_primary( loadoutValueArray )
{
	weapon = "none";
		
	// First try to pick primary weapon randomly from all difficulties with my personality
	difficulties = [ "veteran", "hardened", "regular", "recruit" ];
	difficulties = array_randomize( difficulties );
	foreach( difficulty in difficulties )
	{
		weapon = bot_loadout_choose_from_statstable( "weap_statstable", loadoutValueArray, "loadoutPrimary", self.personality, difficulty );
		if ( weapon != "none" )
			return weapon;
	}

	// Then try to pick primary weapon randomly from all personalities and difficulties
	if ( IsDefined( level.bot_personality_list ) )
	{
		personalities = array_randomize( level.bot_personality_list );
		foreach( personality in personalities )
		{
			foreach( difficulty in difficulties )
			{
				weapon = bot_loadout_choose_from_statstable( "weap_statstable", loadoutValueArray, "loadoutPrimary", personality, difficulty );
				if ( weapon != "none" )
				{
					self.bot_fallback_personality = personality;
					return weapon;
				}
			}
		}
	}
	
	// Still no primary weapon found, pick one randomly from custom default classes
	if ( IsUsingMatchRulesData() )
	{
		validCount = 0.0;
		index = 0;
		defaultClassWeapon = "none";
		while( index < SCR_CONST_MAX_CUSTOM_DEFAULT_LOADOUTS )
		{
			if ( getMatchRulesData( "defaultClasses", self bot_loadout_team(), index, "class", "inUse" ) )
			{
				weapon = self bot_loadout_choose_from_custom_default_class( index, "loadoutPrimary" );
				if ( weapon != "none" )
				{
					validCount = validCount + 1.0;
					if ( RandomFloat( 1.0 ) >= 1.0 / validCount )
						defaultClassWeapon = weapon;
				}
			}
			index++;
		}
		
		if ( defaultClassWeapon != "none" )
		{
			self.bot_fallback_personality = "weapon";
			return defaultClassWeapon;
		}
	}

	// Still no primary weapon found, pick level.bot_fallback_weapon (knife)
	self.bot_fallback_personality = "weapon";
	return level.bot_fallback_weapon;
}

bot_loadout_team() // self = bot
{
	if ( !IsDefined( level.teambased ) || !level.teambased )
		return "allies";
	
	return self maps\mp\bots\_bots::bot_get_player_team();
}

bot_default_class_random() // self = bot
{
	assert( IsBot( self ) );
	
	// By default we choose from the standard default class name strings
	selections = [ "class1", "class2", "class3", "class4", "class5" ];

	// Unless its a private match with custom default classes in which case we selectively replace items in the array with an int
	if ( IsUsingMatchRulesData() )
	{
		for ( i = 0; i < selections.size; i++ )
		{
			if ( getMatchRulesData( "defaultClasses", self bot_loadout_team(), i, "class", "inUse" ) )
				selections[i] = i;
		}
	}
	
	classChoice = random( selections );
	loadoutValues = [];

	foreach ( loadoutValueName in level.bot_loadout_fields )
	{
		if ( IsString( classChoice ) )
			loadoutValues[loadoutValueName] = self bot_loadout_choose_from_default_class( classChoice, loadoutValueName );
		else
			loadoutValues[loadoutValueName] = self bot_loadout_choose_from_custom_default_class( classChoice, loadoutValueName );
	}
	
	return loadoutValues;
}

bot_pick_personality_from_weapon( weapon ) // self = bot player
{
	if ( IsDefined( weapon ) )
	{
		weapPers = level.bot_weap_personality[weapon];
		if ( IsDefined( weapPers ) )
		{
			personalityChoices = StrTok( weapPers, "| " );
			if ( personalityChoices.size > 0 )
				self maps\mp\bots\_bots_util::bot_set_personality( random( personalityChoices ) );
		}
	}
}

bot_loadout_set_has_wildcard( loadout_fields, wildcard )
{
	if ( loadout_fields["loadoutWildcard1"] == wildcard)
		return true;
	if ( loadout_fields["loadoutWildcard2"] == wildcard)
		return true;
	if ( loadout_fields["loadoutWildcard3"] == wildcard)
		return true;
	
	return false;
}

/#
assert_field_valid( field, value, filename, all_previous_fields )
{
	// Make sure we actually found this value in the classtable
	if ( field != "" )
	{
		AssertEx( value != "", "Field " + field + " not defined in " + filename );
	}
	
	// Make sure the killstreaks loaded are valid in general for bots
	if ( field == "loadoutStreak1" || field == "loadoutStreak2" || field == "loadoutStreak3" )
	{
		killstreaks = StrTok( value, "| " );
		foreach( streak in killstreaks )
		{
			if ( streak != "none" && GetSubStr( streak, 0, 9 ) != "template_" && GetSubStr( streak, 0, 5 ) != "class" )
				assert_streak_valid_for_bots_in_general(streak);
		}
	}
	
	if ( field == "loadoutOffhand" && !IsSubStr( value, "template_" ) && !IsSubStr( value, "classtable_" ) )
	{
		has_dual_lethals = false;
		if ( IsDefined(all_previous_fields) )
			has_dual_lethals = bot_loadout_set_has_wildcard(all_previous_fields, "specialty_wildcard_duallethals");
		
		exo_abilities = StrTok( value, "|" );
		foreach( ability in exo_abilities )
			AssertEx( maps\mp\gametypes\_class::isValidOffhand( ability, has_dual_lethals ), "'" + ability + "' not valid for '" + field + "' in " + filename );
	}
	
	if ( field == "loadoutOffhand2" && value != "specialty_null" )
	{
		AssertMsg( "Field 'loadOffhand2' is deprecated(?) and should not be used for bots.  Value was '" + value + "' in " + filename );
	}
	
	if ( IsSubStr( field, "Reticle" ) )
	{
		reticles = StrTok( value, "|" );
		foreach( reticle in reticles )
			AssertEx( reticle == "reticletable" || maps\mp\gametypes\_class::isValidReticle( reticle, true ), "'" + reticle + "' not valid for '" + field + "' in " + filename );
	}
	
	if ( IsSubStr( field, "Camo" ) )
	{
		camos = StrTok( value, "|" );
		foreach( camo in camos )
			AssertEx( camo == "camotable" || maps\mp\gametypes\_class::isValidCamo( camo ), "'" + camo + "' not valid for '" + field + "' in " + filename );
	}
	
	if ( IsSubStr( field, "Equipment" ) && !IsSubStr( value, "template_" ) && !IsSubStr( value, "classtable_" ) )
	{
		has_dual_tacticals = false;
		if ( IsDefined(all_previous_fields) )
			has_dual_tacticals = bot_loadout_set_has_wildcard(all_previous_fields, "specialty_wildcard_dualtacticals");
		
		equipments = StrTok( value, "|" );
		foreach( equipment in equipments )
			AssertEx( maps\mp\gametypes\_class::isValidEquipment( equipment, has_dual_tacticals ), "'" + equipment + "' not valid for '" + field + "' in " + filename );
	}
	
	if ( IsSubStr( field, "Perk" ) && !IsSubStr( value, "classtable_" ) && value != "specialty_null" && IsDefined(all_previous_fields) )
	{
		perk_num = int( GetSubStr( field, 11 ) );
		
		wildcard_required = "";
		if ( perk_num == 2 )
			wildcard_required = "specialty_wildcard_perkslot1";
		else if ( perk_num == 4 )
			wildcard_required = "specialty_wildcard_perkslot2";
		else if ( perk_num == 6 )
			wildcard_required = "specialty_wildcard_perkslot3";
			
		if ( wildcard_required != "" )
			AssertEx( bot_loadout_set_has_wildcard(all_previous_fields, wildcard_required), "'" + value + "' not valid for '" + field + "' in " + filename );
	}
}
#/

bot_loadout_fields()
{
	filename = "mp/botClassTable.csv";
	if ( !IsDefined(level.bot_loadout_fields) )
	{
		level.bot_loadout_fields = [];
		
		// Start with the 3 wildcards since they have to be first (because they can modify other slots)
		level.bot_loadout_fields[0] = "loadoutWildcard1";
		level.bot_loadout_fields[1] = "loadoutWildcard2";
		level.bot_loadout_fields[2] = "loadoutWildcard3";
		
		row = 2;
		while(1)
		{
			rowHeading = TableLookupByRow( filename, row, 0 );
			if ( rowHeading == "" )
				break;
			if ( !IsSubStr( rowHeading, "Wildcard" ) )
				level.bot_loadout_fields[level.bot_loadout_fields.size] = rowHeading;
			
			row++;
		}
	}

	return level.bot_loadout_fields;
}

bot_loadout_set( personality, difficulty, createIfNeeded )
{
	setName = difficulty + "_" + personality;

	if ( !isDefined( level.botLoadoutSets ) )
		level.botLoadoutSets = [];
	
	if ( !isDefined( level.botLoadoutSets[setName] ) && createIfNeeded )
	{
		level.botLoadoutSets[setName] = SpawnStruct();
		level.botLoadoutSets[setName].loadouts = [];
	}
	
	if ( isDefined( level.botLoadoutSets[setName] ) )
		return level.botLoadoutSets[setName];
}

bot_loadout_pick( personality, difficulty )
{
	loadoutSet = bot_loadout_set( personality, difficulty, false );	
	if ( IsDefined( loadoutSet ) && isDefined( loadoutSet.loadouts ) && loadoutSet.loadouts.size > 0 )
	{
		loadoutChoice = RandomInt( loadoutSet.loadouts.size );
		return loadoutSet.loadouts[loadoutChoice].loadoutValues;
	}
}

bot_validate_weapon( weaponName, attachment1, attachment2, attachment3  )
{
	attachment_array = [];
	
	attachment_start_num = 0;
	if ( IsDefined( level.bot_weap_built_in_attachments[weaponName] ) && level.bot_weap_built_in_attachments[weaponName] != "none" )
	{
		attachment_array[attachment_array.size] = level.bot_weap_built_in_attachments[weaponName];
		attachment_start_num++;		// Don't do normal verification for this attachment since it is built in, but still check it for allowable attachment combos
	}
	
	if ( IsDefined( attachment1 ) && attachment1 != "none" )
		attachment_array[ attachment_array.size ] = attachment1;
	
	if ( IsDefined( attachment2 ) && attachment2 != "none" )
		attachment_array[ attachment_array.size ] = attachment2;
	
	if ( IsDefined( attachment3 ) && attachment3 != "none" )
		attachment_array[attachment_array.size] = attachment3;
	
	validAttachments = getWeaponAttachmentArrayFromStats( weaponName );
	
	for( i = attachment_start_num; i < attachment_array.size; i++ )
	{
		Assert( IsDefined(attachment_array[i]) && attachment_array[i] != "none" );
		
		base_attachment = attachmentMap_toBase( attachment_array[i] );
		if ( base_attachment != attachment_array[i] )
		{
			AssertMsg( "Bots should only pick base attachments, i.e. " + base_attachment + " not " + attachment_array[i] );
			return false;
		}
		
		if ( !bot_loadout_item_allowed( "attachment", attachment_array[i], undefined ) )
			return false;
		
		if ( !array_contains( validAttachments, attachment_array[i] ) )
			return false;
		
		num_attachment_duplicates = 0;
		for( j = i-1; j >= 0; j-- )
		{
			if ( attachment_array[i] == attachment_array[j] )
			{
				// Found the same attachment, check if that is allowed
				
				num_attachment_duplicates++;
				if ( num_attachment_duplicates == 1 )
				{
					// Found one duplicate attachment, check if that is not allowed
					if ( !IsDefined(level.allowable_double_attachments[attachment_array[i]]) )
						return false;
				}
				else if ( num_attachment_duplicates > 1 )
				{
					// Found more than one duplicate attachment, which is never allowed
					return false;
				}
			}
			else
			{
				if ( IsDefined( level.bot_invalid_attachment_combos[attachment_array[i]] ) )
				{
					if ( IsDefined(level.bot_invalid_attachment_combos[attachment_array[i]][attachment_array[j]]) )
						return false;
				}
			}
		}
	}
	
	return true;
}

bot_validate_reticle( loadoutBaseName, loadoutValueArray, choice )
{
	if ( !maps\mp\gametypes\_class::isValidReticle( choice, true ) )
		return false;
	
	// The only attachments that are valid to have a reticle are the ones listed in the level.bot_attachment_reticle array
	attachment_names = ["Attachment","Attachment2","Attachment3"];
	foreach( name in attachment_names )
	{
		attachmentLoadoutName = loadoutBaseName + name;
		attachmentLoadoutValue = loadoutValueArray[attachmentLoadoutName];
		if ( isDefined( attachmentLoadoutValue ) && IsDefined( level.bot_attachment_reticle[attachmentLoadoutValue] ) )
			return true;
	}
	
	return false;
}

init_bot_weap_statstable()
{
	fileName = "mp/statstable.csv";
	colName = 4;
	colPers = 57;
	colDiff = 58;
	colBuiltInAttach = 40;

	level.bot_weap_statstable = [];
	level.bot_weap_personality = [];
	level.bot_weap_built_in_attachments = [];
	
	/#
	level.all_bot_attachments_from_attachtable = level.bot_attachmenttable["veteran"];
	#/
	
	level.bot_weap_personality["iw5_combatknife"] = "cqb";
	
	// level.bot_weap_statstable[loadoutValueName][self.personality][self.difficulty]
	
	row = 1;
	while(1)
	{
		rowNum = TableLookupByRow( fileName, row, 0 );
		if ( rowNum == "" )
			break;
		
		weapName = TableLookupByRow( fileName, row, colName );
		weapDiff = TableLookupByRow( fileName, row, colDiff );
		weapPers = TableLookupByRow( fileName, row, colPers );
		
		if ( weapName != "" && weapPers != "" )
			level.bot_weap_personality[weapName] = weapPers;

		if ( weapDiff != "" && weapName != "" && weapPers != "" )
		{
			baseWeaponName = weapName;
			if( isLootWeapon( weapName ) )
				baseWeaponName = maps\mp\gametypes\_class::getBaseFromLootVersion( weapName );
			
			slotType = "loadoutPrimary";
			if ( maps\mp\gametypes\_class::isValidSecondary( baseWeaponName, false ) )
			{
				slotType = "loadoutSecondary";
			}
			else if ( !maps\mp\gametypes\_class::isValidPrimary( baseWeaponName ) )
			{
				row++;
				continue;
			}
			
			/#
			// Validate the attachments for this weapon from the statstable array
			validAttachments = getWeaponAttachmentArrayFromStats( weapName );
			foreach( attachment in validAttachments )
			{
				in_table = array_contains( level.bot_attachmenttable["veteran"], attachment );
				AssertEx( in_table, "Attachment '" + attachment + "' found in statstable for weapon '" + weapName + "' but not found in attachmenttable" );
				
				if ( array_contains( level.all_bot_attachments_from_attachtable, attachment ) )
					level.all_bot_attachments_from_attachtable = array_remove( level.all_bot_attachments_from_attachtable, attachment );
			}
			#/
			
			if ( !IsDefined( level.bot_weap_statstable[slotType] ) )
				level.bot_weap_statstable[slotType] = [];
							
			persList = StrTok( weapPers, "| " );
			diffList = StrTok( weapDiff, "| " );
			
			foreach ( personality in persList )
			{
				if ( !IsDefined( level.bot_weap_statstable[slotType][personality] ) )
					level.bot_weap_statstable[slotType][personality] = [];
										
				foreach ( difficulty in diffList )
				{
					if ( !IsDefined( level.bot_weap_statstable[slotType][personality][difficulty] ) )
						level.bot_weap_statstable[slotType][personality][difficulty] = [];

					newIndex = level.bot_weap_statstable[slotType][personality][difficulty].size;
					level.bot_weap_statstable[slotType][personality][difficulty][newIndex] = weapName;
				}
			}
			
			built_in_attachment = TableLookupByRow( fileName, row, colBuiltInAttach );
			if ( built_in_attachment != "" )
			{
				level.bot_weap_built_in_attachments[weapName] = built_in_attachment;
				
				/#
				// Validate that the built-in attachment is a valid attachment
				in_table = array_contains( level.bot_attachmenttable["veteran"], built_in_attachment );
				AssertEx( in_table, "Built-in attachment '" + built_in_attachment + "' found in statstable for weapon '" + weapName + "' but not found in attachmenttable" );
				#/
			}
		}

		row++;
	}
	
	/#
	foreach( attachment in level.all_bot_attachments_from_attachtable )
		AssertMsg( "attachment '" + attachment + "' enabled for bots in attachmenttable.csv but not found on any bot weapon in statstable.csv" );
	level.all_bot_attachments_from_attachtable = undefined;
	#/
}

bot_loadout_choose_from_statstable( loadoutValue, loadoutValueArray, loadoutValueName, personality, difficulty )
{
	result = "none";
	if ( personality == "default" )
		personality = "run_and_gun";
	
	loadoutValueNameLookup = loadoutValueName;
	if ( loadoutValueNameLookup == "loadoutSecondary" && bot_loadout_set_has_wildcard( loadoutValueArray, "specialty_wildcard_dualprimaries" ) )
	{
		loadoutValueNameLookup = "loadoutPrimary";
		
		// For campers / run-and-gunners, pick a second primary from the CQB loadouts as generally we would want something good for close range as a secondary
		// For CQB, pick a potentially longer range weapon from the run_and_gun loadouts
		if ( personality == "camper" || personality == "run_and_gun" )
			personality = "cqb";
		else if ( personality == "cqb" )
			personality = "run_and_gun";
	}
		
	if ( !IsDefined( level.bot_weap_statstable ) )
		return result;

	if ( !IsDefined( level.bot_weap_statstable[loadoutValueNameLookup] ) )
		return result;
	
	if ( !IsDefined( level.bot_weap_statstable[loadoutValueNameLookup][personality] ) )
		return result;

	if ( !IsDefined( level.bot_weap_statstable[loadoutValueNameLookup][personality][difficulty] ) )
		return result;

	result = bot_loadout_choose_from_set( level.bot_weap_statstable[loadoutValueNameLookup][personality][difficulty], loadoutValue, loadoutValueArray, loadoutValueName );
	
	return result;
}

bot_validate_perk( choice, loadoutValueName, loadoutValueArray )
{
	perkClass = "Perk_Slot1";
	if ( loadoutValueName == "loadoutPerk3" || loadoutValueName == "loadoutPerk4" )
		perkClass = "Perk_Slot2";
	else if ( loadoutValueName == "loadoutPerk5" || loadoutValueName == "loadoutPerk6" )
		perkClass = "Perk_Slot3";
	
	if ( !bot_loadout_item_allowed( "perk", choice, perkClass ) )
		return false;
	
	index = int( GetSubStr( loadoutValueName, 11 ) );
	
	wildcard_required = "";
	if ( index == 2 )
		wildcard_required = "specialty_wildcard_perkslot1";
	else if ( index == 4 )
		wildcard_required = "specialty_wildcard_perkslot2";
	else if ( index == 6 )
		wildcard_required = "specialty_wildcard_perkslot3";
	
	if ( wildcard_required != "" )
	{
		if ( !bot_loadout_set_has_wildcard(loadoutValueArray, wildcard_required) )
			return false;
	}
		
	for ( i = index - 1; i > 0; i-- )
	{
		prevPerkName = "loadoutPerk" + i;
		
		if ( loadoutValueArray[prevPerkName] == "none" || loadoutValueArray[prevPerkName] == "specialty_null" )
			continue;

		// Make sure we havent picked the same perk twice
		if ( choice == loadoutValueArray[prevPerkName] )
			return false;
	}
	
	return true;
}

bot_loadout_choose_from_default_class( class, loadoutValueName )
{
	class_num = int(GetSubStr( class, 5, 6 )) - 1;

	switch ( loadoutValueName )
	{
		case "loadoutPrimary":
			return maps\mp\gametypes\_class::table_getWeapon( level.classTableName, class_num, 0 );
		case "loadoutPrimaryAttachment":
			return maps\mp\gametypes\_class::table_getWeaponAttachment( level.classTableName, class_num, 0, 0);
		case "loadoutPrimaryAttachment2":
			return maps\mp\gametypes\_class::table_getWeaponAttachment( level.classTableName, class_num, 0, 1 );
		case "loadoutPrimaryAttachment3":
			return maps\mp\gametypes\_class::table_getWeaponAttachment( level.classTableName, class_num, 0, 2 );
		case "loadoutPrimaryCamo":
			return maps\mp\gametypes\_class::table_getWeaponCamo( level.classTableName, class_num, 0 );
		case "loadoutPrimaryReticle":
			return maps\mp\gametypes\_class::table_getWeaponReticle( level.classTableName, class_num, 0 );
		case "loadoutSecondary":
			return maps\mp\gametypes\_class::table_getWeapon( level.classTableName, class_num, 1 );
		case "loadoutSecondaryAttachment":
			return maps\mp\gametypes\_class::table_getWeaponAttachment( level.classTableName, class_num, 1, 0);
		case "loadoutSecondaryAttachment2":
			return maps\mp\gametypes\_class::table_getWeaponAttachment( level.classTableName, class_num, 1, 1 );
		case "loadoutSecondaryAttachment3":
			return maps\mp\gametypes\_class::table_getWeaponAttachment( level.classTableName, class_num, 1, 2 );
		case "loadoutSecondaryCamo":
			return maps\mp\gametypes\_class::table_getWeaponCamo( level.classTableName, class_num, 1 );
		case "loadoutSecondaryReticle":
			return maps\mp\gametypes\_class::table_getWeaponReticle( level.classTableName, class_num, 1 );
		case "loadoutEquipment":
			return maps\mp\gametypes\_class::table_getEquipment( level.classTableName, class_num );
		case "loadoutEquipment2":
			return tableLookup( level.classTableName, 0, "loadoutEquipment2", (class_num + 1) );	// we want the actual string, not 0/1 from table_GetEquipmentExtra
		case "loadoutOffhand":
			return maps\mp\gametypes\_class::table_getOffhand( level.classTableName, class_num );
		case "loadoutOffhand2":
			if ( maps\mp\gametypes\_class::table_getOffhandExtra( level.classTableName, class_num ) )
				return maps\mp\gametypes\_class::table_getOffhand( level.classTableName, class_num );
			else
				return "specialty_null";
		case "loadoutStreak1":
			return maps\mp\gametypes\_class::table_getKillstreak( level.classTableName, class_num, 0 );
		case "loadoutStreakModule1a":
			return maps\mp\gametypes\_class::table_getKillstreakModule( level.classTableName, class_num, 0, 0 );
		case "loadoutStreakModule1b":
			return maps\mp\gametypes\_class::table_getKillstreakModule( level.classTableName, class_num, 0, 1 );
		case "loadoutStreakModule1c":
			return maps\mp\gametypes\_class::table_getKillstreakModule( level.classTableName, class_num, 0, 2 );
		case "loadoutStreak2":
			return maps\mp\gametypes\_class::table_getKillstreak( level.classTableName, class_num, 1 );
		case "loadoutStreakModule2a":
			return maps\mp\gametypes\_class::table_getKillstreakModule( level.classTableName, class_num, 1, 0 );
		case "loadoutStreakModule2b":
			return maps\mp\gametypes\_class::table_getKillstreakModule( level.classTableName, class_num, 1, 1 );
		case "loadoutStreakModule2c":
			return maps\mp\gametypes\_class::table_getKillstreakModule( level.classTableName, class_num, 1, 2 );
		case "loadoutStreak3":
			return maps\mp\gametypes\_class::table_getKillstreak( level.classTableName, class_num, 2 );
		case "loadoutStreakModule3a":
			return maps\mp\gametypes\_class::table_getKillstreakModule( level.classTableName, class_num, 2, 0 );
		case "loadoutStreakModule3b":
			return maps\mp\gametypes\_class::table_getKillstreakModule( level.classTableName, class_num, 2, 1 );
		case "loadoutStreakModule3c":
			return maps\mp\gametypes\_class::table_getKillstreakModule( level.classTableName, class_num, 2, 2 );
		case "loadoutStreak4":
			return maps\mp\gametypes\_class::table_getKillstreak( level.classTableName, class_num, 3 );
		case "loadoutStreakModule4a":
			return maps\mp\gametypes\_class::table_getKillstreakModule( level.classTableName, class_num, 3, 0 );
		case "loadoutStreakModule4b":
			return maps\mp\gametypes\_class::table_getKillstreakModule( level.classTableName, class_num, 3, 1 );
		case "loadoutStreakModule4c":
			return maps\mp\gametypes\_class::table_getKillstreakModule( level.classTableName, class_num, 3, 2 );
		case "loadoutPerk1":
		case "loadoutPerk2":
		case "loadoutPerk3":
		case "loadoutPerk4":
		case "loadoutPerk5":
		case "loadoutPerk6":
			{
				perkIndex = int( GetSubStr( loadoutValueName, 11 ) ) - 1;	
				perk = maps\mp\gametypes\_class::table_getPerk( level.classTableName, class_num, perkIndex );
				if ( perk == "" )
					return "specialty_null";
				return perk;
			}
		case "loadoutWildcard1":
			return maps\mp\gametypes\_class::table_getWildcard( level.classTableName, class_num, 0 );
		case "loadoutWildcard2":
			return maps\mp\gametypes\_class::table_getWildcard( level.classTableName, class_num, 1 );
		case "loadoutWildcard3":
			return maps\mp\gametypes\_class::table_getWildcard( level.classTableName, class_num, 2 );
	}
				
	AssertMsg( "Loadout type '" + loadoutValueName + "' is not supported for bot_loadout_choose_from_default_class('" + class + "')" );
	
	return class;
}

bot_loadout_choose_from_custom_default_class( index, loadoutValueName ) // self = bot
{
	assert( IsUsingMatchRulesData() );
	assert( IsBot( self ) );
	
	team = self bot_loadout_team();

	switch ( loadoutValueName )
	{
		case "loadoutPrimary":
			return getMatchRulesData( "defaultClasses", team, index, "class", "weaponSetups", 0, "weapon" );
		case "loadoutPrimaryAttachment":
			return getMatchRulesData( "defaultClasses", team, index, "class", "weaponSetups", 0, "attachment", 0 );
		case "loadoutPrimaryAttachment2":
			return getMatchRulesData( "defaultClasses", team, index, "class", "weaponSetups", 0, "attachment", 1 );
		case "loadoutPrimaryAttachment3":
			return getMatchRulesData( "defaultClasses", team, index, "class", "weaponSetups", 0, "attachment", 2 );
		case "loadoutPrimaryCamo":
			return getMatchRulesData( "defaultClasses", team, index, "class", "weaponSetups", 0, "camo" );
		case "loadoutPrimaryReticle":
			return getMatchRulesData( "defaultClasses", team, index, "class", "weaponSetups", 0, "reticle" );
		case "loadoutSecondary":
			return getMatchRulesData( "defaultClasses", team, index, "class", "weaponSetups", 1, "weapon" );
		case "loadoutSecondaryAttachment":
			return getMatchRulesData( "defaultClasses", team, index, "class", "weaponSetups", 1, "attachment", 0 );
		case "loadoutSecondaryAttachment2":
			return getMatchRulesData( "defaultClasses", team, index, "class", "weaponSetups", 1, "attachment", 1 );
		case "loadoutSecondaryAttachment3":
			return getMatchRulesData( "defaultClasses", team, index, "class", "weaponSetups", 1, "attachment", 2 );
		case "loadoutSecondaryCamo":
			return getMatchRulesData( "defaultClasses", team, index, "class", "weaponSetups", 1, "camo" );
		case "loadoutSecondaryReticle":
			return getMatchRulesData( "defaultClasses", team, index, "class", "weaponSetups", 1, "reticle" );
		case "loadoutEquipment":
			return getMatchRulesData( "defaultClasses", team, index, "class", "equipmentSetups", 0, "equipment" );
		case "loadoutEquipment2":
			if ( getMatchRulesData( "defaultClasses", team, index, "class", "equipmentSetups", 0, "extra" ) )
				return getMatchRulesData( "defaultClasses", team, index, "class", "equipmentSetups", 0, "equipment" );
			else
				return "specialty_null";
		case "loadoutOffhand":
			return getMatchRulesData( "defaultClasses", team, index, "class", "equipmentSetups", 1, "equipment" );
		case "loadoutOffhand2":
			return "specialty_null";
		case "loadoutStreak1":
			return getMatchRulesData( "defaultClasses", team, index, "class", "assaultStreaks", 0, "streak" );
		case "loadoutStreakModule1a":
			return getMatchRulesData( "defaultClasses", team, index, "class", "assaultStreaks", 0, "modules", 0 );
		case "loadoutStreakModule1b":
			return getMatchRulesData( "defaultClasses", team, index, "class", "assaultStreaks", 0, "modules", 1 );
		case "loadoutStreakModule1c":
			return getMatchRulesData( "defaultClasses", team, index, "class", "assaultStreaks", 0, "modules", 2 );
		case "loadoutStreak2":
			return getMatchRulesData( "defaultClasses", team, index, "class", "assaultStreaks", 1, "streak" );
		case "loadoutStreakModule2a":
			return getMatchRulesData( "defaultClasses", team, index, "class", "assaultStreaks", 1, "modules", 0 );
		case "loadoutStreakModule2b":
			return getMatchRulesData( "defaultClasses", team, index, "class", "assaultStreaks", 1, "modules", 1 );
		case "loadoutStreakModule2c":
			return getMatchRulesData( "defaultClasses", team, index, "class", "assaultStreaks", 1, "modules", 2 );
		case "loadoutStreak3":
			return getMatchRulesData( "defaultClasses", team, index, "class", "assaultStreaks", 2, "streak" );
		case "loadoutStreakModule3a":
			return getMatchRulesData( "defaultClasses", team, index, "class", "assaultStreaks", 2, "modules", 0 );
		case "loadoutStreakModule3b":
			return getMatchRulesData( "defaultClasses", team, index, "class", "assaultStreaks", 2, "modules", 1 );
		case "loadoutStreakModule3c":
			return getMatchRulesData( "defaultClasses", team, index, "class", "assaultStreaks", 2, "modules", 2 );
		case "loadoutStreak4":
			return getMatchRulesData( "defaultClasses", team, index, "class", "assaultStreaks", 3, "streak" );
		case "loadoutStreakModule4a":
			return getMatchRulesData( "defaultClasses", team, index, "class", "assaultStreaks", 3, "modules", 0 );
		case "loadoutStreakModule4b":
			return getMatchRulesData( "defaultClasses", team, index, "class", "assaultStreaks", 3, "modules", 1 );
		case "loadoutStreakModule4c":
			return getMatchRulesData( "defaultClasses", team, index, "class", "assaultStreaks", 3, "modules", 2 );
		case "loadoutPerk1":
			return getMatchRulesData( "defaultClasses", team, index, "class", "perkSlots", 0 );
		case "loadoutPerk2":
			return getMatchRulesData( "defaultClasses", team, index, "class", "perkSlots", 1 );
		case "loadoutPerk3":
			return getMatchRulesData( "defaultClasses", team, index, "class", "perkSlots", 2 );
		case "loadoutPerk4":
			return getMatchRulesData( "defaultClasses", team, index, "class", "perkSlots", 3 );
		case "loadoutPerk5":
			return getMatchRulesData( "defaultClasses", team, index, "class", "perkSlots", 4 );
		case "loadoutPerk6":
			return getMatchRulesData( "defaultClasses", team, index, "class", "perkSlots", 5 );
		case "loadoutWildcard1":
			return getMatchRulesData( "defaultClasses", team, index, "class", "wildcardSlots", 0 );
		case "loadoutWildcard2":
			return getMatchRulesData( "defaultClasses", team, index, "class", "wildcardSlots", 1 );
		case "loadoutWildcard3":
			return getMatchRulesData( "defaultClasses", team, index, "class", "wildcardSlots", 2 );
		default:
			AssertMsg("'" + loadoutValueName + "' not supported in bot_loadout_choose_from_custom_default_class()");
	}
	
	return "none";
}


init_bot_attachmenttable()
{
	fileName = "mp/attachmenttable.csv";
	colName = 3;
	colDiff = 15;
	colReticle = 7;

	level.bot_attachmenttable = [];
	level.bot_attachment_reticle = [];
	
	/#
	level.bot_att_cross_reference = [];
	#/
		
	row = 1;
	while(1)
	{
		attachmentName = TableLookupByRow( fileName, row, colName );
		
		if ( attachmentName == "done" )
		{
			AssertEx( TableLookupByRow( fileName, row+1, colName ) == "", fileName + " needs to have 'done' as its last element so bots can parse it correctly" );
			break;
		}
		
		attachmentDiff = TableLookupByRow( fileName, row, colDiff );
		
		if ( attachmentName != "" && attachmentDiff != "" )
		{
			/#
			if ( IsDefined( level.bot_att_cross_reference[attachmentName] ) && level.bot_att_cross_reference[attachmentName] != attachmentDiff )
				AssertMsg( "base ref '" + attachmentName + "' is associated with conflicting Bot Difficulty in attachmenttable.csv" );
			level.bot_att_cross_reference[attachmentName] = attachmentDiff;
			#/
					
			attachmentReticle = TableLookupByRow( fileName, row, colReticle );
			if ( attachmentReticle == "TRUE" )
				level.bot_attachment_reticle[attachmentName] = true;

			diffList = StrTok( attachmentDiff, "| " );
			
			// We are assuming that Veteran bots can use all bot attachments.  If this is false, need to adjust the attachment validation logic in init_bot_weap_statstable
			Assert( array_contains( diffList, "veteran" ) );
			
			foreach ( difficulty in diffList )
			{
				if ( !IsDefined( level.bot_attachmenttable[difficulty] ) )
					level.bot_attachmenttable[difficulty] = [];
				
				if ( !array_contains( level.bot_attachmenttable[difficulty], attachmentName ) )
			    {
					newIndex = level.bot_attachmenttable[difficulty].size;
					level.bot_attachmenttable[difficulty][newIndex] = attachmentName;
				}
			}
		}

		row++;
		
		AssertEx(row < 999, "Missing 'done' at the bottom of attachmenttable.csv");
	}

	/#
	level.bot_att_cross_reference = undefined;
	#/
	
	level.bot_invalid_attachment_combos = [];
	level.allowable_double_attachments = []; // Attachments that are allowed twice
	
	attachmentComboTable = "mp/attachmentcombos.csv";
	column = 0;
	while(1)
	{
		column++;
		currentAttachment = TableLookupByRow( attachmentComboTable, 0, column );
		if ( currentAttachment == "" )
			break;
		
		row = 0;
		while(1)
		{
			row++;
			attachmentTesting = TableLookupByRow( attachmentComboTable, row, 0 );
			if ( attachmentTesting == "" )
				break;
			
			if ( attachmentTesting == currentAttachment )
			{
				if ( TableLookupByRow( attachmentComboTable, row, column ) != "no" )
					level.allowable_double_attachments[attachmentTesting] = true;
			}
			else
			{
				if ( TableLookupByRow( attachmentComboTable, row, column ) == "no" )
					level.bot_invalid_attachment_combos[currentAttachment][attachmentTesting] = true;
			}
		}
	}
}

bot_loadout_choose_from_attachmenttable( loadoutValue, loadoutValueArray, loadoutValueName, personality, difficulty )
{
	// Set fallbacks in case nothing is found that matches
	result = "none";
	
	if ( !IsDefined( level.bot_attachmenttable ) )
		return result;

	if ( !IsDefined( level.bot_attachmenttable[difficulty] ) )
		return result;

	result = bot_loadout_choose_from_set( level.bot_attachmenttable[difficulty], loadoutValue, loadoutValueArray, loadoutValueName );
	
	return result;
}

init_bot_reticletable()
{
	fileName = "mp/reticletable.csv";
	colName = 1;
	colBotValid = 8;
	
	level.bot_reticletable = [];
	
	row = 0;
	while ( 1 )
	{
		reticleName = TableLookupByRow( fileName, row, colName );
		
		if ( !IsDefined( reticleName ) || reticleName == "" )
			break;
		
		AssertEx(maps\mp\gametypes\_class::isValidReticle( reticleName, true ),"Reticle '" + reticleName + "' found in reticletable.csv but not valid according to isValidReticle");
		botValid = TableLookupByRow( fileName, row, colBotValid );
		if ( IsDefined( botValid ) && Int( botValid ) )
			level.bot_reticletable[ level.bot_reticletable.size ] = reticleName;
		
		row++;
	}
}

bot_loadout_choose_from_reticletable( loadoutValue, loadoutValueArray, loadoutValueName, personality, difficulty )
{
	// Set fallbacks in case nothing is found that matches
	result = "none";
	
	if ( !IsDefined( level.bot_reticletable ) )
		return result;
	
	should_pick_reticle = RandomInt(100) > 50;
	if ( should_pick_reticle )
		result = bot_loadout_choose_from_set( level.bot_reticletable, loadoutValue, loadoutValueArray, loadoutValueName );
	
	return result;
}

init_bot_camotable()
{
	fileName = "mp/camotable.csv";
	colName = 1;
	colMaterial = 8;
	colBotValid = 9;

	level.bot_camotable = [];
	
	row = 0;
	while ( 1 )
	{
		camoName = TableLookupByRow( fileName, row, colName );
		
		if ( !IsDefined( camoName ) || camoName == "" )
			break;
		
		camoMaterial = TableLookupByRow( fileName, row, colMaterial );
		if ( IsDefined( camoMaterial ) && camoMaterial != "" )
		{
			AssertEx(maps\mp\gametypes\_class::isValidCamo( camoName, true ),"Camo '" + camoName + "' found in camotable.csv but not valid according to isValidCamo");
			botValid = TableLookupByRow( fileName, row, colBotValid );
			if ( IsDefined( botValid ) && Int( botValid ) )
				level.bot_camotable[ level.bot_camotable.size ] = camoName;
		}
		
		row++;
	}
}

bot_loadout_choose_from_camotable( loadoutValue, loadoutValueArray, loadoutValueName, personality, difficulty )
{
	// Set fallbacks in case nothing is found that matches
	result = "none";
	
	if ( !IsDefined( level.bot_camotable ) )
		return result;
	
	should_pick_camo = RandomInt(100) > 50;
	if ( should_pick_camo )
		result = bot_loadout_choose_from_set( level.bot_camotable, loadoutValue, loadoutValueArray, loadoutValueName );
	
	return result;
}

bot_loadout_item_valid_for_rank( loadoutValue, item, difficulty )
{
	if ( !IsPlayer(self) )
		return true;
	
	if ( !IsDefined( level.bot_min_rank_for_item[item] ) )
	{
		table_name = "mp/unlockTable.csv";
		row = TableLookupRowNum( table_name, 0, item );
		level.bot_min_rank_for_item[item] = INT(TableLookupByRow( table_name, row, 2 ));
	}
	
	if ( loadoutValue == "classtable_any" && difficulty == "recruit" )
		return true;		// Anything is valid for Recruits if it is in the classtable
	
	if ( !IsDefined(self.rank_for_items) )
	{
		if ( difficulty == "recruit" )
		{
			self.rank_for_items = 3;
		}
		else
		{
			self.rank_for_items = self.pers["rank"];
			if ( !IsDefined(self.rank_for_items) )
				self.rank_for_items = level.bot_rnd_rank[difficulty][0];
		}
	}
	
	if ( level.bot_min_rank_for_item[item] <= self.rank_for_items )
		return true;
	
	return false;
}

bot_loadout_valid_choice( loadoutValue, loadoutValueArray, loadoutValueName, choice )
{
	valid = true;
	
	switch ( loadoutValueName )
	{
		case "loadoutPrimary":
			baseWeaponName = choice;
			if ( isLootWeapon( baseWeaponName ) )
				baseWeaponName = maps\mp\gametypes\_class::getBaseFromLootVersion( baseWeaponName );
			valid = bot_loadout_item_allowed( "weapon", baseWeaponName, undefined );
			valid = valid && maps\mp\gametypes\_class::isValidPrimary( choice );
			break;	
		case "loadoutEquipment":
			valid = bot_loadout_item_allowed( "perk", choice, "Lethal" );
			valid = valid && maps\mp\gametypes\_class::isValidEquipment( choice, bot_loadout_set_has_wildcard(loadoutValueArray, "specialty_wildcard_dualtacticals") );
			valid = valid && bot_loadout_item_valid_for_rank( loadoutValue, choice, self BotGetDifficulty() );
			break;
		case "loadoutEquipment2":
			valid = (choice == "specialty_null" || choice == loadoutValueArray["loadoutEquipment"]);
			break;
		case "loadoutOffhand":
			valid = bot_loadout_item_allowed( "perk", choice, "Tactical" );
			valid = valid && maps\mp\gametypes\_class::isValidOffhand( choice, bot_loadout_set_has_wildcard(loadoutValueArray, "specialty_wildcard_duallethals") );
			valid = valid && bot_loadout_item_valid_for_rank( loadoutValue, choice, self BotGetDifficulty() );
			break;
		case "loadoutOffhand2":
			valid = (choice == "specialty_null");
			AssertEx(valid, "loadoutOffhand2 not supported");
			break;			
		case "loadoutPrimaryAttachment":
			valid = self bot_validate_weapon( loadoutValueArray["loadoutPrimary"], choice );
			break;
		case "loadoutPrimaryAttachment2":
			valid = self bot_validate_weapon( loadoutValueArray["loadoutPrimary"], loadoutValueArray["loadoutPrimaryAttachment"], choice );
			break;
		case "loadoutPrimaryAttachment3":
			valid = self bot_validate_weapon( loadoutValueArray["loadoutPrimary"], loadoutValueArray["loadoutPrimaryAttachment"], loadoutValueArray["loadoutPrimaryAttachment2"], choice );
			break;
		case "loadoutPrimaryReticle":
			valid = self bot_validate_reticle( "loadoutPrimary", loadoutValueArray, choice );
			break;
		case "loadoutPrimaryCamo":
			valid = (!IsDefined( self.botLoadoutFavoriteCamoPrimary ) || (choice == self.botLoadoutFavoriteCamoPrimary));
			valid = valid && maps\mp\gametypes\_class::isValidCamo( choice );
			break;
		case "loadoutSecondary":
			valid = (choice != loadoutValueArray["loadoutPrimary"]);
			baseWeaponName = choice;
			if ( isLootWeapon( baseWeaponName ) )
				baseWeaponName = maps\mp\gametypes\_class::getBaseFromLootVersion( baseWeaponName );
			valid = valid && bot_loadout_item_allowed( "weapon", baseWeaponName, undefined );
			valid = valid && maps\mp\gametypes\_class::isValidSecondary( choice, bot_loadout_set_has_wildcard(loadoutValueArray, "specialty_wildcard_dualprimaries") );
			break;
		case "loadoutSecondaryAttachment":
			valid = self bot_validate_weapon( loadoutValueArray["loadoutSecondary"], choice, "none" );
			break;
		case "loadoutSecondaryAttachment2":
			valid = self bot_validate_weapon( loadoutValueArray["loadoutSecondary"], loadoutValueArray["loadoutSecondaryAttachment"], choice );
			break;
		case "loadoutSecondaryAttachment3":
			valid = self bot_validate_weapon( loadoutValueArray["loadoutSecondary"], loadoutValueArray["loadoutSecondaryAttachment"], loadoutValueArray["loadoutSecondaryAttachment2"], choice );
			break;
		case "loadoutSecondaryReticle":
			valid = self bot_validate_reticle( "loadoutSecondary", loadoutValueArray, choice );
			break;
		case "loadoutSecondaryCamo":
			valid = (!IsDefined(self.botLoadoutFavoriteCamoSecondary ) || (choice == self.botLoadoutFavoriteCamoSecondary));
			valid = valid && maps\mp\gametypes\_class::isValidCamo( choice );
			break;
		case "loadoutStreak1":
		case "loadoutStreak2":
		case "loadoutStreak3":
		case "loadoutStreak4":
			valid = bot_killstreak_is_valid_internal( choice, "bots", undefined );
			valid = valid && bot_loadout_item_allowed( "killstreak", choice, undefined );
			break;
		case "loadoutStreakModule1a":
		case "loadoutStreakModule1b":
		case "loadoutStreakModule1c":
		case "loadoutStreakModule2a":
		case "loadoutStreakModule2b":
		case "loadoutStreakModule2c":
		case "loadoutStreakModule3a":
		case "loadoutStreakModule3b":
		case "loadoutStreakModule3c":
		case "loadoutStreakModule4a":
		case "loadoutStreakModule4b":
		case "loadoutStreakModule4c":
			valid = bot_loadout_item_allowed( "module", choice, undefined );
			break;
		case "loadoutPerk1":
		case "loadoutPerk2":
		case "loadoutPerk3":
		case "loadoutPerk4":
		case "loadoutPerk5":
		case "loadoutPerk6":
			valid = (choice == "specialty_null") || bot_validate_perk( choice, loadoutValueName, loadoutValueArray );
			valid = valid && bot_loadout_item_valid_for_rank( loadoutValue, choice, self BotGetDifficulty() );
			break;
		case "loadoutWildcard1":
		case "loadoutWildcard2":
		case "loadoutWildcard3":
			valid = true;
			break;
		default:
			AssertMsg("'" + loadoutValueName + "' needs validation in bot_loadout_valid_choice");
			break;
	};
	
	return valid;
}

bot_loadout_choose_from_set( valueChoices, loadoutValue, loadoutValueArray, loadoutValueName, isTemplate )
{
	chosenValue = "none";
	chosenTemplate = undefined;
	validCount = 0.0;
	
	if ( array_contains( valueChoices, "specialty_null" ) )
		chosenValue = "specialty_null";
	else if ( loadoutValueName == "loadoutEquipment" || loadoutValueName == "loadoutOffhand" || string_find( loadoutValueName, "Perk" ) >= 0 )
		chosenValue = "specialty_null";
	
	if ( loadoutValue == "classtable_any" )
	{
		// Bot should pick perks / wildcards / etc all from the same default class
		if ( !IsDefined(self.default_class_chosen) )
			self.default_class_chosen = Random(["class1","class2","class3","class4","class5"]);
		valueChoices = [self.default_class_chosen];
	}
	
	foreach ( choice in valueChoices )
	{
		template = undefined;
		
		if ( GetSubStr( choice, 0, 9 ) == "template_" )
		{
			/#
			if ( IsDefined( isTemplate ) && isTemplate )
				AssertMsg( "template_ entries should not reference other template_ entries as the random weighting does not work right for that" );
			#/
																					
			template = choice;
			templateValues = level.botLoadoutTemplates[choice][loadoutValueName];
			assert( IsDefined( templateValues ) );
			choice = bot_loadout_choose_from_set( StrTok( templateValues, "| " ), loadoutValue, loadoutValueArray, loadoutValueName, true );
	
			// If we have already chosen this template for any field, always choose the same template again when given the option
			if ( IsDefined( template ) && IsDefined( self.chosenTemplates[template] ) )
				return choice;
		}
		
		if ( choice == "attachmenttable" )
			return bot_loadout_choose_from_attachmenttable( loadoutValue, loadoutValueArray, loadoutValueName, self.personality, self.difficulty );

		if ( choice == "weap_statstable" )
			return bot_loadout_choose_from_statstable( loadoutValue, loadoutValueArray, loadoutValueName, self.personality, self.difficulty );
		
		if ( choice == "reticletable" )
			return bot_loadout_choose_from_reticletable( loadoutValue, loadoutValueArray, loadoutValueName, self.personality, self.difficulty );
		
		if ( choice == "camotable" )
			return bot_loadout_choose_from_camotable( loadoutValue, loadoutValueArray, loadoutValueName, self.personality, self.difficulty );
		
		if ( GetSubStr( choice, 0, 5 ) == "class" && int( GetSubStr( choice, 5, 6 ) ) > 0 )
			choice = bot_loadout_choose_from_default_class( choice, loadoutValueName );
		
		if ( IsSubStr( loadoutValueName, "loadoutStreakModule" ) )
		{
			streak_num = INT(GetSubStr(loadoutValueName,19,20));
			streak_in_slot = loadoutValueArray["loadoutStreak" + streak_num];
			module_letter = GetSubStr(loadoutValueName,20,21);
			switch ( streak_in_slot )
			{
				case "remote_mg_sentry_turret":
					if ( module_letter == "a" )
						choice = "sentry_guardian";
					break;
				case "warbird":
					if ( module_letter == "a" )
						choice = "warbird_ai_attack";
					break;
				case "assault_ugv":
					if ( module_letter == "a" )
						choice = "assault_ugv_ai";
					else if ( module_letter == "b" )
						choice = "assault_ugv_mg";
					break;
			}
		}

		if ( self bot_loadout_valid_choice( loadoutValue, loadoutValueArray, loadoutValueName, choice ) )
		{
			validCount = validCount + 1.0;
			if ( RandomFloat( 1.0 ) <= (1.0 / validCount) )
			{
				chosenValue = choice;
				chosenTemplate = template;
			}
		}
	}
	
	if ( IsDefined( chosenTemplate ) )
		self.chosenTemplates[chosenTemplate] = true;
	
	return chosenValue;
}

bot_loadout_choose_values( loadoutValueArray )
{
	self.chosenTemplates = [];
		
	foreach( loadoutValueName, loadoutValue in loadoutValueArray )
	{
		valueChoices = StrTok( loadoutValue, "| " );
		
		chosenValue = self bot_loadout_choose_from_set( valueChoices, loadoutValue, loadoutValueArray, loadoutValueName );
/#
		debugLoadoutValue = GetDvar( "bot_Debug" + loadoutValueName, "" );
		if ( IsDefined( debugLoadoutValue ) && debugLoadoutValue != "" )
			chosenValue = debugLoadoutValue;
#/
		loadoutValueArray[loadoutValueName] = chosenValue;	
	}
	
	if ( self BotGetDifficulty() != "recruit" )
	{
		pick13_count = bot_get_pick_13_count( loadoutValueArray );
		num_attempts_tried = 0;
		while( pick13_count < level.customClassPickCount && num_attempts_tried < 5 )
		{
			if ( pick13_count == level.customClassPickCount-1 )
				loadoutValueArray = bot_find_1_loadout_option( loadoutValueArray );
			else if ( pick13_count <= level.customClassPickCount-2 )
				loadoutValueArray = bot_find_2_loadout_options( loadoutValueArray );
			
			pick13_count = bot_get_pick_13_count( loadoutValueArray );
			num_attempts_tried++;
		}
/*/#
		bot_min_allowable_items = level.customClassPickCount;
		if ( level.customClassPickCount > 19 )
			bot_min_allowable_items--;				// If the limit is 20, bots might not be able to fill it completely up, so allow 1 unusued slot
		if ( pick13_count < bot_min_allowable_items )
		{
			println( "loadoutValueArray" );
			keys = getArrayKeys( loadoutValueArray );
			for ( i = 0; i < keys.size; i++ )
				println( " loadoutValueArray[\"" + keys[ i ] + "\"] = ", loadoutValueArray[ keys[ i ] ] );
		
			AssertMsg("Bot loadout has " + pick13_count + " items, expected " + level.customClassPickCount + ".  See log for details.");
		}
#/*/
	}
	
	return loadoutValueArray;
}

bot_find_1_loadout_option( loadoutValueArray )
{
	Assert( bot_get_pick_13_count( loadoutValueArray ) == level.customClassPickCount-1 );
	
	// First try to pick a second secondary attachment
	if ( loadoutValueArray["loadoutSecondaryAttachment2"] == "none" )
	{
		loadoutValueArray["loadoutSecondaryAttachment2"] = self bot_loadout_choose_from_set( ["attachmenttable"], "attachmenttable", loadoutValueArray, "loadoutSecondaryAttachment2" );
/#
		debugLoadoutValue = GetDvar( "bot_DebugloadoutSecondaryAttachment2" );
		if ( IsDefined( debugLoadoutValue ) && debugLoadoutValue != "" )
			loadoutValueArray["loadoutSecondaryAttachment2"] = debugLoadoutValue;
#/
	}
	
	if ( bot_get_pick_13_count( loadoutValueArray ) == level.customClassPickCount )
		return loadoutValueArray;
	
	remove_or_add_grenade = "add";
	has_full_wildcards = loadoutValueArray["loadoutWildcard1"] != "specialty_null" && loadoutValueArray["loadoutWildcard2"] != "specialty_null" && loadoutValueArray["loadoutWildcard3"] != "specialty_null";
	if ( RandomInt(100) < 50 && !has_full_wildcards )
		remove_or_add_grenade = "remove";
	
	if ( remove_or_add_grenade == "remove" )
	{
		if ( loadoutValueArray["loadoutEquipment"] != "specialty_null" )
		{
			loadoutValueArray["loadoutEquipment"] = "specialty_null";
/#
			debugLoadoutValue = GetDvar( "bot_DebugloadoutEquipment" );
			if ( IsDefined( debugLoadoutValue ) && debugLoadoutValue != "" )
				loadoutValueArray["loadoutloadoutEquipment"] = debugLoadoutValue;
#/
			if ( bot_get_pick_13_count( loadoutValueArray ) == level.customClassPickCount-2 )
				return loadoutValueArray;
		}
	}
	else if ( remove_or_add_grenade == "add" )
	{
		if ( loadoutValueArray["loadoutEquipment"] != "specialty_null" && loadoutValueArray["loadoutEquipment2"] == "specialty_null" )
		{
			loadoutValueArray["loadoutEquipment2"] = loadoutValueArray["loadoutEquipment"];
/#
			debugLoadoutValue = GetDvar( "bot_DebugloadoutEquipment2" );
			if ( IsDefined( debugLoadoutValue ) && debugLoadoutValue != "" )
				loadoutValueArray["loadoutloadoutEquipment2"] = debugLoadoutValue;
#/
			if ( bot_get_pick_13_count( loadoutValueArray ) == level.customClassPickCount )
				return loadoutValueArray;
		}
	}
		
	if ( !has_full_wildcards )
	{
		// Finally just try remove stuff so we have 2 open slots instead of 1, and we can put in a wildcard + perk or whatever
		values_to_remove = ["loadoutSecondaryAttachment2","loadoutSecondaryAttachment","loadoutStreak3","loadoutStreak2","loadoutStreak1"];
		foreach ( value in values_to_remove )
		{
			if ( loadoutValueArray[value] != "none" )
			{
				loadoutValueArray[value] = "none";
/#
				debugLoadoutValue = GetDvar( "bot_Debug" + value, "" );
				if ( IsDefined( debugLoadoutValue ) && debugLoadoutValue != "" )
					loadoutValueArray[value] = debugLoadoutValue;
#/
				if ( bot_get_pick_13_count( loadoutValueArray ) == level.customClassPickCount-2 )
					return loadoutValueArray;
			}
		}
	}
	
	return loadoutValueArray;
}

bot_find_2_loadout_options( loadoutValueArray )
{
	Assert( bot_get_pick_13_count( loadoutValueArray ) <= level.customClassPickCount-2 );
	
	all_wildcards = ["loadoutWildcard1","loadoutWildcard2","loadoutWildcard3"];
	
	// First try to pick extra perks
	extra_perk_slots = array_randomize(["loadoutPerk2","loadoutPerk4","loadoutPerk6"]);
	foreach( perk_slot in extra_perk_slots )
	{
		if ( loadoutValueArray[perk_slot] == "specialty_null" )
		{
			wildcard = undefined;
			foreach ( potential_wildcard_slot in all_wildcards )
			{
				if ( loadoutValueArray[potential_wildcard_slot] == "specialty_null" )
				{
					wildcard = potential_wildcard_slot;
					break;
				}
			}
			
			if ( IsDefined(wildcard) )
			{
				has_debug_values_for_wildcard_or_perk = false;
/#
				debugLoadoutValue = GetDvar( "bot_Debug" + wildcard, "" );
				if ( IsDefined( debugLoadoutValue ) && debugLoadoutValue != "" )
					has_debug_values_for_wildcard_or_perk = true;
				debugLoadoutValue = GetDvar( "bot_Debug" + perk_slot + "" );
				if ( IsDefined( debugLoadoutValue ) && debugLoadoutValue != "" )
					has_debug_values_for_wildcard_or_perk = true;
#/
				if ( !has_debug_values_for_wildcard_or_perk )
				{
					perk_num = INT(GetSubStr(perk_slot,11,12));
					Assert(perk_num == 6 || perk_num == 4 || perk_num == 2);
					loadoutValueArray[wildcard] = "specialty_wildcard_perkslot" + (perk_num/2);
					loadoutValueArray[perk_slot] = self bot_loadout_choose_from_set( ["template_any"], "template_any", loadoutValueArray, perk_slot );
					if ( loadoutValueArray[perk_slot] == "specialty_null" )
					{
						// Couldn't find a valid perk for this slot
						loadoutValueArray[wildcard] = "specialty_null";
						continue;
					}
					
					if ( bot_get_pick_13_count( loadoutValueArray ) > level.customClassPickCount-2 )
						return loadoutValueArray;
				}
			}
		}
	}
	
	// Second, try to pick equipment
	if ( loadoutValueArray["loadoutEquipment"] == "specialty_null" )
	{
		template = "template_any_noncamper";
		if ( level.bot_personality_type[self.personality] == "stationary" )
			template = "template_any_camper";
			
		loadoutValueArray["loadoutEquipment"] = self bot_loadout_choose_from_set( [template], template, loadoutValueArray, "loadoutEquipment" );
/#
		debugLoadoutValue = GetDvar( "loadoutEquipment" );
		if ( IsDefined( debugLoadoutValue ) && debugLoadoutValue != "" )
			loadoutValueArray["loadoutEquipment"] = debugLoadoutValue;
#/
	}
	
	if ( loadoutValueArray["loadoutEquipment"] != "specialty_null" && loadoutValueArray["loadoutEquipment2"] == "specialty_null" )
	{
		loadoutValueArray["loadoutEquipment2"] = loadoutValueArray["loadoutEquipment"];
/#
		debugLoadoutValue = GetDvar( "bot_DebugloadoutEquipment2" );
		if ( IsDefined( debugLoadoutValue ) && debugLoadoutValue != "" )
			loadoutValueArray["loadoutloadoutEquipment2"] = debugLoadoutValue;
#/
	}

	return loadoutValueArray;
}

bot_get_pick_13_count( loadoutValueArray )
{
	// copied from _class.gsc
	
	requiredPrimaryAttachment = tablelookup( "mp/statstable.csv", 4, loadoutValueArray["loadoutPrimary"], 40 );
	requiredSecondaryAttachment = tablelookup( "mp/statstable.csv", 4, loadoutValueArray["loadoutSecondary"], 40 );
	
	pick13ActiveWeaponCount = 0;
	if ( loadoutValueArray["loadoutPrimary"] != "iw5_combatknife" )
		pick13ActiveWeaponCount++;
	if ( loadoutValueArray["loadoutPrimaryAttachment"] != "none" && requiredPrimaryAttachment == "" ) // if there is a required attachment, it goes in this spot, and doesn't count against pick 13
		pick13ActiveWeaponCount++;
	if ( loadoutValueArray["loadoutPrimaryAttachment2"] != "none" )
		pick13ActiveWeaponCount++;
	if ( loadoutValueArray["loadoutPrimaryAttachment3"] != "none" )
		pick13ActiveWeaponCount++;
	if ( loadoutValueArray["loadoutSecondary"] != "iw5_combatknife" )
		pick13ActiveWeaponCount++;
	if ( loadoutValueArray["loadoutSecondaryAttachment"] != "none" && requiredSecondaryAttachment == "" ) // if there is a required attachment, it goes in this spot, and doesn't count against pick 13
		pick13ActiveWeaponCount++;
	if ( loadoutValueArray["loadoutSecondaryAttachment2"] != "none" )
		pick13ActiveWeaponCount++;
	if ( loadoutValueArray["loadoutSecondaryAttachment3"] != "none" )
		pick13ActiveWeaponCount++;
	if ( loadoutValueArray["loadoutEquipment"] != "specialty_null" )
		pick13ActiveWeaponCount++;
	if ( loadoutValueArray["loadoutEquipment2"] != "specialty_null" )
		pick13ActiveWeaponCount++;
	if ( loadoutValueArray["loadoutOffhand"] != "specialty_null" )
		pick13ActiveWeaponCount++;
	if ( isDefined( loadoutValueArray["loadoutOffhandExtra"] ) && loadoutValueArray["loadoutOffhandExtra"] )
		pick13ActiveWeaponCount++;
	for ( i = 1; i <= 6; i++ )
	{
		if ( loadoutValueArray["loadoutPerk" + i] != "specialty_null" )
			pick13ActiveWeaponCount++;
	}
	for ( i = 1; i <= 3; i++ )
	{
		if ( loadoutValueArray["loadoutWildcard" + i] != "specialty_null" )
			pick13ActiveWeaponCount++;
	}
	for ( i = 1; i <= 4; i++ )
	{
		if ( loadoutValueArray["loadoutStreak" + i] != "none" )
			pick13ActiveWeaponCount++;
	}
	
	return pick13ActiveWeaponCount;
}

bot_loadout_get_difficulty()
{
	difficulty = self BotGetDifficulty();
	
	if ( difficulty == "default" )
	{
		// Make sure we pick a difficulty if its set to "default"
		self maps\mp\bots\_bots_util::bot_set_difficulty( "default" );
		difficulty = self BotGetDifficulty();
	}
	
	Assert( difficulty != "default" );
	
	return difficulty;
}

bot_loadout_class_callback( force_keep_same_loadout )
{   	
	while( !IsDefined(level.bot_loadouts_initialized) )
		wait(0.05);
	
	while ( !IsDefined( self.personality ) )
		wait(0.05);
	
	force_new_loadout = false;
	force_same_loadout = false;
	if ( IsDefined(force_keep_same_loadout) && force_keep_same_loadout )
	{
		// force_same_loadout should take precedence over force_new_loadout
		force_same_loadout = true;
	}
	else
	{
		if ( IsDefined(self.bot_pick_new_loadout_next_spawn) && self.bot_pick_new_loadout_next_spawn )
		{
			force_new_loadout = true;
			self.bot_pick_new_loadout_next_spawn = undefined;
		}
	}
	Assert(!(force_new_loadout && force_same_loadout));

	loadoutValueArray = [];
	
	difficulty = self bot_loadout_get_difficulty();
	self.difficulty = difficulty;
	personality = self BotGetPersonality();
	
	if ( !IsDefined(self.bot_last_loadout_num) )
		self.bot_cur_loadout_num = 0;
	
	self.bot_last_loadout_num = self.bot_cur_loadout_num;
	
	// If bot already has a loadout, stick with it most of the time
	if ( IsDefined( self.botLastLoadout ) && !force_new_loadout )
	{
		if ( force_same_loadout )
			return self.botLastLoadout;
		
		same_difficulty = self.botLastLoadoutDifficulty == difficulty;
		same_personality = self.botLastLoadoutPersonality == personality;
		if ( same_difficulty && same_personality && ( !IsDefined(self.hasDied) || self.hasDied ) && !IsDefined(self.respawn_with_launcher) )
		{
			should_keep_same_loadout = RandomFloat( 1.0 ) > SCR_CONST_BOT_CHANGE_LOADOUT_CHANCE;
			if ( should_keep_same_loadout )
				return self.botLastLoadout;
		}
	}
	
	if ( !bot_custom_classes_allowed() )
	{
		loadoutValueArray = bot_default_class_random();
		self bot_pick_personality_from_weapon( loadoutValueArray[ "loadoutPrimary" ] );
	}
	else
	{
		loadoutValueArray = self bot_loadout_pick( personality, difficulty );
		loadoutValueArray = self bot_loadout_choose_values( loadoutValueArray );

		if ( isdefined( level.bot_funcs["gametype_loadout_modify"] ) )
			loadoutValueArray = self [[level.bot_funcs["gametype_loadout_modify"]]]( loadoutValueArray );
	
		AssertEx(IsDefined(loadoutValueArray), "Bot '" + self.name + "'spawning (randomized loadout) with loadoutValueArray not defined");
	}
	
/#	
	if ( GetDvarInt("scr_botsDisableLoadout") )
	{
		loadoutValueArray["loadoutPerk1"] = "specialty_null";
		loadoutValueArray["loadoutPerk2"] = "specialty_null";
		loadoutValueArray["loadoutPerk3"] = "specialty_null";
		loadoutValueArray["loadoutPerk4"] = "specialty_null";
		loadoutValueArray["loadoutPerk5"] = "specialty_null";
		loadoutValueArray["loadoutPerk6"] = "specialty_null";
		loadoutValueArray["loadoutEquipment"] = "specialty_null";
		loadoutValueArray["loadoutEquipment2"] = "specialty_null";
		loadoutValueArray["loadoutOffhand"] = "specialty_null";
		
		foreach( index, value in loadoutValueArray )
		{
			if ( index != "loadoutPrimary" && value != "specialty_null" )
				loadoutValueArray[index] = "none";
		}
	}
#/
	
	if ( loadoutValueArray["loadoutPrimary"] == "none" )
	{
		// Choose a fallback weapon and switch personality to match
		self.bot_fallback_personality = undefined;
		loadoutValueArray["loadoutPrimary"] = self bot_loadout_choose_fallback_primary( loadoutValueArray );
		loadoutValueArray["loadoutPrimaryCamo"] = "none";
		loadoutValueArray["loadoutPrimaryAttachment"] = "none";
		loadoutValueArray["loadoutPrimaryAttachment2"] = "none";
		loadoutValueArray["loadoutPrimaryAttachment3"] = "none";
		loadoutValueArray["loadoutPrimaryReticle"] = "none";
		if ( IsDefined( self.bot_fallback_personality ) )
		{
			if ( self.bot_fallback_personality == "weapon" )
				self bot_pick_personality_from_weapon( loadoutValueArray[ "loadoutPrimary" ] );
			else
				self maps\mp\bots\_bots_util::bot_set_personality( self.bot_fallback_personality );
			
			personality = self.personality;

			self.bot_fallback_personality = undefined;
		}
	}
	
	if ( IsDefined( loadoutValueArray["loadoutPrimaryCamo"] ) && loadoutValueArray["loadoutPrimaryCamo"] != "none" && !IsDefined(self.botLoadoutFavoriteCamoPrimary) )
		self.botLoadoutFavoriteCamoPrimary = loadoutValueArray["loadoutPrimaryCamo"];
	if ( IsDefined( loadoutValueArray["loadoutSecondaryCamo"] ) && loadoutValueArray["loadoutSecondaryCamo"] != "none" && !IsDefined(self.botLoadoutFavoriteCamoSecondary) )
	    self.botLoadoutFavoriteCamoSecondary = loadoutValueArray["loadoutSecondaryCamo"];
	
	if ( IsDefined(self.respawn_with_launcher) )
	{
		Assert( IsDefined( level.bot_respawn_launcher_name ) );
		launcher_to_use = level.bot_respawn_launcher_name[self BotGetDifficulty()];
		Assert( IsDefined( launcher_to_use ) );
		Assert( maps\mp\gametypes\_class::isValidSecondary( launcher_to_use, false ) );
		if ( bot_loadout_item_allowed( "weapon", launcher_to_use, undefined ) )
		{
			loadoutValueArray["loadoutSecondary"] = launcher_to_use;
			loadoutValueArray["loadoutSecondaryAttachment"] = "none";
			loadoutValueArray["loadoutSecondaryAttachment2"] = "none";
			loadoutValueArray["loadoutSecondaryAttachment3"] = "none";
			self.bot_pick_new_loadout_next_spawn = true;
		}
		self.respawn_with_launcher = undefined;
	}
	
	// Killstreaks should be valid now so its safe to check them here (these functions assert internally)
	maps\mp\gametypes\_class::isValidKillstreak(loadoutValueArray["loadoutStreak1"]);
	maps\mp\gametypes\_class::isValidKillstreak(loadoutValueArray["loadoutStreak2"]);
	maps\mp\gametypes\_class::isValidKillstreak(loadoutValueArray["loadoutStreak3"]);
	
	/*
	if ( self maps\mp\bots\_bots::bot_israndom() )
	{
		if ( array_contains( self.pers[ "loadoutPerks" ], "specialty_extra_attachment" ) )
		{
			// Pick again for attachment3 and attachment2 on secondary
			otherAttachmentLoadout = self bot_loadout_pick( personality, difficulty );
			loadoutValueArray["loadoutPrimaryAttachment3"] = otherAttachmentLoadout["loadoutPrimaryAttachment2"];
			if ( array_contains( self.pers[ "loadoutPerks" ], "specialty_twoprimaries" ) )
				loadoutValueArray["loadoutSecondaryAttachment2"] = otherAttachmentLoadout["loadoutPrimaryAttachment2"];
			else
				loadoutValueArray["loadoutSecondaryAttachment2"] = otherAttachmentLoadout["loadoutSecondaryAttachment2"];
			loadoutValueArray = self bot_loadout_choose_values( loadoutValueArray );
			loadoutValueArray = self bot_loadout_setup_perks( loadoutValueArray );
		}
	}
	*/
	
	self.bot_cur_loadout_num++;
	self.botLastLoadout = loadoutValueArray;
	self.botLastLoadoutDifficulty = difficulty;
	self.botLastLoadoutPersonality = personality;
	
	AssertEx(IsDefined(loadoutValueArray), "Bot returning undefined from bot_loadout_class_callback");
	return loadoutValueArray;
}

bot_setup_loadout_callback()
{
	personality = self BotGetPersonality();	
	difficulty = self bot_loadout_get_difficulty();
	
	loadoutSet = bot_loadout_set( personality, difficulty, false );
	if ( IsDefined( loadoutSet ) && isDefined( loadoutSet.loadouts ) && loadoutSet.loadouts.size > 0 )
	{
		self.classCallback = ::bot_loadout_class_callback;
		return true;
	}
	
	bot_name_without_personality = GetSubStr(self.name,0,self.name.size-10);	// At this point in the setup, the bot personality might be wrong, so don't display it here
	AssertMsg("Bot <" + bot_name_without_personality + "> has no possible loadouts for personality <" + personality + "> and difficulty <" + difficulty + ">");
	self.classCallback = undefined;
	return false;
}