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

/*QUAKED info_player_start_so (0.0 0.3 1.0) (-16 -16 0) (16 16 72)
Player 1 spawns at these locations in Special Ops games. If there is a player 2 but player2 specific start points, player 2 will be positioned near player 1.*/

/*QUAKED info_player_start_soPlayer2 (0.2 0.5 1.0) (-16 -16 0) (16 16 72)
Player 2 spawns at these locations in Special Ops games. Will use the closest spawn point to player 1.*/

pick_starting_location_so( return_placement )
{
	if ( isdefined( return_placement ) && return_placement )
	{
		if ( isdefined( level._skip_PlayerSetStreamOrigin ) && level._skip_PlayerSetStreamOrigin )
			return undefined;
	}

	if ( isdefined( level._pmc_match ) && level._pmc_match )
	{
		return pick_starting_location_pmc( return_placement );
	}
	
	p1_starts = getentarray( "info_player_start_so", "classname" );
	if ( p1_starts.size <= 0 )
		p1_starts = getentarray( "info_player_start", "classname" );

	assertex( p1_starts.size > 0, "Special Ops requires at least 1 player starting position. Place either an info_player_start_so or info_player_start in the level." );

	p1_start_point = random( p1_starts );

	// return_placement tells it to not actually place the people, and instead send back where it *would* have placed them. 
	// If the map can potentially spawn them in random locations or far apart, this logic breaks down.
	// Used by PlayerSetStreamOrigin for map restarts. Use level.skip_PlayerSetStreamOrigin to avoid it.
	if ( isdefined( return_placement ) && return_placement )
	{
		assert( p1_starts.size == 1 );
		return p1_start_point.origin;
	}

	place_player_at_start_point( level._player, p1_start_point );

	if ( is_coop() )
	{	
		p2_starts = getentarray( "info_player_start_soPlayer2", "classname" );
		if ( p2_starts.size > 0 )
		{
			p2_start_point = getClosest( p1_start_point.origin, p2_starts );
			place_player_at_start_point( level._player2, p2_start_point );
		}
		else
		{
			place_player2_near_player1();
		}
	}
}

isDefendMatch()
{
	return( level._pmc_gametype == "mode_defend" );
}

// Having to include this is sucky, but I don't know a better way.
pick_starting_location_pmc( return_placement )
{
	if ( isDefendMatch() )
	{
		startingLocations = getentarray( "info_player_start_pmcDefend", "classname" );
		assertEx( startingLocations.size > 0, "You don't have any player starting positions in the level for defend mode. Place info_player_start_pmcDefend entities in the level." );
		assertEx( startingLocations.size >= level._players.size, "You don't have enough info_player_start_pmcDefend entities in the level to support " + level._players.size + " players." );
	}
	else
	{
		// Get all of the spawn locations in the map
		startingLocations = getentarray( "info_player_start_pmc", "classname" );
		assertEx( startingLocations.size > 0, "You don't have any player starting positions in the level. Place info_player_start_pmc entities in the level." );
		assertEx( startingLocations.size >= level._players.size, "You don't have enough info_player_start_pmc entities in the level to support " + level._players.size + " players." );
	}


	// Pick one at random then put the player at it and put other players at the next nearest ones
	excluders = [];
	spawnEnt = undefined;
	foreach ( player in level._players )
	{
		if ( !excluders.size )
			spawnEnt = random( startingLocations );
		else
			spawnEnt = get_closest_exclude( spawnEnt.origin, startingLocations, excluders );

		assert( isdefined( spawnEnt ) );

		// See notes in pick_starting_location_so()
		if ( isdefined( return_placement ) && return_placement )
		{
			assert( startingLocations.size <= 2 );
			return spawnEnt.origin;
		}
					
		player setOrigin( spawnEnt.origin );
		player setPlayerAngles( spawnEnt.angles );

		excluders[ excluders.size ] = spawnEnt;
	}
}

place_player_at_start_point( player, start_point )
{
	assertex( isdefined( player ), "place_player_at_start_point() requires a valid player." );
	assertex( isdefined( start_point ), "place_player_at_start_point() requires a valid start_point." );

	player SetOrigin( start_point.origin );
	if ( isdefined( start_point.angles ) )
		player setPlayerAngles( start_point.angles );
}

place_player2_near_player1()
{
	level._player2 setPlayerAngles( level._player.angles + ( 0, -25, 0 ) );
	level._player2 SetOrigin( level._player.origin );

	// Offset player 2 a bit from player 1 (adjusted by angle players are facing).
	ent = spawnstruct();
	ent.entity = level._player2;
	ent.right = -20.0;
	ent.forward = -50.0;
	ent translate_local();
	level._player2 SetOrigin( level._player2.origin );
}

specialops_remove_entity_check( special_op_state )
{
	if ( specialops_remove_flag_check( special_op_state ) )
		return true;
	
	if ( specialops_remove_name_check( special_op_state ) )
		return true;
		
	return false;
}

specialops_remove_flag_check( special_op_state )
{
	if ( !isdefined( self.script_specialops ) )
		return false;
		
	assertex( ( ( self.script_specialops == 0 ) || ( self.script_specialops == 1 ) ), "Entities with script_specialops set must use a value of either 0 or 1. Classname " + self.classname + " at Origin " + self.origin );
	if ( self.script_specialops == special_op_state )
		return false;
	
	return true;
}

specialops_remove_name_check( special_op_state )
{
	if ( !isdefined( self.script_specialopsname ) )
		return false;

	if ( special_op_state == 0 )
		return true;

	tokens = strtok( self.script_specialopsname, ":;, " );
	delete_ent = true;
	foreach ( token in tokens )
	{
		if ( token == level._script )
		{
			delete_ent = false;
			break;
		}
	}
	
	return delete_ent;
}

so_create_hud_item_delay_draw( hudelem )
{
	hudelem.alpha = 0;
	while( !so_hud_can_show() )
		wait 0.5;

	if ( !isdefined( hudelem ) )
		return;

	if ( so_hud_can_toggle( hudelem ) )
	{
		switch( self.so_infohud_toggle_state )
		{
			case "on":	
			case "none":	hudelem fade_over_time( 1, 0.5 ); break;
			case "off":		hudelem fade_over_time( 0, 0.5 ); break;
			default:		assertmsg( "so_create_hud_item_delay_draw() encountered a setting for player.so_infohud_toggle_state (" + self.so_infohud_toggle_state + ") it didn't recognize." );
		}
	}
	else
	{
		hudelem fade_over_time( 1, 0.5 );
	}
	
	if ( !self ent_flag( "so_hud_can_toggle" ) )
		self ent_flag_set( "so_hud_can_toggle" );
}

so_hud_can_show()
{
	if ( isdefined( level._so_waiting_for_players ) && level._so_waiting_for_players )
		return false;

	if ( isdefined( level._challenge_time_force_on ) && level._challenge_time_force_on )
		return true;
		
	if ( !isdefined( self.so_hud_show_time ) )
		return true;
	
	return ( gettime() > self.so_hud_show_time );
}

so_hud_can_toggle( hudelem )
{
	if ( !isdefined( hudelem.so_can_toggle ) || !hudelem.so_can_toggle )
		return false;
		
	if ( !isdefined( self.so_infohud_toggle_state ) )
		return false;
		
	return true;
}

