/*
===========================================================================
Copyright (C) 2000 - 2013, Raven Software, Inc.
Copyright (C) 2001 - 2013, Activision, Inc.
Copyright (C) 2013 - 2015, OpenJK contributors

This file is part of the OpenJK source code.

OpenJK is free software; you can redistribute it and/or modify it
under the terms of the GNU General Public License version 2 as
published by the Free Software Foundation.

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, see <http://www.gnu.org/licenses/>.
===========================================================================
*/

//Client camera controls for cinematics

#include "cg_headers.h"

#include "cg_media.h"

#include "../game/g_roff.h"
#include <JKVR/VrClientInfo.h>

bool		in_camera = false;
bool		in_misccamera = false; // if we are viewing a misc_camera
camera_t	client_camera={};
camera_t	previous_client_camera={};
extern qboolean	player_locked;

extern gentity_t *G_Find (gentity_t *from, int fieldofs, const char *match);
extern void G_UseTargets (gentity_t *ent, gentity_t *activator);
void CGCam_FollowDisable( void );
void CGCam_TrackDisable( void );
void CGCam_Distance( float distance, qboolean initLerp );
void CGCam_DistanceDisable( void );
extern qboolean CG_CalcFOVFromX( float fov_x );
extern void WP_SaberCatch( gentity_t *self, gentity_t *saber, qboolean switchToSaber );

/*
TODO:
CloseUp, FullShot & Longshot commands:

  camera( CLOSEUP, <entity targetname>, angles(pitch yaw roll) )
  Will find the ent, apply angle offset to their head forward(minus pitch),
  get a preset distance away and set the FOV.  Trace to point, if less than
  1.0, put it there and open up FOV accordingly.
  Be sure to frame it so that eyespot and tag_head are positioned at proper
  places in the frame - ie: eyespot not in center, but not closer than 1/4
  screen width to the top...?
*/
/*
-------------------------
CGCam_Init
-------------------------
*/

void CGCam_Init( void )
{
	extern qboolean qbVidRestartOccured;
	if (!qbVidRestartOccured)
	{
		memset( &client_camera, 0, sizeof ( camera_t ) );
	}
}

/*
-------------------------
CGCam_Enable
-------------------------
*/
extern void CG_CalcVrect(void);
void CGCam_Enable( void )
{
	client_camera.bar_alpha = 0.0f;
	client_camera.bar_time = cg.time;

	client_camera.bar_alpha_source = 0.0f;
	client_camera.bar_alpha_dest = 1.0f;

	client_camera.bar_height_source = 0.0f;
	client_camera.bar_height_dest = 480/10;
	client_camera.bar_height = 0.0f;

	client_camera.info_state |= CAMERA_BAR_FADING;

	client_camera.FOV	= CAMERA_DEFAULT_FOV;
	client_camera.FOV2	= CAMERA_DEFAULT_FOV;

	client_camera.has_stored_angles = false;

	in_camera = true;

	client_camera.next_roff_time = 0;

	previous_client_camera = client_camera;

	if ( &g_entities[0] && g_entities[0].client )
	{
		//Player zero not allowed to do anything
		VectorClear( g_entities[0].client->ps.velocity );
		g_entities[0].contents = 0;

		if ( cg.zoomMode )
		{
			// need to shut off some form of zooming
			cg.zoomMode = 0;
		}

		if ( g_entities[0].client->ps.saberInFlight && g_entities[0].client->ps.saber[0].Active() )
		{//saber is out
			gentity_t *saberent = &g_entities[g_entities[0].client->ps.saberEntityNum];
			if ( saberent )
			{
				WP_SaberCatch( &g_entities[0], saberent, qfalse );
			}
		}

		for ( int i = 0; i < NUM_FORCE_POWERS; i++ )
		{//deactivate any active force powers
			g_entities[0].client->ps.forcePowerDuration[i] = 0;
extern void WP_ForcePowerStop( gentity_t *self, forcePowers_t forcePower );
			if ( g_entities[0].client->ps.forcePowerDuration[i] || (g_entities[0].client->ps.forcePowersActive&( 1 << i )) )
			{
				WP_ForcePowerStop( &g_entities[0], (forcePowers_t)i );
			}
		}
	}
}
/*
-------------------------
CGCam_Disable
-------------------------
*/

void CGCam_Disable( void )
{
	in_camera = false;

	client_camera.bar_alpha = 1.0f;
	client_camera.bar_time = cg.time;

	client_camera.bar_alpha_source = 1.0f;
	client_camera.bar_alpha_dest = 0.0f;

	client_camera.bar_height_source = 480/10;
	client_camera.bar_height_dest = 0.0f;

	client_camera.info_state |= CAMERA_BAR_FADING;

	if ( &g_entities[0] && g_entities[0].client )
	{
		g_entities[0].contents = CONTENTS_BODY;//MASK_PLAYERSOLID;
	}

	gi.SendServerCommand( 0, "cts");

	//if ( cg_skippingcin.integer )
	{//We're skipping the cinematic and it's over now
		gi.cvar_set("timescale", "1");
		gi.cvar_set("skippingCinematic", "0");
	}

	//Now's a good time for a reset
	vr->take_snap = true;
	//we just came out of camera, so update cg.refdef.vieworg out of the camera's origin so the snapshot will know our new ori

	VectorCopy( g_entities[0].currentOrigin, cg.refdef.vieworg);
	VectorCopy( g_entities[0].client->ps.viewangles, cg.refdefViewAngles );
}

/*
-------------------------
CGCam_SetPosition
-------------------------
*/

void CGCam_SetPosition( vec3_t org )
{
	VectorCopy( org, client_camera.origin );
	VectorCopy( client_camera.origin, cg.refdef.vieworg );
}

/*
-------------------------
CGCam_Move
-------------------------
*/

void CGCam_Move( vec3_t dest, float duration )
{
	if ( client_camera.info_state & CAMERA_ROFFING )
	{
		client_camera.info_state &= ~CAMERA_ROFFING;
	}

	CGCam_TrackDisable();
	CGCam_DistanceDisable();

	if ( !duration )
	{
		client_camera.info_state &= ~CAMERA_MOVING;
		CGCam_SetPosition( dest );
		return;
	}

	client_camera.info_state |= CAMERA_MOVING;

	VectorCopy( dest, client_camera.origin2 );

	client_camera.move_duration = duration;
	client_camera.move_time = cg.time;
}

/*
-------------------------
CGCam_SetAngles
-------------------------
*/

void CGCam_SetAngles( vec3_t ang )
{
	VectorCopy( ang, client_camera.angles );
	VectorCopy(client_camera.angles, cg.refdefViewAngles );
}

/*
-------------------------
CGCam_Pan
-------------------------
*/

