/*
   Copyright (C) 2006 Pekka Lampila ("Medar"), Damien Deville ("Pb")
   and German Garcia Fernandez ("Jal") for Chasseur de bots association.


   This program is free software; you can redistribute it and/or
   modify it under the terms of the GNU General Public License
   as published by the Free Software Foundation; either version 2
   of the License, or (at your option) any later version.

   This program is distributed in the hope that it will be useful,
   but WITHOUT ANY WARRANTY; without even the implied warranty of
   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

   See the GNU General Public License for more details.

   You should have received a copy of the GNU General Public License
   along with this program; if not, write to the Free Software
   Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
 */

#include "g_local.h"
#include "g_callvotes.h"
#include "g_gametypes.h"

extern cvar_t *g_votable_gametypes;
extern cvar_t *g_disable_vote_gametype;

//tmp: moveme
static void G_UpdateClientLaserGunTrail( edict_t *ent )
{
	firedef_t *firedef;
	vec3_t offset, right, forward, start, end;

	assert( ent && ent->r.client );

	firedef = gs_weaponInfos[WEAP_LASERGUN].firedef_weak;

	AngleVectors( ent->r.client->ps.viewangles, forward, right, NULL );
	VectorSet( offset, 0, 0, ent->viewheight );
	G_ProjectSource( ent->s.origin, offset, forward, right, start );
	VectorMA( start, firedef->timeout, forward, end );

	VectorCopy( end, ent->r.client->lasergunTrail[( level.framenum+1 ) & LASERGUN_WEAK_TRAIL_MASK] );
	ent->r.client->lasergunTrailTimes[( level.framenum+1 ) & LASERGUN_WEAK_TRAIL_MASK] = level.time;
}

//===================================================================

//================
//G_Timeout_Reset
//================
void G_Timeout_Reset( void )
{
	int i;

	gtimeout.active = qfalse;
	gtimeout.time = 0;
	gtimeout.endtime = 0;
	gtimeout.caller = 0;
	for( i = 0; i < MAX_CLIENTS; i++ )
		gtimeout.used[i] = 0;
}

//================
//G_Timeout_Update
//
//Updates the timeout struct and informs clients about the status of the pause
//================
static void G_Timeout_Update( unsigned int msec )
{
	static int timeout_printtime = 0;
	static int timeout_last_endtime = 0;
	static int countdown_set = 1;

	if( !gtimeout.active )
		return;

	if( timeout_last_endtime != gtimeout.endtime ) // force print when endtime is changed
	{
		timeout_printtime = 0;
		timeout_last_endtime = gtimeout.endtime;
	}

	gtimeout.time += msec;
	if( gtimeout.endtime && gtimeout.time >= gtimeout.endtime )
	{
		gtimeout.time = 0;
		gtimeout.caller = -1;
		gtimeout.active = qfalse;

		timeout_printtime = 0;
		timeout_last_endtime = -1;

		G_AnnouncerSound( NULL, trap_SoundIndex( va( S_ANNOUNCER_TIMEOUT_MATCH_RESUMED_1_to_2, ( rand()&1 )+1 ) ),
		                  GS_MAX_TEAMS, qtrue );
		G_CenterPrintMsg( NULL, "Match resumed" );
		G_PrintMsg( NULL, "Match resumed\n" );
	}
	else if( timeout_printtime == 0 || gtimeout.time - timeout_printtime >= 1000 )
	{
		if( gtimeout.endtime )
		{
			int seconds_left = (int)( ( gtimeout.endtime - gtimeout.time ) / 1000.0 + 0.5 );

			if( seconds_left == ( TIMEIN_TIME * 2 ) / 1000 )
			{
				G_AnnouncerSound( NULL, trap_SoundIndex( va( S_ANNOUNCER_COUNTDOWN_READY_1_to_2, ( rand()&1 )+1 ) ),
				                  GS_MAX_TEAMS, qfalse );
				countdown_set = ( rand()&1 )+1;
			}
			else if( seconds_left >= 1 && seconds_left <= 3 )
			{
				G_AnnouncerSound( NULL, trap_SoundIndex( va( S_ANNOUNCER_COUNTDOWN_COUNT_1_to_3_SET_1_to_2, seconds_left,
				                                             countdown_set ) ), GS_MAX_TEAMS, qfalse );
			}

			if( seconds_left > 1 )
				G_CenterPrintMsg( NULL, "Match will resume in %i seconds", seconds_left );
			else
				G_CenterPrintMsg( NULL, "Match will resume in 1 second" );
		}
		else
		{
			G_CenterPrintMsg( NULL, "Match paused" );
		}

		timeout_printtime = gtimeout.time;
	}
}