challenge_timer_player_setup( start_flag, passed_flag, message )
{
	level endon( "challenge_timer_expired" );
	level endon( "new_challenge_timer" );
	level endon( "special_op_terminated" );

	ypos = maps\_specialops::so_hud_ypos();
	self.hud_so_timer_msg = maps\_specialops::so_create_hud_item( 1, ypos, message, self );
	if ( isdefined( level._challenge_time_limit ) )
	{
		self.hud_so_timer_time = maps\_specialops::so_create_hud_item( 1, ypos, undefined /*convert_to_time_string( level.challenge_time_limit, true )*/, self );
		self.hud_so_timer_time SetTenthsTimerStatic( level._challenge_time_limit );
	}
	else
		self.hud_so_timer_time = maps\_specialops::so_create_hud_item( 1, ypos, &"SPECIAL_OPS_TIME_NULL", self );
	self.hud_so_timer_time.alignX = "left";

	thread maps\_specialops::info_hud_wait_for_player();
	thread maps\_specialops::info_hud_handle_fade( self.hud_so_timer_msg );
	thread maps\_specialops::info_hud_handle_fade( self.hud_so_timer_time );

	challenge_timer_wait_start( self.hud_so_timer_msg, self.hud_so_timer_time, start_flag );

/*	if ( !isdefined( level.challenge_progress_manual_update ) || !level.challenge_progress_manual_update )
	{
		thread challenge_timer_detect_3quarter();
		thread challenge_timer_detect_halfway();
		thread challenge_timer_detect_quarter();
	}*/

	if ( isdefined( level._challenge_time_limit ) )
	{
		level._so_challenge_time_left = level._challenge_time_limit;
		thread challenge_timer_show_nudge( self.hud_so_timer_msg, self.hud_so_timer_time );
		thread challenge_timer_show_hurry( self.hud_so_timer_msg, self.hud_so_timer_time );
		thread challenge_timer_show_failed( self.hud_so_timer_msg, self.hud_so_timer_time );
		thread challenge_timer_destroy_prematurely( self.hud_so_timer_msg, self.hud_so_timer_time );
	}

	thread challenge_timer_wait_passed( self.hud_so_timer_msg, self.hud_so_timer_time, passed_flag );
}

// CTW - Better way to do this: Have the timer detect the halfway mark when counting down
// and if you haven't set these flags, tell the player they need to pick it up.
challenge_timer_detect_3quarter()
{
	if ( !flag_exist( "so_challenge_is_3quarter" ) )
		flag_init( "so_challenge_is_3quarter" );
	
	flag_wait( "so_challenge_is_3quarter" );
	
	maps\_specialops::so_dialog_progress_update( 3, 4 );
}

challenge_timer_detect_halfway()
{
	if ( !flag_exist( "so_challenge_is_halfway" ) )
		flag_init( "so_challenge_is_halfway" );
	
	flag_wait( "so_challenge_is_halfway" );
	
	maps\_specialops::so_dialog_progress_update( 2, 4 );
}

challenge_timer_detect_quarter()
{
	if ( !flag_exist( "so_challenge_is_quarter" ) )
		flag_init( "so_challenge_is_quarter" );
		
	flag_wait( "so_challenge_is_quarter" );
	
	maps\_specialops::so_dialog_progress_update( 1, 4 );
}

challenge_timer_wait_start( hud_msg, hud_time, start_flag )
{
	level endon( "special_op_terminated" );

	if ( isdefined( start_flag ) )
		flag_wait( start_flag );

	// Force the hud on if player charges ahead.
	foreach ( player in level._players )
	{
		if ( !player so_hud_can_show() )
			player.so_hud_show_time = gettime();
	}
			
	if ( !isdefined( level._challenge_start_time ) )
	{
		if ( challenge_timer_should_play_alarm() )
			level._player PlaySound( "arcademode_zerodeaths" );
		level._challenge_start_time = gettime();
	}

	hud_time.label = "";
	if ( isdefined( level._challenge_time_limit ) )
		hud_time settenthstimer( level._challenge_time_limit );
	else
		hud_time settenthstimerup( 0.00 );
}

challenge_timer_should_play_alarm()
{
	if ( isdefined( level._challenge_time_limit ) )
		return true;
		
	foreach ( player in level._players )
	{
		if ( player.so_infohud_toggle_state != "off" )
			return true;
	}
	
	return false;
}

challenge_timer_show_nudge( hud_msg, hud_time )
{
	if ( !validate_timer( level._challenge_time_nudge ) )
		return;

	level endon( "challenge_timer_passed" );
	level endon( "new_challenge_timer" );
	level endon( "special_op_terminated" );

//	if ( level.challenge_time_limit > level.challenge_time_nudge )
//		wait level.challenge_time_limit - level.challenge_time_nudge;

	while ( level._so_challenge_time_left > level._challenge_time_nudge )
	{
		wait( 0.1 );
	}

	hud_msg set_hudelem_yellow();
	hud_time set_hudelem_yellow();
	if ( !challenge_timer_be_silent() )
		thread maps\_specialops::so_dialog_time_low_normal();
}

challenge_timer_show_hurry( hud_msg, hud_time )
{
	if ( !validate_timer( level._challenge_time_hurry ) )
		return;

	level endon( "challenge_timer_passed" );
	level endon( "new_challenge_timer" );
	level endon( "special_op_terminated" );

//	if ( level.challenge_time_limit > level.challenge_time_hurry )
//		wait level.challenge_time_limit - level.challenge_time_hurry;
	while ( level._so_challenge_time_left > level._challenge_time_hurry )
	{
		wait( 0.1 );
	}

	hud_msg set_hudelem_red();
	hud_time set_hudelem_red();
	if ( !challenge_timer_be_silent() )
		thread maps\_specialops::so_dialog_time_low_hurry();
}

challenge_timer_be_silent()
{
	if ( self != level._player )
		return true;
	
	if ( !isdefined( level._challenge_time_silent ) )
		return false;
		
	return level._challenge_time_silent;
}

challenge_timer_thread()
{
	level endon( "special_op_terminated" );
	
	level notify( "stop_challenge_timer_thread" );
	level endon( "stop_challenge_timer_thread" );

	while ( level._so_challenge_time_left > 0 )
	{
		wait( 0.05 );
		level._so_challenge_time_left -= 0.05;
		thread challenge_timer_give_alert();
	}

	level notify( "challenge_timer_failed" );
}

challenge_timer_give_alert()
{
	if ( challenge_timer_should_pulse() )
	{
		foreach ( player in level._players )
		{
			player.hud_so_timer_msg thread maps\_specialops::so_hud_pulse_create();
			player.hud_so_timer_time thread maps\_specialops::so_hud_pulse_create();
		}

		if ( level._so_challenge_time_beep < 0 )
			level._player PlaySound( "arcademode_kill_streak_lost" );
		else
			level._player PlaySound( "so_countdown_beep" );
	}
}