void CGCam_Pan( vec3_t dest, vec3_t panDirection, float duration )
{
	//vec3_t	panDirection = {0, 0, 0};
	int		i;
	float	delta1 , delta2;

	CGCam_FollowDisable();
	CGCam_DistanceDisable();

	if ( !duration )
	{
		CGCam_SetAngles( dest );
		client_camera.info_state &= ~CAMERA_PANNING;
		return;
	}

	//FIXME: make the dest an absolute value, and pass in a
	//panDirection as well.  If a panDirection's axis value is
	//zero, find the shortest difference for that axis.
	//Store the delta in client_camera.angles2.
	for( i = 0; i < 3; i++ )
	{
		dest[i] = AngleNormalize360( dest[i] );
		delta1 = dest[i] - AngleNormalize360( client_camera.angles[i] );
		if ( delta1 < 0 )
		{
			delta2 = delta1 + 360;
		}
		else
		{
			delta2 = delta1 - 360;
		}
		if ( !panDirection[i] )
		{//Didn't specify a direction, pick shortest
			if( Q_fabs(delta1) < Q_fabs(delta2) )
			{
				client_camera.angles2[i] = delta1;
			}
			else
			{
				client_camera.angles2[i] = delta2;
			}
		}
		else if ( panDirection[i] < 0 )
		{
			if( delta1 < 0 )
			{
				client_camera.angles2[i] = delta1;
			}
			else if( delta1 > 0 )
			{
				client_camera.angles2[i] = delta2;
			}
			else
			{//exact
				client_camera.angles2[i] = 0;
			}
		}
		else if ( panDirection[i] > 0 )
		{
			if( delta1 > 0 )
			{
				client_camera.angles2[i] = delta1;
			}
			else if( delta1 < 0 )
			{
				client_camera.angles2[i] = delta2;
			}
			else
			{//exact
				client_camera.angles2[i] = 0;
			}
		}
	}
	//VectorCopy( dest, client_camera.angles2 );

	client_camera.info_state |= CAMERA_PANNING;

	client_camera.pan_duration = duration;
	client_camera.pan_time = cg.time;
}

/*
-------------------------
CGCam_SetRoll
-------------------------
*/

void CGCam_SetRoll( float roll )
{
	client_camera.angles[2] = roll;
}

/*
-------------------------
CGCam_Roll
-------------------------
*/

void CGCam_Roll( float	dest, float duration )
{
	if ( !duration )
	{
		CGCam_SetRoll( dest );
		return;
	}

	//FIXME/NOTE: this will override current panning!!!
	client_camera.info_state |= CAMERA_PANNING;

	VectorCopy( client_camera.angles, client_camera.angles2 );
	client_camera.angles2[2] = AngleDelta( dest, client_camera.angles[2] );

	client_camera.pan_duration = duration;
	client_camera.pan_time = cg.time;
}

/*
-------------------------
CGCam_SetFOV
-------------------------
*/

void CGCam_SetFOV( float FOV )
{
	client_camera.FOV = FOV;
}

/*
-------------------------
CGCam_Zoom
-------------------------
*/

void CGCam_Zoom( float FOV, float duration )
{
	if ( !duration )
	{
		CGCam_SetFOV( FOV );
		return;
	}
	client_camera.info_state |= CAMERA_ZOOMING;

	client_camera.FOV_time	= cg.time;
	client_camera.FOV2		= FOV;

	client_camera.FOV_duration = duration;
}

void CGCam_Zoom2( float FOV, float FOV2, float duration )
{
	if ( !duration )
	{
		CGCam_SetFOV( FOV2 );
		return;
	}
	client_camera.info_state |= CAMERA_ZOOMING;

	client_camera.FOV_time	= cg.time;
	client_camera.FOV = FOV;
	client_camera.FOV2		= FOV2;

	client_camera.FOV_duration = duration;
}

void CGCam_ZoomAccel( float initialFOV, float fovVelocity, float fovAccel, float duration)
{
	if ( !duration )
	{
		return;
	}
	client_camera.info_state |= CAMERA_ACCEL;

	client_camera.FOV_time	= cg.time;
	client_camera.FOV2 = initialFOV;
	client_camera.FOV_vel = fovVelocity;
	client_camera.FOV_acc = fovAccel;

	client_camera.FOV_duration = duration;
}

/*
-------------------------
CGCam_Fade
-------------------------
*/

void CGCam_SetFade( vec4_t dest )
{//Instant completion
	client_camera.info_state &= ~CAMERA_FADING;
	client_camera.fade_duration = 0;
	VectorCopy4( dest, client_camera.fade_source );
	VectorCopy4( dest, client_camera.fade_color );
}

/*
-------------------------
CGCam_Fade
-------------------------
*/

void CGCam_Fade( vec4_t source, vec4_t dest, float duration )
{
	if ( !duration )
	{
		CGCam_SetFade( dest );
		return;
	}

	VectorCopy4( source, client_camera.fade_source );
	VectorCopy4( dest, client_camera.fade_dest );

	client_camera.fade_duration = duration;
	client_camera.fade_time = cg.time;

	client_camera.info_state |= CAMERA_FADING;
}

void CGCam_FollowDisable( void )
{
	client_camera.info_state &= ~CAMERA_FOLLOWING;
	client_camera.cameraGroup[0] = 0;
	client_camera.cameraGroupZOfs = 0;
	client_camera.cameraGroupTag[0] = 0;
}

void CGCam_TrackDisable( void )
{
	client_camera.info_state &= ~CAMERA_TRACKING;
	client_camera.trackEntNum = ENTITYNUM_WORLD;
}

void CGCam_DistanceDisable( void )
{
	client_camera.distance = 0;
}
/*
-------------------------
CGCam_Follow
-------------------------
*/

void CGCam_Follow( const char *cameraGroup, float speed, float initLerp )
{
	//Clear any previous
	CGCam_FollowDisable();

	if(!cameraGroup || !cameraGroup[0])
	{
		return;
	}

	if ( Q_stricmp("none", (char *)cameraGroup) == 0 )
	{//Turn off all aiming
		return;
	}

	if ( Q_stricmp("NULL", (char *)cameraGroup) == 0 )
	{//Turn off all aiming
		return;
	}

	//NOTE: if this interrupts a pan before it's done, need to copy the cg.refdef.viewAngles to the camera.angles!
	client_camera.info_state |= CAMERA_FOLLOWING;
	client_camera.info_state &= ~CAMERA_PANNING;

	//NULL terminate last char in case they type a name too long
	Q_strncpyz( client_camera.cameraGroup, cameraGroup, sizeof(client_camera.cameraGroup) );

	if ( speed )
	{
		client_camera.followSpeed = speed;
	}
	else
	{
		client_camera.followSpeed = 100.0f;
	}

	if ( initLerp )
	{
		client_camera.followInitLerp = qtrue;
	}
	else
	{
		client_camera.followInitLerp = qfalse;
	}
}

/*
-------------------------
Q3_CameraAutoAim

  Keeps camera pointed at an entity, usually will be a misc_camera_focus
  misc_camera_focus can be on a track that stays closest to it's subjects on that
  path (like Q3_CameraAutoTrack) or is can simply always put itself between it's subjects.
  misc_camera_focus can also set FOV/camera dist needed to keep the subjects in frame
-------------------------
*/

void CG_CameraAutoAim( const char *name )
{
	/*
	gentity_t *aimEnt = NULL;

	//Clear any previous
	CGCam_FollowDisable();

	if(Q_stricmp("none", (char *)name) == 0)
	{//Turn off all aiming
		return;
	}

	aimEnt = G_Find(NULL, FOFS(targetname), (char *)name);

	if(!aimEnt)
	{
		gi.Printf(S_COLOR_RED"ERROR: %s camera aim target not found\n", name);
		return;
	}

	//Lerp time...
	//aimEnt->aimDebounceTime = level.time;//FIXME: over time
	client_camera.aimEntNum = aimEnt->s.number;
	CGCam_Follow( aimEnt->cameraGroup, aimEnt->speed, aimEnt->spawnflags&1 );
	*/
}