//================
// Disgusting function to stop linear projectiles during timeouts
//================
void G_TimoutFreezeProjectiles( void )
{
	edict_t *ent;

	for( ent = game.edicts + game.maxclients; ENTNUM( ent ) < game.numentities; ent++ )
	{
		if( ent->s.linearProjectile )
			ent->s.linearProjectileTimeStamp += game.frametime;
	}
}

//================
//G_UpdateServerInfo
// update the cvars which show the match state at server browsers
//================
static void G_UpdateServerInfo( void )
{
	// g_match_time
	if( match.state <= MATCH_STATE_WARMUP )
	{
		trap_Cvar_ForceSet( "g_match_time", "Warmup" );
	}
	else if( match.state == MATCH_STATE_COUNTDOWN )
	{
		trap_Cvar_ForceSet( "g_match_time", "Countdown" );
	}
	else if( match.state == MATCH_STATE_PLAYTIME )
	{
		// partly from G_GetMatchState
		char extra[MAX_INFO_STRING];
		int clocktime, timelimit, mins, secs;

		if( match.endtime )
			timelimit = ( ( match.endtime - match.starttime )*0.001 ) / 60;
		else
			timelimit = 0;

		clocktime = (float)( level.time - match.starttime ) * 0.001f;

		if( clocktime <= 0 )
		{
			mins = 0;
			secs = 0;
		}
		else
		{
			mins = clocktime / 60;
			secs = clocktime - mins * 60;
		}

		extra[0] = 0;
		if( match.extendedTime > 0 )
		{
			if( timelimit )
				Q_strncatz( extra, " overtime", sizeof( extra ) );
			else
				Q_strncatz( extra, " suddendeath", sizeof( extra ) );
		}
		if( gtimeout.active )
			Q_strncatz( extra, " (in timeout)", sizeof( extra ) );

		if( timelimit )
			trap_Cvar_ForceSet( "g_match_time", va( "%02i:%02i / %02i:00%s", mins, secs, timelimit, extra ) );
		else
			trap_Cvar_ForceSet( "g_match_time", va( "%02i:%02i%s", mins, secs, extra ) );
	}
	else
	{
		trap_Cvar_ForceSet( "g_match_time", "Finished" );
	}

	// g_match_score
	if( match.state >= MATCH_STATE_PLAYTIME && GS_Gametype_IsTeamBased( game.gametype ) )
	{
		char score[MAX_INFO_STRING];

		score[0] = 0;
		Q_strncatz( score, va( " %s: %i", GS_TeamName( TEAM_ALPHA ), teamlist[TEAM_ALPHA].teamscore ), sizeof( score ) );
		if( TEAM_BETA < TEAM_ALPHA + g_maxteams->integer )
			Q_strncatz( score, va( " %s: %i", GS_TeamName( TEAM_BETA ), teamlist[TEAM_BETA].teamscore ), sizeof( score ) );
		if( TEAM_GAMMA < TEAM_ALPHA + g_maxteams->integer )
			Q_strncatz( score, va( " %s: %i", GS_TeamName( TEAM_GAMMA ), teamlist[TEAM_GAMMA].teamscore ), sizeof( score ) );
		if( TEAM_DELTA < TEAM_ALPHA + g_maxteams->integer )
			Q_strncatz( score, va( " %s: %i", GS_TeamName( TEAM_DELTA ), teamlist[TEAM_DELTA].teamscore ), sizeof( score ) );

		trap_Cvar_ForceSet( "g_match_score", score );
	}
	else
	{
		trap_Cvar_ForceSet( "g_match_score", "" );
	}

	// g_needpass
	if( password->modified )
	{
		if( password->string && strlen( password->string ) )
		{
			trap_Cvar_ForceSet( "g_needpass", "1" );
		}
		else
		{
			trap_Cvar_ForceSet( "g_needpass", "0" );
		}
		password->modified = qfalse;
	}

	// g_gametypes_available
	if( g_votable_gametypes->modified || g_disable_vote_gametype->modified )
	{
		if( g_disable_vote_gametype->integer || !g_votable_gametypes->string || !strlen( g_votable_gametypes->string ) )
		{
			trap_Cvar_ForceSet( "g_gametypes_available", "" );
		}
		else
		{
			int type;
			char votable[MAX_INFO_VALUE];

			votable[0] = 0;
			for( type = GAMETYPE_DM; type < GAMETYPE_TOTAL; type++ )
			{
				if( G_Gametype_IsVotable( type ) )
				{
					Q_strncatz( votable, GS_Gametype_ShortName( type ), sizeof( votable ) );
					Q_strncatz( votable, " ", sizeof( votable ) );
				}
			}
			votable[strlen( votable )-2] = 0; // remove the last space

			trap_Cvar_ForceSet( "g_gametypes_available", votable );
		}

		g_votable_gametypes->modified = qfalse;
		g_disable_vote_gametype->modified = qfalse;
	}
}