challenge_timer_should_pulse()
{
	if ( level._so_challenge_time_left > level._so_challenge_time_beep )
		return false;
		
	for ( i = 0; i <= level._challenge_time_beep_start; i++ )
	{
		if ( level._so_challenge_time_left > i )
		{
			// If the timer has been increased, need to reset our beep tracking.
			if ( level._so_challenge_time_beep < i )
				level._so_challenge_time_beep = i + 1;
			continue;
		}
		
		// We already know time_left is less than time_beep, so go go go!
		level._so_challenge_time_beep = i - 1;
		return true;
	}
	
	return false;
}

challenge_timer_show_failed( hud_msg, hud_time )
{
	if ( !validate_timer( level._challenge_time_limit ) )
		return;

	level endon( "challenge_timer_passed" );
	level endon( "new_challenge_timer" );
	level endon( "special_op_terminated" );

//	wait( level.challenge_time_limit );
	thread challenge_timer_thread();
	level waittill( "challenge_timer_failed" );

	flag_set( "challenge_timer_expired" );
	level._challenge_end_time = gettime();

	challenge_timer_destroy( hud_msg, hud_time );
		
//	radio_dialogue_stop();
	if ( self == level._player )
		thread maps\_specialops::so_dialog_mission_failed_time();

	maps\_specialops::so_force_deadquote( "@SPECIAL_OPS_FAILURE_HINT_TIME", "ui_time_failure" );
	
	if ( self == level._player )
		MissionFailedWrapper();
}

challenge_timer_wait_passed( hud_msg, hud_time, passed_flag )
{
	level endon( "challenge_timer_expired" );
	level endon( "new_challenge_timer" );
	
	if ( flag_exist( "individual_timers" ) && flag( "individual_timers" ) )
		self ent_flag_wait( passed_flag );
	else
		flag_wait( passed_flag );

	flag_set( "challenge_timer_passed" );
	level._challenge_end_time = gettime();

	// This might not work in every circumstance?
	// Freeze the timer where it is for the couple of seconds it remains visible.
	time = ( level._challenge_end_time - level._challenge_start_time ) * 0.001;
	if ( isdefined( level._challenge_time_limit ) )
		time = level._challenge_time_limit - time;
	
	// stupid hack to make this work correctly, because this mission modifies level.challenge_time_limit on the fly
	if ( level._script == "so_snowrace2_cliffhanger" )
		time = level._challenge_time_limit;


	// We cannot have time be 0 when we call SetTenthsTimerStatic()
	if ( time <= 0 )
	{
		time = 0.1;
	}
	
	hud_time SetTenthsTimerStatic( time );
	
	if ( !flag_exist( "individual_timers" ) )
		return;
	if ( flag( "individual_timers" ) )
		return;
	
	challenge_timer_destroy( hud_msg, hud_time );
}

challenge_timer_destroy_prematurely( hud_msg, hud_time )
{
	level waittill( "new_challenge_timer" );
	challenge_timer_destroy( hud_msg, hud_time, true );
}

challenge_timer_destroy( hud_msg, hud_time, destroy_immediately )
{
	if ( isdefined( hud_msg ) )
		hud_msg	thread maps\_specialops::so_remove_hud_item( destroy_immediately );
	if ( isdefined( hud_time ) )
		hud_time thread maps\_specialops::so_remove_hud_item( destroy_immediately );
}

validate_timer( timer )
{
	if ( !isdefined( timer ) )
		return false;
		
	if ( timer <= 0 )
		return false;
		
	return true;
}

failure_summary_display()
{
	if ( getdvarint( "so_nofail" ) )
		return;
		
	thread maps\_specialops::so_dialog_mission_failed_generic();
	
	MissionFailed();
	specialops_mission_over_setup( false );

	// mission failed;
	setDvar( "ui_mission_success", 0 );
	level notify( "so_generate_deathquote" );
	maps\_endmission::coop_eog_summary();

	specialops_summary_player_choice();
}

specialops_mission_over_setup( was_success )
{
	assertex( isdefined( was_success ), "specialops_mission_over_setup() requires a true or false value for the was_success parameter." );

	if ( flag( "special_op_terminated" ) )
	{
		assertmsg( "The special op has somehow tried to end itself more than once. Current Success = " + was_success + " and Previous Success = " + level._special_op_success );
	}
	
	if ( !isdefined( level._challenge_end_time ) )
		level._challenge_end_time = gettime();

	flag_set( "special_op_terminated" );
	level notify( "stop_music" );

	// Initiate a pre-fetch of the texture data to help lower load times a bit.
	stream_origin = undefined;
	if ( isdefined( level._pmc_game ) && level._pmc_game )
		stream_origin = pick_starting_location_pmc( true );
	else
		stream_origin = pick_starting_location_so( true );
	if ( isdefined( stream_origin ) )
	{
		foreach ( player in level._players )
			player PlayerSetStreamOrigin( stream_origin );
	}
	
	ambientStop( 2 );
	music_stop( 1 );
	
	if ( was_success )
		thread specialops_mission_over_setup_success();
	else
		thread specialops_mission_over_setup_failure();
		
	foreach ( player in level._players )
	{
		player AllowJump( false );
		player disableweapons();
		player disableusability();
		player EnableInvulnerability();
		player.ignoreme = true;
	}
	
	thread specialops_blur_player_screen();
	
	wait 3;

	thread specialops_mission_over_remove_ai();

	if ( !flag( "special_op_no_unlink" ) )
	{
		foreach ( player in level._players )
		{
			player Unlink();
	//		org = player spawn_tag_origin();
	//		player PlayerLinkToDelta( org );
		}
	}

	foreach ( player in level._players )
	{
		player freezecontrols( true );
	}
	
	// This needs to happen last as it's expected for all values used in stat collection to be set by now.
	specialops_mission_over_stats( was_success );
}

specialops_blur_player_screen()
{
	foreach ( player in level._players )
	{
		wait 0.1;
		player setBlurForPlayer( 6, 1 );
	}
}

specialops_mission_over_remove_ai()
{
	wait 0.25;
	
	ai_to_delete = getaiarray();
	ai_to_delete = array_merge( ai_to_delete , getaiSpeciesArray( "axis", "dog" ) );
	foreach ( ai in ai_to_delete )
	{
		if ( isdefined( ai.magic_bullet_shield ) )
			ai stop_magic_bullet_shield();
		ai Delete();
	}
}

specialops_mission_over_setup_success()
{
	flag_set( "special_op_succeeded" );
	
	foreach ( player in level._players )
	{
		screen_fade = create_client_overlay( "white", 0, player );
		screen_fade.color = ( 0.7, 0.7, 1 );
		screen_fade thread fade_over_time( 0.25, 2 );
	}
	
	if ( isdefined( level._suppress_challenge_success_print ) )
		return;
	
	passed_hud = maps\_specialops::so_create_hud_item( 3, 0, &"SPECIAL_OPS_CHALLENGE_SUCCESS" );
	passed_hud.alignX = "center";
	passed_hud.horzAlign = "center";
	passed_hud set_hudelem_blue();
	passed_hud SetPulseFX( 60, 2500, 500 );	

	wait 1.5;
	music_play( "so_victory_" + level._so_campaign, undefined, 0, true );
}