/*
-------------------------
CGCam_Track
-------------------------
*/
void CGCam_Track( const char *trackName, float speed, float initLerp )
{
	gentity_t	*trackEnt = NULL;

	CGCam_TrackDisable();

	if(Q_stricmp("none", (char *)trackName) == 0)
	{//turn off tracking
		return;
	}

	//NOTE: if this interrupts a move before it's done, need to copy the cg.refdef.vieworg to the camera.origin!
	//This will find a path_corner now, not a misc_camera_track
	trackEnt = G_Find(NULL, FOFS(targetname), (char *)trackName);

	if ( !trackEnt )
	{
		gi.Printf(S_COLOR_RED"ERROR: %s camera track target not found\n", trackName);
		return;
	}

	client_camera.info_state |= CAMERA_TRACKING;
	client_camera.info_state &= ~CAMERA_MOVING;

	client_camera.trackEntNum = trackEnt->s.number;
	client_camera.initSpeed = speed/10.0f;
	client_camera.speed = speed;
	client_camera.nextTrackEntUpdateTime = cg.time;

	if ( initLerp )
	{
		client_camera.trackInitLerp = qtrue;
	}
	else
	{
		client_camera.trackInitLerp = qfalse;
	}
	/*
	if ( client_camera.info_state & CAMERA_FOLLOWING )
	{//Used to snap angles?  Do what...?
	}
	*/

	//Set a moveDir
	VectorSubtract( trackEnt->currentOrigin, client_camera.origin, client_camera.moveDir );

	if ( !client_camera.trackInitLerp )
	{//want to snap to first position
		//Snap to trackEnt's origin
		VectorCopy( trackEnt->currentOrigin, client_camera.origin );

		//Set new moveDir if trackEnt has a next path_corner
		//Possible that track has no next point, in which case we won't be moving anyway
		if ( trackEnt->target && trackEnt->target[0] )
		{
			gentity_t *newTrackEnt = G_Find( NULL, FOFS(targetname), trackEnt->target );
			if ( newTrackEnt )
			{
				VectorSubtract( newTrackEnt->currentOrigin, client_camera.origin, client_camera.moveDir );
			}
		}
	}

	VectorNormalize( client_camera.moveDir );
}

/*
-------------------------
Q3_CameraAutoTrack

  Keeps camera a certain distance from target entity but on the specified CameraPath
  The distance can be set in a script or derived from a misc_camera_focus.
  Dist will interpolate when changed, can also set acceleration/deceleration values.
  FOV will also interpolate.

  CameraPath might be a MAX path or perhaps a series of path_corners on the map itself
-------------------------
*/

void CG_CameraAutoTrack( const char *name )
{
	/*
	gentity_t *trackEnt = NULL;

	CGCam_TrackDisable();

	if(Q_stricmp("none", (char *)name) == 0)
	{//turn off tracking
		return;
	}

	//This will find a path_corner now, not a misc_camera_track
	trackEnt = G_Find(NULL, FOFS(targetname), (char *)name);

	if(!trackEnt)
	{
		gi.Printf(S_COLOR_RED"ERROR: %s camera track target not found\n", name);
		return;
	}

	//FIXME: last arg will be passed in
	CGCam_Track( trackEnt->s.number, trackEnt->speed, qfalse );
	//FIXME: this will be a seperate call
	CGCam_Distance( trackEnt->radius, qtrue);
	*/
}

/*
-------------------------
CGCam_Distance
-------------------------
*/

void CGCam_Distance( float distance, float initLerp )
{
	client_camera.distance = distance;

	if ( initLerp )
	{
		client_camera.distanceInitLerp = qtrue;
	}
	else
	{
		client_camera.distanceInitLerp = qfalse;
	}
}

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


void CGCam_FollowUpdate ( void )
{
	vec3_t		center, dir, cameraAngles, vec, focus[MAX_CAMERA_GROUP_SUBJECTS];//No more than 16 subjects in a cameraGroup
	gentity_t	*from = NULL;
	centity_t	*fromCent = NULL;
	int			num_subjects = 0, i;
	qboolean	focused = qfalse;

	if ( client_camera.cameraGroup[0] )
	{
		//Stay centered in my cameraGroup, if I have one
		while( NULL != (from = G_Find(from, FOFS(cameraGroup), client_camera.cameraGroup)))
		{
			/*
			if ( from->s.number == client_camera.aimEntNum )
			{//This is the misc_camera_focus, we'll be removing this ent altogether eventually
				continue;
			}
			*/

			if ( num_subjects >= MAX_CAMERA_GROUP_SUBJECTS )
			{
				gi.Printf(S_COLOR_RED"ERROR: Too many subjects in shot composition %s", client_camera.cameraGroup);
				break;
			}

			fromCent = &cg_entities[from->s.number];
			if ( !fromCent )
			{
				continue;
			}

			focused = qfalse;
			if ( from->client && client_camera.cameraGroupTag[0] && fromCent->gent->ghoul2.size() )
			{
				int newBolt = gi.G2API_AddBolt( &fromCent->gent->ghoul2[from->playerModel], client_camera.cameraGroupTag );
				if ( newBolt != -1 )
				{
					mdxaBone_t	boltMatrix;
					vec3_t	fromAngles = {0,from->client->ps.legsYaw,0};

					gi.G2API_GetBoltMatrix( fromCent->gent->ghoul2, from->playerModel, newBolt, &boltMatrix, fromAngles, fromCent->lerpOrigin, cg.time, cgs.model_draw, fromCent->currentState.modelScale );
					gi.G2API_GiveMeVectorFromMatrix( boltMatrix, ORIGIN, focus[num_subjects] );

					focused = qtrue;
				}
			}
			if ( !focused )
			{
				if ( from->s.pos.trType != TR_STATIONARY )
//				if ( from->s.pos.trType == TR_INTERPOLATE )
				{//use interpolated origin?
					if ( !VectorCompare( vec3_origin, fromCent->lerpOrigin ) )
					{//hunh?  Somehow we've never seen this gentity on the client, so there is no lerpOrigin, so cheat over to the game and use the currentOrigin
						VectorCopy( from->currentOrigin, focus[num_subjects] );
					}
					else
					{
						VectorCopy( fromCent->lerpOrigin, focus[num_subjects] );
					}
				}
				else
				{
					VectorCopy(from->currentOrigin, focus[num_subjects]);
				}
				//FIXME: make a list here of their s.numbers instead so we can do other stuff with the list below
				if ( from->client )
				{//Track to their eyes - FIXME: maybe go off a tag?
					//FIXME:
					//Based on FOV and distance to subject from camera, pick the point that
					//keeps eyes 3/4 up from bottom of screen... what about bars?
					focus[num_subjects][2] += from->client->ps.viewheight;
				}
			}
			if ( client_camera.cameraGroupZOfs )
			{
				focus[num_subjects][2] += client_camera.cameraGroupZOfs;
			}
			num_subjects++;
		}

		if ( !num_subjects )	// Bad cameragroup
		{
#ifndef FINAL_BUILD
			gi.Printf(S_COLOR_RED"ERROR: Camera Focus unable to locate cameragroup: %s\n", client_camera.cameraGroup);
#endif
			return;
		}

		//Now average all points
		VectorCopy( focus[0], center );
		for( i = 1; i < num_subjects; i++ )
		{
			VectorAdd( focus[i], center, center );
		}
		VectorScale( center, 1.0f/((float)num_subjects), center );
	}
	else
	{
		return;
	}

	//Need to set a speed to keep a distance from
	//the subject- fixme: only do this if have a distance
	//set
	VectorSubtract( client_camera.subjectPos, center, vec );
	client_camera.subjectSpeed = VectorLengthSquared( vec ) * 100.0f / cg.frametime;

	/*
	if ( !cg_skippingcin.integer )
	{
		Com_Printf( S_COLOR_RED"org: %s\n", vtos(center) );
	}
	*/
	VectorCopy( center, client_camera.subjectPos );

	VectorSubtract( center, cg.refdef.vieworg, dir );//can't use client_camera.origin because it's not updated until the end of the move.

	//Get desired angle
	vectoangles(dir, cameraAngles);

	if ( client_camera.followInitLerp )
	{//Lerping
		float frac = cg.frametime/100.0f * client_camera.followSpeed/100.f;
		for( i = 0; i < 3; i++ )
		{
			cameraAngles[i] = AngleNormalize180( cameraAngles[i] );
			cameraAngles[i] = AngleNormalize180( client_camera.angles[i] + frac * AngleNormalize180(cameraAngles[i] - client_camera.angles[i]) );
			cameraAngles[i] = AngleNormalize180( cameraAngles[i] );
		}
#if 0
		Com_Printf( "%s\n", vtos(cameraAngles) );
#endif
	}
	else
	{//Snapping, should do this first time if follow_lerp_to_start_duration is zero
		//will lerp from this point on
		client_camera.followInitLerp = qtrue;
		for( i = 0; i < 3; i++ )
		{//normalize so that when we start lerping, it doesn't freak out
			cameraAngles[i] = AngleNormalize180( cameraAngles[i] );
		}
		//So tracker doesn't move right away thinking the first angle change
		//is the subject moving... FIXME: shouldn't set this until lerp done OR snapped?
		client_camera.subjectSpeed = 0;
	}

	//Point camera to lerp angles
	/*
	if ( !cg_skippingcin.integer )
	{
		Com_Printf( "ang: %s\n", vtos(cameraAngles) );
	}
	*/
	VectorCopy( cameraAngles, client_camera.angles );
}