//================
//G_CheckCvars
// Check for cvars that have been modified and need the game to be updated
//================
static void G_CheckCvars( void )
{
	if( g_antilag_maxtimedelta->modified )
	{
		if( g_antilag_maxtimedelta->integer < 0 )
			trap_Cvar_SetValue( "g_antilag_maxtimedelta", abs( g_antilag_maxtimedelta->integer ) );
		g_antilag_maxtimedelta->modified = qfalse;
		g_antilag_timenudge->modified = qtrue;
	}

	if( g_antilag_timenudge->modified )
	{
		if( g_antilag_timenudge->integer > g_antilag_maxtimedelta->integer )
			trap_Cvar_SetValue( "g_antilag_timenudge", g_antilag_maxtimedelta->integer );
		else if( g_antilag_timenudge->integer < -g_antilag_maxtimedelta->integer )
			trap_Cvar_SetValue( "g_antilag_timenudge", -g_antilag_maxtimedelta->integer );
		g_antilag_timenudge->modified = qfalse;
	}

	if( g_warmup_enabled->modified )
	{
		// if we are inside warmup period, finish it
		if( !g_warmup_enabled->integer && ( match.state == MATCH_STATE_WARMUP || match.state == MATCH_STATE_COUNTDOWN ) )
			G_Match_SetUpNextState();
		g_warmup_enabled->modified = qfalse;
	}

	if( g_warmup_timelimit->modified )
	{
		// if we are inside timelimit period, update the endtime
		if( match.state == MATCH_STATE_WARMUP )
		{
			if( g_warmup_timelimit->integer )
				match.endtime = match.starttime + fabs( 60 * 1000 * g_warmup_timelimit->integer );
			else
				match.endtime = 0;
		}
		g_warmup_timelimit->modified = qfalse;
	}

	if( g_timelimit->modified )
	{
		// if we are inside timelimit period, update the endtime
		if( match.state == MATCH_STATE_PLAYTIME && match.extendedTime == 0 && game.gametype != GAMETYPE_RACE )
		{
			if( g_timelimit->value )
				match.endtime = match.starttime + fabs( 60 * 1000 * g_timelimit->value );
			else
				match.endtime = 0;
		}
		g_timelimit->modified = qfalse;
	}

	if( g_match_extendedtime->modified )
	{
		// if we are inside extended_time period, update the endtime
		if( match.state == MATCH_STATE_PLAYTIME && match.extendedTime > 0 )
		{
			if( g_match_extendedtime->integer )
			{
				// wsw: Medar: this is little bit stupid
				unsigned int oldendtime = match.endtime;
				match.endtime = match.starttime + fabs( 60 * 1000 * g_timelimit->value );
				while( match.endtime < oldendtime || match.endtime < level.time )
					match.endtime += (int)( fabs( g_match_extendedtime->integer * 60 * 1000 ) );
			}
			else
			{
				match.endtime = 0;
				G_Match_SetUpNextState();
			}
		}
		g_match_extendedtime->modified = qfalse;
	}

	if( g_allow_falldamage->modified )
	{
		G_ServerSettings_ConfigString();
		g_allow_falldamage->modified = qfalse;
	}
}

//===================================================================
//		SNAP FRAMES
//===================================================================

static qboolean g_snapStarted = qfalse;