specialops_mission_over_setup_failure()
{
	flag_set( "special_op_failed" );

	passed_hud = maps\_specialops::so_create_hud_item( 3, 0, &"SPECIAL_OPS_CHALLENGE_FAILURE" );
	passed_hud.hidewhendead = false;
	passed_hud.alignX = "center";
	passed_hud.horzAlign = "center";
	passed_hud set_hudelem_red();
	passed_hud SetPulseFX( 60, 2500, 500 );

	foreach ( player in level._players )
	{
		screen_fade = create_client_overlay( "white", 0, player );
		screen_fade.color = ( 1, 0.4, 0.4 );
		screen_fade thread fade_over_time( 0.25, 2 );
	}

	wait 1.5;
	music_play( "so_defeat_" + level._so_campaign, undefined, 0, true );
}

is_current_level_locked()
{
	specOpsString = self GetLocalPlayerProfileData( "missionSOHighestDifficulty" );
	
	if( !isdefined( specOpsString ) )
		return true;
	
	specOpsSum = 0;
	for( i=0; i<specOpsString.size; i++ )
		specOpsSum += max ( 0, int( specOpsString[i] ) - 1 );

	current_group = tablelookup( "sp/specOpsTable.csv", 1, level._script, 13 );
	
	return ( specOpsSum < int( tablelookup( "sp/specOpsTable.csv", 1, current_group, 5 ) ) );	
}

is_so_player_signed_in()
{
	//return self isSignedIn();

	return true; //always true until code gives me a function	
}

can_save_to_profile()
{
	return self is_so_player_signed_in() && !( self is_current_level_locked() );
}

specialops_achievement_by_stars( specOpsString )
{
	specOpsSum = get_total_stars( specOpsString );
	
	// --------------- Gold Star: earn 1 star in special ops
	if ( specOpsSum >= 1 )
		self player_giveachievement_wrapper( "GOLD_STAR" );
		
	// --------------- Hotel Bravo: earn 4 star in special ops
	if ( specOpsSum >= 4 )
		self player_giveachievement_wrapper( "HOTEL_BRAVO" );	
		
	// --------------- Charlie on our six: earn 8 star in special ops
	if ( specOpsSum >= 8 )
		self player_giveachievement_wrapper( "CHARLIE_ON_OUR_SIX" );
		
	// --------------- Blackjack: earn 21 star in special ops
	if ( specOpsSum >= 21 )
		self player_giveachievement_wrapper( "BLACKJACK" );
		
	// --------------- Specialist: earn 30 star in special ops
	if ( specOpsSum >= 30 )
		self player_giveachievement_wrapper( "SPECIALIST" );

	// --------------- Star 69: earn 69 star in special ops
	if ( specOpsSum >= 69 )
		self player_giveachievement_wrapper( "STAR_69" );

	// =============== It Goes to Eleven: 1 star in 11 different SO
	if ( get_num_of_levels_with_star( specOpsString, 1 ) >= 11 )
		self player_giveachievement_wrapper( "IT_GOES_TO_ELEVEN" );

	// =============== Operational Asset: 3 stars in 5 different SO
	if ( get_num_of_levels_with_star( specOpsString, 3 ) >= 5 )
		self player_giveachievement_wrapper( "OPERATIONAL_ASSET" );
		
	// =============== Honor Roll: 1 star in 23 different SO
	if ( get_num_of_levels_with_star( specOpsString, 1 ) >= 23 )
		self player_giveachievement_wrapper( "HONOR_ROLL" );	
		
	// =============== Operative: 3 stars in 10 different SO
	if ( get_num_of_levels_with_star( specOpsString, 3 ) >= 10 )
		self player_giveachievement_wrapper( "OPERATIVE" );	
		
	// =============== Professional: 3 stars in 15 different SO
	if ( get_num_of_levels_with_star( specOpsString, 3 ) >= 15 )
		self player_giveachievement_wrapper( "PROFESSIONAL" );
}

get_total_stars( specOpsString )
{
	string_size = specOpsString.size;
	if ( string_size > level._specOpsSettings.levels.size )
		string_size = level._specOpsSettings.levels.size;
			
	specOpsSum = 0;
	for( i=0; i<string_size; i++ )
	{
		specOpsSum += max ( 0, int( specOpsString[i] ) - 1 );
	}	
	return specOpsSum;
}

get_num_of_levels_with_star( specOpsString, stars )
{
	string_size = specOpsString.size;
	if ( string_size > level._specOpsSettings.levels.size )
		string_size = level._specOpsSettings.levels.size;
		
	levels = 0;
	for( i=0; i<string_size; i++ )
	{
		if( max ( 0, int( specOpsString[i] ) - 1 ) >= stars )
			levels++;
	}		
	return levels;
}