void CGCam_TrackEntUpdate ( void )
{//FIXME: only do every 100 ms
	gentity_t	*trackEnt = NULL;
	gentity_t	*newTrackEnt = NULL;
	qboolean	reached = qfalse;
	vec3_t		vec;
	float		dist;

	if ( client_camera.trackEntNum >= 0 && client_camera.trackEntNum < ENTITYNUM_WORLD )
	{//We're already heading to a path_corner
		trackEnt = &g_entities[client_camera.trackEntNum];
		VectorSubtract( trackEnt->currentOrigin, client_camera.origin, vec );
		dist = VectorLengthSquared( vec );
		if ( dist < 256 )//16 squared
		{//FIXME: who should be doing the using here?
			G_UseTargets( trackEnt, trackEnt );
			reached = qtrue;
		}
	}

	if ( trackEnt && reached )
	{

		if ( trackEnt->target && trackEnt->target[0] )
		{//Find our next path_corner
			newTrackEnt = G_Find( NULL, FOFS(targetname), trackEnt->target );
			if ( newTrackEnt )
			{
				if ( newTrackEnt->radius < 0 )
				{//Don't bother trying to maintain a radius
					client_camera.distance = 0;
					client_camera.speed = client_camera.initSpeed;
				}
				else if ( newTrackEnt->radius > 0 )
				{
					client_camera.distance = newTrackEnt->radius;
				}

				if ( newTrackEnt->speed < 0 )
				{//go back to our default speed
					client_camera.speed = client_camera.initSpeed;
				}
				else if ( newTrackEnt->speed > 0 )
				{
					client_camera.speed = newTrackEnt->speed/10.0f;
				}
			}
		}
		else
		{//stop thinking if this is the last one
			CGCam_TrackDisable();
		}
	}

	if ( newTrackEnt )
	{//Update will lerp this
		client_camera.info_state |= CAMERA_TRACKING;
		client_camera.trackEntNum = newTrackEnt->s.number;
		VectorCopy( newTrackEnt->currentOrigin, client_camera.trackToOrg );
	}

	client_camera.nextTrackEntUpdateTime = cg.time + 100;
}

void CGCam_TrackUpdate ( void )
{
	vec3_t		goalVec, curVec, trackPos, vec;
	float		goalDist, dist;
	//qboolean	slowDown = qfalse;

	if ( client_camera.nextTrackEntUpdateTime <= cg.time )
	{
		CGCam_TrackEntUpdate();
	}

	VectorSubtract( client_camera.trackToOrg, client_camera.origin, goalVec );
	goalDist = VectorNormalize( goalVec );
	if ( goalDist > 100 )
	{
		goalDist = 100;
	}
	else if ( goalDist < 10 )
	{
		goalDist = 10;
	}

	if ( client_camera.distance && client_camera.info_state & CAMERA_FOLLOWING )
	{
		float	adjust = 0.0f, desiredSpeed = 0.0f;
		float	dot;

		if ( !client_camera.distanceInitLerp )
		{
			VectorSubtract( client_camera.origin, client_camera.subjectPos, vec );
			VectorNormalize( vec );
			//FIXME: use client_camera.moveDir here?
			VectorMA( client_camera.subjectPos, client_camera.distance, vec, client_camera.origin );
			//Snap to first time only
			client_camera.distanceInitLerp = qtrue;
			return;
		}
		else if ( client_camera.subjectSpeed > 0.05f )
		{//Don't start moving until subject moves
			VectorSubtract( client_camera.subjectPos, client_camera.origin, vec );
			dist = VectorNormalize(vec);
			dot = DotProduct(goalVec, vec);

			if ( dist > client_camera.distance )
			{//too far away
				if ( dot > 0 )
				{//Camera is moving toward the subject
					adjust = (dist - client_camera.distance);//Speed up
				}
				else if ( dot < 0 )
				{//Camera is moving away from the subject
					adjust = (dist - client_camera.distance) * -1.0f;//Slow down
				}
			}
			else if ( dist < client_camera.distance )
			{//too close
				if(dot > 0)
				{//Camera is moving toward the subject
					adjust = (client_camera.distance - dist) * -1.0f;//Slow down
				}
				else if(dot < 0)
				{//Camera is moving away from the subject
					adjust = (client_camera.distance - dist);//Speed up
				}
			}

			//Speed of the focus + our error
			//desiredSpeed = aimCent->gent->speed + (adjust * cg.frametime/100.0f);//cg.frameInterpolation);
			desiredSpeed = (adjust);// * cg.frametime/100.0f);//cg.frameInterpolation);

			//self->moveInfo.speed = desiredSpeed;

			//Don't change speeds faster than 10 every 10th of a second
			float	max_allowed_accel = MAX_ACCEL_PER_FRAME * (cg.frametime/100.0f);

			if ( !client_camera.subjectSpeed )
			{//full stop
				client_camera.speed = desiredSpeed;
			}
			else if ( client_camera.speed - desiredSpeed > max_allowed_accel )
			{//new speed much slower, slow down at max accel
				client_camera.speed -= max_allowed_accel;
			}
			else if ( desiredSpeed - client_camera.speed > max_allowed_accel )
			{//new speed much faster, speed up at max accel
				client_camera.speed += max_allowed_accel;
			}
			else
			{//remember this speed
				client_camera.speed = desiredSpeed;
			}

			//Com_Printf("Speed: %4.2f (%4.2f)\n", self->moveInfo.speed, aimCent->gent->speed);
		}
	}
	else
	{
		//slowDown = qtrue;
	}


	//FIXME: this probably isn't right, round it out more
	VectorScale( goalVec, cg.frametime/100.0f, goalVec );
	VectorScale( client_camera.moveDir, (100.0f - cg.frametime)/100.0f, curVec );
	VectorAdd( goalVec, curVec, client_camera.moveDir );
	VectorNormalize( client_camera.moveDir );
	/*if(slowDown)
	{
		VectorMA( client_camera.origin, client_camera.speed * goalDist/100.0f * cg.frametime/100.0f, client_camera.moveDir, trackPos );
	}
	else*/
	{
		VectorMA( client_camera.origin, client_camera.speed * cg.frametime/100.0f , client_camera.moveDir, trackPos );
	}

	//FIXME: Implement
	//Need to find point on camera's path that is closest to the desired distance from subject
	//OR: Need to intelligently pick this desired distance based on framing...
	VectorCopy( trackPos, client_camera.origin );
}

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