//=================
//G_SnapClients
//=================
void G_SnapClients( void )
{
	int i;
	edict_t	*ent;

	// calc the player views now that all pushing and damage has been added
	for( i = 0; i < game.maxclients; i++ )
	{
		ent = game.edicts + 1 + i;
		if( !ent->r.inuse || !ent->r.client )
			continue;

		G_ClientEndSnapFrame( ent );
	}

	G_EndServerFrames_UpdateChaseCam();
}

//=============
// G_EdictsAddSnapEffects
// add effects based on accumulated info along the server frame
//=============
static void G_EdictsAddSnapEffects( void )
{
#define JALFIXME_DAMAGESAVE
	edict_t *ent;
	int i;
	vec3_t dir, origin;

	for( i = 0, ent = &game.edicts[0]; i < game.numentities; i++, ent++ )
	{
		if( !ent->r.inuse || ( ent->r.svflags & SVF_NOCLIENT ) )
			continue;

		// types which can have accumulated damage effects
		if( ( ent->s.type == ET_GENERIC || ent->s.type == ET_PLAYER || ent->s.type == ET_CORPSE )
		   && ent->movetype != MOVETYPE_PUSH ) // doors don't bleed
		{
#ifdef JALFIXME_DAMAGESAVE
			// Until we get a proper damage saved effect, we accumulate both into the blood fx
			// so, at least, we don't send 2 entities where we can send one
			ent->snap.damage_taken += ent->snap.damage_saved;
			//ent->snap.damage_saved = 0;
#endif
			//spawn accumulated damage
			if( ent->snap.damage_taken && !( ent->flags & FL_GODMODE ) )
			{
				edict_t *event;
				float damage = ent->snap.damage_taken;
				if( damage > 120 )
					damage = 120;
				//VectorSubtract( ent->snap.damage_from, ent->s.origin, dir );
				VectorCopy( ent->snap.damage_dir, dir );
				VectorNormalize( dir );
				VectorAdd( ent->s.origin, ent->snap.damage_at, origin );
				event = G_SpawnEvent( EV_BLOOD2, DirToByte( dir ), origin );
				event->s.damage = HEALTH_TO_INT( damage );
				event->r.svflags = SVF_NOORIGIN2;
				event->s.ownerNum = i; // set owner

				// ET_PLAYERS can also spawn sound events
				if( ent->s.type == ET_PLAYER )
				{
					// play an apropriate pain sound
					if( level.time > ent->pain_debounce_time )
					{
						float taken;
						// see if it should pain for a FALL or for damage received
						taken = ent->snap.damage_taken + ent->snap.damage_saved - ent->snap.damage_fall;
						if( ent->snap.damage_fall && ( ent->pain_debounce_time <= level.time ) )
						{
							ent->pain_debounce_time = level.time + 100;
						}

						if( !G_IsDead( ent ) && ( ent->pain_debounce_time <= level.time ) )
						{
							ent->pain_debounce_time = level.time + 700;
							if( ent->r.client->shell_timeout > level.time )
								G_AddEvent( ent, EV_PAIN, PAIN_WARSHELL, qtrue );
							else if( ent->health < 25 )
								G_AddEvent( ent, EV_PAIN, PAIN_25, qtrue );
							else if( ent->health < 50 )
								G_AddEvent( ent, EV_PAIN, PAIN_50, qtrue );
							else if( ent->health < 75 )
								G_AddEvent( ent, EV_PAIN, PAIN_75, qtrue );
							else
								G_AddEvent( ent, EV_PAIN, PAIN_100, qtrue );
						}
					}
				}
			}
#ifndef JALFIXME_DAMAGESAVE
			// This should not be a blood effect, but a armor sparks one
			// or maybe we should just not send any damage saved effect
			//----------------------------------------------------------
			//spawn accumulated save
			if( ent->snap.damage_saved && !( ent->flags & FL_GODMODE ) )
			{
				edict_t *event;
				float save = ent->snap.damage_saved;
				if( save > 120 )
					save = 120;
				VectorCopy( ent->snap.damage_dir, dir );
				VectorNormalize( dir );
				VectorAdd( ent->s.origin, ent->snap.damage_at, origin );
				event = G_SpawnEvent( EV_BLOOD_SAVED, DirToByte( dir ), origin );
				event->s.damage = HEALTH_TO_INT( save );
				event->r.svflags = SVF_NOORIGIN2;
			}
#endif
		}
	}
}