specialops_mission_over_stats( was_success )
{
	if ( !isdefined( was_success ) || !was_success )
		return;
		
	// using map name to get dvar used for saving that map's best time
	best_time_name = tablelookup( "sp/specOpsTable.csv", 1, level._script, 9 );

	// Test maps shouldn't have to SRE out.
	if ( best_time_name == "" )
		return;
	
	// Early out the player that doesnt have the level unlocked, cause we dont give him reward
	foreach( player in level._players )
	{
		if ( !( player can_save_to_profile() ) )
		{
			/#
			PrintLn( ">> UpdateGamerProfileAll():	[ PLAYER 2 SKIPPED ALL PROFILE UPDATES DUE TO LEVEL LOCKED ]" );
			#/
			player.eog_noreward = true;
		}
	}
	
	// PS3 special handler - PS3 splitscreen, guest player always get stars when first player get stars
	if ( issplitscreen() && level._ps3 )
		level._player2.eog_noreward = false;
	
	assert( isdefined( best_time_name ) );
	assert( isdefined( level._challenge_start_time ) );
	assert( isdefined( level._challenge_end_time ) );

	// time is capped at 24 hours
	m_seconds = int( min( ( level._challenge_end_time - level._challenge_start_time ), 86400000 ) );
	
	foreach( player in level._players )
	{	
		if ( isdefined( player.eog_noreward ) && player.eog_noreward )
			continue;
			
		current_best_time = player GetLocalPlayerProfileData( best_time_name );

		if ( !isdefined( current_best_time ) )
			continue;	// non local player
			
		never_played = current_best_time == 0;
		if ( isdefined( player.finish_time ) && isdefined( level._challenge_start_time ) )
			m_seconds = int( min( ( player.finish_time - level._challenge_start_time ), 86400000 ) );
		
		if ( never_played || m_seconds < current_best_time )
			player SetLocalPlayerProfileData( best_time_name, m_seconds );
		
		if ( !never_played && m_seconds < current_best_time )
		{
			player.eog_besttime = true;
			player.eog_besttime_value = m_seconds;
		}
	}
	
	levelIndex = level._specOpsSettings maps\_endmission::getLevelIndex( level._script );

	if ( !isDefined( levelIndex ) )
	{
		// run the same mission again if the nextmission is not defiend.
		MissionSuccess( level._script );
		return;
	}
	
	// update mission difficult dvar
	level._specOpsSettings maps\_endmission::setSoLevelCompleted( levelIndex );

	foreach( player in level._players )
	{
		if ( isdefined( player.eog_noreward ) && player.eog_noreward )
			continue;
			
		missionSpecOps = player GetLocalPlayerProfileData( "missionSpecOps" );	

		if ( !isdefined( missionSpecOps ) )
			continue;			
			
		if ( missionSpecOps < levelindex + 1 )
			player SetLocalPlayerProfileData( "missionSpecOps", levelIndex + 1 );	
	}
	
	foreach( player in level._players )
	{
		if ( isdefined( player.eog_noreward ) && player.eog_noreward )
			continue;
			
		specOpsString = player GetLocalPlayerProfileData( "missionSOHighestDifficulty" );
		
		if( !isdefined( specOpsString ) )
			continue;
			
		assertex( isdefined( level._specOpsSettings ), "Special Op levels are not yet initialized in _endmission.gsc and we are already trying to save level difficulty completed data." );
		
		// specOpsString is a string of something like '0123412324200323404...' ranging from 0 to 4, for special ops we need range of 0 to 3.
		// string size can not count higher then number of levels or we will have 100%+
		string_size = specOpsString.size;
		if ( string_size > level._specOpsSettings.levels.size )
			string_size = level._specOpsSettings.levels.size;
			
		specOpsSum = 0;
		for( i=0; i<string_size; i++ )
			specOpsSum += int( max ( 0, int( specOpsString[i] ) - 1 ) );
		
		assertex( specOpsSum >= 0 && specOpsSum <= ( level._specOpsSettings.levels.size * 3 ), "More stars earned than possible!" );
		
		completion_percentage = 0;
		completion_fraction = 100 * ( ( specOpsSum ) / ( level._specOpsSettings.levels.size * 3 ) );
	
		assert( isdefined( completion_fraction ) );
		
		// round up or down
		if ( int(completion_fraction*100)%100 >= 0.5 )
			completion_percentage = int( completion_fraction ) + 1;
		else
			completion_percentage = int( completion_fraction );

		assertex( ( completion_percentage >= 0 && completion_percentage <= 100 ), "Special Ops' completion percentage [ " + completion_percentage + "% ] is outside of 0 to 100 range!" );
		
		player specialops_achievement_by_stars( specOpsString );
		
		/#
		setlevel = getdvarint( "ui_debug_setlevel" );
		if( setlevel != 0 )	
		{
			player SetLocalPlayerProfileData( "missionspecops", setlevel );
			level._specOpsSettings maps\_endmission::setSoLevelCompleted( max( 0, setlevel-1 ) );
			
			setdvar( "ui_debug_setlevel", "" );
		}
		
		upto = getdvarint( "ui_debug_setlevel_upto" );
		if( upto != 0 )	
		{
			player SetLocalPlayerProfileData( "missionspecops", upto );
			
			difficulty_string = "";
			for( i=0; i<upto; i++ )
			{
				difficulty_string += "4";
				level._specOpsSettings maps\_endmission::setSoLevelCompleted( i );
			}
			player SetLocalPlayerProfileData( "missionsohighestdifficulty", difficulty_string );
			
			setdvar( "ui_debug_setlevel_upto", "" );
		}
		
		// Debug prints
		PrintLn( ">> SO DEBUG: 					[ setlevel:" + setlevel + " setlevel_upto:" + upto + " clearall:" + getdvarint( "ui_debug_clearall" ) + " ]" );
		PrintLn( ">> SO PLAYER ID: 			[" + player GetEntityNumber() + "]" );
		PrintLn( ">> SO PERCENTAGE COMPLETED: 	[" + specOpsString + "]" );
		PrintLn( ">> SO PLAYER PERCENTAGE: 		[" + completion_percentage + "%]" );
		PrintLn( ">> SO PLAYER TOTAL STARS:		[" + specOpsSum + "%]" );
		PrintLn( ">> SO LEVEL INDEX: 			[" + level._specOpsSettings maps\_endmission::getLevelIndex( level._script ) + "]" );
		PrintLn( ">> SO LEVEL NAME: 			[" + level._script + "]" );
		PrintLn( ">> SO STARS EARNED:	 		[" + specOpsSum + "]" );
		PrintLn( ">> SO MAX LEVELS: 			[" + level._specOpsSettings.levels.size + "]" );
		#/
		
		player SetLocalPlayerProfileData( "percentCompleteSO", specOpsSum );
		//player SetLocalPlayerProfileData( "percentCompleteSO", completion_percentage );
	}
		
	UpdateGamerProfileAll();
	/#
	PrintLn( ">> UpdateGamerProfileAll(): [ CALLED ]" );
	#/
}

specialops_summary_player_choice()
{
	for ( ;; )
	{
		response = waittill_either_player();
		
		// if player chooses to return to main menu
		if ( response == "summary_closed" )
		{
			ChangeLevel( "" );
			return;
		}
		
		// if player chooses to play again
		if ( response == "summary_closed_play_again" )
		{
			ChangeLevel( level._script );
			return;
		}
	}
}

wait_all_players_are_touching( trigger_ent )
{
	for ( ;; )
	{
		trigger_ent waittill( "trigger" );

		// If not in co-op, then a simple touch is all we need.
		if ( !is_coop() )
			return;

		level._player thread waiting_message_hide();
		level._player2 thread waiting_message_hide();

		if ( !level._player IsTouching( trigger_ent ) )
		{
			level._player2 thread display_waiting_message();
			continue;
		}
		
		if ( !level._player2 IsTouching( trigger_ent ) )
		{
			level._player thread display_waiting_message();
			continue;
		}
		
		// If we get here then we've succeeded...
		break;
	}
}

wait_all_players_have_touched( trigger_ent, touch_style )
{
	player1_touched = false;
	player2_touched = false;

	for ( ;; )
	{
		trigger_ent waittill( "trigger" );

		// If not in co-op, then a simple touch is all we need.
		if ( !is_coop() )
			return;

		level._player thread waiting_message_hide();
		level._player2 thread waiting_message_hide();

		if ( level._player IsTouching( trigger_ent ) )
		{
			player1_touched = true;
			if ( !player2_touched && !level._player2 IsTouching( trigger_ent ) )
			{
				thread determine_waiting_message( level._player, touch_style );
				continue;
			}
		}

		if ( level._player2 IsTouching( trigger_ent ) )
		{
			player2_touched = true;
			if ( !player1_touched && !level._player IsTouching( trigger_ent ) )
			{
				thread determine_waiting_message( level._player2, touch_style );
				continue;
			}
		}

		// If we get here then we've succeeded...
		break;
	}
}

disable_mission_end_trigger( trigger_ent )
{
	level waittill( "special_op_terminated" );
	
	trigger_ent trigger_off();
}

determine_waiting_message( player, touch_style )
{
	switch ( touch_style )
	{
		case "all"		: player display_waiting_message(); break;
		case "any"		: player display_waiting_message(); break;
		case "freeze"	: player display_frozen_message(); break;
	}
}

display_waiting_message()
{
	self endon( "death" );
	level endon( "challenge_timer_passed" );
	level endon( "challenge_timer_expired" );
	level endon( "special_op_terminated" );

	level notify( "players_touching_hint" );
	level endon( "players_touching_hint" );

	if ( !isdefined( self ) )
		return;
		
	if ( !isdefined( self.waiting_hud ) )
	{
		self.waiting_hud = create_waiting_message( self );
	}
	else
	{
		self.waiting_hud.alpha = 1;
	}
	
	wait 0.05;
	self.waiting_hud fadeovertime( 0.25 );
	self.waiting_hud.alpha = 0;
}