/*
-------------------------
CGCam_UpdateBarFade
-------------------------
*/

void CGCam_UpdateBarFade( void )
{
	if ( client_camera.bar_time + BAR_DURATION < cg.time )
	{
		client_camera.bar_alpha = client_camera.bar_alpha_dest;
		client_camera.info_state &= ~CAMERA_BAR_FADING;
		client_camera.bar_height = client_camera.bar_height_dest;
	}
	else
	{
		client_camera.bar_alpha  = client_camera.bar_alpha_source + ( ( client_camera.bar_alpha_dest - client_camera.bar_alpha_source ) / BAR_DURATION ) * ( cg.time - client_camera.bar_time );;
		client_camera.bar_height = client_camera.bar_height_source + ( ( client_camera.bar_height_dest - client_camera.bar_height_source ) / BAR_DURATION ) * ( cg.time - client_camera.bar_time );;
	}
}

/*
-------------------------
CGCam_UpdateFade
-------------------------
*/

void CGCam_UpdateFade( void )
{
	if ( client_camera.info_state & CAMERA_FADING )
	{
		if ( client_camera.fade_time + client_camera.fade_duration < cg.time )
		{
			VectorCopy4( client_camera.fade_dest, client_camera.fade_color );
			client_camera.info_state &= ~CAMERA_FADING;
		}
		else
		{
			for ( int i = 0; i < 4; i++ )
			{
				client_camera.fade_color[i] = client_camera.fade_source[i] + (( ( client_camera.fade_dest[i] - client_camera.fade_source[i] ) ) / client_camera.fade_duration ) * ( cg.time - client_camera.fade_time );
			}
		}
	}
}
/*
-------------------------
CGCam_Update
-------------------------
*/
static void CGCam_Roff( void );

void CGCam_Update( void )
{
	int	i;
	qboolean	checkFollow = qfalse;
	qboolean	checkTrack = qfalse;

	// Apply new roff data to the camera as needed
	if ( client_camera.info_state & CAMERA_ROFFING )
	{
		CGCam_Roff();
	}

	//Check for a zoom
	if (client_camera.info_state & CAMERA_ACCEL)
	{
		// x = x0 + vt + 0.5*a*t*t
		float	actualFOV_X = client_camera.FOV;
		float	sanityMin = 1, sanityMax = 180;
		float	t = (cg.time - client_camera.FOV_time)*0.001; // mult by 0.001 cuz otherwise t is too darned big
		float	fovDuration = client_camera.FOV_duration;

#ifndef FINAL_BUILD
		if (cg_roffval4.integer)
		{
			fovDuration = cg_roffval4.integer;
		}
#endif
		if ( client_camera.FOV_time + fovDuration < cg.time )
		{
			client_camera.info_state &= ~CAMERA_ACCEL;
		}
		else
		{
			float	initialPosVal = client_camera.FOV2;
			float	velVal = client_camera.FOV_vel;
			float	accVal = client_camera.FOV_acc;

#ifndef FINAL_BUILD
			if (cg_roffdebug.integer)
			{
				if (fabs(cg_roffval1.value) > 0.001f)
				{
					initialPosVal = cg_roffval1.value;
				}
				if (fabs(cg_roffval2.value) > 0.001f)
				{
					velVal = cg_roffval2.value;
				}
				if (fabs(cg_roffval3.value) > 0.001f)
				{
					accVal = cg_roffval3.value;
				}
			}
#endif
			float	initialPos = initialPosVal;
			float	vel = velVal*t;
			float	acc = 0.5*accVal*t*t;

			actualFOV_X = initialPos + vel + acc;
			if (cg_roffdebug.integer)
			{
				Com_Printf("%d: fovaccel from %2.1f using vel = %2.4f, acc = %2.4f (current fov calc = %5.6f)\n",
					cg.time, initialPosVal, velVal, accVal, actualFOV_X);
			}

			if (actualFOV_X < sanityMin)
			{
				actualFOV_X = sanityMin;
			}
			else if (actualFOV_X > sanityMax)
			{
				actualFOV_X = sanityMax;
			}
			client_camera.FOV = actualFOV_X;
		}
		float fov = vr && vr->immersive_cinematics ? vr->fov_x : actualFOV_X;
		CG_CalcFOVFromX( fov );
	}
	else if ( client_camera.info_state & CAMERA_ZOOMING )
	{
		float	actualFOV_X;

		if ( client_camera.FOV_time + client_camera.FOV_duration < cg.time )
		{
			actualFOV_X = client_camera.FOV = client_camera.FOV2;
			client_camera.info_state &= ~CAMERA_ZOOMING;
		}
		else
		{
			actualFOV_X = client_camera.FOV + (( ( client_camera.FOV2 - client_camera.FOV ) ) / client_camera.FOV_duration ) * ( cg.time - client_camera.FOV_time );
		}
		float fov = vr && vr->immersive_cinematics ? vr->fov_x : actualFOV_X;
		CG_CalcFOVFromX( fov );
	}
	else
	{
		float fov = vr && vr->immersive_cinematics ? vr->fov_x : client_camera.FOV;
		CG_CalcFOVFromX( fov );
	}

	//Check for roffing angles
	if ( (client_camera.info_state & CAMERA_ROFFING) && !(client_camera.info_state & CAMERA_FOLLOWING) )
	{
		if (client_camera.info_state & CAMERA_CUT)
		{
			// we're doing a cut, so just go to the new angles. none of this hifalutin lerping business.
			for ( i = 0; i < 3; i++ )
			{
				cg.refdefViewAngles[i] = AngleNormalize360( ( client_camera.angles[i] + client_camera.angles2[i] ) );
			}
		}
		else
		{
			for ( i = 0; i < 3; i++ )
			{
				cg.refdefViewAngles[i] =  client_camera.angles[i] + ( client_camera.angles2[i] / client_camera.pan_duration ) * ( cg.time - client_camera.pan_time );
			}
		}
	}
	else if ( client_camera.info_state & CAMERA_PANNING )
	{
		if (client_camera.info_state & CAMERA_CUT)
		{
			// we're doing a cut, so just go to the new angles. none of this hifalutin lerping business.
			for ( i = 0; i < 3; i++ )
			{
				cg.refdefViewAngles[i] = AngleNormalize360( ( client_camera.angles[i] + client_camera.angles2[i] ) );
			}
		}
		else
		{
			//Note: does not actually change the camera's angles until the pan time is done!
			if ( client_camera.pan_time + client_camera.pan_duration < cg.time )
			{//finished panning
				for ( i = 0; i < 3; i++ )
				{
					client_camera.angles[i] = AngleNormalize360( ( client_camera.angles[i] + client_camera.angles2[i] ) );
				}

				client_camera.info_state &= ~CAMERA_PANNING;
				VectorCopy(client_camera.angles, cg.refdefViewAngles );
			}
			else
			{//still panning
				for ( i = 0; i < 3; i++ )
				{
					//NOTE: does not store the resultant angle in client_camera.angles until pan is done
					cg.refdefViewAngles[i] = client_camera.angles[i] + ( client_camera.angles2[i] / client_camera.pan_duration ) * ( cg.time - client_camera.pan_time );
				}
			}
		}
	}
	else
	{
		checkFollow = qtrue;
	}

	//Check for movement
	if ( client_camera.info_state & CAMERA_MOVING )
	{
		//NOTE: does not actually move the camera until the movement time is done!
		if ( client_camera.move_time + client_camera.move_duration < cg.time )
		{
			VectorCopy( client_camera.origin2, client_camera.origin );
			client_camera.info_state &= ~CAMERA_MOVING;
			VectorCopy( client_camera.origin, cg.refdef.vieworg );
		}
		else
		{
			if (client_camera.info_state & CAMERA_CUT)
			{
				// we're doing a cut, so just go to the new origin. none of this fancypants lerping stuff.
				for ( i = 0; i < 3; i++ )
				{
					cg.refdef.vieworg[i] = client_camera.origin2[i];
				}
			}
			else
			{
				for ( i = 0; i < 3; i++ )
				{
					cg.refdef.vieworg[i] = client_camera.origin[i] + (( ( client_camera.origin2[i] - client_camera.origin[i] ) ) / client_camera.move_duration ) * ( cg.time - client_camera.move_time );
				}
			}
		}
	}
	else
	{
		checkTrack = qtrue;
	}

	if ( checkFollow )
	{
		//Don't follow if immersive
		if ( client_camera.info_state & CAMERA_FOLLOWING )
		{//This needs to be done after camera movement
			CGCam_FollowUpdate();
		}

		if (!vr->immersive_cinematics)
		{
			VectorCopy(client_camera.angles, cg.refdefViewAngles);
		}
	}

	if ( checkTrack )
	{
		if ( client_camera.info_state & CAMERA_TRACKING )
		{//This has to run AFTER Follow if the camera is following a cameraGroup
			CGCam_TrackUpdate();
		}

		VectorCopy( client_camera.origin, cg.refdef.vieworg );
	}

	if (vr->immersive_cinematics)
	{
		//If no stored angles yet, store them
		if (!client_camera.has_stored_angles)
		{
			client_camera.has_stored_angles = true;
			vr->take_snap = true;
		}

		//if camera state has changed, store the angles and reset user's snap orientation
		if (((client_camera.info_state & CAMERA_FOLLOWING) != (previous_client_camera.info_state & CAMERA_FOLLOWING)) ||
				((client_camera.info_state & CAMERA_ROFFING) != (previous_client_camera.info_state & CAMERA_ROFFING)) ||
				((client_camera.info_state & CAMERA_MOVING) != (previous_client_camera.info_state & CAMERA_MOVING)) ||
				((client_camera.info_state & CAMERA_PANNING) != (previous_client_camera.info_state & CAMERA_PANNING)))
		{
			vr->take_snap = true;
		}

		//If the camera has changed position (by over half an in-game metre), but is not in moving mode (last frame)
		//then it is a keyframe that requires a new snap
		if (!(previous_client_camera.info_state & CAMERA_MOVING))
		{
			vec3_t delta;
			VectorSubtract(client_camera.origin, previous_client_camera.origin, delta);
			if (VectorLength(delta) > (0.5f * cg_worldScale.value))
			{
				vr->take_snap = true;
			}
		}

		if (vr->take_snap)
		{
			VectorCopy(client_camera.angles, client_camera.stored_angles);
		}

		//Copy stored YAW angle to refdef whether it has changed or not, use pitch/roll direct from the hmd
		float yaw = client_camera.stored_angles[YAW] + (vr->hmdorientation[YAW] - vr->hmdorientation_snap[YAW]) + vr->snapTurn;
		VectorCopy(vr->hmdorientation, cg.refdefViewAngles);
		cg.refdefViewAngles[YAW] = yaw;

		//store previous state
		previous_client_camera = client_camera;
	}

	//Bar fading
	if ( client_camera.info_state & CAMERA_BAR_FADING )
	{
		CGCam_UpdateBarFade();
	}

	//Normal fading - separate call because can finish after camera is disabled
	CGCam_UpdateFade();

	//Update shaking if there's any
	//CGCam_UpdateSmooth( cg.refdef.vieworg, cg.refdefViewAngles );
	CGCam_UpdateShake( cg.refdef.vieworg, cg.refdefViewAngles );
	AnglesToAxis( cg.refdefViewAngles, cg.refdef.viewaxis );
}