//================
//G_StartFrameSnap
// a snap was just sent, set up for new one
//================
static void G_StartFrameSnap( void )
{
	g_snapStarted = qtrue;
}

// backup entitiy sounds in timeout
static int entity_sound_backup[MAX_EDICTS];

//================
//G_ClearSnap
// We just run G_SnapFrame, the server just sent the snap to the clients,
// it's now time to clean up snap specific data to start the next snap from clean.
//================
void G_ClearSnap( void )
{
	edict_t	*ent;

	game.realtime = trap_Milliseconds(); // level.time etc. might not be real time

	// clear all events in the snap
	for( ent = &game.edicts[0]; ENTNUM( ent ) < game.numentities; ent++ )
	{
		// events only last for a single message
		ent->s.events[0] = ent->s.events[1] = 0;
		ent->s.eventParms[0] = ent->s.eventParms[1] = 0;
		ent->s.teleported = qfalse; // remove teleported bit.

		if( !ent->r.inuse )
			continue;

		// free if no events
		if( !ent->s.events[0] )
		{
			ent->numEvents = 0;
			ent->eventPriority[0] = ent->eventPriority[1] = qfalse;
			if( ent->s.type == ET_EVENT )
				G_FreeEdict( ent );
		}
	}

	// recover some info, let players respawn and finally clear the snap structures
	for( ent = &game.edicts[0]; ENTNUM( ent ) < game.numentities; ent++ )
	{
		if( !gtimeout.active )
		{
			// copy origin to old origin ( this old_origin is for snaps )
			if( !( ent->r.svflags & SVF_TRANSMITORIGIN2 ) )
			{
				VectorCopy( ent->s.origin, ent->s.old_origin );
			}
			G_CheckClientRespawnClick( ent );
		}

		if( gtimeout.active )
			ent->s.sound = entity_sound_backup[ENTNUM( ent )];

		// clear the snap temp info
		memset( &ent->snap, 0, sizeof( ent->snap ) );
	}

	g_snapStarted = qfalse;
}

//================
//G_SnapFrame
// It's time to send a new snap, so set the world up for sending
//================
void G_SnapFrame( void )
{
	edict_t	*ent;
	game.realtime = trap_Milliseconds(); // level.time etc. might not be real time

	//others
	G_GametypeCheckRules();
	G_UpdateServerInfo();
	// exit level
	if( level.exitNow )
	{
		G_ExitLevel();
		return;
	}

	AITools_Frame(); //MbotGame //give think time to AI debug tools

	// finish snap
	G_SnapClients(); // build the playerstate_t structures for all players
	G_EdictsAddSnapEffects(); // add effects based on accumulated info along the frame

	// set entity bits (prepare entities for being sent in the snap)
	for( ent = &game.edicts[0]; ENTNUM( ent ) < game.numentities; ent++ )
	{
		if( ent->s.number != ENTNUM( ent ) )
		{
			if( developer->integer )
				G_Printf( "fixing ent->s.number (etype:%i, classname:%s)\n", ent->s.type, ent->classname ? ent->classname : "noclassname" );
			ent->s.number = ENTNUM( ent );
		}

		// temporary filter (Q2 system to ensure reliability)
		// ignore ents without visible models unless they have an effect
		if( !ent->r.inuse )
		{
			ent->r.svflags |= SVF_NOCLIENT;
			continue;
		}
		else if( ent->s.type >= ET_TOTAL_TYPES || ent->s.type < 0 )
		{                                                     // invalid type?
			if( developer->integer )
				G_Printf( "'G_SnapFrame': Inhibiting invalid entity type %i\n", ent->s.type );
			ent->r.svflags |= SVF_NOCLIENT;
			continue;
		}
		else if( !( ent->r.svflags & SVF_NOCLIENT ) && !ent->s.modelindex && !ent->s.effects && !ent->s.sound && !ent->s.events[0] && !ent->s.light && !ent->r.client )
		{
			if( developer->integer )
				G_Printf( "'G_SnapFrame': fixing missing SVF_NOCLIENT flag (no effect)\n" );
			ent->r.svflags |= SVF_NOCLIENT;
			continue;
		}

		ent->s.effects &= ~EF_TAKEDAMAGE;
		if( ent->takedamage )
			ent->s.effects |= EF_TAKEDAMAGE;
		if( gtimeout.active )
		{               // when in timeout, we don't send entity sounds
			entity_sound_backup[ENTNUM( ent )] = ent->s.sound;
			ent->s.sound = 0;
		}
	}
}