display_frozen_message()
{
	if ( !isdefined( self ) )
		return;
		
	// Don't need to fade in again if already active.
	if ( isdefined( self.frozen_and_waiting ) && self.frozen_and_waiting )
		return;
		
	self.frozen_and_waiting = true;
	self EnableInvulnerability();
	self FreezeControls( true );
	self.ignoreme = true;
	
	self setBlurForPlayer( 6, 1 );

	self.waiting_hud = create_waiting_message( self );
}	

create_waiting_message( player )
{
	self notify( "rebuilding_waiting_hud" );
	self endon( "rebuilding_waiting_hud" );

	waiting_hud = maps\_specialops::so_create_hud_item( 3, 0, &"SPECIAL_OPS_WAITING_OTHER_PLAYER", player );
	waiting_hud.alignX = "center";
	waiting_hud.horzAlign = "center";
	waiting_hud set_hudelem_blue();

	thread waiting_message_delete_on_so_end( waiting_hud );

	return waiting_hud;
}

waiting_message_hide()
{
	if ( !isdefined( self ) )
		return;
		
	if ( !isdefined( self.waiting_hud ) )
		return;

	if ( isdefined( self.frozen_and_waiting ) && self.frozen_and_waiting )
		return;
		
	self.waiting_hud fadeovertime( 0.25 );
	self.waiting_hud.alpha = 0;
}

waiting_message_delete_on_so_end( waiting_hud )
{
	self endon( "rebuilding_waiting_hud" );
	
	level waittill( "special_op_terminated" );

	if ( isdefined( waiting_hud ) )
		waiting_hud Destroy();
}
	
disable_saving()
{
	flag_set( "disable_autosaves" );

	for ( ;; )
	{
		flag_wait( "can_save" );
		flag_clear( "can_save" );
	}
}

disable_escape_warning()
{
	if ( flag( "player_has_escaped" ) )
	{
		return true;
	}
		
	if ( self is_touching_escape_trigger() )
	{
		return false;
	}

//	self.escape_hint_active = undefined;
			
//	self.escape_hint_active = undefined;
//	if ( isdefined( self.ping_escape_splash ) )
//		self.ping_escape_splash Destroy();
	return true;
}

is_touching_escape_trigger()
{
	if ( !IsDefined( level._escape_warning_triggers ) )
	{
		return false;
	}

	foreach ( trigger in level._escape_warning_triggers )
	{
		if ( self IsTouching( trigger ) )
		{
			return true;
		}
	}	

	return false;
}

ping_escape_warning()
{
	if ( isdefined( self.ping_escape_splash ) )
		return;

	self endon( "death" );
	
	self.ping_escape_splash = maps\_specialops::so_create_hud_item( 3.5, 0, &"SPECIAL_OPS_ESCAPE_WARNING", self );
	self.ping_escape_splash.alignx = "center";
	self.ping_escape_splash.horzAlign = "center";
//	self.ping_escape_splash set_hudelem_red();

	while ( ping_escape_warning_valid() )
	{
//		self PlayLocalSound( "coop_player_exit_warning" );
		self.ping_escape_splash.alpha = 1;
		self.ping_escape_splash FadeOverTime( 1 ) ;
		self.ping_escape_splash.alpha = 0.5;
		
		self.ping_escape_splash.fontscale = 1.5;
		self.ping_escape_splash ChangeFontScaleOverTime( 1 );
		self.ping_escape_splash.fontscale = 1;
		
		wait 1;
	}

	self.ping_escape_splash.alpha = 0.5;
	self.ping_escape_splash FadeOverTime( 0.25 );
	self.ping_escape_splash.alpha = 0;
	wait 0.25;

	self.escape_hint_active = undefined;
	
	if ( isdefined( self.ping_escape_splash ) )
		self.ping_escape_splash Destroy();
}

ping_escape_warning_valid()
{
	if ( flag( "special_op_terminated" ) )
		return false;

//	if ( !isdefined( self.escape_hint_active ) )
//	{
//		return false;
//	}

	if ( !self is_touching_escape_trigger() )
	{
		return false;
	}
		
	return true;
}

so_dialog_play( dialog, wait_time, force_stop )
{
	assertex( isdefined( dialog ), "so_dialog_play() requires valid dialog to be passed in." );
	if ( isdefined( wait_time ) )
		wait wait_time;

	// This isn't a good long term solution to SO spam. The Battle Chatter stuff (reviving) needs to be switched
	// to the actual battle chatter system.
	if ( isdefined( force_stop ) && force_stop )
		radio_dialogue_stop();
	radio_dialogue( dialog );
}

specialops_dialog_init()
{
	// Let's do this.
	// Ready up.
	level._scr_radio[ "so_tf_1_plyr_prep" ]			= "so_tf_1_plyr_prep";

	// That's the way it's done. 
	// Good job team.
	// Mission accomplished.
	level._scr_radio[ "so_tf_1_success_generic" ]	= "so_tf_1_success_generic";

	// Not bad, but I've seen better.
	// Well done, but you can do better.
	// Not the best, but you made it.
	level._scr_radio[ "so_tf_1_success_jerk" ]		= "so_tf_1_success_jerk";

	//Nicely done, you beat your previous best.
	//A new personal record, great work.
	//You beat your previous best, keep it up.
	//Excellent job, a new personal best.
	level._scr_radio[ "so_tf_1_success_best" ]		= "so_tf_1_success_best";

	// Mission failed. We’ll get 'em next time.
	// We need to rethink our strategy. Let's run it again.
	level._scr_radio[ "so_tf_1_fail_generic" ]		= "so_tf_1_fail_generic";

	// Well that was a disaster! Let's try it again.
	// Bloody 'ell, we just got our arses kicked.
	level._scr_radio[ "so_tf_1_fail_generic_jerk" ]	= "so_tf_1_fail_generic_jerk";

	// We ran outta time. Keep an eye the clock.
	// Too slow mate. Next time watch the clock, eh?
	level._scr_radio[ "so_tf_1_fail_time" ]			= "so_tf_1_fail_time";

	// We win as a team, we lose as a team. Got it?
	// Look - we work as a team. No one gets left behind!
	level._scr_radio[ "so_tf_1_fail_bleedout" ]		= "so_tf_1_fail_bleedout";

	// We're running out of time!
	// The clock's ticking...
	// Not much time left!
	level._scr_radio[ "so_tf_1_time_generic" ]		= "so_tf_1_time_generic";

	// Time's running out! Go! Go! Go!
	// We're almost outta time! Move! Move!
	// Time's almost up! This is gonna be close!!
	level._scr_radio[ "so_tf_1_time_hurry" ] 		= "so_tf_1_time_hurry";

	// Watch your fire, avoid civilians!
	// Civilian casualties are unacceptable!
	// Careful, you're killing innocent civilians!
	level._scr_radio[ "so_tf_1_civ_kill_warning" ] 	= "so_tf_1_civ_kill_warning";
	
	// 5 more.
	// 5 left.
	// 5 remaining
	level._scr_radio[ "so_tf_1_progress_5more" ]		= "so_tf_1_progress_5more";

	// 4 more.
	// 4 left.
	// 4 remaining
	level._scr_radio[ "so_tf_1_progress_4more" ]		= "so_tf_1_progress_4more";

	// 3 more.
	// 3 left.
	// 3 remaining
	level._scr_radio[ "so_tf_1_progress_3more" ]		= "so_tf_1_progress_3more";

	// 2 more.
	// 2 left.
	// 2 remaining
	level._scr_radio[ "so_tf_1_progress_2more" ]		= "so_tf_1_progress_2more";

	// 1 more.
	// 1 left.
	// 1 remaining
	level._scr_radio[ "so_tf_1_progress_1more" ]		= "so_tf_1_progress_1more";
	
	// Cutting it close, need to pick up the pace.
	// You're running behind, hurry up or you won't make it.
	// Pay attention to the time, you're moving too slow.
	level._scr_radio[ "so_tf_1_time_status_late" ]	= "so_tf_1_time_status_late";

	// Looking good, keep up this pace.
	// Your time is solid, keep it up.
	// Keep going, you're making good time.
	level._scr_radio[ "so_tf_1_time_status_good" ]	= "so_tf_1_time_status_good";

	// 25% done… keep going.
	// You've made it a quarter of the way, don't stop.
	// 25% down, 75% to go.
	level._scr_radio[ "so_tf_1_progress_3quarter" ]	= "so_tf_1_progress_3quarter";

	// Hallfway there.
	// You're halfway done.
	// Halfway through the mission.
	level._scr_radio[ "so_tf_1_progress_half" ]		= "so_tf_1_progress_half";

	// Getting close, keep it up.
	// You're doing well, almost done.
	// Just a little bit left, keep going.
	level._scr_radio[ "so_tf_1_progress_quarter" ]	= "so_tf_1_progress_quarter";
}