/*
-------------------------
CGCam_DrawWideScreen
-------------------------
*/

void CGCam_DrawWideScreen( void )
{
	vec4_t	modulate;

	//Only draw if visible
	if ( client_camera.bar_alpha )
	{
		CGCam_UpdateBarFade();

		modulate[0] = modulate[1] = modulate[2] = 0.0f;
		modulate[3] = client_camera.bar_alpha;

		CG_FillRect( cg.refdef.x, cg.refdef.y, 640, client_camera.bar_height, modulate  );
		CG_FillRect( cg.refdef.x, cg.refdef.y + 480 - client_camera.bar_height, 640, client_camera.bar_height, modulate  );
	}

	//NOTENOTE: Camera always draws the fades unless the alpha is 0
	if ( client_camera.fade_color[3] == 0.0f )
		return;

	CG_FillRect( cg.refdef.x, cg.refdef.y, 640, 480, client_camera.fade_color );
}

/*
-------------------------
CGCam_RenderScene
-------------------------
*/
void CGCam_RenderScene( void )
{
	CGCam_Update();
	CG_CalcVrect();
}

/*
-------------------------
CGCam_Shake
-------------------------
*/

void CGCam_Shake( float intensity, int duration )
{
	if ( intensity > MAX_SHAKE_INTENSITY )
		intensity = MAX_SHAKE_INTENSITY;

	client_camera.shake_intensity = intensity;
	client_camera.shake_duration = duration;
	client_camera.shake_start = cg.time;
}

/*
-------------------------
CGCam_UpdateShake

This doesn't actually affect the camera's info, but passed information instead
-------------------------
*/

void CGCam_UpdateShake( vec3_t origin, vec3_t angles )
{
	vec3_t	moveDir;
	float	intensity_scale, intensity;

	if ( client_camera.shake_duration <= 0 )
		return;

	if ( cg.time > ( client_camera.shake_start + client_camera.shake_duration ) )
	{
		client_camera.shake_intensity = 0;
		client_camera.shake_duration = 0;
		client_camera.shake_start = 0;
		return;
	}

	//intensity_scale now also takes into account FOV with 90.0 as normal
	intensity_scale = 1.0f - ( (float) ( cg.time - client_camera.shake_start ) / (float) client_camera.shake_duration ) * (((client_camera.FOV+client_camera.FOV2)/2.0f)/90.0f);

	intensity = client_camera.shake_intensity * intensity_scale;

	for ( int i = 0; i < 3; i++ )
	{
		moveDir[i] = ( Q_flrand(-1.0f, 1.0f) * intensity );
	}

	//FIXME: Lerp

	//Move the camera
	VectorAdd( origin, moveDir, origin );

	for ( int i = 0; i < 2; i++ ) // Don't do ROLL
		moveDir[i] = ( Q_flrand(-1.0f, 1.0f) * intensity );

	//FIXME: Lerp

	//Move the angles
	VectorAdd( angles, moveDir, angles );
}

void CGCam_Smooth( float intensity, int duration )
{
	client_camera.smooth_active=false; // means smooth_origin and angles are valid
	if ( intensity>1.0f||intensity==0.0f||duration<1)
	{
		client_camera.info_state &= ~CAMERA_SMOOTHING;
		return;
	}
	client_camera.info_state |= CAMERA_SMOOTHING;
	client_camera.smooth_intensity = intensity;
	client_camera.smooth_duration = duration;
	client_camera.smooth_start = cg.time;
}