//===================================================================
//		WORLD FRAMES
//===================================================================

//================
//G_UpdateFrameTime
//================
static void G_UpdateFrameTime( unsigned int msec )
{
	game.frametime = msec;
	G_Timeout_Update( msec );
	game.realtime = trap_Milliseconds(); // level.time etc. might not be real time
}

//================
//G_RunEntities
// treat each object in turn
// even the world and clients get a chance to think
//================
static void G_RunEntities( void )
{
	edict_t	*ent;

	for( ent = &game.edicts[0]; ENTNUM( ent ) < game.numentities; ent++ )
	{
		if( !ent->r.inuse )
			continue;
		if( ent->s.type == ET_EVENT )
			continue; // events do not think

		level.current_entity = ent;

		// backup oldstate ( for world frame ).
		ent->olds = ent->s;

		// if the ground entity moved, make sure we are still on it
		if( !ent->r.client )
		{
			if( ( ent->groundentity ) && ( ent->groundentity->r.linkcount != ent->groundentity_linkcount ) )
			{
				G_CheckGround( ent );
			}
		}

		G_RunEntity( ent );
	}
}

void G_ChargeGunblades( unsigned int msecs )
{
#define AUTOCHARGE_FRAMETIME 1000
	firedef_t *firedef;
	edict_t	*ent;

	if( game.gametype == GAMETYPE_RACE )
		return;

	firedef = GS_FiredefForAmmo( AMMO_CELLS );
	if( !firedef )
		return;

	for( ent = game.edicts + 1; PLAYERNUM( ent ) < game.maxclients; ent++ )
	{
		if( !ent->r.client || !ent->r.client->inventory[WEAP_GUNBLADE] )
			continue;

		ent->r.client->gunblade_charge_waitingtime += msecs;
		if( ent->r.client->gunblade_charge_waitingtime < AUTOCHARGE_FRAMETIME )
			continue;

		while( ent->r.client->gunblade_charge_waitingtime > AUTOCHARGE_FRAMETIME )
		{
			ent->r.client->gunblade_charge_waitingtime -= AUTOCHARGE_FRAMETIME;

			if( firedef->ammo_max > ent->r.client->inventory[AMMO_CELLS] )
			{
				ent->r.client->inventory[AMMO_CELLS] += firedef->ammo_pickup;
				if( ent->r.client->inventory[AMMO_CELLS] > firedef->ammo_max )
					ent->r.client->inventory[AMMO_CELLS] = firedef->ammo_max;
			}
		}
	}
#undef AUTOCHARGE_FRAMETIME
}

//================
//G_RunClients
//================
static void G_RunClients( void )
{
	int i, step;
	edict_t *ent;

	if( level.framenum & 1 )
	{
		i = game.maxclients - 1;
		step = -1;
	}
	else
	{
		i = 0;
		step = 1;
	}

	for( ; i < game.maxclients && i >= 0; i += step )
	{
		ent = game.edicts + 1 + i;
		if( ent->r.inuse && trap_GetClientState( PLAYERNUM( ent ) ) >= CS_SPAWNED )
		{
			trap_ExecuteClientThinks( PLAYERNUM( ent ) );
			if( !gtimeout.active )
			{
				// force thinking of clients weapons once each frame, even if played didn't fire
				Think_Weapon( ent, game.frametime );
				G_UpdateClientLaserGunTrail( ent );
			}
			ent->snap.buttons |= ent->r.client->buttons;
		}
	}
}

//================
//G_RunFrame
//Advances the world
//================
void G_RunFrame( unsigned int msec, unsigned int serverTime )
{
	G_CheckCvars();

	game.serverTime = serverTime;
	G_UpdateFrameTime( msec );

	if( !g_snapStarted )
		G_StartFrameSnap();

	if( gtimeout.active )
	{
		G_TimoutFreezeProjectiles();
		G_RunClients();
		return;
	}

	level.framenum++;
	level.time += msec;

	G_ChargeGunblades( msec );

	// run the world
	G_RunClients();
	G_RunEntities();

	GClip_BackUpCollisionFrame();

	G_CheckMM();
}