set_hudelem_white()
{
	self maps\_specialops::set_hud_white();
}

set_hudelem_blue()
{
	self maps\_specialops::set_hud_blue();
}

set_hudelem_green()
{
	self maps\_specialops::set_hud_green();
}

set_hudelem_yellow()
{
	self maps\_specialops::set_hud_yellow();
}

set_hudelem_red()
{
	self maps\_specialops::set_hud_red();
}

so_hud_pulse_single( scale_start, scale_end, time )
{
	self endon( "update_hud_pulse" );
	self endon( "destroying" );
	self endon( "death" );

	self.fontscale = scale_start;
	self changefontscaleovertime( time );
	self.fontscale = scale_end;
	
	wait time;
}

so_hud_pulse_loop()
{
	self endon( "update_hud_pulse" );
	self endon( "destroying" );
	self endon( "death" );

	if ( self.pulse_start_big )
		so_hud_pulse_single( self.pulse_scale_big, self.pulse_scale_loop_normal, self.pulse_time );
		
	while( isdefined( self.pulse_loop ) && self.pulse_loop )
	{
		so_hud_pulse_single( self.pulse_scale_loop_normal, self.pulse_scale_loop_big, self.pulse_time_loop );
		so_hud_pulse_single( self.pulse_scale_loop_big, self.pulse_scale_loop_normal, self.pulse_time_loop );
	}
}

so_hud_pulse_init()
{
	if ( !isdefined( self ) )
		return false;
	
	// Bang defaults
	if ( !isdefined( self.pulse_time ) )
		self.pulse_time = 0.5;

	if ( !isdefined( self.pulse_scale_normal ) )
		self.pulse_scale_normal = 1.0;

	if ( !isdefined( self.pulse_scale_big ) )
		self.pulse_scale_big = 1.6;

	// Looping defaults
	if ( !isdefined( self.pulse_loop ) )
		self.pulse_loop = false;

	if ( !isdefined( self.pulse_time_loop ) )
		self.pulse_time_loop = 1.0;
		
	if ( !isdefined( self.pulse_scale_loop_normal ) )
		self.pulse_scale_loop_normal = 1.0;
		
	if ( !isdefined( self.pulse_scale_loop_big ) )
		self.pulse_scale_loop_big = 1.15;

	if ( !isdefined( self.pulse_start_big ) )
		self.pulse_start_big = true;

	// Successful initialization!		
	return true;
}

specialops_detect_death()
{
	level endon( "special_op_terminated" );
	
	array_thread( level._players, ::specialops_detect_player_death );
		
	level waittill( "so_player_has_died" );
	
	level._challenge_end_time = gettime();
	MissionFailedWrapper();
}

specialops_detect_player_death()
{
	level endon( "special_op_terminated" );

	self waittill( "death" );

	level notify( "so_player_has_died" );
}

waittill_either_player()
{
	if ( !flag_exist( "summary_response" ) )
		flag_init( "summary_response" );
	
	if( level._players.size > 1 )
	{
		// We must clear this each time we get in here, incase a menuresponse previously was not desired
		// example, options menuresponse or "back" (online player in pause menu when everyone died)
		// This will allow us to return to the EOG fine.
		flag_clear( "summary_response" );

		thread waittill_player_respond( 0 );
		thread waittill_player_respond( 1 );
		
		flag_wait( "summary_response" );
		return level._summary_response;
	}
	else
	{
		level._players[0] waittill( "menuresponse", menu, response );
		return response;
	}
}

waittill_player_respond( player_num )
{
	level endon( "summary_response" );
	level._players[ player_num ] waittill( "menuresponse", menu, response );
	level._summary_response = response;
	flag_set( "summary_response" );
}

so_special_failure_hint_reset_dvars( icon_dvar )
{
	setdvar( "ui_dog_death", 0 );
	setdvar( "ui_vehicle_death", 0 );
	setdvar( "ui_destructible_death", 0 );
	setdvar( "ui_barrel_death", 0 );
	setdvar( "ui_grenade_death", 0 );
	setdvar( "ui_time_failure", 0 );
	setdvar( "ui_ff_death", 0 );
	setdvar( "ui_juggernaut_death", 0 );
	setdvar( "ui_bled_out", 0 );
	setdvar( "ui_icon_partner", 0 );
	setdvar( "ui_icon_obj", 0 );
	setdvar( "ui_icon_obj_offscreen", 0 );
	setdvar( "ui_icon_stars", 0 );
	setdvar( "ui_icon_claymore", 0 );
	setdvar( "ui_icon_stealth_stance", 0 );
	
	if ( isdefined( icon_dvar ) )
		setdvar( icon_dvar, 1 );
}

so_special_failure_hint()
{
	so_special_failure_hint_reset_dvars();
	
	foreach( player in level._players )
	{
		player thread maps\_load::player_throwgrenade_timer();
		player thread so_special_death_hint_tracker();
	}
	
	level waittill( "so_generate_deathquote" );
	maps\_quotes::setDeadQuote_so();
}