void CGCam_UpdateSmooth( vec3_t origin, vec3_t angles )
{
	if (!(client_camera.info_state&CAMERA_SMOOTHING)||cg.time > ( client_camera.smooth_start + client_camera.smooth_duration ))
	{
		client_camera.info_state &= ~CAMERA_SMOOTHING;
		return;
	}
	if (!client_camera.smooth_active)
	{
		client_camera.smooth_active=true;
		VectorCopy(origin,client_camera.smooth_origin);
		return;
	}
	float factor=client_camera.smooth_intensity;
	if (client_camera.smooth_duration>200&&cg.time > ( client_camera.smooth_start + client_camera.smooth_duration-100 ))
	{
		factor+=(1.0f-client_camera.smooth_intensity)*
			(100.0f-(client_camera.smooth_start + client_camera.smooth_duration-cg.time))/100.0f;
	}
	int i;
	for (i=0;i<3;i++)
	{
		client_camera.smooth_origin[i]*=(1.0f-factor);
		client_camera.smooth_origin[i]+=factor*origin[i];
		origin[i]=client_camera.smooth_origin[i];
	}
}

void CGCam_NotetrackProcessFov(const char *addlArg)
{
	int a = 0;
	char t[64];

	if (!addlArg || !addlArg[0])
	{
		Com_Printf("camera roff 'fov' notetrack missing fov argument\n", addlArg);
		return;
	}
	if (isdigit(addlArg[a]))
	{
		// "fov <new fov>"
		int d = 0, tsize = 64;

		memset(t, 0, tsize*sizeof(char));
		while (addlArg[a] && d < tsize)
		{
			t[d++] = addlArg[a++];
		}
		// now the contents of t represent our desired fov
		float newFov = atof(t);
#ifndef FINAL_BUILD
		if (cg_roffdebug.integer)
		{
			if (fabs(cg_roffval1.value) > 0.001f)
			{
				newFov = cg_roffval1.value;
			}
		}
#endif
		if (cg_roffdebug.integer)
		{
			Com_Printf("notetrack: 'fov %2.2f' on frame %d\n", newFov, client_camera.roff_frame);
		}
		CGCam_Zoom(newFov, 0);
	}
}

void CGCam_NotetrackProcessFovZoom(const char *addlArg)
{
	int		a = 0;
	float	beginFOV = 0, endFOV = 0, fovTime = 0;

	if (!addlArg || !addlArg[0])
	{
		Com_Printf("camera roff 'fovzoom' notetrack missing arguments\n", addlArg);
		return;
	}
	//
	// "fovzoom <begin fov> <end fov> <time>"
	//
	char t[64];
	int d = 0, tsize = 64;

	memset(t, 0, tsize*sizeof(char));
	while (addlArg[a] && !isspace(addlArg[a]) && d < tsize)
	{
		t[d++] = addlArg[a++];
	}
	if (!isdigit(t[0]))
	{
		// assume a non-number here means we should start from our current fov
		beginFOV = client_camera.FOV;
	}
	else
	{
		// now the contents of t represent our beginning fov
		beginFOV = atof(t);
	}

	// eat leading whitespace
	while (addlArg[a] && addlArg[a] == ' ')
	{
		a++;
	}
	if (addlArg[a])
	{
		d = 0;
		memset(t, 0, tsize*sizeof(char));
		while (addlArg[a] && !isspace(addlArg[a]) && d < tsize)
		{
			t[d++] = addlArg[a++];
		}
		// now the contents of t represent our end fov
		endFOV = atof(t);

		// eat leading whitespace
		while (addlArg[a] && addlArg[a] == ' ')
		{
			a++;
		}
		if (addlArg[a])
		{
			d = 0;
			memset(t, 0, tsize*sizeof(char));
			while (addlArg[a] && !isspace(addlArg[a]) && d < tsize)
			{
				t[d++] = addlArg[a++];
			}
			// now the contents of t represent our time
			fovTime = atof(t);
		}
		else
		{
			Com_Printf("camera roff 'fovzoom' notetrack missing 'time' argument\n", addlArg);
			return;
		}
#ifndef FINAL_BUILD
		if (cg_roffdebug.integer)
		{
			if (fabs(cg_roffval1.value) > 0.001f)
			{
				beginFOV = cg_roffval1.value;
			}
			if (fabs(cg_roffval2.value) > 0.001f)
			{
				endFOV = cg_roffval2.value;
			}
			if (fabs(cg_roffval3.value) > 0.001f)
			{
				fovTime = cg_roffval3.value;
			}
		}
#endif
		if (cg_roffdebug.integer)
		{
			Com_Printf("notetrack: 'fovzoom %2.2f %2.2f %5.1f' on frame %d\n", beginFOV, endFOV, fovTime, client_camera.roff_frame);
		}
		CGCam_Zoom2(beginFOV, endFOV, fovTime);
	}
	else
	{
		Com_Printf("camera roff 'fovzoom' notetrack missing 'end fov' argument\n", addlArg);
		return;
	}
}

void CGCam_NotetrackProcessFovAccel(const char *addlArg)
{
	int		a = 0;
	float	beginFOV = 0, fovDelta = 0, fovDelta2 = 0, fovTime = 0;

	if (!addlArg || !addlArg[0])
	{
		Com_Printf("camera roff 'fovaccel' notetrack missing arguments\n", addlArg);
		return;
	}
	//
	// "fovaccel <begin fov> <fov delta> <fov delta2> <time>"
	//
	// where 'begin fov' is initial position, 'fov delta' is velocity, and 'fov delta2' is acceleration.
	char t[64];
	int d = 0, tsize = 64;

	memset(t, 0, tsize*sizeof(char));
	while (addlArg[a] && !isspace(addlArg[a]) && d < tsize)
	{
		t[d++] = addlArg[a++];
	}
	if (!isdigit(t[0]))
	{
		// assume a non-number here means we should start from our current fov
		beginFOV = client_camera.FOV;
	}
	else
	{
		// now the contents of t represent our beginning fov
		beginFOV = atof(t);
	}

	// eat leading whitespace
	while (addlArg[a] && addlArg[a] == ' ')
	{
		a++;
	}
	if (addlArg[a])
	{
		d = 0;
		memset(t, 0, tsize*sizeof(char));
		while (addlArg[a] && !isspace(addlArg[a]) && d < tsize)
		{
			t[d++] = addlArg[a++];
		}
		// now the contents of t represent our delta
		fovDelta = atof(t);

		// eat leading whitespace
		while (addlArg[a] && addlArg[a] == ' ')
		{
			a++;
		}
		if (addlArg[a])
		{
			d = 0;
			memset(t, 0, tsize*sizeof(char));
			while (addlArg[a] && !isspace(addlArg[a]) && d < tsize)
			{
				t[d++] = addlArg[a++];
			}
			// now the contents of t represent our fovDelta2
			fovDelta2 = atof(t);

			// eat leading whitespace
			while (addlArg[a] && addlArg[a] == ' ')
			{
				a++;
			}
			if (addlArg[a])
			{
				d = 0;
				memset(t, 0, tsize*sizeof(char));
				while (addlArg[a] && !isspace(addlArg[a]) && d < tsize)
				{
					t[d++] = addlArg[a++];
				}
				// now the contents of t represent our time
				fovTime = atof(t);
			}
			else
			{
				Com_Printf("camera roff 'fovaccel' notetrack missing 'time' argument\n", addlArg);
				return;
			}
			if (cg_roffdebug.integer)
			{
				Com_Printf("notetrack: 'fovaccel %2.2f %3.5f %3.5f %d' on frame %d\n", beginFOV, fovDelta, fovDelta2, fovTime, client_camera.roff_frame);
			}
			CGCam_ZoomAccel(beginFOV, fovDelta, fovDelta2, fovTime);
		}
		else
		{
			Com_Printf("camera roff 'fovaccel' notetrack missing 'delta2' argument\n", addlArg);
			return;
		}
	}
	else
	{
		Com_Printf("camera roff 'fovaccel' notetrack missing 'delta' argument\n", addlArg);
		return;
	}
}