so_special_death_hint_tracker()
{
	level endon( "so_special_failure_hint_set" );
	
	// NOTE: weapon may be undefined, so be sure to check for it if you use it.
	self waittill( "death", attacker, cause, weapon );

	if ( isdefined( self.coop_death_reason ) )
	{
		attacker = self.coop_death_reason[ "attacker" ];
		cause = self.coop_death_reason[ "cause" ];
		weapon = self.coop_death_reason[ "weapon_name" ];
	}

	if ( so_claymore_death( cause, weapon ) )
		return;

	if ( so_friendly_fire_death( attacker ) )
		return;

	if ( so_radiation_death( attacker ) )
		return;
		
	if ( so_dog_death( attacker ) )
		return;
		
	if ( so_juggernaut_death( attacker ) )
		return;
	
	if ( so_grenade_suicide_death( cause ) )
		return;
		
	if ( so_destructible_death( attacker, cause ) )
		return;

	if ( so_exploding_barrel_death( cause ) )
		return;

	if ( so_grenade_regular_death( cause ) )
		return;
		
	if ( so_vehicle_death( attacker, cause ) )
		return;
}

so_claymore_death( cause, weapon )
{
	if ( !IsDefined( cause ) )
	{
		return false;
	}

	if ( !IsDefined( weapon ) || ( IsDefined( weapon ) && weapon != "claymore" ) )
	{
		return false;
	}

	// No message if killed by a claymore

	return true;
}

so_friendly_fire_death( attacker )
{
	if ( !is_coop() )
		return false;
		
	if ( !isdefined( attacker ) )
		return false;
		
	other_player = get_other_player( self );
	if ( other_player != attacker )
		return false;
		
	so_special_failure_hint_set( "@DEADQUOTE_SO_FRIENDLY_FIRE_KILL", "ui_ff_death" );
	return true;
}

so_radiation_death( attacker )
{
	if ( !isdefined( attacker ) || ( attacker.classname != "worldspawn" ) )
		return false;
	
	if ( !isdefined( self.radiationDeath ) || !self.radiationDeath )
		return false;

	so_special_failure_hint_set( "@SCRIPT_RADIATION_DEATH" );
	return true;
}

so_dog_death( attacker )
{
	if ( !isdefined( attacker ) )
		return false;
		
	if ( !isdefined( attacker.weapon ) )
		return false;

	if ( attacker.weapon != "dog_bite" )
		return false;
	
	switch( level._dog_death_type )
	{
		case "soon":	so_special_failure_hint_set( "@NEW_DOG_DEATH_TOO_SOON_ALT", "ui_dog_death" ); break;
		case "late":	so_special_failure_hint_set( "@NEW_DOG_DEATH_TOO_LATE_ALT", "ui_dog_death" ); break;
		default:		so_special_failure_hint_set( "@NEW_DOG_DEATH_DO_NOTHING_ALT", "ui_dog_death" ); break;
	}
	
	return true;
}

so_juggernaut_death( attacker )
{
	if ( !isdefined( attacker ) )
		return false;
	
	if ( !isdefined( attacker.juggernaut ) )
		return false;

	deadquotes = [];

	deadquotes[ 0 ] = "@DEADQUOTE_JUGGERNAUT_FLASHBANGS";
	deadquotes[ 1 ] = "@DEADQUOTE_JUGGERNAUT_HIGHCALIBER_WEAPONS";
	deadquotes[ 2 ] = "@DEADQUOTE_JUGGERNAUT_HEADSHOTS";
	deadquotes[ 3 ] = "@DEADQUOTE_JUGGERNAUT_EXPLOSIVES";
	deadquotes[ 4 ] = "@DEADQUOTE_JUGGERNAUT_CORNERED";

	so_special_failure_hint_set_array( deadquotes, "ui_juggernaut_death" );

	return true;
}

so_destructible_death( attacker, cause )
{
	if ( !isdefined( cause ) )
		return false;
		
	if ( !isdefined( attacker ) )
		return false;	
				
	if ( cause != "MOD_EXPLOSIVE" )
		return false;
		
	if ( !isdefined( attacker.destructible_type ) )
		return false;
	
	if ( issubstr( attacker.destructible_type, "vehicle" )  )
	{
		// You were killed by an exploding vehicle. Vehicles on fire are likely to explode.
		so_special_failure_hint_set( "@SCRIPT_EXPLODING_VEHICLE_DEATH", "ui_vehicle_death" );
	}
	else
	{
		// You were killed by an explosion.\nSome burning objects can explode.
		so_special_failure_hint_set( "@SCRIPT_EXPLODING_DESTRUCTIBLE_DEATH", "ui_destructible_death" );
	}

	return true;
}

so_exploding_barrel_death( cause )
{
	if ( !isdefined( cause ) )
		return false;
			
	if ( cause != "MOD_EXPLOSIVE" )
		return false;
		
	// check if the death was caused by a barrel
	// have to check time and location against the last explosion because the attacker isn't the
	// barrel because the ent that damaged the barrel is passed through as the attacker instead
	if ( !isdefined( level._lastExplodingBarrel ) )
		return false;

	// killed the same frame a barrel exploded
	if ( getTime() != level._lastExplodingBarrel[ "time" ] )
		return false;

	// within the blast radius of the barrel that exploded
	d = distance( self.origin, level._lastExplodingBarrel[ "origin" ] );
	if ( d > level._lastExplodingBarrel[ "radius" ] )
		return false;

	// You were killed by an exploding barrel. Red barrels will explode when shot.
	so_special_failure_hint_set( "@SCRIPT_EXPLODING_BARREL_DEATH", "ui_barrel_death" );

	return true;
}

so_grenade_suicide_death( cause )
{
	if ( !isdefined( cause ) )
		return false;
			
	if ( cause != "MOD_SUICIDE" )
		return false;
		
	// magic number copied from fraggrenade asset.
	if ( ( self.lastgrenadetime - gettime() ) > 3.5 * 1000 )
		return false;

	// You died holding a grenade for too long.
	// Holding ^3[{+frag}]^7 allows you to cook off live grenades.
	so_special_failure_hint_set( "@SCRIPT_GRENADE_SUICIDE_COMBINED" );
	
	return true;
}

so_grenade_regular_death( cause )
{
	if ( !isdefined( cause ) )
	{
		return false;
	}
			
	if ( cause != "MOD_GRENADE" && cause != "MOD_GRENADE_SPLASH" )
	{
		return false;
	}

	so_special_failure_hint_set( "@SCRIPT_GRENADE_DEATH", "ui_grenade_death" );

	return true;
}

so_vehicle_death( attacker, cause )
{
	if ( !isdefined( cause ) )
		return false;
		
	if ( !isdefined( attacker ) )
		return false;	
				
	if ( cause != "MOD_CRUSH" )
		return false;
		
	if ( attacker.code_classname != "script_vehicle" )
		return false;
		
	// You were run over. Keep on eye out for enemy vehicles on the move.
	so_special_failure_hint_set( "@DEADQUOTE_SO_RUN_OVER_BY_VEHICLE" );
	return true;
}

so_special_failure_hint_set( quote, icon_dvar )
{
	maps\_specialops::so_force_deadquote( quote, icon_dvar );
	level notify( "so_special_failure_hint_set" );
}

so_special_failure_hint_set_array( quotes, icon_dvar )
{
	maps\_specialops::so_force_deadquote_array( quotes, icon_dvar );
	level notify( "so_special_failure_hint_set" );
}