// 3/18/03 kef -- blatantly thieved from G_RoffNotetrackCallback
static void CG_RoffNotetrackCallback(const char *notetrack)
{
	int i = 0, r = 0;
	char type[256];
//	char argument[512];
	char addlArg[512];
	int addlArgs = 0;

	if (!notetrack)
	{
		return;
	}

	//notetrack = "fov 65";

	while (notetrack[i] && notetrack[i] != ' ')
	{
		type[i] = notetrack[i];
		i++;
	}

	type[i] = '\0';

	//if (notetrack[i] != ' ')
	//{ //didn't pass in a valid notetrack type, or forgot the argument for it
	//	return;
	//}

/*	i++;

	while (notetrack[i] && notetrack[i] != ' ')
	{
		if (notetrack[i] != '\n' && notetrack[i] != '\r')
		{ //don't read line ends for an argument
			argument[r] = notetrack[i];
			r++;
		}
		i++;
	}
	argument[r] = '\0';
	if (!r)
	{
		return;
	}
*/

	if (notetrack[i] == ' ')
	{ //additional arguments...
		addlArgs = 1;

		i++;
		r = 0;
		while (notetrack[i])
		{
			addlArg[r] = notetrack[i];
			r++;
			i++;
		}
		addlArg[r] = '\0';
	}

	if (strcmp(type, "cut") == 0)
	{
		client_camera.info_state |= CAMERA_CUT;
		if (cg_roffdebug.integer)
		{
			Com_Printf("notetrack: 'cut' on frame %d\n", client_camera.roff_frame);
		}

		// this is just a really hacky way of getting a cut and a fov command on the same frame
		if (addlArgs)
		{
			CG_RoffNotetrackCallback(addlArg);
		}
	}
	else if (strcmp(type, "fov") == 0)
	{
		if (addlArgs)
		{
			CGCam_NotetrackProcessFov(addlArg);
			return;
		}
		Com_Printf("camera roff 'fov' notetrack missing fov argument\n", addlArg);
	}
	else if (strcmp(type, "fovzoom") == 0)
	{
		if (addlArgs)
		{
			CGCam_NotetrackProcessFovZoom(addlArg);
			return;
		}
		Com_Printf("camera roff 'fovzoom' notetrack missing 'begin fov' argument\n", addlArg);
	}
	else if (strcmp(type, "fovaccel") == 0)
	{
		if (addlArgs)
		{
			CGCam_NotetrackProcessFovAccel(addlArg);
			return;
		}
		Com_Printf("camera roff 'fovaccel' notetrack missing 'begin fov' argument\n", addlArg);
	}
}

/*
-------------------------
CGCam_StartRoff

Sets up the camera to use
a rof file
-------------------------
*/

void CGCam_StartRoff( char *roff )
{
	CGCam_FollowDisable();
	CGCam_TrackDisable();

	// Set up the roff state info..we'll hijack the moving and panning code until told otherwise
	//	...CAMERA_FOLLOWING would be a case that could override this..
	client_camera.info_state |= CAMERA_MOVING;
	client_camera.info_state |= CAMERA_PANNING;

	if ( !G_LoadRoff( roff ) )
	{
		// The load failed so don't turn on the roff playback...
		Com_Printf( S_COLOR_RED"ROFF camera playback failed\n" );
		return;
	};

	client_camera.info_state |= CAMERA_ROFFING;

	Q_strncpyz(client_camera.sRoff,roff,sizeof(client_camera.sRoff));
	client_camera.roff_frame = 0;
	client_camera.next_roff_time = cg.time;	// I can work right away
}

/*
-------------------------
CGCam_StopRoff

Stops camera rof
-------------------------
*/

static void CGCam_StopRoff( void )
{
	// Clear the roff flag
	client_camera.info_state &= ~CAMERA_ROFFING;
	client_camera.info_state &= ~CAMERA_MOVING;
}

/*
------------------------------------------------------
CGCam_Roff

Applies the sampled roff data to the camera and does
the lerping itself...this is done because the current
camera interpolation doesn't seem to work all that
great when you are adjusting the camera org and angles
so often...or maybe I'm just on crack.
------------------------------------------------------
*/

static void CGCam_Roff( void )
{
 while ( client_camera.next_roff_time <= cg.time )
 {
	// Make sure that the roff is cached
	const int roff_id = G_LoadRoff( client_camera.sRoff );

	if ( !roff_id )
	{
		return;
	}

	// The ID is one higher than the array index
	const roff_list_t	*roff	= &roffs[ roff_id - 1 ];
	vec3_t org, ang;

	if ( roff->type == 2 )
	{
		move_rotate2_t	*data	= &((move_rotate2_t *)roff->data)[ client_camera.roff_frame ];
		VectorCopy( data->origin_delta, org );
		VectorCopy( data->rotate_delta, ang );

		// since we just hit a new frame, clear our CUT flag
		client_camera.info_state &= ~CAMERA_CUT;

		if (data->mStartNote != -1 || data->mNumNotes)
		{
			CG_RoffNotetrackCallback(roffs[roff_id - 1].mNoteTrackIndexes[data->mStartNote]);
		}
	}
	else
	{
		move_rotate_t	*data	= &((move_rotate_t *)roff->data)[ client_camera.roff_frame ];
		VectorCopy( data->origin_delta, org );
		VectorCopy( data->rotate_delta, ang );
	}

	// Yeah, um, I guess this just has to be negated?
	//ang[PITCH]	=- ang[PITCH];
	ang[ROLL]	= -ang[ROLL];
	// might need to to yaw as well.  need a test...

	if ( cg_developer.integer )
	{
		Com_Printf( S_COLOR_GREEN"CamROFF: frame: %d o:<%.2f %.2f %.2f> a:<%.2f %.2f %.2f>\n",
					client_camera.roff_frame,
					org[0], org[1], org[2],
					ang[0], ang[1], ang[2] );
	}

	if ( client_camera.roff_frame )
	{
		// Don't mess with angles if we are following
		if ( !(client_camera.info_state & CAMERA_FOLLOWING) )
		{
			VectorAdd( client_camera.angles, client_camera.angles2, client_camera.angles );
		}

		VectorCopy( client_camera.origin2, client_camera.origin );
	}

	// Don't mess with angles if we are following
	if ( !(client_camera.info_state & CAMERA_FOLLOWING) )
	{
		VectorCopy( ang, client_camera.angles2 );
		client_camera.pan_time = cg.time;
		client_camera.pan_duration = roff->mFrameTime;
	}

	VectorAdd( client_camera.origin, org, client_camera.origin2 );

	client_camera.move_time = cg.time;
	client_camera.move_duration = roff->mFrameTime;

	if ( ++client_camera.roff_frame >= roff->frames )
	{
		CGCam_StopRoff();
		return;
	}

	// Check back in frameTime to get the next roff entry
	client_camera.next_roff_time += roff->mFrameTime;
 }
}

void CMD_CGCam_Disable( void )
{
	vec4_t	fade = {0, 0, 0, 0};

	CGCam_Disable();
	CGCam_SetFade( fade );
	player_locked = qfalse;
}