From 01beb9919b95479d8be040bec74abc5cc67a5e43 Mon Sep 17 00:00:00 2001
From: Mikko Tiusanen <ams@daug.net>
Date: Sun, 4 May 2014 01:18:52 +0300
Subject: Initial import.

---
 src/cgame/cg_animation.c   |  113 ++
 src/cgame/cg_animmapobj.c  |  227 +++
 src/cgame/cg_attachment.c  |  404 +++++
 src/cgame/cg_buildable.c   | 1547 +++++++++++++++++
 src/cgame/cg_consolecmds.c |  294 ++++
 src/cgame/cg_draw.c        | 4011 ++++++++++++++++++++++++++++++++++++++++++++
 src/cgame/cg_drawtools.c   |  439 +++++
 src/cgame/cg_ents.c        | 1253 ++++++++++++++
 src/cgame/cg_event.c       | 1305 ++++++++++++++
 src/cgame/cg_local.h       | 2124 +++++++++++++++++++++++
 src/cgame/cg_main.c        | 1963 ++++++++++++++++++++++
 src/cgame/cg_marks.c       |  287 ++++
 src/cgame/cg_particles.c   | 2599 ++++++++++++++++++++++++++++
 src/cgame/cg_players.c     | 2512 +++++++++++++++++++++++++++
 src/cgame/cg_playerstate.c |  324 ++++
 src/cgame/cg_predict.c     |  900 ++++++++++
 src/cgame/cg_public.h      |  262 +++
 src/cgame/cg_scanner.c     |  447 +++++
 src/cgame/cg_servercmds.c  | 1381 +++++++++++++++
 src/cgame/cg_snapshot.c    |  408 +++++
 src/cgame/cg_syscalls.asm  |  121 ++
 src/cgame/cg_syscalls.c    |  592 +++++++
 src/cgame/cg_trails.c      | 1531 +++++++++++++++++
 src/cgame/cg_tutorial.c    |  731 ++++++++
 src/cgame/cg_view.c        | 1517 +++++++++++++++++
 src/cgame/cg_weapons.c     | 2106 +++++++++++++++++++++++
 26 files changed, 29398 insertions(+)
 create mode 100644 src/cgame/cg_animation.c
 create mode 100644 src/cgame/cg_animmapobj.c
 create mode 100644 src/cgame/cg_attachment.c
 create mode 100644 src/cgame/cg_buildable.c
 create mode 100644 src/cgame/cg_consolecmds.c
 create mode 100644 src/cgame/cg_draw.c
 create mode 100644 src/cgame/cg_drawtools.c
 create mode 100644 src/cgame/cg_ents.c
 create mode 100644 src/cgame/cg_event.c
 create mode 100644 src/cgame/cg_local.h
 create mode 100644 src/cgame/cg_main.c
 create mode 100644 src/cgame/cg_marks.c
 create mode 100644 src/cgame/cg_particles.c
 create mode 100644 src/cgame/cg_players.c
 create mode 100644 src/cgame/cg_playerstate.c
 create mode 100644 src/cgame/cg_predict.c
 create mode 100644 src/cgame/cg_public.h
 create mode 100644 src/cgame/cg_scanner.c
 create mode 100644 src/cgame/cg_servercmds.c
 create mode 100644 src/cgame/cg_snapshot.c
 create mode 100644 src/cgame/cg_syscalls.asm
 create mode 100644 src/cgame/cg_syscalls.c
 create mode 100644 src/cgame/cg_trails.c
 create mode 100644 src/cgame/cg_tutorial.c
 create mode 100644 src/cgame/cg_view.c
 create mode 100644 src/cgame/cg_weapons.c

(limited to 'src/cgame')

diff --git a/src/cgame/cg_animation.c b/src/cgame/cg_animation.c
new file mode 100644
index 0000000..e92a6e5
--- /dev/null
+++ b/src/cgame/cg_animation.c
@@ -0,0 +1,113 @@
+/*
+===========================================================================
+Copyright (C) 1999-2005 Id Software, Inc.
+Copyright (C) 2000-2009 Darklegion Development
+
+This file is part of Tremulous.
+
+Tremulous 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.
+
+Tremulous 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 Tremulous; if not, write to the Free Software
+Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
+===========================================================================
+*/
+
+#include "cg_local.h"
+
+/*
+===============
+CG_RunLerpFrame
+
+Sets cg.snap, cg.oldFrame, and cg.backlerp
+cg.time should be between oldFrameTime and frameTime after exit
+===============
+*/
+void CG_RunLerpFrame( lerpFrame_t *lf, float scale )
+{
+  int     f, numFrames;
+  animation_t *anim;
+
+  // debugging tool to get no animations
+  if( cg_animSpeed.integer == 0 )
+  {
+    lf->oldFrame = lf->frame = lf->backlerp = 0;
+    return;
+  }
+
+  // if we have passed the current frame, move it to
+  // oldFrame and calculate a new frame
+  if( cg.time >= lf->frameTime )
+  {
+    lf->oldFrame = lf->frame;
+    lf->oldFrameTime = lf->frameTime;
+
+    // get the next frame based on the animation
+    anim = lf->animation;
+    if( !anim->frameLerp )
+      return;   // shouldn't happen
+
+    if( cg.time < lf->animationTime )
+      lf->frameTime = lf->animationTime;    // initial lerp
+    else
+      lf->frameTime = lf->oldFrameTime + anim->frameLerp;
+
+    f = ( lf->frameTime - lf->animationTime ) / anim->frameLerp;
+    f *= scale;
+    numFrames = anim->numFrames;
+
+    if( anim->flipflop )
+      numFrames *= 2;
+
+    if( f >= numFrames )
+    {
+      f -= numFrames;
+      if( anim->loopFrames )
+      {
+        f %= anim->loopFrames;
+        f += anim->numFrames - anim->loopFrames;
+      }
+      else
+      {
+        f = numFrames - 1;
+        // the animation is stuck at the end, so it
+        // can immediately transition to another sequence
+        lf->frameTime = cg.time;
+      }
+    }
+
+    if( anim->reversed )
+      lf->frame = anim->firstFrame + anim->numFrames - 1 - f;
+    else if( anim->flipflop && f >= anim->numFrames )
+      lf->frame = anim->firstFrame + anim->numFrames - 1 - ( f % anim->numFrames );
+    else
+      lf->frame = anim->firstFrame + f;
+
+    if( cg.time > lf->frameTime )
+    {
+      lf->frameTime = cg.time;
+      if( cg_debugAnim.integer )
+        CG_Printf( "Clamp lf->frameTime\n" );
+    }
+  }
+
+  if( lf->frameTime > cg.time + 200 )
+    lf->frameTime = cg.time;
+
+  if( lf->oldFrameTime > cg.time )
+    lf->oldFrameTime = cg.time;
+
+  // calculate current lerp value
+  if( lf->frameTime == lf->oldFrameTime )
+    lf->backlerp = 0;
+  else
+    lf->backlerp = 1.0 - (float)( cg.time - lf->oldFrameTime ) / ( lf->frameTime - lf->oldFrameTime );
+}
diff --git a/src/cgame/cg_animmapobj.c b/src/cgame/cg_animmapobj.c
new file mode 100644
index 0000000..72464f7
--- /dev/null
+++ b/src/cgame/cg_animmapobj.c
@@ -0,0 +1,227 @@
+/*
+===========================================================================
+Copyright (C) 1999-2005 Id Software, Inc.
+Copyright (C) 2000-2009 Darklegion Development
+
+This file is part of Tremulous.
+
+Tremulous 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.
+
+Tremulous 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 Tremulous; if not, write to the Free Software
+Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
+===========================================================================
+*/
+
+#include "cg_local.h"
+
+
+/*
+===============
+CG_DoorAnimation
+===============
+*/
+static void CG_DoorAnimation( centity_t *cent, int *old, int *now, float *backLerp )
+{
+  CG_RunLerpFrame( &cent->lerpFrame, 1.0f );
+
+  *old      = cent->lerpFrame.oldFrame;
+  *now      = cent->lerpFrame.frame;
+  *backLerp = cent->lerpFrame.backlerp;
+}
+
+
+/*
+===============
+CG_ModelDoor
+===============
+*/
+void CG_ModelDoor( centity_t *cent )
+{
+  refEntity_t     ent;
+  entityState_t   *es;
+  animation_t     anim;
+  lerpFrame_t     *lf = &cent->lerpFrame;
+
+  es = &cent->currentState;
+
+  if( !es->modelindex )
+    return;
+
+  //create the render entity
+  memset( &ent, 0, sizeof( ent ) );
+  VectorCopy( cent->lerpOrigin, ent.origin );
+  VectorCopy( cent->lerpOrigin, ent.oldorigin );
+  AnglesToAxis( cent->lerpAngles, ent.axis );
+
+  ent.renderfx = RF_NOSHADOW;
+
+  //add the door model
+  ent.skinNum = 0;
+  ent.hModel = cgs.gameModels[ es->modelindex ];
+
+  //scale the door
+  VectorScale( ent.axis[ 0 ], es->origin2[ 0 ], ent.axis[ 0 ] );
+  VectorScale( ent.axis[ 1 ], es->origin2[ 1 ], ent.axis[ 1 ] );
+  VectorScale( ent.axis[ 2 ], es->origin2[ 2 ], ent.axis[ 2 ] );
+  ent.nonNormalizedAxes = qtrue;
+
+  //setup animation
+  anim.firstFrame   = es->misc;
+  anim.numFrames    = es->weapon;
+  anim.reversed     = !es->legsAnim;
+  anim.flipflop     = qfalse;
+  anim.loopFrames   = 0;
+  anim.frameLerp    = 1000 / es->torsoAnim;
+  anim.initialLerp  = 1000 / es->torsoAnim;
+
+  //door changed state
+  if( es->legsAnim != cent->doorState )
+  {
+    lf->animationTime = lf->frameTime + anim.initialLerp;
+    cent->doorState = es->legsAnim;
+  }
+
+  lf->animation = &anim;
+
+  //run animation
+  CG_DoorAnimation( cent, &ent.oldframe, &ent.frame, &ent.backlerp );
+
+  trap_R_AddRefEntityToScene( &ent );
+}
+
+
+/*
+===============
+CG_AMOAnimation
+===============
+*/
+static void CG_AMOAnimation( centity_t *cent, int *old, int *now, float *backLerp )
+{
+  if( !( cent->currentState.eFlags & EF_MOVER_STOP ) || cent->animPlaying )
+  {
+    int delta = cg.time - cent->miscTime;
+
+    //hack to prevent "pausing" mucking up the lerping
+    if( delta > 900 )
+    {
+      cent->lerpFrame.oldFrameTime  += delta;
+      cent->lerpFrame.frameTime     += delta;
+    }
+
+    CG_RunLerpFrame( &cent->lerpFrame, 1.0f );
+    cent->miscTime = cg.time;
+  }
+
+  *old      = cent->lerpFrame.oldFrame;
+  *now      = cent->lerpFrame.frame;
+  *backLerp = cent->lerpFrame.backlerp;
+}
+
+
+/*
+==================
+CG_animMapObj
+==================
+*/
+void CG_AnimMapObj( centity_t *cent )
+{
+  refEntity_t     ent;
+  entityState_t   *es;
+  float           scale;
+  animation_t     anim;
+
+  es = &cent->currentState;
+
+  // if set to invisible, skip
+  if( !es->modelindex || ( es->eFlags & EF_NODRAW ) )
+    return;
+
+  memset( &ent, 0, sizeof( ent ) );
+
+  VectorCopy( es->angles, cent->lerpAngles );
+  AnglesToAxis( cent->lerpAngles, ent.axis );
+
+  ent.hModel = cgs.gameModels[ es->modelindex ];
+
+  VectorCopy( cent->lerpOrigin, ent.origin);
+  VectorCopy( cent->lerpOrigin, ent.oldorigin);
+
+  ent.nonNormalizedAxes = qfalse;
+
+  //scale the model
+  if( es->angles2[ 0 ] )
+  {
+    scale = es->angles2[ 0 ];
+    VectorScale( ent.axis[ 0 ], scale, ent.axis[ 0 ] );
+    VectorScale( ent.axis[ 1 ], scale, ent.axis[ 1 ] );
+    VectorScale( ent.axis[ 2 ], scale, ent.axis[ 2 ] );
+    ent.nonNormalizedAxes = qtrue;
+  }
+
+  //setup animation
+  anim.firstFrame = es->misc;
+  anim.numFrames = es->weapon;
+  anim.reversed = qfalse;
+  anim.flipflop = qfalse;
+
+  // if numFrames is negative the animation is reversed
+  if( anim.numFrames < 0 )
+  {
+    anim.numFrames = -anim.numFrames;
+    anim.reversed = qtrue;
+  }
+
+  anim.loopFrames = es->torsoAnim;
+
+  if( !es->legsAnim )
+  {
+    anim.frameLerp = 1000;
+    anim.initialLerp = 1000;
+  }
+  else
+  {
+    anim.frameLerp = 1000 / es->legsAnim;
+    anim.initialLerp = 1000 / es->legsAnim;
+  }
+
+  cent->lerpFrame.animation = &anim;
+ 
+  if( !anim.loopFrames )
+  {
+    // add one frame to allow the animation to play the last frame
+    // add another to get it to stop playing at the first frame
+    anim.numFrames += 2;
+
+    if( !cent->animInit )
+    {
+      cent->animInit = qtrue;
+      cent->animPlaying = !( cent->currentState.eFlags & EF_MOVER_STOP );
+    }
+    else
+    {
+      if( cent->animLastState !=
+          !( cent->currentState.eFlags & EF_MOVER_STOP ) )
+      {
+        cent->animPlaying = qtrue;
+        cent->lerpFrame.animationTime = cg.time;
+        cent->lerpFrame.frameTime = cg.time;
+      }
+    }
+    cent->animLastState = !( cent->currentState.eFlags & EF_MOVER_STOP );
+  }
+
+  //run animation
+  CG_AMOAnimation( cent, &ent.oldframe, &ent.frame, &ent.backlerp );
+
+  // add to refresh list
+  trap_R_AddRefEntityToScene(&ent);
+}
diff --git a/src/cgame/cg_attachment.c b/src/cgame/cg_attachment.c
new file mode 100644
index 0000000..da094a5
--- /dev/null
+++ b/src/cgame/cg_attachment.c
@@ -0,0 +1,404 @@
+/*
+===========================================================================
+Copyright (C) 2000-2009 Darklegion Development
+
+This file is part of Tremulous.
+
+Tremulous 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.
+
+Tremulous 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 Tremulous; if not, write to the Free Software
+Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
+===========================================================================
+*/
+
+// cg_attachment.c -- an abstract attachment system
+
+#include "cg_local.h"
+
+/*
+===============
+CG_AttachmentPoint
+
+Return the attachment point
+===============
+*/
+qboolean CG_AttachmentPoint( attachment_t *a, vec3_t v )
+{
+  centity_t   *cent;
+
+  if( !a )
+    return qfalse;
+
+  // if it all breaks, then use the last point we know was correct
+  VectorCopy( a->lastValidAttachmentPoint, v );
+
+  switch( a->type )
+  {
+    case AT_STATIC:
+      if( !a->staticValid )
+        return qfalse;
+
+      VectorCopy( a->origin, v );
+      break;
+
+    case AT_TAG:
+      if( !a->tagValid )
+        return qfalse;
+
+      AxisCopy( axisDefault, a->re.axis );
+      CG_PositionRotatedEntityOnTag( &a->re, &a->parent,
+                                     a->model, a->tagName );
+      VectorCopy( a->re.origin, v );
+      break;
+
+    case AT_CENT:
+      if( !a->centValid )
+        return qfalse;
+
+      if( a->centNum == cg.predictedPlayerState.clientNum )
+      {
+        // this is smoother if it's the local client
+        VectorCopy( cg.predictedPlayerState.origin, v );
+      }
+      else
+      {
+        cent = &cg_entities[ a->centNum ];
+        VectorCopy( cent->lerpOrigin, v );
+      }
+      break;
+
+    case AT_PARTICLE:
+      if( !a->particleValid )
+        return qfalse;
+
+      if( !a->particle->valid )
+      {
+        a->particleValid = qfalse;
+        return qfalse;
+      }
+      else
+        VectorCopy( a->particle->origin, v );
+      break;
+
+    default:
+      CG_Printf( S_COLOR_RED "ERROR: Invalid attachmentType_t in attachment\n" );
+      break;
+  }
+
+  if( a->hasOffset )
+    VectorAdd( v, a->offset, v );
+
+  VectorCopy( v, a->lastValidAttachmentPoint );
+
+  return qtrue;
+}
+
+/*
+===============
+CG_AttachmentDir
+
+Return the attachment direction
+===============
+*/
+qboolean CG_AttachmentDir( attachment_t *a, vec3_t v )
+{
+  vec3_t      forward;
+  centity_t   *cent;
+
+  if( !a )
+    return qfalse;
+
+  switch( a->type )
+  {
+    case AT_STATIC:
+      return qfalse;
+      break;
+
+    case AT_TAG:
+      if( !a->tagValid )
+        return qfalse;
+
+      VectorCopy( a->re.axis[ 0 ], v );
+      break;
+
+    case AT_CENT:
+      if( !a->centValid )
+        return qfalse;
+
+      cent = &cg_entities[ a->centNum ];
+      AngleVectors( cent->lerpAngles, forward, NULL, NULL );
+      VectorCopy( forward, v );
+      break;
+
+    case AT_PARTICLE:
+      if( !a->particleValid )
+        return qfalse;
+
+      if( !a->particle->valid )
+      {
+        a->particleValid = qfalse;
+        return qfalse;
+      }
+      else
+        VectorCopy( a->particle->velocity, v );
+      break;
+
+    default:
+      CG_Printf( S_COLOR_RED "ERROR: Invalid attachmentType_t in attachment\n" );
+      break;
+  }
+
+  VectorNormalize( v );
+  return qtrue;
+}
+
+/*
+===============
+CG_AttachmentAxis
+
+Return the attachment axis
+===============
+*/
+qboolean CG_AttachmentAxis( attachment_t *a, vec3_t axis[ 3 ] )
+{
+  centity_t   *cent;
+
+  if( !a )
+    return qfalse;
+
+  switch( a->type )
+  {
+    case AT_STATIC:
+      return qfalse;
+      break;
+
+    case AT_TAG:
+      if( !a->tagValid )
+        return qfalse;
+
+      AxisCopy( a->re.axis, axis );
+      break;
+
+    case AT_CENT:
+      if( !a->centValid )
+        return qfalse;
+
+      cent = &cg_entities[ a->centNum ];
+      AnglesToAxis( cent->lerpAngles, axis );
+      break;
+
+    case AT_PARTICLE:
+      return qfalse;
+      break;
+
+    default:
+      CG_Printf( S_COLOR_RED "ERROR: Invalid attachmentType_t in attachment\n" );
+      break;
+  }
+
+  return qtrue;
+}
+
+/*
+===============
+CG_AttachmentVelocity
+
+If the attachment can have velocity, return it
+===============
+*/
+qboolean CG_AttachmentVelocity( attachment_t *a, vec3_t v )
+{
+  if( !a )
+    return qfalse;
+
+  if( a->particleValid && a->particle->valid )
+  {
+    VectorCopy( a->particle->velocity, v );
+    return qtrue;
+  }
+  else if( a->centValid )
+  {
+    centity_t *cent = &cg_entities[ a->centNum ];
+
+    VectorCopy( cent->currentState.pos.trDelta, v );
+    return qtrue;
+  }
+
+  return qfalse;
+}
+
+/*
+===============
+CG_AttachmentCentNum
+
+If the attachment has a centNum, return it
+===============
+*/
+int CG_AttachmentCentNum( attachment_t *a )
+{
+  if( !a || !a->centValid )
+    return -1;
+
+  return a->centNum;
+}
+
+/*
+===============
+CG_Attached
+
+If the attachment is valid, return qtrue
+===============
+*/
+qboolean CG_Attached( attachment_t *a )
+{
+  if( !a )
+    return qfalse;
+
+  return a->attached;
+}
+
+/*
+===============
+CG_AttachToPoint
+
+Attach to a point in space
+===============
+*/
+void CG_AttachToPoint( attachment_t *a )
+{
+  if( !a || !a->staticValid )
+    return;
+
+  a->type = AT_STATIC;
+  a->attached = qtrue;
+}
+
+/*
+===============
+CG_AttachToCent
+
+Attach to a centity_t
+===============
+*/
+void CG_AttachToCent( attachment_t *a )
+{
+  if( !a || !a->centValid )
+    return;
+
+  a->type = AT_CENT;
+  a->attached = qtrue;
+}
+
+/*
+===============
+CG_AttachToTag
+
+Attach to a model tag
+===============
+*/
+void CG_AttachToTag( attachment_t *a )
+{
+  if( !a || !a->tagValid )
+    return;
+
+  a->type = AT_TAG;
+  a->attached = qtrue;
+}
+
+/*
+===============
+CG_AttachToParticle
+
+Attach to a particle
+===============
+*/
+void CG_AttachToParticle( attachment_t *a )
+{
+  if( !a || !a->particleValid )
+    return;
+
+  a->type = AT_PARTICLE;
+  a->attached = qtrue;
+}
+
+/*
+===============
+CG_SetAttachmentPoint
+===============
+*/
+void CG_SetAttachmentPoint( attachment_t *a, vec3_t v )
+{
+  if( !a )
+    return;
+
+  VectorCopy( v, a->origin );
+  a->staticValid = qtrue;
+}
+
+/*
+===============
+CG_SetAttachmentCent
+===============
+*/
+void CG_SetAttachmentCent( attachment_t *a, centity_t *cent )
+{
+  if( !a || !cent )
+    return;
+
+  a->centNum = cent->currentState.number;
+  a->centValid = qtrue;
+}
+
+/*
+===============
+CG_SetAttachmentTag
+===============
+*/
+void CG_SetAttachmentTag( attachment_t *a, refEntity_t parent,
+    qhandle_t model, char *tagName )
+{
+  if( !a )
+    return;
+
+  a->parent = parent;
+  a->model = model;
+  strncpy( a->tagName, tagName, MAX_STRING_CHARS );
+  a->tagValid = qtrue;
+}
+
+/*
+===============
+CG_SetAttachmentParticle
+===============
+*/
+void CG_SetAttachmentParticle( attachment_t *a, particle_t *p )
+{
+  if( !a )
+    return;
+
+  a->particle = p;
+  a->particleValid = qtrue;
+}
+
+/*
+===============
+CG_SetAttachmentOffset
+===============
+*/
+void CG_SetAttachmentOffset( attachment_t *a, vec3_t v )
+{
+  if( !a )
+    return;
+
+  VectorCopy( v, a->offset );
+  a->hasOffset = qtrue;
+}
diff --git a/src/cgame/cg_buildable.c b/src/cgame/cg_buildable.c
new file mode 100644
index 0000000..aaeaf11
--- /dev/null
+++ b/src/cgame/cg_buildable.c
@@ -0,0 +1,1547 @@
+/*
+===========================================================================
+Copyright (C) 1999-2005 Id Software, Inc.
+Copyright (C) 2000-2009 Darklegion Development
+
+This file is part of Tremulous.
+
+Tremulous 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.
+
+Tremulous 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 Tremulous; if not, write to the Free Software
+Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
+===========================================================================
+*/
+
+
+#include "cg_local.h"
+
+char *cg_buildableSoundNames[ MAX_BUILDABLE_ANIMATIONS ] =
+{
+  "construct1.wav",
+  "construct2.wav",
+  "idle1.wav",
+  "idle2.wav",
+  "idle3.wav",
+  "attack1.wav",
+  "attack2.wav",
+  "spawn1.wav",
+  "spawn2.wav",
+  "pain1.wav",
+  "pain2.wav",
+  "destroy1.wav",
+  "destroy2.wav",
+  "destroyed.wav"
+};
+
+static sfxHandle_t defaultAlienSounds[ MAX_BUILDABLE_ANIMATIONS ];
+static sfxHandle_t defaultHumanSounds[ MAX_BUILDABLE_ANIMATIONS ];
+
+/*
+===================
+CG_AlienBuildableExplosion
+
+Generated a bunch of gibs launching out from a location
+===================
+*/
+void CG_AlienBuildableExplosion( vec3_t origin, vec3_t dir, int buildable )
+{
+  particleSystem_t  *ps;
+
+  trap_S_StartSound( origin, ENTITYNUM_WORLD, CHAN_AUTO, cgs.media.alienBuildableExplosion );
+
+  //particle system
+  if (buildable == BA_A_OVERMIND)
+    ps = CG_SpawnNewParticleSystem( cgs.media.alienBuildableDestroyedPS );
+  else
+    ps = CG_SpawnNewParticleSystem( cgs.media.alienBuildableDestroyedPS );
+
+  if( CG_IsParticleSystemValid( &ps ) )
+  {
+    CG_SetAttachmentPoint( &ps->attachment, origin );
+    CG_SetParticleSystemNormal( ps, dir );
+    CG_AttachToPoint( &ps->attachment );
+  }
+}
+
+
+/*
+===================
+CG_AlienSPITEFUL_ABCESSExplosion
+
+Generated a bunch of gibs launching out from a location
+===================
+*/
+void CG_AlienSPITEFUL_ABCESSExplosion( vec3_t origin, vec3_t dir )
+{
+  particleSystem_t  *ps;
+
+  trap_S_StartSound( origin, ENTITYNUM_WORLD, CHAN_AUTO, cgs.media.alienBuildableExplosion );
+
+  //particle system
+  ps = CG_SpawnNewParticleSystem( cgs.media.alienSpiteful_AbcessDestroyedPS );
+
+  if( CG_IsParticleSystemValid( &ps ) )
+  {
+    CG_SetAttachmentPoint( &ps->attachment, origin );
+    CG_SetParticleSystemNormal( ps, dir );
+    CG_AttachToPoint( &ps->attachment );
+  }
+}
+
+/*
+=================
+CG_HumanBuildableExplosion
+
+Called for human buildables as they are destroyed
+=================
+*/
+void CG_HumanBuildableExplosion( vec3_t origin, vec3_t dir, int buildable )
+{
+  particleSystem_t  *ps;
+
+  trap_S_StartSound( origin, ENTITYNUM_WORLD, CHAN_AUTO, cgs.media.humanBuildableExplosion );
+
+  //particle system
+  if (buildable == BA_H_REACTOR)
+    ps = CG_SpawnNewParticleSystem( cgs.media.humanBuildableDestroyedPS );
+  else
+    ps = CG_SpawnNewParticleSystem( cgs.media.humanBuildableDestroyedPS );
+
+  if( CG_IsParticleSystemValid( &ps ) )
+  {
+    CG_SetAttachmentPoint( &ps->attachment, origin );
+    CG_SetParticleSystemNormal( ps, dir );
+    CG_AttachToPoint( &ps->attachment );
+  }
+}
+
+
+#define CREEP_SIZE            64.0f
+#define CREEP_DISTANCE        228.0f
+
+/*
+==================
+CG_Creep
+==================
+*/
+static void CG_Creep( centity_t *cent )
+{
+  int           msec;
+  float         size, frac;
+  trace_t       tr;
+  vec3_t        temp, origin;
+  int           scaleUpTime = BG_Buildable( cent->currentState.modelindex )->buildTime;
+  int           time;
+  int creepSize;
+  time = cent->currentState.time;
+
+  //should the creep be growing or receding?
+  if( time >= 0 )
+  {
+    msec = cg.time - time;
+    if( msec >= 0 && msec < scaleUpTime )
+      frac = (float)msec / scaleUpTime;
+    else
+      frac = 1.0f;
+  }
+  else if( time < 0 )
+  {
+    msec = cg.time + time;
+    if( msec >= 0 && msec < CREEP_SCALEDOWN_TIME )
+      frac = 1.0f - ( (float)msec / CREEP_SCALEDOWN_TIME );
+    else
+      frac = 0.0f;
+  }
+
+  VectorCopy( cent->currentState.origin2, temp );
+  VectorScale( temp, -CREEP_DISTANCE, temp );
+  VectorAdd( temp, cent->lerpOrigin, temp );
+
+  
+  
+  CG_Trace( &tr, cent->lerpOrigin, NULL, NULL, temp, cent->currentState.number, MASK_PLAYERSOLID );
+  VectorCopy( tr.endpos, origin );
+  
+
+//  size = CREEP_SIZE * frac;
+size = BG_Buildable( cent->currentState.modelindex )->creepSize * frac;
+
+  if( size > 0.0f && tr.fraction < 1.0f )
+    CG_ImpactMark( cgs.media.creepShader, //qhandle_t markShader
+								  origin, //const vec3_t origin 
+			  cent->currentState.origin2, //const vec3_t dir
+									0.0f, //float orientation
+									1.0f, //red
+									1.0f, //green
+									1.0f, //blue
+									1.0f, //float alpha
+								  qtrue, //qboolean alphaFade
+								    size, //float radius
+								   qtrue);//qboolean temporary
+								   
+					   
+
+}
+
+
+/*
+======================
+CG_ParseBuildableAnimationFile
+
+Read a configuration file containing animation counts and rates
+models/buildables/hivemind/animation.cfg, etc
+======================
+*/
+static qboolean CG_ParseBuildableAnimationFile( const char *filename, buildable_t buildable )
+{
+  char          *text_p;
+  int           len;
+  int           i;
+  char          *token;
+  float         fps;
+  char          text[ 20000 ];
+  fileHandle_t  f;
+  animation_t   *animations;
+
+  animations = cg_buildables[ buildable ].animations;
+
+  // load the file
+  len = trap_FS_FOpenFile( filename, &f, FS_READ );
+  if( len < 0 )
+    return qfalse;
+
+  if( len == 0 || len >= sizeof( text ) - 1 )
+  {
+    trap_FS_FCloseFile( f );
+    CG_Printf( "File %s is %s\n", filename, len == 0 ? "empty" : "too long" );
+    return qfalse;
+  }
+
+  trap_FS_Read( text, len, f );
+  text[ len ] = 0;
+  trap_FS_FCloseFile( f );
+
+  // parse the text
+  text_p = text;
+
+  // read information for each frame
+  for( i = BANIM_NONE + 1; i < MAX_BUILDABLE_ANIMATIONS; i++ )
+  {
+
+    token = COM_Parse( &text_p );
+    if( !*token )
+      break;
+
+    animations[ i ].firstFrame = atoi( token );
+
+    token = COM_Parse( &text_p );
+    if( !*token )
+      break;
+
+    animations[ i ].numFrames = atoi( token );
+    animations[ i ].reversed = qfalse;
+    animations[ i ].flipflop = qfalse;
+
+    // if numFrames is negative the animation is reversed
+    if( animations[ i ].numFrames < 0 )
+    {
+      animations[ i ].numFrames = -animations[ i ].numFrames;
+      animations[ i ].reversed = qtrue;
+    }
+
+    token = COM_Parse( &text_p );
+    if ( !*token )
+      break;
+
+    animations[i].loopFrames = atoi( token );
+
+    token = COM_Parse( &text_p );
+    if( !*token )
+      break;
+
+    fps = atof( token );
+    if( fps == 0 )
+      fps = 1;
+
+    animations[ i ].frameLerp = 1000 / fps;
+    animations[ i ].initialLerp = 1000 / fps;
+  }
+
+  if( i != MAX_BUILDABLE_ANIMATIONS )
+  {
+    CG_Printf( "Error parsing animation file: %s\n", filename );
+    return qfalse;
+  }
+
+  return qtrue;
+}
+
+/*
+======================
+CG_ParseBuildableSoundFile
+
+Read a configuration file containing sound properties
+sound/buildables/hivemind/sound.cfg, etc
+======================
+*/
+static qboolean CG_ParseBuildableSoundFile( const char *filename, buildable_t buildable )
+{
+  char          *text_p;
+  int           len;
+  int           i;
+  char          *token;
+  char          text[ 20000 ];
+  fileHandle_t  f;
+  sound_t       *sounds;
+
+  sounds = cg_buildables[ buildable ].sounds;
+
+  // load the file
+  len = trap_FS_FOpenFile( filename, &f, FS_READ );
+  if ( len < 0 )
+    return qfalse;
+
+  if ( len == 0 || len >= sizeof( text ) - 1 )
+  {
+    trap_FS_FCloseFile( f );
+    CG_Printf( "File %s is %s\n", filename, len == 0 ? "empty" : "too long" );
+    return qfalse;
+  }
+
+  trap_FS_Read( text, len, f );
+  text[len] = 0;
+  trap_FS_FCloseFile( f );
+
+  // parse the text
+  text_p = text;
+
+  // read information for each frame
+  for( i = BANIM_NONE + 1; i < MAX_BUILDABLE_ANIMATIONS; i++ )
+  {
+
+    token = COM_Parse( &text_p );
+    if ( !*token )
+      break;
+
+    sounds[ i ].enabled = atoi( token );
+
+    token = COM_Parse( &text_p );
+    if ( !*token )
+      break;
+
+    sounds[ i ].looped = atoi( token );
+
+  }
+
+  if( i != MAX_BUILDABLE_ANIMATIONS )
+  {
+    CG_Printf( "Error parsing sound file: %s\n", filename );
+    return qfalse;
+  }
+
+  return qtrue;
+}
+/*
+===============
+CG_InitBuildables
+
+Initialises the animation db
+===============
+*/
+void CG_InitBuildables( void )
+{
+  char          filename[ MAX_QPATH ];
+  char          soundfile[ MAX_QPATH ];
+  char          *buildableName;
+  char          *modelFile;
+  int           i;
+  int           j;
+  fileHandle_t  f;
+
+  memset( cg_buildables, 0, sizeof( cg_buildables ) );
+
+  //default sounds
+  for( j = BANIM_NONE + 1; j < MAX_BUILDABLE_ANIMATIONS; j++ )
+  {
+    strcpy( soundfile, cg_buildableSoundNames[ j - 1 ] );
+
+    Com_sprintf( filename, sizeof( filename ), "sound/buildables/alien/%s", soundfile );
+    defaultAlienSounds[ j ] = trap_S_RegisterSound( filename, qfalse );
+
+    Com_sprintf( filename, sizeof( filename ), "sound/buildables/human/%s", soundfile );
+    defaultHumanSounds[ j ] = trap_S_RegisterSound( filename, qfalse );
+  }
+
+  cg.buildablesFraction = 0.0f;
+
+  for( i = BA_NONE + 1; i < BA_NUM_BUILDABLES; i++ )
+  {
+    buildableName = BG_Buildable( i )->name;
+
+    //animation.cfg
+    Com_sprintf( filename, sizeof( filename ), "models/buildables/%s/animation.cfg", buildableName );
+    if ( !CG_ParseBuildableAnimationFile( filename, i ) )
+      Com_Printf( S_COLOR_YELLOW "WARNING: failed to load animation file %s\n", filename );
+
+    //sound.cfg
+    Com_sprintf( filename, sizeof( filename ), "sound/buildables/%s/sound.cfg", buildableName );
+    if ( !CG_ParseBuildableSoundFile( filename, i ) )
+      Com_Printf( S_COLOR_YELLOW "WARNING: failed to load sound file %s\n", filename );
+
+    //models
+    for( j = 0; j <= 3; j++ )
+    {
+      modelFile = BG_BuildableConfig( i )->models[ j ];
+      if( strlen( modelFile ) > 0 )
+        cg_buildables[ i ].models[ j ] = trap_R_RegisterModel( modelFile );
+    }
+
+    //sounds
+    for( j = BANIM_NONE + 1; j < MAX_BUILDABLE_ANIMATIONS; j++ )
+    {
+      strcpy( soundfile, cg_buildableSoundNames[ j - 1 ] );
+      Com_sprintf( filename, sizeof( filename ), "sound/buildables/%s/%s", buildableName, soundfile );
+
+      if( cg_buildables[ i ].sounds[ j ].enabled )
+      {
+        if( trap_FS_FOpenFile( filename, &f, FS_READ ) > 0 )
+        {
+          //file exists so close it
+          trap_FS_FCloseFile( f );
+
+          cg_buildables[ i ].sounds[ j ].sound = trap_S_RegisterSound( filename, qfalse );
+        }
+        else
+        {
+          //file doesn't exist - use default
+          if( BG_Buildable( i )->team == TEAM_ALIENS )
+            cg_buildables[ i ].sounds[ j ].sound = defaultAlienSounds[ j ];
+          else
+            cg_buildables[ i ].sounds[ j ].sound = defaultHumanSounds[ j ];
+        }
+      }
+    }
+
+    cg.buildablesFraction = (float)i / (float)( BA_NUM_BUILDABLES - 1 );
+    trap_UpdateScreen( );
+  }
+
+  cgs.media.teslaZapTS = CG_RegisterTrailSystem( "models/buildables/tesla/zap" );
+  //f�r slime
+  cgs.media.slimeTS = CG_RegisterTrailSystem( "models/buildables/infestationslime" );
+
+}
+
+/*
+===============
+CG_SetBuildableLerpFrameAnimation
+
+may include ANIM_TOGGLEBIT
+===============
+*/
+static void CG_SetBuildableLerpFrameAnimation( buildable_t buildable, lerpFrame_t *lf, int newAnimation )
+{
+  animation_t *anim;
+
+  lf->animationNumber = newAnimation;
+
+  if( newAnimation < 0 || newAnimation >= MAX_BUILDABLE_ANIMATIONS )
+    CG_Error( "Bad animation number: %i", newAnimation );
+
+  anim = &cg_buildables[ buildable ].animations[ newAnimation ];
+
+  //this item has just spawned so lf->frameTime will be zero
+  if( !lf->animation )
+    lf->frameTime = cg.time + 1000; //1 sec delay before starting the spawn anim
+
+  lf->animation = anim;
+  lf->animationTime = lf->frameTime + anim->initialLerp;
+
+  if( cg_debugAnim.integer )
+    CG_Printf( "Anim: %i\n", newAnimation );
+}
+
+/*
+===============
+CG_RunBuildableLerpFrame
+
+Sets cg.snap, cg.oldFrame, and cg.backlerp
+cg.time should be between oldFrameTime and frameTime after exit
+===============
+*/
+static void CG_RunBuildableLerpFrame( centity_t *cent )
+{
+  buildable_t           buildable = cent->currentState.modelindex;
+  lerpFrame_t           *lf = &cent->lerpFrame;
+  buildableAnimNumber_t newAnimation = cent->buildableAnim & ~( ANIM_TOGGLEBIT|ANIM_FORCEBIT );
+
+  // see if the animation sequence is switching
+  if( newAnimation != lf->animationNumber || !lf->animation )
+  {
+    if( cg_debugRandom.integer )
+      CG_Printf( "newAnimation: %d lf->animationNumber: %d lf->animation: %d\n",
+                 newAnimation, lf->animationNumber, lf->animation );
+
+    CG_SetBuildableLerpFrameAnimation( buildable, lf, newAnimation );
+
+    if( !cg_buildables[ buildable ].sounds[ newAnimation ].looped &&
+        cg_buildables[ buildable ].sounds[ newAnimation ].enabled )
+    {
+      if( cg_debugRandom.integer )
+        CG_Printf( "Sound for animation %d for a %s\n",
+            newAnimation, BG_Buildable( buildable )->humanName );
+
+      trap_S_StartSound( cent->lerpOrigin, cent->currentState.number, CHAN_AUTO,
+        cg_buildables[ buildable ].sounds[ newAnimation ].sound );
+    }
+  }
+
+  if( cg_buildables[ buildable ].sounds[ lf->animationNumber ].looped &&
+      cg_buildables[ buildable ].sounds[ lf->animationNumber ].enabled )
+    trap_S_AddLoopingSound( cent->currentState.number, cent->lerpOrigin, vec3_origin,
+      cg_buildables[ buildable ].sounds[ lf->animationNumber ].sound );
+
+  CG_RunLerpFrame( lf, 1.0f );
+
+  // animation ended
+  if( lf->frameTime == cg.time )
+  {
+    cent->buildableAnim = cent->currentState.torsoAnim;
+    cent->buildableIdleAnim = qtrue;
+  }
+}
+
+/*
+===============
+CG_BuildableAnimation
+===============
+*/
+static void CG_BuildableAnimation( centity_t *cent, int *old, int *now, float *backLerp )
+{
+  entityState_t *es = &cent->currentState;
+
+  //if no animation is set default to idle anim
+  if( cent->buildableAnim == BANIM_NONE )
+  {
+    cent->buildableAnim = es->torsoAnim;
+    cent->buildableIdleAnim = qtrue;
+  }
+
+  //display the first frame of the construction anim if not yet spawned
+  if( !( es->eFlags & EF_B_SPAWNED ) )
+  {
+    animation_t *anim = &cg_buildables[ es->modelindex ].animations[ BANIM_CONSTRUCT1 ];
+
+    //so that when animation starts for real it has sensible numbers
+    cent->lerpFrame.oldFrameTime =
+      cent->lerpFrame.frameTime =
+      cent->lerpFrame.animationTime =
+      cg.time;
+
+    *old      = cent->lerpFrame.oldFrame = anim->firstFrame;
+    *now      = cent->lerpFrame.frame    = anim->firstFrame;
+    *backLerp = cent->lerpFrame.backlerp = 0.0f;
+
+    //ensure that an animation is triggered once the buildable has spawned
+    cent->oldBuildableAnim = BANIM_NONE;
+  }
+  else
+  {
+    if( ( cent->oldBuildableAnim ^ es->legsAnim ) & ANIM_TOGGLEBIT )
+    {
+      if( cg_debugAnim.integer )
+        CG_Printf( "%d->%d l:%d t:%d %s(%d)\n",
+                   cent->oldBuildableAnim, cent->buildableAnim,
+                   es->legsAnim, es->torsoAnim,
+                   BG_Buildable( es->modelindex )->humanName, es->number );
+
+      if( cent->buildableAnim == es->torsoAnim || es->legsAnim & ANIM_FORCEBIT )
+      {
+        cent->buildableAnim = cent->oldBuildableAnim = es->legsAnim;
+        cent->buildableIdleAnim = qfalse;
+      }
+      else
+      {
+        cent->buildableAnim = cent->oldBuildableAnim = es->torsoAnim;
+        cent->buildableIdleAnim = qtrue;
+      }
+    }
+    else if( cent->buildableIdleAnim == qtrue &&
+             cent->buildableAnim != es->torsoAnim )
+    {
+      cent->buildableAnim = es->torsoAnim;
+    }
+
+    CG_RunBuildableLerpFrame( cent );
+
+    *old      = cent->lerpFrame.oldFrame;
+    *now      = cent->lerpFrame.frame;
+    *backLerp = cent->lerpFrame.backlerp;
+  }
+}
+
+#define TRACE_DEPTH 64.0f
+
+/*
+===============
+CG_PositionAndOrientateBuildable
+===============
+*/
+static void CG_PositionAndOrientateBuildable( const vec3_t angles, const vec3_t inOrigin,
+                                              const vec3_t normal, const int skipNumber,
+                                              const vec3_t mins, const vec3_t maxs,
+                                              vec3_t outAxis[ 3 ], vec3_t outOrigin )
+{
+  vec3_t  forward, start, end;
+  trace_t tr, box_tr;
+  float mag, fraction;
+
+  AngleVectors( angles, forward, NULL, NULL );
+  VectorCopy( normal, outAxis[ 2 ] );
+  ProjectPointOnPlane( outAxis[ 0 ], forward, outAxis[ 2 ] );
+
+  if( !VectorNormalize( outAxis[ 0 ] ) )
+  {
+    AngleVectors( angles, NULL, NULL, forward );
+    ProjectPointOnPlane( outAxis[ 0 ], forward, outAxis[ 2 ] );
+    VectorNormalize( outAxis[ 0 ] );
+  }
+
+  CrossProduct( outAxis[ 0 ], outAxis[ 2 ], outAxis[ 1 ] );
+  outAxis[ 1 ][ 0 ] = -outAxis[ 1 ][ 0 ];
+  outAxis[ 1 ][ 1 ] = -outAxis[ 1 ][ 1 ];
+  outAxis[ 1 ][ 2 ] = -outAxis[ 1 ][ 2 ];
+
+  VectorMA( inOrigin, -TRACE_DEPTH, normal, end );
+  VectorMA( inOrigin, 1.0f, normal, start );
+
+  // Take both capsule and box traces. If the capsule trace does not differ
+  //  significantly from the box trace use it. This may cause buildables to be
+  //  positioned *inside* the surface on which it is placed. This is intentional
+
+  CG_CapTrace( &tr, start, mins, maxs, end, skipNumber,
+               MASK_PLAYERSOLID );
+
+  CG_Trace( &box_tr, start, mins, maxs, end, skipNumber,
+            MASK_PLAYERSOLID );
+
+  mag = Distance( tr.endpos, box_tr.endpos );
+
+  fraction = tr.fraction;
+
+  // this is either too far off of the bbox to be useful for gameplay purposes
+  //  or the model is positioned in thin air anyways.
+  if( mag > 15.0f || tr.fraction == 1.0f )
+    fraction = box_tr.fraction; 
+
+  VectorMA( inOrigin, fraction * -TRACE_DEPTH, normal, outOrigin );
+}
+
+/*
+==================
+CG_GhostBuildable
+==================
+*/
+void CG_GhostBuildable( buildable_t buildable )
+{
+  refEntity_t     ent;
+  playerState_t   *ps;
+  vec3_t          angles, entity_origin;
+  vec3_t          mins, maxs;
+  trace_t         tr;
+  float           scale;
+
+  ps = &cg.predictedPlayerState;
+
+  memset( &ent, 0, sizeof( ent ) );
+
+  BG_BuildableBoundingBox( buildable, mins, maxs );
+
+  BG_PositionBuildableRelativeToPlayer( ps, mins, maxs, CG_Trace, entity_origin, angles, &tr );
+
+  CG_PositionAndOrientateBuildable( ps->viewangles, entity_origin, tr.plane.normal, ps->clientNum,
+                                    mins, maxs, ent.axis, ent.origin );
+
+  //offset on the Z axis if required
+  VectorMA( ent.origin, BG_BuildableConfig( buildable )->zOffset, tr.plane.normal, ent.origin );
+
+  VectorCopy( ent.origin, ent.lightingOrigin );
+  VectorCopy( ent.origin, ent.oldorigin ); // don't positionally lerp at all
+
+  ent.hModel = cg_buildables[ buildable ].models[ 0 ];
+
+  if( !(ps->stats[ STAT_BUILDABLE ] & SB_VALID_TOGGLEBIT) )
+  ent.customShader = cgs.media.redBuildShader;
+
+  //rescale the model
+  scale = BG_BuildableConfig( buildable )->modelScale;
+
+  if( scale != 1.0f )
+  {
+    VectorScale( ent.axis[ 0 ], scale, ent.axis[ 0 ] );
+    VectorScale( ent.axis[ 1 ], scale, ent.axis[ 1 ] );
+    VectorScale( ent.axis[ 2 ], scale, ent.axis[ 2 ] );
+
+    ent.nonNormalizedAxes = qtrue;
+  }
+  else
+    ent.nonNormalizedAxes = qfalse;
+
+  // add to refresh list
+  trap_R_AddRefEntityToScene( &ent );
+}
+
+/*
+==================
+CG_BuildableParticleEffects
+==================
+*/
+static void CG_BuildableParticleEffects( centity_t *cent )
+{
+  entityState_t   *es = &cent->currentState;
+  team_t          team = BG_Buildable( es->modelindex )->team;
+  int             health = es->generic1;
+  float           healthFrac = (float)health / BG_Buildable( es->modelindex )->health;
+
+  if( !( es->eFlags & EF_B_SPAWNED ) )
+    return;
+	
+	
+	    //ORGANIC BULB LIGHT / BA_H_LIGHT - VIA PS
+    if( es->modelindex == BA_A_ORGANIC_BULB )
+  {
+    if( !CG_IsParticleSystemValid( &cent->buildablePS ) )
+    {
+      cent->buildablePS = CG_SpawnNewParticleSystem( cgs.media.organicbulbPS );
+
+      if( CG_IsParticleSystemValid( &cent->buildablePS ) )
+      {
+        CG_SetAttachmentCent( &cent->buildablePS->attachment, cent );
+        CG_AttachToCent( &cent->buildablePS->attachment );
+      }
+    }
+  }
+
+  else if( team == TEAM_HUMANS )
+  {
+    if( healthFrac < 0.33f && !CG_IsParticleSystemValid( &cent->buildablePS ) )
+    {
+      cent->buildablePS = CG_SpawnNewParticleSystem( cgs.media.humanBuildableDamagedPS );
+
+      if( CG_IsParticleSystemValid( &cent->buildablePS ) )
+      {
+        CG_SetAttachmentCent( &cent->buildablePS->attachment, cent );
+        CG_AttachToCent( &cent->buildablePS->attachment );
+      }
+    }
+    else if( healthFrac >= 0.33f && CG_IsParticleSystemValid( &cent->buildablePS ) )
+      CG_DestroyParticleSystem( &cent->buildablePS );
+  }
+  else if( team == TEAM_ALIENS )
+  {
+    if( healthFrac < 0.33f && !CG_IsParticleSystemValid( &cent->buildablePS ) )
+    {
+      cent->buildablePS = CG_SpawnNewParticleSystem( cgs.media.alienBuildableDamagedPS );
+
+      if( CG_IsParticleSystemValid( &cent->buildablePS ) )
+      {
+        CG_SetAttachmentCent( &cent->buildablePS->attachment, cent );
+        CG_SetParticleSystemNormal( cent->buildablePS, es->origin2 );
+        CG_AttachToCent( &cent->buildablePS->attachment );
+      }
+    }
+    else if( healthFrac >= 0.33f && CG_IsParticleSystemValid( &cent->buildablePS ) )
+      CG_DestroyParticleSystem( &cent->buildablePS );
+  }
+}
+
+/*
+==================
+CG_BuildableStatusParse
+==================
+*/
+void CG_BuildableStatusParse( const char *filename, buildStat_t *bs )
+{
+  pc_token_t token;
+  int        handle;
+  const char *s;
+  int        i;
+  float      f;
+  vec4_t     c;
+
+  handle = trap_Parse_LoadSource( filename );
+  if( !handle )
+    return;
+  while( 1 )
+  {
+    if( !trap_Parse_ReadToken( handle, &token ) )
+      break;
+    if( !Q_stricmp( token.string, "frameShader" ) )
+    {
+      if( PC_String_Parse( handle, &s ) )
+        bs->frameShader = trap_R_RegisterShader( s );
+      continue;
+    }
+    else if( !Q_stricmp( token.string, "overlayShader" ) )
+    {
+      if( PC_String_Parse( handle, &s ) )
+        bs->overlayShader = trap_R_RegisterShader( s );
+      continue;
+    }
+    else if( !Q_stricmp( token.string, "noPowerShader" ) )
+    {
+      if( PC_String_Parse( handle, &s ) )
+        bs->noPowerShader = trap_R_RegisterShader( s );
+      continue;
+    }
+    else if( !Q_stricmp( token.string, "markedShader" ) )
+    {
+      if( PC_String_Parse( handle, &s ) )
+        bs->markedShader = trap_R_RegisterShader( s );
+      continue;
+    }
+    else if( !Q_stricmp( token.string, "healthSevereColor" ) )
+    {
+      if( PC_Color_Parse( handle, &c ) )
+        Vector4Copy( c, bs->healthSevereColor );
+      continue;
+    }
+    else if( !Q_stricmp( token.string, "healthHighColor" ) )
+    {
+      if( PC_Color_Parse( handle, &c ) )
+        Vector4Copy( c, bs->healthHighColor );
+      continue;
+    }
+    else if( !Q_stricmp( token.string, "healthElevatedColor" ) )
+    {
+      if( PC_Color_Parse( handle, &c ) )
+        Vector4Copy( c, bs->healthElevatedColor );
+      continue;
+    }
+    else if( !Q_stricmp( token.string, "healthGuardedColor" ) )
+    {
+      if( PC_Color_Parse( handle, &c ) )
+        Vector4Copy( c, bs->healthGuardedColor );
+      continue;
+    }
+    else if( !Q_stricmp( token.string, "healthLowColor" ) )
+    {
+      if( PC_Color_Parse( handle, &c ) )
+        Vector4Copy( c, bs->healthLowColor );
+      continue;
+    }
+    else if( !Q_stricmp( token.string, "foreColor" ) )
+    {
+      if( PC_Color_Parse( handle, &c ) )
+        Vector4Copy( c, bs->foreColor );
+      continue;
+    }
+    else if( !Q_stricmp( token.string, "backColor" ) )
+    {
+      if( PC_Color_Parse( handle, &c ) )
+        Vector4Copy( c, bs->backColor );
+      continue;
+    }
+    else if( !Q_stricmp( token.string, "frameHeight" ) )
+    {
+      if( PC_Int_Parse( handle, &i ) )
+        bs->frameHeight = i;
+      continue;
+    }
+    else if( !Q_stricmp( token.string, "frameWidth" ) )
+    {
+      if( PC_Int_Parse( handle, &i ) )
+        bs->frameWidth = i;
+      continue;
+    }
+    else if( !Q_stricmp( token.string, "healthPadding" ) )
+    {
+      if( PC_Int_Parse( handle, &i ) )
+        bs->healthPadding = i;
+      continue;
+    }
+    else if( !Q_stricmp( token.string, "overlayHeight" ) )
+    {
+      if( PC_Int_Parse( handle, &i ) )
+        bs->overlayHeight = i;
+      continue;
+    }
+    else if( !Q_stricmp( token.string, "overlayWidth" ) )
+    {
+      if( PC_Int_Parse( handle, &i ) )
+        bs->overlayWidth = i;
+      continue;
+    }
+    else if( !Q_stricmp( token.string, "verticalMargin" ) )
+    {
+      if( PC_Float_Parse( handle, &f ) )
+        bs->verticalMargin = f;
+      continue;
+    }
+    else if( !Q_stricmp( token.string, "horizontalMargin" ) )
+    {
+      if( PC_Float_Parse( handle, &f ) )
+        bs->horizontalMargin = f;
+      continue;
+    }
+    else
+    {
+      Com_Printf("CG_BuildableStatusParse: unknown token %s in %s\n",
+        token.string, filename );
+      bs->loaded = qfalse;
+      trap_Parse_FreeSource( handle );
+      return;
+    }
+  }
+  bs->loaded = qtrue;
+  trap_Parse_FreeSource( handle );
+}
+
+#define STATUS_FADE_TIME      200
+#define STATUS_MAX_VIEW_DIST  900.0f
+#define STATUS_PEEK_DIST      20
+/*
+==================
+CG_BuildableStatusDisplay
+==================
+*/
+static void CG_BuildableStatusDisplay( centity_t *cent )
+{
+  entityState_t   *es = &cent->currentState;
+  vec3_t          origin;
+  float           healthScale;
+  int             health;
+  float           x, y;
+  vec4_t          color;
+  qboolean        powered, marked;
+  trace_t         tr;
+  float           d;
+  buildStat_t     *bs;
+  int             i, j;
+  int             entNum;
+  vec3_t          trOrigin;
+  vec3_t          right;
+  qboolean        visible = qfalse;
+  vec3_t          mins, maxs;
+  entityState_t   *hit;
+  int             anim;
+
+  if( BG_Buildable( es->modelindex )->team == TEAM_ALIENS )
+    bs = &cgs.alienBuildStat;
+  else
+    bs = &cgs.humanBuildStat;
+
+  if( !bs->loaded )
+    return;
+
+  d = Distance( cent->lerpOrigin, cg.refdef.vieworg );
+  if( d > STATUS_MAX_VIEW_DIST )
+    return;
+
+  Vector4Copy( bs->foreColor, color );
+
+  // trace for center point
+  BG_BuildableBoundingBox( es->modelindex, mins, maxs );
+
+  // hack for shrunken barricades
+  anim = es->torsoAnim & ~( ANIM_FORCEBIT | ANIM_TOGGLEBIT );
+  if( es->modelindex == BA_A_BARRICADE &&
+      ( anim == BANIM_DESTROYED || !( es->eFlags & EF_B_SPAWNED ) ) )
+    maxs[ 2 ] = (int)( maxs[ 2 ] * BARRICADE_SHRINKPROP );
+
+  VectorCopy( cent->lerpOrigin, origin );
+  
+    // hack for shrunken shield
+  anim = es->torsoAnim & ~( ANIM_FORCEBIT | ANIM_TOGGLEBIT );
+  if( es->modelindex == BA_H_SHIELD &&
+      ( anim == BANIM_DESTROYED || !( es->eFlags & EF_B_SPAWNED ) ) )
+    maxs[ 2 ] = (int)( maxs[ 2 ] * SHIELD_SHRINKPROP );
+
+  VectorCopy( cent->lerpOrigin, origin );
+
+  // center point
+  origin[ 2 ] += mins[ 2 ];
+  origin[ 2 ] += ( abs( mins[ 2 ] ) + abs( maxs[ 2 ] ) ) / 2;
+
+  entNum = cg.predictedPlayerState.clientNum;
+
+  // if first try fails, step left, step right
+  for( j = 0; j < 3; j++ )
+  {
+    VectorCopy( cg.refdef.vieworg, trOrigin );
+    switch( j )
+    {
+      case 1:
+        // step right
+        AngleVectors( cg.refdefViewAngles, NULL, right, NULL );
+        VectorMA( trOrigin, STATUS_PEEK_DIST, right, trOrigin );
+        break;
+      case 2:
+        // step left
+        AngleVectors( cg.refdefViewAngles, NULL, right, NULL );
+        VectorMA( trOrigin, -STATUS_PEEK_DIST, right, trOrigin );
+        break;
+      default:
+        break;
+    }
+    // look through up to 3 players and/or transparent buildables
+    for( i = 0; i < 3; i++ )
+    {
+      CG_Trace( &tr, trOrigin, NULL, NULL, origin, entNum, MASK_SHOT );
+      if( tr.entityNum == cent->currentState.number )
+      {
+        visible = qtrue;
+        break;
+      }
+
+      if( tr.entityNum == ENTITYNUM_WORLD )
+        break;
+
+      hit  = &cg_entities[ tr.entityNum ].currentState;
+
+      if( tr.entityNum < MAX_CLIENTS || ( hit->eType == ET_BUILDABLE &&
+          ( !( es->eFlags & EF_B_SPAWNED ) ||
+            BG_Buildable( hit->modelindex )->transparentTest ) ) )
+      {
+        entNum = tr.entityNum;
+        VectorCopy( tr.endpos, trOrigin );
+      }
+      else
+        break;
+    }
+  }
+  // hack to make the kit obscure view
+  if( cg_drawGun.integer && visible &&
+      cg.predictedPlayerState.stats[ STAT_TEAM ] == TEAM_HUMANS &&
+      CG_WorldToScreen( origin, &x, &y ) )
+  {
+    if( x > 450 && y > 290 )
+      visible = qfalse;
+  }
+
+  if( !visible && cent->buildableStatus.visible )
+  {
+    cent->buildableStatus.visible   = qfalse;
+    cent->buildableStatus.lastTime  = cg.time;
+  }
+  else if( visible && !cent->buildableStatus.visible )
+  {
+    cent->buildableStatus.visible   = qtrue;
+    cent->buildableStatus.lastTime  = cg.time;
+  }
+
+  // Fade up
+  if( cent->buildableStatus.visible )
+  {
+    if( cent->buildableStatus.lastTime + STATUS_FADE_TIME > cg.time )
+      color[ 3 ] = (float)( cg.time - cent->buildableStatus.lastTime ) / STATUS_FADE_TIME;
+  }
+
+  // Fade down
+  if( !cent->buildableStatus.visible )
+  {
+    if( cent->buildableStatus.lastTime + STATUS_FADE_TIME > cg.time )
+      color[ 3 ] = 1.0f - (float)( cg.time - cent->buildableStatus.lastTime ) / STATUS_FADE_TIME;
+    else
+      return;
+  }
+
+  health = es->generic1;
+  healthScale = (float)health / BG_Buildable( es->modelindex )->health;
+
+  if( health > 0 && healthScale < 0.01f )
+    healthScale = 0.01f;
+  else if( healthScale < 0.0f )
+    healthScale = 0.0f;
+  else if( healthScale > 1.0f )
+    healthScale = 1.0f;
+
+  if( CG_WorldToScreen( origin, &x, &y ) )
+  {
+    float  picH = bs->frameHeight;
+    float  picW = bs->frameWidth;
+    float  picX = x;
+    float  picY = y;
+    float  scale;
+    float  subH, subY;
+    float  clipX, clipY, clipW, clipH;
+    vec4_t frameColor;
+
+    // this is fudged to get the width/height in the cfg to be more realistic
+    scale = ( picH / d ) * 3;
+
+    powered = es->eFlags & EF_B_POWERED;
+    marked = es->eFlags & EF_B_MARKED;
+
+    picH *= scale;
+    picW *= scale;
+    picX -= ( picW * 0.5f );
+    picY -= ( picH * 0.5f );
+
+    // sub-elements such as icons and number
+    subH = picH - ( picH * bs->verticalMargin );
+    subY = picY + ( picH * 0.5f ) - ( subH * 0.5f );
+
+    clipW = ( 640.0f * cg_viewsize.integer ) / 100.0f;
+    clipH = ( 480.0f * cg_viewsize.integer ) / 100.0f;
+    clipX = 320.0f - ( clipW * 0.5f );
+    clipY = 240.0f - ( clipH * 0.5f );
+    CG_SetClipRegion( clipX, clipY, clipW, clipH );
+
+    if( bs->frameShader )
+    {
+      Vector4Copy( bs->backColor, frameColor );
+      frameColor[ 3 ] = color[ 3 ];
+      trap_R_SetColor( frameColor );
+      CG_DrawPic( picX, picY, picW, picH, bs->frameShader );
+      trap_R_SetColor( NULL );
+    }
+
+    if( health > 0 )
+    {
+      float hX, hY, hW, hH;
+      vec4_t healthColor;
+
+      hX = picX + ( bs->healthPadding * scale );
+      hY = picY + ( bs->healthPadding * scale );
+      hH = picH - ( bs->healthPadding * 2.0f * scale );
+      hW = picW * healthScale - ( bs->healthPadding * 2.0f * scale );
+
+      if( healthScale == 1.0f )
+        Vector4Copy( bs->healthLowColor, healthColor );
+      else if( healthScale >= 0.75f )
+        Vector4Copy( bs->healthGuardedColor, healthColor );
+      else if( healthScale >= 0.50f )
+        Vector4Copy( bs->healthElevatedColor, healthColor );
+      else if( healthScale >= 0.25f )
+        Vector4Copy( bs->healthHighColor, healthColor );
+      else
+        Vector4Copy( bs->healthSevereColor, healthColor );
+
+      healthColor[ 3 ] = color[ 3 ];
+      trap_R_SetColor( healthColor );
+
+      CG_DrawPic( hX, hY, hW, hH, cgs.media.whiteShader );
+      trap_R_SetColor( NULL );
+    }
+
+    if( bs->overlayShader )
+    {
+      float oW = bs->overlayWidth;
+      float oH = bs->overlayHeight;
+      float oX = x;
+      float oY = y;
+
+      oH *= scale;
+      oW *= scale;
+      oX -= ( oW * 0.5f );
+      oY -= ( oH * 0.5f );
+
+      trap_R_SetColor( frameColor );
+      CG_DrawPic( oX, oY, oW, oH, bs->overlayShader );
+      trap_R_SetColor( NULL );
+    }
+
+    trap_R_SetColor( color );
+    if( !powered )
+    {
+      float pX;
+
+      pX = picX + ( subH * bs->horizontalMargin );
+	  trap_R_SetColor( NULL );
+      CG_DrawPic( pX, subY, subH, subH, bs->noPowerShader );
+    }
+
+    if( marked )
+    {
+      float mX;
+
+      mX = picX + picW - ( subH * bs->horizontalMargin ) - subH;
+      CG_DrawPic( mX, subY, subH, subH, bs->markedShader );
+    }
+
+    {
+      float nX;
+      int healthMax;
+      int healthPoints;
+
+      healthMax = BG_Buildable( es->modelindex )->health;
+      healthPoints = (int)( healthScale * healthMax );
+      if( health > 0 && healthPoints < 1 )
+        healthPoints = 1;
+      nX = picX + ( picW * 0.5f ) - 2.0f - ( ( subH * 4 ) * 0.5f );
+
+      if( healthPoints > 999 )
+        nX -= 0.0f;
+      else if( healthPoints > 99 )
+        nX -= subH * 0.5f;
+      else if( healthPoints > 9 )
+        nX -= subH * 1.0f;
+      else
+        nX -= subH * 1.5f;
+
+      CG_DrawField( nX, subY, 4, subH, subH, healthPoints );
+    }
+
+    trap_R_SetColor( NULL );
+    CG_ClearClipRegion( );
+  }
+}
+
+/*
+==================
+CG_SortDistance
+==================
+*/
+int CG_SortDistance( const void *a, const void *b )
+{
+  centity_t    *aent, *bent;
+  float        adist, bdist;
+
+  aent = &cg_entities[ *(int *)a ];
+  bent = &cg_entities[ *(int *)b ];
+  adist = Distance( cg.refdef.vieworg, aent->lerpOrigin );
+  bdist = Distance( cg.refdef.vieworg, bent->lerpOrigin );
+  if( adist > bdist )
+    return -1;
+  else if( adist < bdist )
+    return 1;
+  else
+    return 0;
+}
+
+/*
+==================
+CG_PlayerIsBuilder
+==================
+*/
+static qboolean CG_PlayerIsBuilder( buildable_t buildable )
+{
+  switch( cg.predictedPlayerState.weapon )
+  {
+    case WP_ABUILD:
+    case WP_ABUILD2:
+    case WP_HBUILD:
+      return BG_Buildable( buildable )->team ==
+             BG_Weapon( cg.predictedPlayerState.weapon )->team;
+
+    default:
+      return qfalse;
+  }
+}
+
+/*
+==================
+CG_BuildableRemovalPending
+==================
+*/
+static qboolean CG_BuildableRemovalPending( int entityNum )
+{
+  int           i;
+  playerState_t *ps = &cg.snap->ps;
+
+  if( !( ps->stats[ STAT_BUILDABLE ] & SB_VALID_TOGGLEBIT ) )
+    return qfalse;
+
+  for( i = 0; i < MAX_MISC; i++ )
+  {
+    if( ps->misc[ i ] == entityNum )
+      return qtrue;
+  }
+
+  return qfalse;
+}
+
+/*
+==================
+CG_DrawBuildableStatus
+==================
+*/
+void CG_DrawBuildableStatus( void )
+{
+  int             i;
+  centity_t       *cent;
+  entityState_t   *es;
+  int             buildableList[ MAX_ENTITIES_IN_SNAPSHOT ];
+  int             buildables = 0;
+
+  for( i = 0; i < cg.snap->numEntities; i++ )
+  {
+    cent  = &cg_entities[ cg.snap->entities[ i ].number ];
+    es    = &cent->currentState;
+
+    if( es->eType == ET_BUILDABLE && CG_PlayerIsBuilder( es->modelindex ) )
+      buildableList[ buildables++ ] = cg.snap->entities[ i ].number;
+  }
+
+  qsort( buildableList, buildables, sizeof( int ), CG_SortDistance );
+  for( i = 0; i < buildables; i++ )
+    CG_BuildableStatusDisplay( &cg_entities[ buildableList[ i ] ] );
+}
+
+#define BUILDABLE_SOUND_PERIOD  500
+
+/*
+==================
+CG_Buildable
+==================
+*/
+void CG_Buildable( centity_t *cent )
+{
+  refEntity_t     ent;
+  entityState_t   *es = &cent->currentState;
+  vec3_t          angles;
+  vec3_t          surfNormal, xNormal, mins, maxs;
+  vec3_t          refNormal = { 0.0f, 0.0f, 1.0f };
+  float           rotAngle;
+  team_t          team = BG_Buildable( es->modelindex )->team;
+  float           scale;
+  int             health;
+
+  //must be before EF_NODRAW check
+  if( team == TEAM_ALIENS )
+    CG_Creep( cent );
+
+  // if set to invisible, skip
+  if( es->eFlags & EF_NODRAW )
+  {
+    if( CG_IsParticleSystemValid( &cent->buildablePS ) )
+      CG_DestroyParticleSystem( &cent->buildablePS );
+
+    return;
+  }
+
+  memset ( &ent, 0, sizeof( ent ) );
+
+  VectorCopy( cent->lerpOrigin, ent.origin );
+  VectorCopy( cent->lerpOrigin, ent.oldorigin );
+  VectorCopy( cent->lerpOrigin, ent.lightingOrigin );
+
+  VectorCopy( es->origin2, surfNormal );
+
+  VectorCopy( es->angles, angles );
+  BG_BuildableBoundingBox( es->modelindex, mins, maxs );
+
+  if( es->pos.trType == TR_STATIONARY )
+  {
+    // Positioning a buildable involves potentially up to two traces, and
+    // seeing as buildables rarely move, we cache the results and recalculate
+    // only if the buildable moves or changes orientation
+    if( VectorCompare( cent->buildableCache.cachedOrigin, cent->lerpOrigin ) &&
+        VectorCompare( cent->buildableCache.cachedNormal, surfNormal ) )
+    {
+      VectorCopy( cent->buildableCache.axis[ 0 ], ent.axis[ 0 ] );
+      VectorCopy( cent->buildableCache.axis[ 1 ], ent.axis[ 1 ] );
+      VectorCopy( cent->buildableCache.axis[ 2 ], ent.axis[ 2 ] );
+      VectorCopy( cent->buildableCache.origin, ent.origin );
+    }
+    else
+    {
+      CG_PositionAndOrientateBuildable( angles, ent.origin, surfNormal,
+                                        es->number, mins, maxs, ent.axis,
+                                        ent.origin );
+      VectorCopy( ent.axis[ 0 ], cent->buildableCache.axis[ 0 ] );
+      VectorCopy( ent.axis[ 1 ], cent->buildableCache.axis[ 1 ] );
+      VectorCopy( ent.axis[ 2 ], cent->buildableCache.axis[ 2 ] );
+      VectorCopy( ent.origin, cent->buildableCache.origin );
+      VectorCopy( cent->lerpOrigin, cent->buildableCache.cachedOrigin );
+      VectorCopy( surfNormal, cent->buildableCache.cachedNormal );
+    }
+  }
+
+  //offset on the Z axis if required
+  VectorMA( ent.origin, BG_BuildableConfig( es->modelindex )->zOffset, surfNormal, ent.origin );
+
+  VectorCopy( ent.origin, ent.oldorigin ); // don't positionally lerp at all
+  VectorCopy( ent.origin, ent.lightingOrigin );
+
+  ent.hModel = cg_buildables[ es->modelindex ].models[ 0 ];
+
+  if( !( es->eFlags & EF_B_SPAWNED ) )
+  {
+    sfxHandle_t prebuildSound = cgs.media.humanBuildablePrebuild;
+
+    if( team == TEAM_HUMANS )
+    {
+      ent.customShader = cgs.media.humanSpawningShader;
+      prebuildSound = cgs.media.humanBuildablePrebuild;
+    }
+    else if( team == TEAM_ALIENS )
+      prebuildSound = cgs.media.alienBuildablePrebuild;
+
+    trap_S_AddLoopingSound( es->number, cent->lerpOrigin, vec3_origin, prebuildSound );
+  }
+
+  CG_BuildableAnimation( cent, &ent.oldframe, &ent.frame, &ent.backlerp );
+
+  //rescale the model
+  scale = BG_BuildableConfig( es->modelindex )->modelScale;
+
+  if( scale != 1.0f )
+  {
+    VectorScale( ent.axis[ 0 ], scale, ent.axis[ 0 ] );
+    VectorScale( ent.axis[ 1 ], scale, ent.axis[ 1 ] );
+    VectorScale( ent.axis[ 2 ], scale, ent.axis[ 2 ] );
+
+    ent.nonNormalizedAxes = qtrue;
+  }
+  else
+    ent.nonNormalizedAxes = qfalse;
+
+  if( CG_PlayerIsBuilder( es->modelindex ) && CG_BuildableRemovalPending( es->number ) )
+    ent.customShader = cgs.media.redBuildShader;
+
+  //add to refresh list
+  trap_R_AddRefEntityToScene( &ent );
+
+  CrossProduct( surfNormal, refNormal, xNormal );
+  VectorNormalize( xNormal );
+  rotAngle = RAD2DEG( acos( DotProduct( surfNormal, refNormal ) ) );
+
+  //turret barrel bit
+  if( cg_buildables[ es->modelindex ].models[ 1 ] )
+  {
+    refEntity_t turretBarrel;
+    vec3_t      flatAxis[ 3 ];
+
+    memset( &turretBarrel, 0, sizeof( turretBarrel ) );
+
+    turretBarrel.hModel = cg_buildables[ es->modelindex ].models[ 1 ];
+
+    CG_PositionEntityOnTag( &turretBarrel, &ent, ent.hModel, "tag_turret" );
+    VectorCopy( cent->lerpOrigin, turretBarrel.lightingOrigin );
+    AnglesToAxis( es->angles2, flatAxis );
+
+    RotatePointAroundVector( turretBarrel.axis[ 0 ], xNormal, flatAxis[ 0 ], -rotAngle );
+    RotatePointAroundVector( turretBarrel.axis[ 1 ], xNormal, flatAxis[ 1 ], -rotAngle );
+    RotatePointAroundVector( turretBarrel.axis[ 2 ], xNormal, flatAxis[ 2 ], -rotAngle );
+
+    turretBarrel.oldframe = ent.oldframe;
+    turretBarrel.frame    = ent.frame;
+    turretBarrel.backlerp = ent.backlerp;
+
+    turretBarrel.customShader = ent.customShader;
+
+    if( scale != 1.0f )
+    {
+      VectorScale( turretBarrel.axis[ 0 ], scale, turretBarrel.axis[ 0 ] );
+      VectorScale( turretBarrel.axis[ 1 ], scale, turretBarrel.axis[ 1 ] );
+      VectorScale( turretBarrel.axis[ 2 ], scale, turretBarrel.axis[ 2 ] );
+
+      turretBarrel.nonNormalizedAxes = qtrue;
+    }
+    else
+      turretBarrel.nonNormalizedAxes = qfalse;
+
+    if( CG_PlayerIsBuilder( es->modelindex ) && CG_BuildableRemovalPending( es->number ) )
+      turretBarrel.customShader = cgs.media.redBuildShader;
+
+    trap_R_AddRefEntityToScene( &turretBarrel );
+  }
+
+  //turret barrel bit
+  if( cg_buildables[ es->modelindex ].models[ 2 ] )
+  {
+    refEntity_t turretTop;
+    vec3_t      flatAxis[ 3 ];
+    vec3_t      swivelAngles;
+
+    memset( &turretTop, 0, sizeof( turretTop ) );
+
+    VectorCopy( es->angles2, swivelAngles );
+    swivelAngles[ PITCH ] = 0.0f;
+
+    turretTop.hModel = cg_buildables[ es->modelindex ].models[ 2 ];
+
+    CG_PositionRotatedEntityOnTag( &turretTop, &ent, ent.hModel, "tag_turret" );
+    VectorCopy( cent->lerpOrigin, turretTop.lightingOrigin );
+    AnglesToAxis( swivelAngles, flatAxis );
+
+    RotatePointAroundVector( turretTop.axis[ 0 ], xNormal, flatAxis[ 0 ], -rotAngle );
+    RotatePointAroundVector( turretTop.axis[ 1 ], xNormal, flatAxis[ 1 ], -rotAngle );
+    RotatePointAroundVector( turretTop.axis[ 2 ], xNormal, flatAxis[ 2 ], -rotAngle );
+
+    turretTop.oldframe = ent.oldframe;
+    turretTop.frame    = ent.frame;
+    turretTop.backlerp = ent.backlerp;
+
+    turretTop.customShader = ent.customShader;
+
+    if( scale != 1.0f )
+    {
+      VectorScale( turretTop.axis[ 0 ], scale, turretTop.axis[ 0 ] );
+      VectorScale( turretTop.axis[ 1 ], scale, turretTop.axis[ 1 ] );
+      VectorScale( turretTop.axis[ 2 ], scale, turretTop.axis[ 2 ] );
+
+      turretTop.nonNormalizedAxes = qtrue;
+    }
+    else
+      turretTop.nonNormalizedAxes = qfalse;
+
+    if( CG_PlayerIsBuilder( es->modelindex ) && CG_BuildableRemovalPending( es->number ) )
+      turretTop.customShader = cgs.media.redBuildShader;
+
+    trap_R_AddRefEntityToScene( &turretTop );
+  }
+
+  //weapon effects for turrets
+  if( es->eFlags & EF_FIRING )
+  {
+    weaponInfo_t  *weapon = &cg_weapons[ es->weapon ];
+
+    if( cg.time - cent->muzzleFlashTime > MUZZLE_FLASH_TIME ||
+        BG_Buildable( es->modelindex )->turretProjType == WP_TESLAGEN )
+    {
+      if( weapon->wim[ WPM_PRIMARY ].flashDlightColor[ 0 ] ||
+          weapon->wim[ WPM_PRIMARY ].flashDlightColor[ 1 ] ||
+          weapon->wim[ WPM_PRIMARY ].flashDlightColor[ 2 ] )
+      {
+        trap_R_AddLightToScene( cent->lerpOrigin, 300 + ( rand( ) & 31 ),
+            weapon->wim[ WPM_PRIMARY ].flashDlightColor[ 0 ],
+            weapon->wim[ WPM_PRIMARY ].flashDlightColor[ 1 ],
+            weapon->wim[ WPM_PRIMARY ].flashDlightColor[ 2 ] );
+      }
+    }
+
+    if( weapon->wim[ WPM_PRIMARY ].firingSound )
+    {
+      trap_S_AddLoopingSound( es->number, cent->lerpOrigin, vec3_origin,
+          weapon->wim[ WPM_PRIMARY ].firingSound );
+    }
+    else if( weapon->readySound )
+      trap_S_AddLoopingSound( es->number, cent->lerpOrigin, vec3_origin, weapon->readySound );
+  }
+
+  health = es->generic1;
+
+  if( health < cent->lastBuildableHealth &&
+      ( es->eFlags & EF_B_SPAWNED ) )
+  {
+    if( cent->lastBuildableDamageSoundTime + BUILDABLE_SOUND_PERIOD < cg.time )
+    {
+      if( team == TEAM_HUMANS )
+      {
+        int i = rand( ) % 4;
+        trap_S_StartSound( NULL, es->number, CHAN_BODY, cgs.media.humanBuildableDamage[ i ] );
+      }
+      else if( team == TEAM_ALIENS )
+        trap_S_StartSound( NULL, es->number, CHAN_BODY, cgs.media.alienBuildableDamage );
+
+      cent->lastBuildableDamageSoundTime = cg.time;
+    }
+  }
+
+  cent->lastBuildableHealth = health;
+
+  //smoke etc for damaged buildables
+  CG_BuildableParticleEffects( cent );
+}
diff --git a/src/cgame/cg_consolecmds.c b/src/cgame/cg_consolecmds.c
new file mode 100644
index 0000000..cf6628e
--- /dev/null
+++ b/src/cgame/cg_consolecmds.c
@@ -0,0 +1,294 @@
+/*
+===========================================================================
+Copyright (C) 1999-2005 Id Software, Inc.
+Copyright (C) 2000-2009 Darklegion Development
+
+This file is part of Tremulous.
+
+Tremulous 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.
+
+Tremulous 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 Tremulous; if not, write to the Free Software
+Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
+===========================================================================
+*/
+
+// cg_consolecmds.c -- text commands typed in at the local console, or
+// executed by a key binding
+
+
+#include "cg_local.h"
+
+
+
+/*
+=================
+CG_SizeUp_f
+
+Keybinding command
+=================
+*/
+static void CG_SizeUp_f( void )
+{
+  trap_Cvar_Set( "cg_viewsize", va( "%i", MIN( cg_viewsize.integer + 10, 100 ) ) );
+}
+
+
+/*
+=================
+CG_SizeDown_f
+
+Keybinding command
+=================
+*/
+static void CG_SizeDown_f( void )
+{
+  trap_Cvar_Set( "cg_viewsize", va( "%i", MAX( cg_viewsize.integer - 10, 30 ) ) );
+}
+
+
+/*
+=============
+CG_Viewpos_f
+
+Debugging command to print the current position
+=============
+*/
+static void CG_Viewpos_f( void )
+{
+  CG_Printf( "(%i %i %i) : %i\n", (int)cg.refdef.vieworg[ 0 ],
+    (int)cg.refdef.vieworg[ 1 ], (int)cg.refdef.vieworg[ 2 ],
+    (int)cg.refdefViewAngles[ YAW ] );
+}
+
+qboolean CG_RequestScores( void )
+{
+  if( cg.scoresRequestTime + 2000 < cg.time )
+  {
+    // the scores are more than two seconds out of data,
+    // so request new ones
+    cg.scoresRequestTime = cg.time;
+    trap_SendClientCommand( "score\n" );
+
+    return qtrue;
+  }
+  else
+    return qfalse;
+}
+
+extern menuDef_t *menuScoreboard;
+
+static void CG_scrollScoresDown_f( void )
+{
+  if( menuScoreboard && cg.scoreBoardShowing )
+  {
+    Menu_ScrollFeeder( menuScoreboard, FEEDER_ALIENTEAM_LIST, qtrue );
+    Menu_ScrollFeeder( menuScoreboard, FEEDER_HUMANTEAM_LIST, qtrue );
+  }
+}
+
+
+static void CG_scrollScoresUp_f( void )
+{
+  if( menuScoreboard && cg.scoreBoardShowing )
+  {
+    Menu_ScrollFeeder( menuScoreboard, FEEDER_ALIENTEAM_LIST, qfalse );
+    Menu_ScrollFeeder( menuScoreboard, FEEDER_HUMANTEAM_LIST, qfalse );
+  }
+
+}
+
+static void CG_ScoresDown_f( void )
+{
+  if( !cg.showScores )
+  {
+    Menu_SetFeederSelection( menuScoreboard, FEEDER_ALIENTEAM_LIST, 0, NULL );
+    Menu_SetFeederSelection( menuScoreboard, FEEDER_HUMANTEAM_LIST, 0, NULL );
+  }
+  if( CG_RequestScores( ) )
+  {
+    // leave the current scores up if they were already
+    // displayed, but if this is the first hit, clear them out
+    if( !cg.showScores )
+    {
+      if( cg_debugRandom.integer )
+        CG_Printf( "CG_ScoresDown_f: scores out of date\n" );
+
+      cg.showScores = qtrue;
+      cg.numScores = 0;
+    }
+  }
+  else
+  {
+    // show the cached contents even if they just pressed if it
+    // is within two seconds
+    cg.showScores = qtrue;
+  }
+}
+
+static void CG_ScoresUp_f( void )
+{
+  if( cg.showScores )
+  {
+    cg.showScores = qfalse;
+    cg.scoreFadeTime = cg.time;
+  }
+}
+
+void CG_ClientList_f( void )
+{
+  clientInfo_t *ci;
+  int i;
+  int count = 0;
+
+  for( i = 0; i < MAX_CLIENTS; i++ ) 
+  {
+    ci = &cgs.clientinfo[ i ];
+    if( !ci->infoValid ) 
+      continue;
+
+    switch( ci->team ) 
+    {
+      case TEAM_ALIENS:
+        Com_Printf( "%2d " S_COLOR_RED "A   " S_COLOR_WHITE "%s\n", i,
+          ci->name );
+        break;
+
+      case TEAM_HUMANS:
+        Com_Printf( "%2d " S_COLOR_CYAN "H   " S_COLOR_WHITE "%s\n", i,
+          ci->name );
+        break;
+
+      default:
+      case TEAM_NONE:
+      case NUM_TEAMS:
+        Com_Printf( "%2d S   %s\n", i, ci->name );
+        break;
+    }
+
+    count++;
+  }
+
+  Com_Printf( "Listed %2d clients\n", count );
+}
+
+static void CG_UIMenu_f( void )
+{
+  trap_SendConsoleCommand( va( "menu %s\n", CG_Argv( 1 ) ) );
+}
+
+static consoleCommand_t commands[ ] =
+{
+  { "+scores", CG_ScoresDown_f },
+  { "-scores", CG_ScoresUp_f },
+  { "cgame_memory", BG_MemoryInfo },
+  { "clientlist", CG_ClientList_f },
+  { "destroyTestPS", CG_DestroyTestPS_f },
+  { "destroyTestTS", CG_DestroyTestTS_f },
+  { "nextframe", CG_TestModelNextFrame_f },
+  { "nextskin", CG_TestModelNextSkin_f },
+  { "prevframe", CG_TestModelPrevFrame_f },
+  { "prevskin", CG_TestModelPrevSkin_f },
+  { "scoresDown", CG_scrollScoresDown_f },
+  { "scoresUp", CG_scrollScoresUp_f },
+  { "sizedown", CG_SizeDown_f },
+  { "sizeup", CG_SizeUp_f },
+  { "testgun", CG_TestGun_f },
+  { "testmodel", CG_TestModel_f },
+  { "testPS", CG_TestPS_f },
+  { "testTS", CG_TestTS_f },
+  { "ui_menu", CG_UIMenu_f },
+  { "viewpos", CG_Viewpos_f },
+  { "weapnext", CG_NextWeapon_f },
+  { "weapon", CG_Weapon_f },
+  { "weapprev", CG_PrevWeapon_f }
+};
+
+/*
+=================
+CG_ConsoleCommand
+
+The string has been tokenized and can be retrieved with
+Cmd_Argc() / Cmd_Argv()
+=================
+*/
+qboolean CG_ConsoleCommand( void )
+{
+  consoleCommand_t *cmd;
+
+  cmd = bsearch( CG_Argv( 0 ), commands,
+    sizeof( commands ) / sizeof( commands[ 0 ]), sizeof( commands[ 0 ] ),
+    cmdcmp );
+
+  if( !cmd )
+    return qfalse;
+
+  cmd->function( );
+  return qtrue;
+}
+
+
+/*
+=================
+CG_InitConsoleCommands
+
+Let the client system know about all of our commands
+so it can perform tab completion
+=================
+*/
+void CG_InitConsoleCommands( void )
+{
+  int   i;
+
+  for( i = 0 ; i < sizeof( commands ) / sizeof( commands[ 0 ] ) ; i++ )
+    trap_AddCommand( commands[ i ].cmd );
+
+  //
+  // the game server will interpret these commands, which will be automatically
+  // forwarded to the server after they are not recognized locally
+  //
+  trap_AddCommand( "kill" );
+  trap_AddCommand( "ui_messagemode" );
+  trap_AddCommand( "ui_messagemode2" );
+  trap_AddCommand( "ui_messagemode3" );
+  trap_AddCommand( "ui_messagemode4" );
+  trap_AddCommand( "say" );
+  trap_AddCommand( "say_team" );
+  trap_AddCommand( "vsay" );
+  trap_AddCommand( "vsay_team" );
+  trap_AddCommand( "vsay_local" );
+  trap_AddCommand( "m" );
+  trap_AddCommand( "mt" );
+  trap_AddCommand( "give" );
+  trap_AddCommand( "god" );
+  trap_AddCommand( "notarget" );
+  trap_AddCommand( "noclip" );
+  trap_AddCommand( "team" );
+  trap_AddCommand( "follow" );
+  trap_AddCommand( "levelshot" );
+  trap_AddCommand( "setviewpos" );
+  trap_AddCommand( "callvote" );
+  trap_AddCommand( "vote" );
+  trap_AddCommand( "callteamvote" );
+  trap_AddCommand( "teamvote" );
+  trap_AddCommand( "class" );
+  trap_AddCommand( "build" );
+  trap_AddCommand( "buy" );
+  trap_AddCommand( "sell" );
+  trap_AddCommand( "reload" );
+  trap_AddCommand( "itemact" );
+  trap_AddCommand( "itemdeact" );
+  trap_AddCommand( "itemtoggle" );
+  trap_AddCommand( "destroy" );
+  trap_AddCommand( "deconstruct" );
+  trap_AddCommand( "ignore" );
+  trap_AddCommand( "unignore" );
+}
diff --git a/src/cgame/cg_draw.c b/src/cgame/cg_draw.c
new file mode 100644
index 0000000..2ec205f
--- /dev/null
+++ b/src/cgame/cg_draw.c
@@ -0,0 +1,4011 @@
+/*
+===========================================================================
+Copyright (C) 1999-2005 Id Software, Inc.
+Copyright (C) 2000-2009 Darklegion Development
+
+This file is part of Tremulous.
+
+Tremulous 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.
+
+Tremulous 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 Tremulous; if not, write to the Free Software
+Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
+===========================================================================
+*/
+
+// cg_draw.c -- draw all of the graphical elements during
+// active (after loading) gameplay
+
+
+#include "cg_local.h"
+#include "../ui/ui_shared.h"
+
+menuDef_t *menuScoreboard = NULL;
+
+static void CG_AlignText( rectDef_t *rect, const char *text, float scale,
+                          float w, float h,
+                          int align, int valign,
+                          float *x, float *y )
+{
+  float tx, ty;
+
+  if( scale > 0.0f )
+  {
+    w = UI_Text_Width( text, scale );
+    h = UI_Text_Height( text, scale );
+  }
+
+  switch( align )
+  {
+    default:
+    case ALIGN_LEFT:
+      tx = 0.0f;
+      break;
+
+    case ALIGN_RIGHT:
+      tx = rect->w - w;
+      break;
+
+    case ALIGN_CENTER:
+      tx = ( rect->w - w ) / 2.0f;
+      break;
+
+    case ALIGN_NONE:
+      tx = 0;
+      break;
+  }
+
+  switch( valign )
+  {
+    default:
+    case VALIGN_BOTTOM:
+      ty = rect->h;
+      break;
+
+    case VALIGN_TOP:
+      ty = h;
+      break;
+
+    case VALIGN_CENTER:
+      ty = h + ( ( rect->h - h ) / 2.0f );
+      break;
+
+    case VALIGN_NONE:
+      ty = 0;
+      break;
+  }
+
+  if( x )
+    *x = rect->x + tx;
+
+  if( y )
+    *y = rect->y + ty;
+}
+
+/*
+==============
+CG_DrawFieldPadded
+
+Draws large numbers for status bar
+==============
+*/
+static void CG_DrawFieldPadded( int x, int y, int width, int cw, int ch, int value )
+{
+  char  num[ 16 ], *ptr;
+  int   l, orgL;
+  int   frame;
+  int   charWidth, charHeight;
+
+  if( !( charWidth = cw ) )
+    charWidth = CHAR_WIDTH;
+
+  if( !( charHeight = ch ) )
+    charHeight = CHAR_HEIGHT;
+
+  if( width < 1 )
+    return;
+
+  // draw number string
+  if( width > 4 )
+    width = 4;
+
+  switch( width )
+  {
+    case 1:
+      value = value > 9 ? 9 : value;
+      value = value < 0 ? 0 : value;
+      break;
+    case 2:
+      value = value > 99 ? 99 : value;
+      value = value < -9 ? -9 : value;
+      break;
+    case 3:
+      value = value > 999 ? 999 : value;
+      value = value < -99 ? -99 : value;
+      break;
+    case 4:
+      value = value > 9999 ? 9999 : value;
+      value = value < -999 ? -999 : value;
+      break;
+  }
+
+  Com_sprintf( num, sizeof( num ), "%d", value );
+  l = strlen( num );
+
+  if( l > width )
+    l = width;
+
+  orgL = l;
+
+  x += ( 2.0f * cgDC.aspectScale );
+
+  ptr = num;
+  while( *ptr && l )
+  {
+    if( width > orgL )
+    {
+      CG_DrawPic( x,y, charWidth, charHeight, cgs.media.numberShaders[ 0 ] );
+      width--;
+      x += charWidth;
+      continue;
+    }
+
+    if( *ptr == '-' )
+      frame = STAT_MINUS;
+    else
+      frame = *ptr - '0';
+
+    CG_DrawPic( x,y, charWidth, charHeight, cgs.media.numberShaders[ frame ] );
+    x += charWidth;
+    ptr++;
+    l--;
+  }
+}
+
+/*
+==============
+CG_DrawField
+
+Draws large numbers for status bar
+==============
+*/
+void CG_DrawField( float x, float y, int width, float cw, float ch, int value )
+{
+  char  num[ 16 ], *ptr;
+  int   l;
+  int   frame;
+  float charWidth, charHeight;
+
+  if( !( charWidth = cw ) )
+    charWidth = CHAR_WIDTH;
+
+  if( !( charHeight = ch ) )
+    charHeight = CHAR_HEIGHT;
+
+  if( width < 1 )
+    return;
+
+  // draw number string
+  if( width > 4 )
+    width = 4;
+
+  switch( width )
+  {
+    case 1:
+      value = value > 9 ? 9 : value;
+      value = value < 0 ? 0 : value;
+      break;
+    case 2:
+      value = value > 99 ? 99 : value;
+      value = value < -9 ? -9 : value;
+      break;
+    case 3:
+      value = value > 999 ? 999 : value;
+      value = value < -99 ? -99 : value;
+      break;
+    case 4:
+      value = value > 9999 ? 9999 : value;
+      value = value < -999 ? -999 : value;
+      break;
+  }
+
+  Com_sprintf( num, sizeof( num ), "%d", value );
+  l = strlen( num );
+
+  if( l > width )
+    l = width;
+
+  x += ( 2.0f * cgDC.aspectScale ) + charWidth * ( width - l );
+
+  ptr = num;
+  while( *ptr && l )
+  {
+    if( *ptr == '-' )
+      frame = STAT_MINUS;
+    else
+      frame = *ptr -'0';
+
+    CG_DrawPic( x,y, charWidth, charHeight, cgs.media.numberShaders[ frame ] );
+    x += charWidth;
+    ptr++;
+    l--;
+  }
+}
+
+static void CG_DrawProgressBar( rectDef_t *rect, vec4_t color, float scale,
+                                int align, int textalign, int textStyle,
+                                float borderSize, float progress )
+{
+  float   rimWidth;
+  float   doneWidth, leftWidth;
+  float   tx, ty;
+  char    textBuffer[ 8 ];
+
+  if( borderSize >= 0.0f )
+    rimWidth = borderSize;
+  else
+  {
+    rimWidth = rect->h / 20.0f;
+    if( rimWidth < 0.6f )
+      rimWidth = 0.6f;
+  }
+
+  if( progress < 0.0f )
+    progress = 0.0f;
+  else if( progress > 1.0f )
+    progress = 1.0f;
+
+  doneWidth = ( rect->w - 2 * rimWidth ) * progress;
+  leftWidth = ( rect->w - 2 * rimWidth ) - doneWidth;
+
+  trap_R_SetColor( color );
+
+  //draw rim and bar
+  if( align == ALIGN_RIGHT )
+  {
+    CG_DrawPic( rect->x, rect->y, rimWidth, rect->h, cgs.media.whiteShader );
+    CG_DrawPic( rect->x + rimWidth, rect->y,
+      leftWidth, rimWidth, cgs.media.whiteShader );
+    CG_DrawPic( rect->x + rimWidth, rect->y + rect->h - rimWidth,
+      leftWidth, rimWidth, cgs.media.whiteShader );
+    CG_DrawPic( rect->x + rimWidth + leftWidth, rect->y,
+      rimWidth + doneWidth, rect->h, cgs.media.whiteShader );
+  }
+  else
+  {
+    CG_DrawPic( rect->x, rect->y, rimWidth + doneWidth, rect->h, cgs.media.whiteShader );
+    CG_DrawPic( rimWidth + rect->x + doneWidth, rect->y,
+      leftWidth, rimWidth, cgs.media.whiteShader );
+    CG_DrawPic( rimWidth + rect->x + doneWidth, rect->y + rect->h - rimWidth,
+      leftWidth, rimWidth, cgs.media.whiteShader );
+    CG_DrawPic( rect->x + rect->w - rimWidth, rect->y, rimWidth, rect->h, cgs.media.whiteShader );
+  }
+
+  trap_R_SetColor( NULL );
+
+  //draw text
+  if( scale > 0.0 )
+  {
+    Com_sprintf( textBuffer, sizeof( textBuffer ), "%d%%", (int)( progress * 100 ) );
+    CG_AlignText( rect, textBuffer, scale, 0.0f, 0.0f, textalign, VALIGN_CENTER, &tx, &ty );
+
+    UI_Text_Paint( tx, ty, scale, color, textBuffer, 0, 0, textStyle );
+  }
+}
+
+//=============== TA: was cg_newdraw.c
+
+#define NO_CREDITS_TIME 2000
+
+static void CG_DrawPlayerCreditsValue( rectDef_t *rect, vec4_t color, qboolean padding )
+{
+  int           value;
+  playerState_t *ps;
+  centity_t     *cent;
+
+  cent = &cg_entities[ cg.snap->ps.clientNum ];
+  ps = &cg.snap->ps;
+
+  //if the build timer pie is showing don't show this
+  if( ( cent->currentState.weapon == WP_ABUILD ||
+      cent->currentState.weapon == WP_ABUILD2 ) && ps->stats[ STAT_MISC ] )
+    return;
+
+  value = ps->persistant[ PERS_CREDIT ];
+  if( value > -1 )
+  {
+    if( cg.predictedPlayerState.stats[ STAT_TEAM ] == TEAM_ALIENS )
+    {
+      if( !BG_AlienCanEvolve( cg.predictedPlayerState.stats[ STAT_CLASS ],
+                              value, cgs.alienStage ) &&
+          cg.time - cg.lastEvolveAttempt <= NO_CREDITS_TIME &&
+          ( ( cg.time - cg.lastEvolveAttempt ) / 300 ) & 1 )
+      {
+        color[ 3 ] = 0.0f;
+      }
+
+      value /= ALIEN_CREDITS_PER_KILL;
+    }
+
+    trap_R_SetColor( color );
+
+    if( padding )
+      CG_DrawFieldPadded( rect->x, rect->y, 4, rect->w / 4, rect->h, value );
+    else
+      CG_DrawField( rect->x, rect->y, 2, rect->w, rect->h, value );
+
+    trap_R_SetColor( NULL );
+  }
+}
+
+static void CG_DrawPlayerCreditsFraction( rectDef_t *rect, vec4_t color, qhandle_t shader )
+{
+  float fraction;
+  float height;
+
+  if( cg.predictedPlayerState.stats[ STAT_TEAM ] != TEAM_ALIENS )
+    return;
+
+  fraction = ((float)(cg.predictedPlayerState.persistant[ PERS_CREDIT ] %
+    ALIEN_CREDITS_PER_KILL)) / ALIEN_CREDITS_PER_KILL;
+
+  CG_AdjustFrom640( &rect->x, &rect->y, &rect->w, &rect->h );
+  height = rect->h * fraction;
+
+  trap_R_SetColor( color );
+  trap_R_DrawStretchPic( rect->x, rect->y - height + rect->h, rect->w,
+    height, 0.0f, 1.0f - fraction, 1.0f, 1.0f, shader );
+  trap_R_SetColor( NULL );
+}
+
+
+/*
+==============
+CG_DrawPlayerStamina
+==============
+*/
+static void CG_DrawPlayerStamina( int ownerDraw, rectDef_t *rect,
+                                  vec4_t backColor, vec4_t foreColor,
+                                  qhandle_t shader )
+{
+  playerState_t *ps = &cg.snap->ps;
+  float         stamina = ps->stats[ STAT_STAMINA ];
+  float         maxStaminaBy3 = (float)STAMINA_MAX / 3.0f;
+  float         progress;
+  vec4_t        color;
+
+  switch( ownerDraw )
+  {
+    case CG_PLAYER_STAMINA_1:
+      progress = ( stamina - 2 * (int)maxStaminaBy3 ) / maxStaminaBy3;
+      break;
+    case CG_PLAYER_STAMINA_2:
+      progress = ( stamina - (int)maxStaminaBy3 ) / maxStaminaBy3;
+      break;
+    case CG_PLAYER_STAMINA_3:
+  progress = stamina / maxStaminaBy3;
+      break;
+    case CG_PLAYER_STAMINA_4:
+      progress = ( stamina + STAMINA_MAX ) / STAMINA_MAX;
+      break;
+    default:
+      return;
+  }
+
+  if( progress > 1.0f )
+    progress  = 1.0f;
+  else if( progress < 0.0f )
+    progress = 0.0f;
+
+  Vector4Lerp( progress, backColor, foreColor, color );
+
+  trap_R_SetColor( color );
+  CG_DrawPic( rect->x, rect->y, rect->w, rect->h, shader );
+  trap_R_SetColor( NULL );
+}
+
+/*
+==============
+CG_DrawPlayerStaminaBolt
+==============
+*/
+static void CG_DrawPlayerStaminaBolt( rectDef_t *rect, vec4_t backColor,
+                                      vec4_t foreColor, qhandle_t shader )
+{
+  float         stamina = cg.snap->ps.stats[ STAT_STAMINA ];
+  vec4_t        color;
+
+  if( cg.predictedPlayerState.stats[ STAT_STATE ] & SS_SPEEDBOOST )
+  {
+    if( stamina >= 0 )
+      Vector4Lerp( ( sin( cg.time / 150.0f ) + 1 ) / 2,
+                   backColor, foreColor, color );
+    else
+      Vector4Lerp( ( sin( cg.time / 2000.0f ) + 1 ) / 2,
+                   backColor, foreColor, color );
+  }
+  else
+  {
+    if( stamina < 0 )
+      Vector4Copy( backColor, color );
+    else
+      Vector4Copy( foreColor, color );
+  }
+
+  trap_R_SetColor( color );
+  CG_DrawPic( rect->x, rect->y, rect->w, rect->h, shader );
+  trap_R_SetColor( NULL );
+}
+
+/*
+==============
+CG_DrawPlayerClipsRing
+==============
+*/
+static void CG_DrawPlayerClipsRing( rectDef_t *rect, vec4_t backColor,
+                                    vec4_t foreColor, qhandle_t shader )
+{
+  playerState_t *ps = &cg.snap->ps;
+  centity_t     *cent;
+  float         buildTime = ps->stats[ STAT_MISC ];
+  float         progress;
+  float         maxDelay;
+  weapon_t      weapon;
+  vec4_t        color;
+
+  cent = &cg_entities[ cg.snap->ps.clientNum ];
+  weapon = BG_GetPlayerWeapon( ps );
+
+  switch( weapon )
+  {
+    case WP_ABUILD:
+    case WP_ABUILD2:
+    case WP_HBUILD:
+      if( buildTime > MAXIMUM_BUILD_TIME )
+        buildTime = MAXIMUM_BUILD_TIME;
+      progress = ( MAXIMUM_BUILD_TIME - buildTime ) / MAXIMUM_BUILD_TIME;
+
+      Vector4Lerp( progress, backColor, foreColor, color );
+      break;
+
+    default:
+      if( ps->weaponstate == WEAPON_RELOADING )
+      {
+        maxDelay = (float)BG_Weapon( cent->currentState.weapon )->reloadTime;
+        progress = ( maxDelay - (float)ps->weaponTime ) / maxDelay;
+
+        Vector4Lerp( progress, backColor, foreColor, color );
+      }
+      else
+        Com_Memcpy( color, foreColor, sizeof( color ) );
+      break;
+  }
+
+  trap_R_SetColor( color );
+  CG_DrawPic( rect->x, rect->y, rect->w, rect->h, shader );
+  trap_R_SetColor( NULL );
+}
+
+/*
+==============
+CG_DrawPlayerBuildTimerRing
+==============
+*/
+static void CG_DrawPlayerBuildTimerRing( rectDef_t *rect, vec4_t backColor,
+                                         vec4_t foreColor, qhandle_t shader )
+{
+  playerState_t *ps = &cg.snap->ps;
+  centity_t     *cent;
+  float         buildTime = ps->stats[ STAT_MISC ];
+  float         progress;
+  vec4_t        color;
+
+  cent = &cg_entities[ cg.snap->ps.clientNum ];
+
+  if( buildTime > MAXIMUM_BUILD_TIME )
+    buildTime = MAXIMUM_BUILD_TIME;
+
+  progress = ( MAXIMUM_BUILD_TIME - buildTime ) / MAXIMUM_BUILD_TIME;
+
+  Vector4Lerp( progress, backColor, foreColor, color );
+
+  trap_R_SetColor( color );
+  CG_DrawPic( rect->x, rect->y, rect->w, rect->h, shader );
+  trap_R_SetColor( NULL );
+}
+
+/*
+==============
+CG_DrawPlayerBoosted
+==============
+*/
+static void CG_DrawPlayerBoosted( rectDef_t *rect, vec4_t backColor,
+                                  vec4_t foreColor, qhandle_t shader )
+{
+  if( cg.snap->ps.stats[ STAT_STATE ] & SS_BOOSTED )
+    trap_R_SetColor( foreColor );
+  else
+    trap_R_SetColor( backColor );
+
+  CG_DrawPic( rect->x, rect->y, rect->w, rect->h, shader );
+  trap_R_SetColor( NULL );
+}
+
+/*
+==============
+CG_DrawPlayerBoosterBolt
+==============
+*/
+static void CG_DrawPlayerBoosterBolt( rectDef_t *rect, vec4_t backColor,
+                                      vec4_t foreColor, qhandle_t shader )
+{
+  vec4_t color;
+
+  // Flash bolts when the boost is almost out
+  if( ( cg.snap->ps.stats[ STAT_STATE ] & SS_BOOSTED ) &&
+      ( cg.snap->ps.stats[ STAT_STATE ] & SS_BOOSTEDWARNING ) )
+    Vector4Lerp( ( sin( cg.time / 100.0f ) + 1 ) / 2,
+                 backColor, foreColor, color );
+  else
+    Vector4Copy( foreColor, color );
+
+  trap_R_SetColor( color );
+  CG_DrawPic( rect->x, rect->y, rect->w, rect->h, shader );
+  trap_R_SetColor( NULL );
+}
+
+
+
+/*
+==============
+CG_DrawInvisbleStatus
+==============
+*/
+static void CG_DrawInvisbleStatus( rectDef_t *rect, vec4_t color, qhandle_t shader )
+{
+  qhandle_t   detail1;
+  if( cg.snap->ps.stats[ STAT_STATE ] & SS_INVI && cg.snap->ps.weapon == WP_ALEVEL1_UPG)
+  {
+
+			 detail1 = trap_R_RegisterShader( "icons/advbasiinvi.tga" );
+			 CG_DrawPic( rect->x, rect->y, rect->w, rect->h, detail1 );
+   }
+}
+
+/*
+==============
+CG_DrawInvisbleOverlay
+==============
+*/
+static void CG_DrawInvisbleOverlay( rectDef_t *rect, vec4_t color, qhandle_t shader )
+{
+  qhandle_t   detail2;
+  if( cg.snap->ps.stats[ STAT_STATE ] & SS_INVI && cg.snap->ps.weapon == WP_ALEVEL1_UPG)
+  {
+		     detail2 = trap_R_RegisterShader( "gfx/edge/basi_invisble_overlay" );
+			 CG_DrawPic( rect->x, rect->y, rect->w, rect->h, detail2 );
+   }
+}
+
+
+/*
+==============
+CG_DrawPlayerPrickles (ammo)
+==============
+*/
+static void CG_DrawPlayerPrickles( rectDef_t *rect, vec4_t color, qhandle_t shader )
+{
+  qboolean vertical;
+  float    x = rect->x, y = rect->y;
+  float    width = rect->w, height = rect->h;
+  float    diff;
+  int      iconsize, numBarbs, maxBarbs;
+
+  maxBarbs = BG_Weapon( cg.snap->ps.weapon )->maxAmmo;
+  numBarbs = cg.snap->ps.ammo;
+  
+if( cg.snap->ps.weapon == WP_ALEVEL2_UPG || cg.snap->ps.weapon == WP_ALEVEL3 || cg.snap->ps.weapon == WP_ALEVEL3_UPG || cg.snap->ps.weapon == WP_ALEVEL1 || cg.snap->ps.weapon == WP_ALEVEL1_UPG || cg.snap->ps.weapon == WP_ALEVEL4 )
+  return;
+  
+  if( maxBarbs <= 0 || numBarbs <= 0 )
+    return;
+
+  // adjust these first to ensure the aspect ratio of the barb image is
+  // preserved
+  CG_AdjustFrom640( &x, &y, &width, &height );
+
+  if( height > width )
+  {
+    vertical = qtrue;
+    iconsize = width;
+    if( maxBarbs != 1 ) // avoid division by zero
+      diff = ( height - iconsize ) / (float)( maxBarbs - 1 );
+    else
+      diff = 0; // doesn't matter, won't be used
+  }
+  else
+  {
+    vertical = qfalse;
+    iconsize = height;
+    if( maxBarbs != 1 )
+      diff = ( width - iconsize ) / (float)( maxBarbs - 1 );
+    else
+      diff = 0;
+  }
+
+
+  for( ; numBarbs > 0; numBarbs-- )
+  {
+    trap_R_DrawStretchPic( x, y, iconsize, iconsize, 0, 0, 1, 1, shader );
+    if( vertical )
+      y += diff;
+    else
+      x += diff;
+  }
+  
+
+  trap_R_SetColor( NULL );
+}
+
+
+/*
+==============
+CG_DrawPlayerFbreath (ammo)
+==============
+*/
+static void CG_DrawPlayerFbreath( rectDef_t *rect, vec4_t color, qhandle_t shader )
+{
+  qboolean vertical;
+  float    x = rect->x, y = rect->y;
+  float    width = rect->w, height = rect->h;
+  float    diff;
+  int      iconsize, numBarbs, maxBarbs;
+
+  maxBarbs = BG_Weapon( cg.snap->ps.weapon )->maxAmmo;
+  numBarbs = cg.snap->ps.ammo;
+  
+  if( cg.snap->ps.weapon == WP_ALEVEL2_UPG || cg.snap->ps.weapon == WP_ALEVEL3 || cg.snap->ps.weapon == WP_ALEVEL3_UPG || cg.snap->ps.weapon == WP_ALEVEL1 || cg.snap->ps.weapon == WP_ALEVEL1_UPG || cg.snap->ps.weapon == WP_ALEVEL5 )
+  return;
+  
+  if( maxBarbs <= 0 || numBarbs <= 0 )
+    return;
+
+  // adjust these first to ensure the aspect ratio of the barb image is
+  // preserved
+  CG_AdjustFrom640( &x, &y, &width, &height );
+
+  if( height > width )
+  {
+    vertical = qtrue;
+    iconsize = width;
+    if( maxBarbs != 1 ) // avoid division by zero
+      diff = ( height - iconsize ) / (float)( maxBarbs - 1 );
+    else
+      diff = 0; // doesn't matter, won't be used
+  }
+  else
+  {
+    vertical = qfalse;
+    iconsize = height;
+    if( maxBarbs != 1 )
+      diff = ( width - iconsize ) / (float)( maxBarbs - 1 );
+    else
+      diff = 0;
+  }
+
+  //trap_R_SetColor( color );
+
+  for( ; numBarbs > 0; numBarbs-- )
+  {
+    trap_R_DrawStretchPic( x, y, iconsize, iconsize, 0, 0, 1, 1, shader );
+    if( vertical )
+      y += diff;
+    else
+      x += diff;
+  }
+  
+
+  trap_R_SetColor( NULL );
+}
+
+
+/*
+==============
+CG_DrawPlayerBombs
+==============
+*/
+static void CG_DrawPlayerBombs( rectDef_t *rect, vec4_t color, qhandle_t shader )
+{
+  qboolean vertical;
+  float    x = rect->x, y = rect->y;
+  float    width = rect->w, height = rect->h;
+  float    diff;
+  int      iconsize, numBarbs, maxBarbs;
+
+  maxBarbs = BG_Weapon( cg.snap->ps.weapon )->maxAmmo;
+  numBarbs = cg.snap->ps.ammo;
+  
+  if( !(cg.snap->ps.weapon == WP_ALEVEL1 || cg.snap->ps.weapon == WP_ALEVEL1_UPG) )
+  return;
+  
+  if( maxBarbs <= 0 || numBarbs <= 0 )
+    return;
+
+  // adjust these first to ensure the aspect ratio of the barb image is
+  // preserved
+  CG_AdjustFrom640( &x, &y, &width, &height );
+
+  if( height > width )
+  {
+    vertical = qtrue;
+    iconsize = width;
+    if( maxBarbs != 1 ) // avoid division by zero
+      diff = ( height - iconsize ) / (float)( maxBarbs - 1 );
+    else
+      diff = 0; // doesn't matter, won't be used
+  }
+  else
+  {
+    vertical = qfalse;
+    iconsize = height;
+    if( maxBarbs != 1 )
+      diff = ( width - iconsize ) / (float)( maxBarbs - 1 );
+    else
+      diff = 0;
+  }
+
+
+
+  for( ; numBarbs > 0; numBarbs-- )
+  {
+    trap_R_DrawStretchPic( x, y, iconsize, iconsize, 0, 0, 1, 1, shader );
+    if( vertical )
+      y += diff;
+    else
+      x += diff;
+  }
+  
+
+  trap_R_SetColor( NULL );
+}
+
+
+
+/*
+==============
+CG_DrawPlayerPoisonBarbs
+==============
+*/
+static void CG_DrawPlayerPoisonBarbs( rectDef_t *rect, vec4_t color, qhandle_t shader )
+{
+  qboolean vertical;
+  float    x = rect->x, y = rect->y;
+  float    width = rect->w, height = rect->h;
+  float    diff;
+  int      iconsize, numBarbs, maxBarbs;
+
+  maxBarbs = BG_Weapon( cg.snap->ps.weapon )->maxAmmo;
+  numBarbs = cg.snap->ps.ammo;
+  
+  if( !(cg.snap->ps.weapon == WP_ALEVEL2_UPG || cg.snap->ps.weapon == WP_ALEVEL3 || cg.snap->ps.weapon == WP_ALEVEL3_UPG))
+  return;
+  
+  if( maxBarbs <= 0 || numBarbs <= 0 )
+    return;
+
+  // adjust these first to ensure the aspect ratio of the barb image is
+  // preserved
+  CG_AdjustFrom640( &x, &y, &width, &height );
+
+  if( height > width )
+  {
+    vertical = qtrue;
+    iconsize = width;
+    if( maxBarbs != 1 ) // avoid division by zero
+      diff = ( height - iconsize ) / (float)( maxBarbs - 1 );
+    else
+      diff = 0; // doesn't matter, won't be used
+  }
+  else
+  {
+    vertical = qfalse;
+    iconsize = height;
+    if( maxBarbs != 1 )
+      diff = ( width - iconsize ) / (float)( maxBarbs - 1 );
+    else
+      diff = 0;
+  }
+
+  trap_R_SetColor( color );
+
+  for( ; numBarbs > 0; numBarbs-- )
+  {
+    trap_R_DrawStretchPic( x, y, iconsize, iconsize, 0, 0, 1, 1, shader );
+    if( vertical )
+      y += diff;
+    else
+      x += diff;
+  }
+  
+
+  trap_R_SetColor( NULL );
+}
+
+/*
+==============
+CG_DrawPlayerWallclimbing
+==============
+*/
+static void CG_DrawPlayerWallclimbing( rectDef_t *rect, vec4_t backColor, vec4_t foreColor, qhandle_t shader )
+{
+  if( cg.snap->ps.stats[ STAT_STATE ] & SS_WALLCLIMBING )
+    trap_R_SetColor( foreColor );
+  else
+    trap_R_SetColor( backColor );
+
+  CG_DrawPic( rect->x, rect->y, rect->w, rect->h, shader );
+  trap_R_SetColor( NULL );
+}
+
+static void CG_DrawPlayerAmmoValue( rectDef_t *rect, vec4_t color )
+{
+  int value;
+  int valueMarked = -1;
+  qboolean bp = qfalse;
+
+  switch( BG_PrimaryWeapon( cg.snap->ps.stats ) )
+  {
+    case WP_NONE:
+    case WP_BLASTER:
+      return;
+
+    case WP_ABUILD:
+    case WP_ABUILD2:
+    case WP_HBUILD:
+      value = cg.snap->ps.persistant[ PERS_BP ];
+      valueMarked = cg.snap->ps.persistant[ PERS_MARKEDBP ];
+      bp = qtrue;
+      break;
+	  
+    default:
+      value = cg.snap->ps.ammo;
+      break;
+  }
+
+  if( value > 9999 )
+  value = 9999;
+  if( valueMarked > 9999 )
+  valueMarked = 9999;
+
+	
+  if( value >= 0 )
+  {
+    float tx, ty;
+    char *text;
+    int len;
+
+    trap_R_SetColor( color );
+    if( !bp )
+    {
+      CG_DrawField( rect->x - 5, rect->y, 4, rect->w / 4, rect->h, value );
+      trap_R_SetColor( NULL );
+      return;
+    }
+//New buildpoints + colony stuff
+	
+    text = va( "%d", value );
+    CG_AlignText( rect, text, 0.4, 0.0f, 0.0f, ALIGN_RIGHT, VALIGN_CENTER, &tx, &ty );
+    UI_Text_Paint( tx-46, ty, 0.4, color, text, 0, 0, ITEM_TEXTSTYLE_NORMAL );
+    trap_R_SetColor( NULL );
+//	  
+    text = va( "%d", valueMarked);
+ 
+    CG_AlignText( rect, text, 0.35, 0.0f, 0.0f, ALIGN_RIGHT, VALIGN_CENTER, &tx, &ty );
+    UI_Text_Paint( tx - 19, ty, 0.35, color, text, 0, 0, ITEM_TEXTSTYLE_NORMAL );
+    trap_R_SetColor( NULL );
+//	
+    text = va( "%d", valueMarked / 75);
+
+
+    CG_AlignText( rect, text, 0.35, 0.0f, 0.0f, ALIGN_RIGHT, VALIGN_CENTER, &tx, &ty );
+    UI_Text_Paint( tx-4, ty, 0.35, color, text, 0, 0, ITEM_TEXTSTYLE_NORMAL );
+    trap_R_SetColor( NULL );
+  }
+  
+ if( value <0 )
+  {
+    float tx, ty;
+    char *text;
+    int len;
+
+    trap_R_SetColor( color );
+    if( !bp )
+    {
+      CG_DrawField( rect->x - 5, rect->y, 4, rect->w / 4, rect->h, value );
+      trap_R_SetColor( NULL );
+      return;
+    }
+//New buildpoints + colony stuff
+	
+    text = va( "^3%d", value );
+    CG_AlignText( rect, text, 0.35, 0.0f, 0.0f, ALIGN_RIGHT, VALIGN_CENTER, &tx, &ty );
+    UI_Text_Paint( tx - 40, ty, 0.35, color, text, 0, 0, ITEM_TEXTSTYLE_NORMAL );
+    trap_R_SetColor( NULL );
+//	  
+    text = va( "%d", valueMarked);
+ 
+    CG_AlignText( rect, text, 0.35, 0.0f, 0.0f, ALIGN_RIGHT, VALIGN_CENTER, &tx, &ty );
+    UI_Text_Paint( tx - 20, ty, 0.35, color, text, 0, 0, ITEM_TEXTSTYLE_NORMAL );
+    trap_R_SetColor( NULL );
+//	
+    text = va( "%d", valueMarked / 75);
+
+
+    CG_AlignText( rect, text, 0.35, 0.0f, 0.0f, ALIGN_RIGHT, VALIGN_CENTER, &tx, &ty );
+    UI_Text_Paint( tx - 4, ty, 0.35, color, text, 0, 0, ITEM_TEXTSTYLE_NORMAL );
+    trap_R_SetColor( NULL );
+
+  }
+}
+
+
+/*
+==============
+CG_DrawAlienSense
+==============
+*/
+static void CG_DrawAlienSense( rectDef_t *rect )
+{
+  if( BG_ClassHasAbility( cg.snap->ps.stats[ STAT_CLASS ], SCA_ALIENSENSE ) )
+    CG_AlienSense( rect );
+}
+
+
+/*
+==============
+CG_DrawHumanScanner
+==============
+*/
+static void CG_DrawHumanScanner( rectDef_t *rect, qhandle_t shader, vec4_t color )
+{
+  if( BG_InventoryContainsUpgrade( UP_HELMET, cg.snap->ps.stats ) )
+    CG_Scanner( rect, shader, color );
+}
+
+
+/*
+==============
+CG_DrawUsableBuildable
+==============
+*/
+static void CG_DrawUsableBuildable( rectDef_t *rect, qhandle_t shader, vec4_t color )
+{
+  vec3_t        view, point;
+  trace_t       trace;
+  entityState_t *es;
+
+  AngleVectors( cg.refdefViewAngles, view, NULL, NULL );
+  VectorMA( cg.refdef.vieworg, 64, view, point );
+  CG_Trace( &trace, cg.refdef.vieworg, NULL, NULL,
+            point, cg.predictedPlayerState.clientNum, MASK_SHOT );
+
+  es = &cg_entities[ trace.entityNum ].currentState;
+
+  if( es->eType == ET_BUILDABLE && BG_Buildable( es->modelindex )->usable &&
+      cg.predictedPlayerState.stats[ STAT_TEAM ] == BG_Buildable( es->modelindex )->team )
+  {
+    //hack to prevent showing the usable buildable when you aren't carrying an energy weapon
+    if( ( es->modelindex == BA_H_REACTOR || es->modelindex == BA_H_REPEATER ) &&
+        ( !BG_Weapon( cg.snap->ps.weapon )->usesEnergy ||
+          BG_Weapon( cg.snap->ps.weapon )->infiniteAmmo ) )
+    {
+      cg.nearUsableBuildable = BA_NONE;
+      return;
+    }
+
+    trap_R_SetColor( color );
+    CG_DrawPic( rect->x, rect->y, rect->w, rect->h, shader );
+    trap_R_SetColor( NULL );
+    cg.nearUsableBuildable = es->modelindex;
+  }
+  else
+    cg.nearUsableBuildable = BA_NONE;
+}
+
+
+#define BUILD_DELAY_TIME  2000
+
+static void CG_DrawPlayerBuildTimer( rectDef_t *rect, vec4_t color )
+{
+  int           index;
+  playerState_t *ps;
+
+  ps = &cg.snap->ps;
+
+  if( ps->stats[ STAT_MISC ] <= 0 )
+    return;
+
+  switch( BG_PrimaryWeapon( ps->stats ) )
+  {
+    case WP_ABUILD:
+    case WP_ABUILD2:
+    case WP_HBUILD:
+      break;
+
+    default:
+      return;
+  }
+
+  index = 8 * ( ps->stats[ STAT_MISC ] - 1 ) / MAXIMUM_BUILD_TIME;
+  if( index > 7 )
+    index = 7;
+  else if( index < 0 )
+    index = 0;
+
+  if( cg.time - cg.lastBuildAttempt <= BUILD_DELAY_TIME &&
+      ( ( cg.time - cg.lastBuildAttempt ) / 300 ) % 2 )
+  {
+    color[ 0 ] = 1.0f;
+    color[ 1 ] = color[ 2 ] = 0.0f;
+    color[ 3 ] = 1.0f;
+  }
+
+  trap_R_SetColor( color );
+  CG_DrawPic( rect->x, rect->y, rect->w, rect->h,
+              cgs.media.buildWeaponTimerPie[ index ] );
+  trap_R_SetColor( NULL );
+}
+
+static void CG_DrawPlayerClipsValue( rectDef_t *rect, vec4_t color )
+{
+  int           value;
+  playerState_t *ps = &cg.snap->ps;
+
+  switch( BG_PrimaryWeapon( ps->stats ) )
+  {
+    case WP_NONE:
+    case WP_BLASTER:
+    case WP_ABUILD:
+    case WP_ABUILD2:
+    case WP_HBUILD:
+      return;
+
+    default:
+      value = ps->clips;
+
+      if( value > -1 )
+      {
+        trap_R_SetColor( color );
+        CG_DrawField( rect->x, rect->y, 4, rect->w / 4, rect->h, value );
+        trap_R_SetColor( NULL );
+      }
+      break;
+  }
+}
+
+static void CG_DrawPlayerHealthValue( rectDef_t *rect, vec4_t color )
+{
+  trap_R_SetColor( color );
+  CG_DrawField( rect->x, rect->y, 4, rect->w / 4, rect->h,
+                cg.snap->ps.stats[ STAT_HEALTH ] );
+  trap_R_SetColor( NULL );
+}
+
+/*
+==============
+CG_DrawPlayerHealthCross
+==============
+*/
+static void CG_DrawPlayerHealthCross( rectDef_t *rect, vec4_t ref_color )
+{
+  qhandle_t shader;
+  vec4_t color;
+  float ref_alpha;
+
+  // Pick the current icon
+  shader = cgs.media.healthCross;
+  if( cg.snap->ps.stats[ STAT_STATE ] & SS_HEALING_3X )
+    shader = cgs.media.healthCross3X;
+  else if( cg.snap->ps.stats[ STAT_STATE ] & SS_HEALING_2X )
+  {
+    if( cg.snap->ps.stats[ STAT_TEAM ] == TEAM_ALIENS )
+      shader = cgs.media.healthCross2X;
+    else
+      shader = cgs.media.healthCrossMedkit;
+  }
+  else if( cg.snap->ps.stats[ STAT_STATE ] & SS_POISONED )
+    shader = cgs.media.healthCrossPoisoned;
+  else if( cg.snap->ps.stats[ STAT_STATE ] & SS_INFECTED )
+    shader = cgs.media.healthCrossPoisoned;
+
+  // Pick the alpha value
+  Vector4Copy( ref_color, color );
+  if( cg.snap->ps.stats[ STAT_TEAM ] == TEAM_HUMANS &&
+      cg.snap->ps.stats[ STAT_HEALTH ] < 10 )
+  {
+    color[ 0 ] = 1.0f;
+    color[ 1 ] = color[ 2 ] = 0.0f;
+  }
+  ref_alpha = ref_color[ 3 ];
+  if( cg.snap->ps.stats[ STAT_STATE ] & SS_HEALING_ACTIVE )
+    ref_alpha = 1.0f;
+
+  // Don't fade from nothing
+  if( !cg.lastHealthCross )
+    cg.lastHealthCross = shader;
+
+  // Fade the icon during transition
+  if( cg.lastHealthCross != shader )
+  {
+    cg.healthCrossFade += cg.frametime / 500.0f;
+    if( cg.healthCrossFade > 1.0f )
+    {
+      cg.healthCrossFade = 0.0f;
+      cg.lastHealthCross = shader;
+    }
+    else
+    {
+      // Fading between two icons
+      color[ 3 ] = ref_alpha * cg.healthCrossFade;
+      trap_R_SetColor( color );
+      CG_DrawPic( rect->x, rect->y, rect->w, rect->h, shader );
+      color[ 3 ] = ref_alpha * ( 1.0f - cg.healthCrossFade );
+      trap_R_SetColor( color );
+      CG_DrawPic( rect->x, rect->y, rect->w, rect->h, cg.lastHealthCross );
+      trap_R_SetColor( NULL );
+      return;
+    }
+  }
+
+  // Not fading, draw a single icon
+  color[ 3 ] = ref_alpha;
+  trap_R_SetColor( color );
+  CG_DrawPic( rect->x, rect->y, rect->w, rect->h, shader );
+  trap_R_SetColor( NULL );
+}
+
+static float CG_ChargeProgress( void )
+{
+  float progress;
+  int min = 0, max = 0;
+
+  if( cg.snap->ps.weapon == WP_ALEVEL5 )
+  {
+    min = LEVEL5_POUNCE_TIME_MIN;
+    max = LEVEL5_POUNCE_TIME;
+  }
+  if( cg.snap->ps.weapon == WP_ALEVEL0_UPG )
+  {
+    min = LEVEL0_DRILL_TIME_MIN;
+    max = LEVEL0_DRILL_TIME;
+  }
+  else if( cg.snap->ps.weapon == WP_ALEVEL3 )
+  {
+    min = LEVEL3_POUNCE_TIME_MIN;
+    max = LEVEL3_POUNCE_TIME;
+  }
+  else if( cg.snap->ps.weapon == WP_ALEVEL3_UPG )
+  {
+    min = LEVEL3_POUNCE_TIME_MIN;
+    max = LEVEL3_POUNCE_TIME_UPG;
+  }
+  else if( cg.snap->ps.weapon == WP_ALEVEL4 )
+  {
+    if( cg.predictedPlayerState.stats[ STAT_STATE ] & SS_CHARGING )
+    {
+      min = 0;
+      max = LEVEL4_TRAMPLE_DURATION;
+    }
+    else
+    {
+      min = LEVEL4_TRAMPLE_CHARGE_MIN;
+      max = LEVEL4_TRAMPLE_CHARGE_MAX;
+    }
+  }
+  else if( cg.snap->ps.weapon == WP_LUCIFER_CANNON || cg.snap->ps.weapon == WP_FLAMER )
+
+  {
+    min = LCANNON_CHARGE_TIME_MIN;
+    max = LCANNON_CHARGE_TIME_MAX;
+  }
+
+  if( max - min <= 0.0f )
+    return 0.0f;
+
+  progress = ( (float)cg.predictedPlayerState.stats[ STAT_MISC ] - min ) /
+             ( max - min );
+
+  if( progress > 1.0f )
+    return 1.0f;
+
+  if( progress < 0.0f )
+    return 0.0f;
+
+  return progress;
+}
+
+#define CHARGE_BAR_FADE_RATE 0.002f
+
+static void CG_DrawPlayerChargeBarBG( rectDef_t *rect, vec4_t ref_color,
+                                      qhandle_t shader )
+{
+  vec4_t color;
+
+  if( !cg_drawChargeBar.integer || cg.chargeMeterAlpha <= 0.0f )
+    return;
+
+  color[ 0 ] = ref_color[ 0 ];
+  color[ 1 ] = ref_color[ 1 ];
+  color[ 2 ] = ref_color[ 2 ];
+  color[ 3 ] = ref_color[ 3 ] * cg.chargeMeterAlpha;
+
+  // Draw meter background
+  trap_R_SetColor( color );
+  CG_DrawPic( rect->x, rect->y, rect->w, rect->h, shader );
+  trap_R_SetColor( NULL );
+}
+
+// FIXME: This should come from the element info
+#define CHARGE_BAR_CAP_SIZE 3
+
+static void CG_DrawPlayerChargeBar( rectDef_t *rect, vec4_t ref_color,
+                                    qhandle_t shader )
+{
+  vec4_t color;
+  float x, y, width, height, cap_size, progress;
+
+  if( !cg_drawChargeBar.integer )
+    return;
+
+  // Get progress proportion and pump fade
+  progress = CG_ChargeProgress();
+  if( progress <= 0.0f )
+  {
+    cg.chargeMeterAlpha -= CHARGE_BAR_FADE_RATE * cg.frametime;
+    if( cg.chargeMeterAlpha <= 0.0f )
+    {
+      cg.chargeMeterAlpha = 0.0f;
+      return;
+    }
+  }
+  else
+  {
+    cg.chargeMeterValue = progress;
+    cg.chargeMeterAlpha += CHARGE_BAR_FADE_RATE * cg.frametime;
+    if( cg.chargeMeterAlpha > 1.0f )
+      cg.chargeMeterAlpha = 1.0f;
+  }
+
+  color[ 0 ] = ref_color[ 0 ];
+  color[ 1 ] = ref_color[ 1 ];
+  color[ 2 ] = ref_color[ 2 ];
+  color[ 3 ] = ref_color[ 3 ] * cg.chargeMeterAlpha;
+
+  // Flash red for Lucifer Cannon warning
+ if( (cg.snap->ps.weapon == WP_LUCIFER_CANNON || cg.snap->ps.weapon == WP_FLAMER ) &&
+
+      cg.snap->ps.stats[ STAT_MISC ] >= LCANNON_CHARGE_TIME_WARN &&
+      ( cg.time & 128 ) )
+  {
+    color[ 0 ] = 1.0f;
+    color[ 1 ] = 0.0f;
+    color[ 2 ] = 0.0f;
+  }
+
+  x = rect->x;
+  y = rect->y;
+
+  // Horizontal charge bar
+  if( rect->w >= rect->h )
+  {
+    width = ( rect->w - CHARGE_BAR_CAP_SIZE * 2 ) * cg.chargeMeterValue;
+    height = rect->h;
+    CG_AdjustFrom640( &x, &y, &width, &height );
+    cap_size = CHARGE_BAR_CAP_SIZE * cgs.screenXScale;
+
+    // Draw the meter
+    trap_R_SetColor( color );
+    trap_R_DrawStretchPic( x, y, cap_size, height, 0, 0, 1, 1, shader );
+    trap_R_DrawStretchPic( x + width + cap_size, y, cap_size, height,
+                           1, 0, 0, 1, shader );
+    trap_R_DrawStretchPic( x + cap_size, y, width, height, 1, 0, 1, 1, shader );
+    trap_R_SetColor( NULL );
+  }
+
+  // Vertical charge bar
+  else
+  {
+    y += rect->h;
+    width = rect->w;
+    height = ( rect->h - CHARGE_BAR_CAP_SIZE * 2 ) * cg.chargeMeterValue;
+    CG_AdjustFrom640( &x, &y, &width, &height );
+    cap_size = CHARGE_BAR_CAP_SIZE * cgs.screenYScale;
+
+    // Draw the meter
+    trap_R_SetColor( color );
+    trap_R_DrawStretchPic( x, y - cap_size, width, cap_size,
+                           0, 1, 1, 0, shader );
+    trap_R_DrawStretchPic( x, y - height - cap_size * 2, width,
+                           cap_size, 0, 0, 1, 1, shader );
+    trap_R_DrawStretchPic( x, y - height - cap_size, width, height,
+                           0, 1, 1, 1, shader );
+    trap_R_SetColor( NULL );
+  }
+}
+
+static void CG_DrawProgressLabel( rectDef_t *rect, float text_x, float text_y, vec4_t color,
+                                  float scale, int textalign, int textvalign,
+                                  const char *s, float fraction )
+{
+  vec4_t white = { 1.0f, 1.0f, 1.0f, 1.0f };
+  float tx, ty;
+
+  CG_AlignText( rect, s, scale, 0.0f, 0.0f, textalign, textvalign, &tx, &ty );
+
+  if( fraction < 1.0f )
+    UI_Text_Paint( text_x + tx, text_y + ty, scale, white,
+      s, 0, 0, ITEM_TEXTSTYLE_NORMAL );
+  else
+    UI_Text_Paint( text_x + tx, text_y + ty, scale, color,
+      s, 0, 0, ITEM_TEXTSTYLE_NEON );
+}
+
+static void CG_DrawMediaProgress( rectDef_t *rect, vec4_t color, float scale,
+                                  int align, int textalign, int textStyle,
+                                  float borderSize )
+{
+  CG_DrawProgressBar( rect, color, scale, align, textalign, textStyle,
+                      borderSize, cg.mediaFraction );
+}
+
+static void CG_DrawMediaProgressLabel( rectDef_t *rect, float text_x, float text_y,
+                                       vec4_t color, float scale, int textalign, int textvalign )
+{
+  CG_DrawProgressLabel( rect, text_x, text_y, color, scale, textalign, textvalign,
+                        "Map and Textures", cg.mediaFraction );
+}
+
+static void CG_DrawBuildablesProgress( rectDef_t *rect, vec4_t color,
+                                       float scale, int align, int textalign,
+                                       int textStyle, float borderSize )
+{
+  CG_DrawProgressBar( rect, color, scale, align, textalign, textStyle,
+                      borderSize, cg.buildablesFraction );
+}
+
+static void CG_DrawBuildablesProgressLabel( rectDef_t *rect, float text_x, float text_y,
+                                            vec4_t color, float scale, int textalign, int textvalign )
+{
+  CG_DrawProgressLabel( rect, text_x, text_y, color, scale, textalign, textvalign,
+                        "Buildable Models", cg.buildablesFraction );
+}
+
+static void CG_DrawCharModelProgress( rectDef_t *rect, vec4_t color,
+                                      float scale, int align, int textalign,
+                                      int textStyle, float borderSize )
+{
+  CG_DrawProgressBar( rect, color, scale, align, textalign, textStyle,
+                      borderSize, cg.charModelFraction );
+}
+
+static void CG_DrawCharModelProgressLabel( rectDef_t *rect, float text_x, float text_y,
+                                           vec4_t color, float scale, int textalign, int textvalign )
+{
+  CG_DrawProgressLabel( rect, text_x, text_y, color, scale, textalign, textvalign,
+                        "Character Models", cg.charModelFraction );
+}
+
+static void CG_DrawOverallProgress( rectDef_t *rect, vec4_t color, float scale,
+                                    int align, int textalign, int textStyle,
+                                    float borderSize )
+{
+  float total;
+
+  total = cg.charModelFraction + cg.buildablesFraction + cg.mediaFraction;
+  total /= 3.0f;
+
+  CG_DrawProgressBar( rect, color, scale, align, textalign, textStyle,
+                      borderSize, total );
+}
+
+static void CG_DrawLevelShot( rectDef_t *rect )
+{
+  const char  *s;
+  const char  *info;
+  qhandle_t   levelshot;
+
+
+  info = CG_ConfigString( CS_SERVERINFO );
+  s = Info_ValueForKey( info, "mapname" );
+  levelshot = trap_R_RegisterShaderNoMip( va( "levelshots/%s.tga", s ) );
+
+  if( !levelshot )
+    levelshot = trap_R_RegisterShaderNoMip( "gfx/2d/load_screen" );
+
+  trap_R_SetColor( NULL );
+  CG_DrawPic( rect->x, rect->y, rect->w, rect->h, levelshot );
+
+
+}
+
+
+static void CG_DrawEdgeShot( rectDef_t *rect )
+{
+  const char  *info;
+  qhandle_t   pic;
+
+
+  info = CG_ConfigString( CS_SERVERINFO );
+  pic = trap_R_RegisterShaderNoMip( "ui/assets/warp_splash" );
+
+  trap_R_SetColor( NULL );
+  CG_DrawPic( rect->x, rect->y, rect->w, rect->h, pic );
+
+}
+
+static void CG_DrawLevelName( rectDef_t *rect, float text_x, float text_y,
+                              vec4_t color, float scale,
+                              int textalign, int textvalign, int textStyle )
+{
+  const char  *s;
+
+  s = CG_ConfigString( CS_MESSAGE );
+
+  UI_DrawTextBlock( rect, text_x, text_y, color, scale, textalign, textvalign, textStyle, s );
+}
+
+static void CG_DrawMOTD( rectDef_t *rect, float text_x, float text_y,
+                         vec4_t color, float scale,
+                         int textalign, int textvalign, int textStyle )
+{
+  const char  *s;
+  char parsed[ MAX_STRING_CHARS ];
+
+  s = CG_ConfigString( CS_MOTD );
+
+  Q_ParseNewlines( parsed, s, sizeof( parsed ) );
+
+  UI_DrawTextBlock( rect, text_x, text_y, color, scale, textalign, textvalign, textStyle, parsed );
+}
+
+static void CG_DrawHostname( rectDef_t *rect, float text_x, float text_y,
+                             vec4_t color, float scale,
+                             int textalign, int textvalign, int textStyle )
+{
+  char buffer[ 1024 ];
+  const char  *info;
+
+  info = CG_ConfigString( CS_SERVERINFO );
+
+  UI_EscapeEmoticons( buffer, Info_ValueForKey( info, "sv_hostname" ), sizeof( buffer ) );
+  Q_CleanStr( buffer );
+
+  UI_DrawTextBlock( rect, text_x, text_y, color, scale, textalign, textvalign, textStyle, buffer );
+}
+
+/*
+==============
+CG_DrawDemoPlayback
+==============
+*/
+static void CG_DrawDemoPlayback( rectDef_t *rect, vec4_t color, qhandle_t shader )
+{
+  if( !cg_drawDemoState.integer )
+    return;
+
+  if( trap_GetDemoState( ) != DS_PLAYBACK )
+    return;
+
+  trap_R_SetColor( color );
+  CG_DrawPic( rect->x, rect->y, rect->w, rect->h, shader );
+  trap_R_SetColor( NULL );
+}
+
+/*
+==============
+CG_DrawDemoRecording
+==============
+*/
+static void CG_DrawDemoRecording( rectDef_t *rect, vec4_t color, qhandle_t shader )
+{
+  if( !cg_drawDemoState.integer )
+    return;
+
+  if( trap_GetDemoState( ) != DS_RECORDING )
+    return;
+
+  trap_R_SetColor( color );
+  CG_DrawPic( rect->x, rect->y, rect->w, rect->h, shader );
+  trap_R_SetColor( NULL );
+}
+
+/*
+======================
+CG_UpdateMediaFraction
+
+======================
+*/
+void CG_UpdateMediaFraction( float newFract )
+{
+  cg.mediaFraction = newFract;
+
+  trap_UpdateScreen( );
+}
+
+/*
+====================
+CG_DrawLoadingScreen
+
+Draw all the status / pacifier stuff during level loading
+====================
+*/
+void CG_DrawLoadingScreen( void )
+{
+  menuDef_t *menu = Menus_FindByName( "Loading" );
+
+  Menu_Update( menu );
+  Menu_Paint( menu, qtrue );
+}
+
+float CG_GetValue( int ownerDraw )
+{
+  centity_t *cent;
+  playerState_t *ps;
+  weapon_t weapon;
+
+  cent = &cg_entities[ cg.snap->ps.clientNum ];
+  ps = &cg.snap->ps;
+  weapon = BG_GetPlayerWeapon( ps );
+
+  switch( ownerDraw )
+  {
+    case CG_PLAYER_AMMO_VALUE:
+      if( weapon )
+        return ps->ammo;
+      break;
+    case CG_PLAYER_CLIPS_VALUE:
+      if( weapon )
+        return ps->clips;
+      break;
+    case CG_PLAYER_HEALTH:
+      return ps->stats[ STAT_HEALTH ];
+      break;
+    default:
+      break;
+  }
+
+  return -1;
+}
+
+const char *CG_GetKillerText( )
+{
+  const char *s = "";
+  if( cg.killerName[ 0 ] )
+    s = va( "Fragged by %s", cg.killerName );
+
+  return s;
+}
+
+
+static void CG_DrawKiller( rectDef_t *rect, float scale, vec4_t color,
+                           qhandle_t shader, int textStyle )
+{
+  // fragged by ... line
+ if( cg.killerName[ 0 ] )
+ {
+   int x = rect->x + rect->w / 2;
+   UI_Text_Paint( x - UI_Text_Width( CG_GetKillerText( ), scale ) / 2,
+     rect->y + rect->h, scale, color, CG_GetKillerText( ), 0, 0, textStyle );
+  }
+}
+
+
+#define SPECTATORS_PIXELS_PER_SECOND 30.0f
+
+/*
+==================
+CG_DrawTeamSpectators
+==================
+*/
+static void CG_DrawTeamSpectators( rectDef_t *rect, float scale, int textvalign, vec4_t color, qhandle_t shader )
+{
+  float y;
+  char  *text = cg.spectatorList;
+  float textWidth = UI_Text_Width( text, scale );
+
+  CG_AlignText( rect, text, scale, 0.0f, 0.0f, ALIGN_LEFT, textvalign, NULL, &y );
+
+  if( textWidth > rect->w )
+  {
+    // The text is too wide to fit, so scroll it
+    int now = trap_Milliseconds( );
+    int delta = now - cg.spectatorTime;
+
+    CG_SetClipRegion( rect->x, rect->y, rect->w, rect->h );
+
+    UI_Text_Paint( rect->x - cg.spectatorOffset, y, scale, color, text, 0, 0, 0 );
+    UI_Text_Paint( rect->x + textWidth - cg.spectatorOffset, y, scale, color, text, 0, 0, 0 );
+
+    CG_ClearClipRegion( );
+
+    cg.spectatorOffset += ( delta / 1000.0f ) * SPECTATORS_PIXELS_PER_SECOND;
+
+    while( cg.spectatorOffset > textWidth )
+      cg.spectatorOffset -= textWidth;
+
+    cg.spectatorTime = now;
+  }
+  else
+  {
+    UI_Text_Paint( rect->x, y, scale, color, text, 0, 0, 0 );
+  }
+}
+
+#define FOLLOWING_STRING "following "
+#define CHASING_STRING "chasing "
+
+/*
+==================
+CG_DrawFollow
+==================
+*/
+static void CG_DrawFollow( rectDef_t *rect, float text_x, float text_y,
+    vec4_t color, float scale, int textalign, int textvalign, int textStyle )
+{
+  float tx, ty;
+
+  if( cg.snap && cg.snap->ps.pm_flags & PMF_FOLLOW )
+  {
+    char buffer[ MAX_STRING_CHARS ];
+
+    if( !cg.chaseFollow )
+      strcpy( buffer, FOLLOWING_STRING );
+    else
+      strcpy( buffer, CHASING_STRING );
+
+    strcat( buffer, cgs.clientinfo[ cg.snap->ps.clientNum ].name );
+
+    CG_AlignText( rect, buffer, scale, 0, 0, textalign, textvalign, &tx, &ty );
+    UI_Text_Paint( text_x + tx, text_y + ty, scale, color, buffer, 0, 0,
+                   textStyle );
+  }
+}
+
+/*
+==================
+CG_DrawTeamLabel
+==================
+*/
+static void CG_DrawTeamLabel( rectDef_t *rect, team_t team, float text_x, float text_y,
+    vec4_t color, float scale, int textalign, int textvalign, int textStyle )
+{
+  char  *t;
+  char  stage[ MAX_TOKEN_CHARS ];
+  char  *s;
+  float tx, ty;
+
+  stage[ 0 ] = '\0';
+
+  switch( team )
+  {
+    case TEAM_ALIENS:
+      t = "Aliens";
+      if( cg.intermissionStarted )
+        Com_sprintf( stage, MAX_TOKEN_CHARS, "(Stage %d)", cgs.alienStage + 1 );
+      break;
+
+    case TEAM_HUMANS:
+      t = "Humans";
+      if( cg.intermissionStarted )
+        Com_sprintf( stage, MAX_TOKEN_CHARS, "(Stage %d)", cgs.humanStage + 1 );
+      break;
+
+    default:
+      t = "";
+      break;
+  }
+
+  switch( textalign )
+  {
+    default:
+    case ALIGN_LEFT:
+      s = va( "%s %s", t, stage );
+      break;
+
+    case ALIGN_RIGHT:
+      s = va( "%s %s", stage, t );
+      break;
+  }
+
+  CG_AlignText( rect, s, scale, 0.0f, 0.0f, textalign, textvalign, &tx, &ty );
+  UI_Text_Paint( text_x + tx, text_y + ty, scale, color, s, 0, 0, textStyle );
+}
+
+/*
+==================
+CG_DrawStageReport
+==================
+*/
+static void CG_DrawStageReport( rectDef_t *rect, float text_x, float text_y,
+    vec4_t color, float scale, int textalign, int textvalign, int textStyle )
+{
+  char  s[ MAX_TOKEN_CHARS ];
+  float tx, ty;
+
+  if( cg.intermissionStarted )
+    return;
+
+  if( cg.snap->ps.stats[ STAT_TEAM ] == TEAM_NONE )
+//    return;
+  {
+      Com_sprintf( s, MAX_TOKEN_CHARS, " %d [ye2]HUMANS |STAGE| ALIENS[Ye] %d ", cgs.humanStage + 1 , cgs.alienStage + 1);
+
+  }
+
+  if( cg.snap->ps.stats[ STAT_TEAM ] == TEAM_ALIENS )
+  {
+    int kills = ceil( (float)(cgs.alienNextStageThreshold - cgs.alienCredits) / ALIEN_CREDITS_PER_KILL );
+    if( kills < 0 )
+      kills = 0;
+
+    if( cgs.alienNextStageThreshold < 0 )
+      Com_sprintf( s, MAX_TOKEN_CHARS, "Stage %d", cgs.alienStage + 1 );
+    else if( kills == 1 )
+      Com_sprintf( s, MAX_TOKEN_CHARS, "Stage %d, 1 frag for next stage",
+          cgs.alienStage + 1 );
+    else
+      Com_sprintf( s, MAX_TOKEN_CHARS, "Stage %d, %d frags for next stage",
+          cgs.alienStage + 1, kills );
+  }
+  else if( cg.snap->ps.stats[ STAT_TEAM ] == TEAM_HUMANS )
+  {
+    int credits = cgs.humanNextStageThreshold - cgs.humanCredits;
+
+    if( credits < 0 )
+      credits = 0;
+
+    if( cgs.humanNextStageThreshold < 0 )
+      Com_sprintf( s, MAX_TOKEN_CHARS, "Stage %d", cgs.humanStage + 1 );
+    else if( credits == 1 )
+      Com_sprintf( s, MAX_TOKEN_CHARS, "Stage %d, 1 credit for next stage",
+          cgs.humanStage + 1 );
+    else
+      Com_sprintf( s, MAX_TOKEN_CHARS, "Stage %d, %d credits for next stage",
+          cgs.humanStage + 1, credits );
+  }
+
+  CG_AlignText( rect, s, scale, 0.0f, 0.0f, textalign, textvalign, &tx, &ty );
+
+  UI_Text_Paint( text_x + tx, text_y + ty, scale, color, s, 0, 0, textStyle );
+}
+
+/*
+==================
+CG_DrawFPS
+==================
+*/
+#define FPS_FRAMES  20
+#define FPS_STRING  "fps"
+static void CG_DrawFPS( rectDef_t *rect, float text_x, float text_y,
+                        float scale, vec4_t color,
+                        int textalign, int textvalign, int textStyle,
+                        qboolean scalableText )
+{
+  char        *s;
+  float       tx, ty;
+  float       w, h, totalWidth;
+  int         strLength;
+  static int  previousTimes[ FPS_FRAMES ];
+  static int  index;
+  int         i, total;
+  int         fps;
+  static int  previous;
+  int         t, frameTime;
+
+  if( !cg_drawFPS.integer )
+    return;
+
+  // don't use serverTime, because that will be drifting to
+  // correct for internet lag changes, timescales, timedemos, etc
+  t = trap_Milliseconds( );
+  frameTime = t - previous;
+  previous = t;
+
+  previousTimes[ index % FPS_FRAMES ] = frameTime;
+  index++;
+
+  if( index > FPS_FRAMES )
+  {
+    // average multiple frames together to smooth changes out a bit
+    total = 0;
+
+    for( i = 0 ; i < FPS_FRAMES ; i++ )
+      total += previousTimes[ i ];
+
+    if( !total )
+      total = 1;
+
+    fps = 1000 * FPS_FRAMES / total;
+
+    s = va( "%d", fps );
+    w = UI_Text_Width( "0", scale );
+    h = UI_Text_Height( "0", scale );
+    strLength = CG_DrawStrlen( s );
+    totalWidth = UI_Text_Width( FPS_STRING, scale ) + w * strLength;
+
+    CG_AlignText( rect, s, 0.0f, totalWidth, h, textalign, textvalign, &tx, &ty );
+
+    if( scalableText )
+    {
+      for( i = 0; i < strLength; i++ )
+      {
+        char c[ 2 ];
+
+        c[ 0 ] = s[ i ];
+        c[ 1 ] = '\0';
+
+        UI_Text_Paint( text_x + tx + i * w, text_y + ty, scale, color, c, 0, 0, textStyle );
+      }
+
+      UI_Text_Paint( text_x + tx + i * w, text_y + ty, scale, color, FPS_STRING, 0, 0, textStyle );
+    }
+    else
+    {
+      trap_R_SetColor( color );
+      CG_DrawField( rect->x, rect->y, 3, rect->w / 3, rect->h, fps );
+      trap_R_SetColor( NULL );
+    }
+  }
+}
+
+
+/*
+=================
+CG_DrawTimerMins
+=================
+*/
+static void CG_DrawTimerMins( rectDef_t *rect, vec4_t color )
+{
+  int     mins, seconds;
+  int     msec;
+
+  if( !cg_drawTimer.integer )
+    return;
+
+  msec = cg.time - cgs.levelStartTime;
+
+  seconds = msec / 1000;
+  mins = seconds / 60;
+  seconds -= mins * 60;
+
+  trap_R_SetColor( color );
+  CG_DrawField( rect->x, rect->y, 3, rect->w / 3, rect->h, mins );
+  trap_R_SetColor( NULL );
+}
+
+
+/*
+=================
+CG_DrawTimerSecs
+=================
+*/
+static void CG_DrawTimerSecs( rectDef_t *rect, vec4_t color )
+{
+  int     mins, seconds;
+  int     msec;
+
+  if( !cg_drawTimer.integer )
+    return;
+
+  msec = cg.time - cgs.levelStartTime;
+
+  seconds = msec / 1000;
+  mins = seconds / 60;
+  seconds -= mins * 60;
+
+  trap_R_SetColor( color );
+  CG_DrawFieldPadded( rect->x, rect->y, 2, rect->w / 2, rect->h, seconds );
+  trap_R_SetColor( NULL );
+}
+
+
+/*
+=================
+CG_DrawTimer
+=================
+*/
+static void CG_DrawTimer( rectDef_t *rect, float text_x, float text_y,
+                          float scale, vec4_t color,
+                          int textalign, int textvalign, int textStyle )
+{
+  char    *s;
+  float   tx, ty;
+  int     i, strLength;
+  float   w, h, totalWidth;
+  int     mins, seconds, tens;
+  int     msec;
+
+  if( !cg_drawTimer.integer )
+    return;
+
+  msec = cg.time - cgs.levelStartTime;
+
+  seconds = msec / 1000;
+  mins = seconds / 60;
+  seconds -= mins * 60;
+  tens = seconds / 10;
+  seconds -= tens * 10;
+
+  s = va( "%d:%d%d", mins, tens, seconds );
+  w = UI_Text_Width( "0", scale );
+  h = UI_Text_Height( "0", scale );
+  strLength = CG_DrawStrlen( s );
+  totalWidth = w * strLength;
+
+  CG_AlignText( rect, s, 0.0f, totalWidth, h, textalign, textvalign, &tx, &ty );
+
+  for( i = 0; i < strLength; i++ )
+  {
+    char c[ 2 ];
+
+    c[ 0 ] = s[ i ];
+    c[ 1 ] = '\0';
+
+    UI_Text_Paint( text_x + tx + i * w, text_y + ty, scale, color, c, 0, 0, textStyle );
+  }
+}
+
+/*
+=================
+CG_DrawTeamOverlay
+=================
+*/
+
+typedef enum
+{
+  TEAMOVERLAY_OFF,
+  TEAMOVERLAY_ALL,
+  TEAMOVERLAY_SUPPORT,
+  TEAMOVERLAY_NEARBY,
+} teamOverlayMode_t;
+
+typedef enum
+{
+  TEAMOVERLAY_SORT_NONE,
+  TEAMOVERLAY_SORT_SCORE,
+  TEAMOVERLAY_SORT_WEAPONCLASS,
+} teamOverlaySort_t;
+
+static int QDECL SortScore( const void *a, const void *b )
+{
+  int na = *(int *)a;
+  int nb = *(int *)b;
+
+  return( cgs.clientinfo[ nb ].score - cgs.clientinfo[ na ].score );
+}
+
+static int QDECL SortWeaponClass( const void *a, const void *b )
+{
+  int out;
+  clientInfo_t *ca = cgs.clientinfo + *(int *)a;
+  clientInfo_t *cb = cgs.clientinfo + *(int *)b;
+
+  out = cb->curWeaponClass - ca->curWeaponClass;
+
+  // We want grangers on top. ckits are already on top without the special case.
+  if( ca->team == TEAM_ALIENS )
+  {
+    if( ca->curWeaponClass == PCL_ALIEN_BUILDER0_UPG || 
+        cb->curWeaponClass == PCL_ALIEN_BUILDER0_UPG ||
+        ca->curWeaponClass == PCL_ALIEN_BUILDER0 || 
+        cb->curWeaponClass == PCL_ALIEN_BUILDER0 )
+    {
+      out = -out;
+    }
+  }
+
+  return( out );
+}
+
+static void CG_DrawTeamOverlay( rectDef_t *rect, float scale, vec4_t color )
+{
+  char              *s;
+  int               i;
+  float             x = rect->x;
+  float             y;
+  clientInfo_t      *ci, *pci;
+  vec4_t            tcolor;
+  float             iconSize = rect->h / 8.0f;
+  float             leftMargin = 4.0f;
+  float             iconTopMargin = 2.0f;
+  float             midSep = 2.0f;
+  float             backgroundWidth = rect->w;
+  float             fontScale = 0.30f;
+  float             vPad = 0.0f;
+  float             nameWidth = 0.5f * rect->w;
+  char              name[ MAX_NAME_LENGTH + 2 ];
+  int               maxDisplayCount = 0;
+  int               displayCount = 0;
+  float             nameMaxX, nameMaxXCp;
+  float             maxX = rect->x + rect->w;
+  float             maxXCp = maxX;
+  weapon_t          curWeapon = WP_NONE;
+  teamOverlayMode_t mode = cg_drawTeamOverlay.integer;
+  teamOverlaySort_t sort = cg_teamOverlaySortMode.integer;
+  int               displayClients[ MAX_CLIENTS ];
+
+  if( cg.predictedPlayerState.pm_type == PM_SPECTATOR )
+    return;
+
+  if( mode == TEAMOVERLAY_OFF || !cg_teamOverlayMaxPlayers.integer )
+    return;
+
+  if( !cgs.teaminfoReceievedTime )
+    return;
+
+  if( cg.showScores ||
+      cg.predictedPlayerState.pm_type == PM_INTERMISSION )
+    return;
+
+  pci = cgs.clientinfo + cg.snap->ps.clientNum;
+
+  if( mode == TEAMOVERLAY_ALL || mode == TEAMOVERLAY_SUPPORT )
+  {
+    for( i = 0; i < MAX_CLIENTS; i++ )
+    {
+      ci = cgs.clientinfo + i;
+      if( ci->infoValid && pci != ci && ci->team == pci->team )
+      {
+        if( mode == TEAMOVERLAY_ALL )
+          displayClients[ maxDisplayCount++ ] = i;
+        else
+        {
+          if( ci->curWeaponClass == PCL_ALIEN_BUILDER0 || 
+              ci->curWeaponClass == PCL_ALIEN_BUILDER0_UPG ||
+              ci->curWeaponClass == PCL_ALIEN_LEVEL1 || 
+              ci->curWeaponClass == PCL_ALIEN_LEVEL1_UPG ||
+              ci->curWeaponClass == WP_HBUILD )
+          {
+            displayClients[ maxDisplayCount++ ] = i;
+          }
+        }
+      }
+    }
+  }
+  else // find nearby
+  {
+    for( i = 0; i < cg.snap->numEntities; i++ )
+    {
+      centity_t *cent = &cg_entities[ cg.snap->entities[ i ].number ];
+      vec3_t relOrigin = { 0.0f, 0.0f, 0.0f };
+      int team = cent->currentState.misc & 0x00FF;
+
+      if( cent->currentState.eType != ET_PLAYER || 
+          team != pci->team ||
+          cent->currentState.eFlags & EF_DEAD )
+      {
+        continue;
+      }
+
+      VectorSubtract( cent->lerpOrigin, cg.predictedPlayerState.origin, relOrigin );
+
+      if( VectorLength( relOrigin ) < HELMET_RANGE )
+        displayClients[ maxDisplayCount++ ] = cg.snap->entities[ i ].number;
+    }
+  }
+
+  // Sort
+  if( sort == TEAMOVERLAY_SORT_SCORE )
+  {
+    qsort( displayClients, maxDisplayCount,
+      sizeof( displayClients[ 0 ] ), SortScore );
+  }
+  else if( sort == TEAMOVERLAY_SORT_WEAPONCLASS )
+  {
+    qsort( displayClients, maxDisplayCount,
+      sizeof( displayClients[ 0 ] ), SortWeaponClass );
+  }
+
+  if( maxDisplayCount > cg_teamOverlayMaxPlayers.integer )
+    maxDisplayCount = cg_teamOverlayMaxPlayers.integer;
+
+  iconSize *= scale;
+  leftMargin *= scale;
+  iconTopMargin *= scale;
+  midSep *= scale;
+  backgroundWidth *= scale;
+  fontScale *= scale;
+  nameWidth *= scale;
+
+  vPad = ( rect->h - ( (float) maxDisplayCount * iconSize ) ) / 2.0f;
+  y = rect->y + vPad;
+
+  tcolor[ 0 ] = 1.0f;
+  tcolor[ 1 ] = 1.0f;
+  tcolor[ 2 ] = 1.0f;
+  tcolor[ 3 ] = color[ 3 ];
+
+  for( i = 0; i < MAX_CLIENTS && displayCount < maxDisplayCount; i++ )
+  {
+    ci = cgs.clientinfo + displayClients[ i ];
+
+    if( !ci->infoValid || pci == ci || ci->team != pci->team )
+      continue;
+
+    Com_sprintf( name, sizeof( name ), "%s^7", ci->name );
+
+    trap_R_SetColor( color );
+    CG_DrawPic( x, y, backgroundWidth,
+                iconSize, cgs.media.teamOverlayShader );
+    trap_R_SetColor( tcolor );
+    if( ci->health <= 0 || !ci->curWeaponClass )
+      s = "";
+    else
+    {
+      if( ci->team == TEAM_HUMANS )
+        curWeapon = ci->curWeaponClass;
+      else if( ci->team == TEAM_ALIENS )
+        curWeapon = BG_Class( ci->curWeaponClass )->startWeapon;
+
+      CG_DrawPic( x + leftMargin, y, iconSize, iconSize,
+                  cg_weapons[ curWeapon ].weaponIcon );
+      if( cg.predictedPlayerState.stats[ STAT_TEAM ] == TEAM_HUMANS )
+      {
+        if( ci->upgrade != UP_NONE )
+        {
+          CG_DrawPic( x + iconSize + leftMargin, y, iconSize, 
+                      iconSize, cg_upgrades[ ci->upgrade ].upgradeIcon );
+        }
+      }
+      else
+      {
+        if( curWeapon == WP_ABUILD2 || curWeapon == WP_ALEVEL1_UPG ||
+            curWeapon == WP_ALEVEL2_UPG || curWeapon == WP_ALEVEL3_UPG )
+        {
+          CG_DrawPic( x + iconSize + leftMargin, y, iconSize, 
+                      iconSize, cgs.media.upgradeClassIconShader );
+					  
+        }
+
+      }
+	  
+
+
+	  
+      s = va( " [^%c%3d^7] ^7%s",
+              CG_GetColorCharForHealth( displayClients[ i ] ),
+              ci->health,
+              CG_ConfigString( CS_LOCATIONS + ci->location ) );
+    }
+
+    trap_R_SetColor( NULL );
+    nameMaxX = nameMaxXCp = x + 2.0f * iconSize +
+                            leftMargin + midSep + nameWidth;
+    UI_Text_Paint_Limit( &nameMaxXCp, x + 2.0f * iconSize + leftMargin + midSep, 
+                         y + iconSize - iconTopMargin, fontScale, tcolor, name,
+                         0, 0 );
+
+    maxXCp = maxX;
+
+    UI_Text_Paint_Limit( &maxXCp, nameMaxX, y + iconSize - iconTopMargin, 
+                         fontScale, tcolor, s, 0, 0 );
+    y += iconSize;
+    displayCount++;
+  }
+}
+
+/*
+=================
+CG_DrawClock
+=================
+*/
+static void CG_DrawClock( rectDef_t *rect, float text_x, float text_y,
+                          float scale, vec4_t color,
+                          int textalign, int textvalign, int textStyle )
+{
+  char    *s;
+  float   tx, ty;
+  int     i, strLength;
+  float   w, h, totalWidth;
+  qtime_t qt;
+  int     t;
+
+  if( !cg_drawClock.integer )
+    return;
+
+  t = trap_RealTime( &qt );
+
+  if( cg_drawClock.integer == 2 )
+  {
+    s = va( "%02d%s%02d", qt.tm_hour, ( qt.tm_sec % 2 ) ? ":" : " ",
+      qt.tm_min );
+  }
+  else
+  {
+    char *pm = "am";
+    int h = qt.tm_hour;
+
+    if( h == 0 )
+      h = 12;
+    else if( h == 12 )
+      pm = "pm";
+    else if( h > 12 )
+    {
+      h -= 12;
+      pm = "pm";
+    }
+
+    s = va( "%d%s%02d%s", h, ( qt.tm_sec % 2 ) ? ":" : " ", qt.tm_min, pm );
+  }
+  w = UI_Text_Width( "0", scale );
+  h = UI_Text_Height( "0", scale );
+  strLength = CG_DrawStrlen( s );
+  totalWidth = w * strLength;
+
+  CG_AlignText( rect, s, 0.0f, totalWidth, h, textalign, textvalign, &tx, &ty );
+
+  for( i = 0; i < strLength; i++ )
+  {
+    char c[ 2 ];
+
+    c[ 0 ] = s[ i ];
+    c[ 1 ] = '\0';
+
+    UI_Text_Paint( text_x + tx + i * w, text_y + ty, scale, color, c, 0, 0, textStyle );
+  }
+}
+
+/*
+==================
+CG_DrawSnapshot
+==================
+*/
+static void CG_DrawSnapshot( rectDef_t *rect, float text_x, float text_y,
+                             float scale, vec4_t color,
+                             int textalign, int textvalign, int textStyle )
+{
+  char    *s;
+  float   tx, ty;
+
+  if( !cg_drawSnapshot.integer )
+    return;
+
+  s = va( "time:%d snap:%d cmd:%d", cg.snap->serverTime,
+    cg.latestSnapshotNum, cgs.serverCommandSequence );
+
+  CG_AlignText( rect, s, scale, 0.0f, 0.0f, textalign, textvalign, &tx, &ty );
+
+  UI_Text_Paint( text_x + tx, text_y + ty, scale, color, s, 0, 0, textStyle );
+}
+
+/*
+===============================================================================
+
+LAGOMETER
+
+===============================================================================
+*/
+
+#define LAG_SAMPLES   128
+
+typedef struct
+{
+  int frameSamples[ LAG_SAMPLES ];
+  int frameCount;
+  int snapshotFlags[ LAG_SAMPLES ];
+  int snapshotSamples[ LAG_SAMPLES ];
+  int snapshotCount;
+} lagometer_t;
+
+lagometer_t   lagometer;
+
+/*
+==============
+CG_AddLagometerFrameInfo
+
+Adds the current interpolate / extrapolate bar for this frame
+==============
+*/
+void CG_AddLagometerFrameInfo( void )
+{
+  int     offset;
+
+  offset = cg.time - cg.latestSnapshotTime;
+  lagometer.frameSamples[ lagometer.frameCount & ( LAG_SAMPLES - 1 ) ] = offset;
+  lagometer.frameCount++;
+}
+
+/*
+==============
+CG_AddLagometerSnapshotInfo
+
+Each time a snapshot is received, log its ping time and
+the number of snapshots that were dropped before it.
+
+Pass NULL for a dropped packet.
+==============
+*/
+#define PING_FRAMES 40
+void CG_AddLagometerSnapshotInfo( snapshot_t *snap )
+{
+  static int  previousPings[ PING_FRAMES ];
+  static int  index;
+  int         i;
+
+  // dropped packet
+  if( !snap )
+  {
+    lagometer.snapshotSamples[ lagometer.snapshotCount & ( LAG_SAMPLES - 1 ) ] = -1;
+    lagometer.snapshotCount++;
+    return;
+  }
+
+  // add this snapshot's info
+  lagometer.snapshotSamples[ lagometer.snapshotCount & ( LAG_SAMPLES - 1 ) ] = snap->ping;
+  lagometer.snapshotFlags[ lagometer.snapshotCount & ( LAG_SAMPLES - 1 ) ] = snap->snapFlags;
+  lagometer.snapshotCount++;
+
+  cg.ping = 0;
+  if( cg.snap )
+  {
+    previousPings[ index++ ] = cg.snap->ping;
+    index = index % PING_FRAMES;
+
+    for( i = 0; i < PING_FRAMES; i++ )
+    {
+      cg.ping += previousPings[ i ];
+    }
+
+    cg.ping /= PING_FRAMES;
+  }
+}
+
+/*
+==============
+CG_DrawDisconnect
+
+Should we draw something differnet for long lag vs no packets?
+==============
+*/
+static void CG_DrawDisconnect( void )
+{
+  float       x, y;
+  int         cmdNum;
+  usercmd_t   cmd;
+  const char  *s;
+  int         w;
+  vec4_t      color = { 1.0f, 1.0f, 1.0f, 1.0f };
+
+  // draw the phone jack if we are completely past our buffers
+  cmdNum = trap_GetCurrentCmdNumber( ) - CMD_BACKUP + 1;
+  trap_GetUserCmd( cmdNum, &cmd );
+
+  // special check for map_restart
+  if( cmd.serverTime <= cg.snap->ps.commandTime || cmd.serverTime > cg.time )
+    return;
+
+  // also add text in center of screen
+  s = "Connection Interrupted";
+  w = UI_Text_Width( s, 0.7f );
+  UI_Text_Paint( 320 - w / 2, 100, 0.7f, color, s, 0, 0, ITEM_TEXTSTYLE_SHADOWED );
+
+  // blink the icon
+  if( ( cg.time >> 9 ) & 1 )
+    return;
+
+  x = 640 - 48;
+  y = 480 - 48;
+
+  CG_DrawPic( x, y, 48, 48, trap_R_RegisterShader( "gfx/2d/net.tga" ) );
+}
+
+#define MAX_LAGOMETER_PING  900
+#define MAX_LAGOMETER_RANGE 300
+
+
+/*
+==============
+CG_DrawLagometer
+==============
+*/
+static void CG_DrawLagometer( rectDef_t *rect, float text_x, float text_y,
+    float scale, vec4_t textColor )
+{
+  int     a, x, y, i;
+  float   v;
+  float   ax, ay, aw, ah, mid, range;
+  int     color;
+  vec4_t  adjustedColor;
+  float   vscale;
+  char    *ping;
+
+  if( cg.snap->ps.pm_type == PM_INTERMISSION )
+    return;
+
+  if( !cg_lagometer.integer )
+    return;
+
+  if( cg.demoPlayback )
+    return;
+
+  Vector4Copy( textColor, adjustedColor );
+  adjustedColor[ 3 ] = 0.25f;
+
+  trap_R_SetColor( adjustedColor );
+  CG_DrawPic( rect->x, rect->y, rect->w, rect->h, cgs.media.whiteShader );
+  trap_R_SetColor( NULL );
+
+  //
+  // draw the graph
+  //
+  ax = x = rect->x;
+  ay = y = rect->y;
+  aw = rect->w;
+  ah = rect->h;
+
+  trap_R_SetColor( NULL );
+
+  CG_AdjustFrom640( &ax, &ay, &aw, &ah );
+
+  color = -1;
+  range = ah / 3;
+  mid = ay + range;
+
+  vscale = range / MAX_LAGOMETER_RANGE;
+
+  // draw the frame interpoalte / extrapolate graph
+  for( a = 0 ; a < aw ; a++ )
+  {
+    i = ( lagometer.frameCount - 1 - a ) & ( LAG_SAMPLES - 1 );
+    v = lagometer.frameSamples[ i ];
+    v *= vscale;
+
+    if( v > 0 )
+    {
+      if( color != 1 )
+      {
+        color = 1;
+        trap_R_SetColor( g_color_table[ ColorIndex( COLOR_YELLOW ) ] );
+      }
+
+      if( v > range )
+        v = range;
+
+      trap_R_DrawStretchPic( ax + aw - a, mid - v, 1, v, 0, 0, 0, 0, cgs.media.whiteShader );
+    }
+    else if( v < 0 )
+    {
+      if( color != 2 )
+      {
+        color = 2;
+        trap_R_SetColor( g_color_table[ ColorIndex( COLOR_BLUE ) ] );
+      }
+
+      v = -v;
+      if( v > range )
+        v = range;
+
+      trap_R_DrawStretchPic( ax + aw - a, mid, 1, v, 0, 0, 0, 0, cgs.media.whiteShader );
+    }
+  }
+
+  // draw the snapshot latency / drop graph
+  range = ah / 2;
+  vscale = range / MAX_LAGOMETER_PING;
+
+  for( a = 0 ; a < aw ; a++ )
+  {
+    i = ( lagometer.snapshotCount - 1 - a ) & ( LAG_SAMPLES - 1 );
+    v = lagometer.snapshotSamples[ i ];
+
+    if( v > 0 )
+    {
+      if( lagometer.snapshotFlags[ i ] & SNAPFLAG_RATE_DELAYED )
+      {
+        if( color != 5 )
+        {
+          color = 5;  // YELLOW for rate delay
+          trap_R_SetColor( g_color_table[ ColorIndex( COLOR_YELLOW ) ] );
+        }
+      }
+      else
+      {
+        if( color != 3 )
+        {
+          color = 3;
+
+          trap_R_SetColor( g_color_table[ ColorIndex( COLOR_GREEN ) ] );
+        }
+      }
+
+      v = v * vscale;
+
+      if( v > range )
+        v = range;
+
+      trap_R_DrawStretchPic( ax + aw - a, ay + ah - v, 1, v, 0, 0, 0, 0, cgs.media.whiteShader );
+    }
+    else if( v < 0 )
+    {
+      if( color != 4 )
+      {
+        color = 4;    // RED for dropped snapshots
+        trap_R_SetColor( g_color_table[ ColorIndex( COLOR_RED ) ] );
+      }
+
+      trap_R_DrawStretchPic( ax + aw - a, ay + ah - range, 1, range, 0, 0, 0, 0, cgs.media.whiteShader );
+    }
+  }
+
+  trap_R_SetColor( NULL );
+
+  if( cg_nopredict.integer || cg_synchronousClients.integer )
+    ping = "snc";
+  else
+    ping = va( "%d", cg.ping );
+  ax = rect->x + ( rect->w / 2.0f ) -
+       ( UI_Text_Width( ping, scale ) / 2.0f ) + text_x;
+  ay = rect->y + ( rect->h / 2.0f ) +
+       ( UI_Text_Height( ping, scale ) / 2.0f ) + text_y;
+
+  Vector4Copy( textColor, adjustedColor );
+  adjustedColor[ 3 ] = 0.5f;
+  UI_Text_Paint( ax, ay, scale, adjustedColor, ping, 0, 0,
+                 ITEM_TEXTSTYLE_NORMAL );
+
+  CG_DrawDisconnect( );
+}
+
+#define SPEEDOMETER_NUM_SAMPLES 160
+#define SPEEDOMETER_DRAW_TEXT   0x1
+#define SPEEDOMETER_DRAW_GRAPH  0x2
+#define SPEEDOMETER_IGNORE_Z    0x4
+float speedSamples[ SPEEDOMETER_NUM_SAMPLES ];
+// array indices
+int oldestSpeedSample = 0;
+int maxSpeedSample = 0;
+
+/*
+===================
+CG_AddSpeed
+
+append a speed to the sample history
+===================
+*/
+void CG_AddSpeed( void )
+{
+  float speed;
+  vec3_t vel;
+
+  VectorCopy( cg.snap->ps.velocity, vel );
+
+  if( cg_drawSpeed.integer & SPEEDOMETER_IGNORE_Z )
+    vel[ 2 ] = 0;
+
+  speed = VectorLength( vel );
+
+  if( speed > speedSamples[ maxSpeedSample ] )
+  {
+    maxSpeedSample = oldestSpeedSample;
+    speedSamples[ oldestSpeedSample++ ] = speed;
+    oldestSpeedSample %= SPEEDOMETER_NUM_SAMPLES;
+    return;
+  }
+
+  speedSamples[ oldestSpeedSample ] = speed;
+  if( maxSpeedSample == oldestSpeedSample++ )
+  {
+    // if old max was overwritten find a new one
+    int i;
+    for( maxSpeedSample = 0, i = 1; i < SPEEDOMETER_NUM_SAMPLES; i++ )
+    {
+      if( speedSamples[ i ] > speedSamples[ maxSpeedSample ] )
+        maxSpeedSample = i;
+    }
+  }
+
+  oldestSpeedSample %= SPEEDOMETER_NUM_SAMPLES;
+}
+
+#define SPEEDOMETER_MIN_RANGE 900
+#define SPEED_MED 1000.f
+#define SPEED_FAST 1600.f
+
+/*
+===================
+CG_DrawSpeedGraph
+===================
+*/
+static void CG_DrawSpeedGraph( rectDef_t *rect, vec4_t foreColor,
+                               vec4_t backColor )
+{
+  int i;
+  float val, max, top;
+  // colour of graph is interpolated between these values
+  const vec3_t slow = { 0.0, 0.0, 1.0 };
+  const vec3_t medium = { 0.0, 1.0, 0.0 };
+  const vec3_t fast = { 1.0, 0.0, 0.0 };
+  vec4_t color;
+
+  max = speedSamples[ maxSpeedSample ];
+  if( max < SPEEDOMETER_MIN_RANGE )
+    max = SPEEDOMETER_MIN_RANGE;
+
+  trap_R_SetColor( backColor );
+  CG_DrawPic( rect->x, rect->y, rect->w, rect->h, cgs.media.whiteShader );
+
+  Vector4Copy( foreColor, color );
+
+  for( i = 1; i < SPEEDOMETER_NUM_SAMPLES; i++ )
+  {
+    val = speedSamples[ ( oldestSpeedSample + i ) % SPEEDOMETER_NUM_SAMPLES ];
+    if( val < SPEED_MED )
+      VectorLerp( val / SPEED_MED, slow, medium, color );
+    else if( val < SPEED_FAST )
+      VectorLerp( ( val - SPEED_MED ) / ( SPEED_FAST - SPEED_MED ),
+                  medium, fast, color );
+    else
+      VectorCopy( fast, color );
+    trap_R_SetColor( color );
+    top = rect->y + ( 1 - val / max ) * rect->h;
+    CG_DrawPic( rect->x + ( i / (float)SPEEDOMETER_NUM_SAMPLES ) * rect->w, top,
+                rect->w / (float)SPEEDOMETER_NUM_SAMPLES, val * rect->h / max,
+                cgs.media.whiteShader );
+  }
+  trap_R_SetColor( NULL );
+}
+
+/*
+===================
+CG_DrawSpeedText
+===================
+*/
+static void CG_DrawSpeedText( rectDef_t *rect, float text_x, float text_y,
+                              float scale, vec4_t foreColor )
+{
+  char speedstr[ 16 ];
+  float val;
+  vec4_t color;
+
+  VectorCopy( foreColor, color );
+  color[ 3 ] = 1;
+  if( cg.predictedPlayerState.clientNum == cg.clientNum )
+  {
+    vec3_t vel;
+    VectorCopy( cg.predictedPlayerState.velocity, vel );
+    if( cg_drawSpeed.integer & SPEEDOMETER_IGNORE_Z )
+      vel[ 2 ] = 0;
+    val = VectorLength( vel );
+  }
+  else if( oldestSpeedSample == 0 )
+    val = speedSamples[ SPEEDOMETER_NUM_SAMPLES - 1 ];
+  else
+    val = speedSamples[ oldestSpeedSample - 1 ];
+
+  Com_sprintf( speedstr, sizeof( speedstr ), "%d", (int)val );
+
+  UI_Text_Paint(
+      rect->x + ( rect->w - UI_Text_Width( speedstr, scale ) ) / 2.0f,
+      rect->y + ( rect->h + UI_Text_Height( speedstr, scale ) ) / 2.0f,
+      scale, color, speedstr, 0, 0, ITEM_TEXTSTYLE_NORMAL );
+}
+
+/*
+===================
+CG_DrawSpeed
+===================
+*/
+static void CG_DrawSpeed( rectDef_t *rect, float text_x, float text_y,
+                          float scale, vec4_t foreColor, vec4_t backColor )
+{
+  if( cg_drawSpeed.integer & SPEEDOMETER_DRAW_GRAPH )
+    CG_DrawSpeedGraph( rect, foreColor, backColor );
+  if( cg_drawSpeed.integer & SPEEDOMETER_DRAW_TEXT )
+    CG_DrawSpeedText( rect, text_x, text_y, scale, foreColor );
+}
+
+/*
+===================
+CG_DrawConsole
+===================
+*/
+static void CG_DrawConsole( rectDef_t *rect, float text_x, float text_y, vec4_t color,
+                            float scale, int textalign, int textvalign, int textStyle )
+{
+  UI_DrawTextBlock( rect, text_x, text_y, color, scale, textalign, textvalign, textStyle, cg.consoleText );
+}
+
+/*
+===================
+CG_DrawTutorial
+===================
+*/
+static void CG_DrawTutorial( rectDef_t *rect, float text_x, float text_y, vec4_t color,
+                            float scale, int textalign, int textvalign, int textStyle )
+{
+  if( !cg_tutorial.integer )
+    return;
+
+  UI_DrawTextBlock( rect, text_x, text_y, color, scale, textalign, textvalign, textStyle, CG_TutorialText( ) );
+}
+
+/*
+===================
+CG_DrawWeaponIcon
+===================
+*/
+void CG_DrawWeaponIcon( rectDef_t *rect, vec4_t color )
+{
+  int           maxAmmo;
+  centity_t     *cent;
+  playerState_t *ps;
+  weapon_t      weapon;
+
+  cent = &cg_entities[ cg.snap->ps.clientNum ];
+  ps = &cg.snap->ps;
+  weapon = BG_GetPlayerWeapon( ps );
+
+  maxAmmo = BG_Weapon( weapon )->maxAmmo;
+
+  // don't display if dead
+  if( cg.predictedPlayerState.stats[ STAT_HEALTH ] <= 0 )
+    return;
+
+  if( weapon <= WP_NONE || weapon >= WP_NUM_WEAPONS )
+  {
+    CG_Error( "CG_DrawWeaponIcon: weapon out of range: %d\n", weapon );
+    return;
+  }
+
+  if( !cg_weapons[ weapon ].registered )
+  {
+    Com_Printf( S_COLOR_YELLOW "WARNING: CG_DrawWeaponIcon: weapon %d (%s) "
+        "is not registered\n", weapon, BG_Weapon( weapon )->name );
+    return;
+  }
+
+  if( ps->clips == 0 && !BG_Weapon( weapon )->infiniteAmmo )
+  {
+    float ammoPercent = (float)ps->ammo / (float)maxAmmo;
+
+    if( ammoPercent < 0.33f )
+    {
+      color[ 0 ] = 1.0f;
+      color[ 1 ] = color[ 2 ] = 0.0f;
+    }
+  }
+
+  if( cg.predictedPlayerState.stats[ STAT_TEAM ] == TEAM_ALIENS &&
+      !BG_AlienCanEvolve( cg.predictedPlayerState.stats[ STAT_CLASS ],
+                          ps->persistant[ PERS_CREDIT ], cgs.alienStage ) )
+  {
+    if( cg.time - cg.lastEvolveAttempt <= NO_CREDITS_TIME )
+    {
+      if( ( ( cg.time - cg.lastEvolveAttempt ) / 300 ) % 2 )
+        color[ 3 ] = 0.0f;
+    }
+  }
+
+  trap_R_SetColor( color );
+  CG_DrawPic( rect->x, rect->y, rect->w, rect->h,
+              cg_weapons[ weapon ].weaponIcon );
+  trap_R_SetColor( NULL );
+}
+
+
+
+/*
+================================================================================
+
+CROSSHAIR
+
+================================================================================
+*/
+
+
+
+/*
+=================
+CG_DrawCrosshair
+=================
+*/
+static void CG_DrawCrosshair( rectDef_t *rect, vec4_t color )
+{
+  float         w, h;
+  qhandle_t     hShader;
+  float         x, y;
+  weaponInfo_t  *wi;
+  weapon_t      weapon;
+  weapon_t      curWeapon = WP_NONE;
+  weapon = BG_GetPlayerWeapon( &cg.snap->ps );
+
+  
+	
+  if( cg_drawCrosshair.integer == CROSSHAIR_ALWAYSOFF )
+    return;
+
+  if( cg_drawCrosshair.integer == CROSSHAIR_RANGEDONLY &&
+      !BG_Weapon( weapon )->longRanged )
+    return;
+
+  if( cg.snap->ps.persistant[ PERS_SPECSTATE ] != SPECTATOR_NOT )
+    return;
+
+  if( cg.renderingThirdPerson )
+    return;
+
+  if( cg.snap->ps.pm_type == PM_INTERMISSION )
+    return;
+
+  wi = &cg_weapons[ weapon ];
+
+  w = h = wi->crossHairSize * cg_crosshairSize.value;
+  w *= cgDC.aspectScale;
+
+  //FIXME: this still ignores the width/height of the rect, but at least it's
+  //neater than cg_crosshairX/cg_crosshairY
+  x = rect->x + ( rect->w / 2 ) - ( w / 2 );
+  y = rect->y + ( rect->h / 2 ) - ( h / 2 );
+
+  hShader = wi->crossHair;
+	  
+  //aiming at a friendly player/buildable, dim the crosshair
+  if( cg.time == cg.crosshairClientTime || cg.crosshairBuildable >= 0 )
+  {
+    int i;
+    for( i = 0; i < 3; i++ )
+      color[i] *= .5f;
+
+  }
+  
+
+  if( hShader != 0 )
+  {
+
+    trap_R_SetColor( color );
+    CG_DrawPic( x, y, w, h, hShader );
+    trap_R_SetColor( NULL );
+  }
+
+}
+
+
+
+/*
+=================
+CG_ScanForCrosshairEntity
+=================
+*/
+static void CG_ScanForCrosshairEntity( void )
+{
+  trace_t   trace;
+  vec3_t    start, end;
+  int       content;
+  team_t    team;
+
+
+  VectorCopy( cg.refdef.vieworg, start );
+  VectorMA( start, 131072, cg.refdef.viewaxis[ 0 ], end );
+
+  CG_Trace( &trace, start, vec3_origin, vec3_origin, end,
+    cg.snap->ps.clientNum, CONTENTS_SOLID|CONTENTS_BODY );
+
+  // if the player is in fog, don't show it
+  content = trap_CM_PointContents( trace.endpos, 0 );
+  if( content & CONTENTS_FOG )
+    return;
+
+  if( trace.entityNum >= MAX_CLIENTS )
+  {
+    entityState_t *s = &cg_entities[ trace.entityNum ].currentState;
+    if( s->eType == ET_BUILDABLE && BG_Buildable( s->modelindex )->team ==
+        cg.snap->ps.stats[ STAT_TEAM ] )
+      cg.crosshairBuildable = trace.entityNum;
+    else
+      cg.crosshairBuildable = -1;
+
+    return;
+  }
+
+  team = cgs.clientinfo[ trace.entityNum ].team;
+
+  if( cg.snap->ps.stats[ STAT_TEAM ] != TEAM_NONE )
+  {
+    //only display team names of those on the same team as this player
+    if( team != cg.snap->ps.stats[ STAT_TEAM ] )
+      return;
+  }
+
+  // update the fade timer
+  cg.crosshairClientNum = trace.entityNum;
+  cg.crosshairClientTime = cg.time;
+}
+
+
+/*
+=====================
+CG_DrawLocation
+=====================
+*/
+static void CG_DrawLocation( rectDef_t *rect, float scale, int textalign, vec4_t color )
+{
+  const char    *location;
+  centity_t     *locent;
+  float         maxX;
+  float         tx = rect->x, ty = rect->y;
+
+  if( cg.intermissionStarted )
+    return;
+
+  maxX = rect->x + rect->w;
+
+  locent = CG_GetPlayerLocation( );
+  if( locent )
+    location = CG_ConfigString( CS_LOCATIONS + locent->currentState.generic1 );
+  else
+    location = CG_ConfigString( CS_LOCATIONS );
+
+  // need to skip horiz. align if it's too long, but valign must be run either way
+  if( UI_Text_Width( location, scale ) < rect->w )
+  {
+    CG_AlignText( rect, location, scale, 0.0f, 0.0f, textalign, VALIGN_CENTER, &tx, &ty );
+    UI_Text_Paint( tx, ty, scale, color, location, 0, 0, ITEM_TEXTSTYLE_NORMAL );
+  }
+  else
+  {
+    CG_AlignText( rect, location, scale, 0.0f, 0.0f, ALIGN_NONE, VALIGN_CENTER, &tx, &ty );
+    UI_Text_Paint_Limit( &maxX, tx, ty, scale, color, location, 0, 0 );
+  }
+
+  trap_R_SetColor( NULL );
+}
+
+/*
+=====================
+CG_DrawCrosshairNames
+=====================
+*/
+static void CG_DrawCrosshairNames( rectDef_t *rect, float scale, int textStyle )
+{
+  float   *color;
+  char    *name;
+  float   w, x;
+
+  if( !cg_drawCrosshairNames.integer )
+    return;
+
+  if( cg.renderingThirdPerson )
+    return;
+
+  // scan the known entities to see if the crosshair is sighted on one
+  CG_ScanForCrosshairEntity( );
+
+  // draw the name of the player being looked at
+  color = CG_FadeColor( cg.crosshairClientTime, CROSSHAIR_CLIENT_TIMEOUT );
+  if( !color )
+  {
+    trap_R_SetColor( NULL );
+    return;
+  }
+
+  // add health from overlay info to the crosshair client name
+  name = cgs.clientinfo[ cg.crosshairClientNum ].name;
+  if( cg_teamOverlayUserinfo.integer &&
+      cg.snap->ps.stats[ STAT_TEAM ] != TEAM_NONE &&
+      cgs.teaminfoReceievedTime &&
+      cgs.clientinfo[ cg.crosshairClientNum ].health > 0 )
+  {
+    name = va( "%s ^7[^%c%d^7]", name,
+               CG_GetColorCharForHealth( cg.crosshairClientNum ),
+               cgs.clientinfo[ cg.crosshairClientNum ].health );
+  }
+
+  w = UI_Text_Width( name, scale );
+  x = rect->x + rect->w / 2.0f;
+  UI_Text_Paint( x - w / 2.0f, rect->y + rect->h, scale, color, name, 0, 0, textStyle );
+  trap_R_SetColor( NULL );
+}
+
+/*
+===============
+CG_OwnerDraw
+
+Draw an owner drawn item
+===============
+*/
+void CG_OwnerDraw( float x, float y, float w, float h, float text_x,
+                   float text_y, int ownerDraw, int ownerDrawFlags,
+                   int align, int textalign, int textvalign, float borderSize,
+                   float scale, vec4_t foreColor, vec4_t backColor,
+                   qhandle_t shader, int textStyle )
+{
+  rectDef_t rect;
+
+  rect.x = x;
+  rect.y = y;
+  rect.w = w;
+  rect.h = h;
+
+  switch( ownerDraw )
+  {
+    case CG_PLAYER_CREDITS_VALUE:
+      CG_DrawPlayerCreditsValue( &rect, foreColor, qtrue );
+      break;
+    case CG_PLAYER_CREDITS_FRACTION:
+      CG_DrawPlayerCreditsFraction( &rect, foreColor, shader );
+      break;
+    case CG_PLAYER_CREDITS_VALUE_NOPAD:
+      CG_DrawPlayerCreditsValue( &rect, foreColor, qfalse );
+      break;
+    case CG_PLAYER_STAMINA_1:
+    case CG_PLAYER_STAMINA_2:
+    case CG_PLAYER_STAMINA_3:
+    case CG_PLAYER_STAMINA_4:
+      CG_DrawPlayerStamina( ownerDraw, &rect, backColor, foreColor, shader );
+      break;
+    case CG_PLAYER_STAMINA_BOLT:
+      CG_DrawPlayerStaminaBolt( &rect, backColor, foreColor, shader );
+      break;
+    case CG_PLAYER_AMMO_VALUE:
+      CG_DrawPlayerAmmoValue( &rect, foreColor );
+      break;
+    case CG_PLAYER_CLIPS_VALUE:
+      CG_DrawPlayerClipsValue( &rect, foreColor );
+      break;
+    case CG_PLAYER_BUILD_TIMER:
+      CG_DrawPlayerBuildTimer( &rect, foreColor );
+      break;
+    case CG_PLAYER_HEALTH:
+      CG_DrawPlayerHealthValue( &rect, foreColor );
+      break;
+    case CG_PLAYER_HEALTH_CROSS:
+      CG_DrawPlayerHealthCross( &rect, foreColor );
+      break;
+    case CG_PLAYER_CHARGE_BAR_BG:
+      CG_DrawPlayerChargeBarBG( &rect, foreColor, shader );
+      break;
+    case CG_PLAYER_CHARGE_BAR:
+      CG_DrawPlayerChargeBar( &rect, foreColor, shader );
+      break;
+    case CG_PLAYER_CLIPS_RING:
+      CG_DrawPlayerClipsRing( &rect, backColor, foreColor, shader );
+      break;
+    case CG_PLAYER_BUILD_TIMER_RING:
+      CG_DrawPlayerBuildTimerRing( &rect, backColor, foreColor, shader );
+      break;
+    case CG_PLAYER_WALLCLIMBING:
+      CG_DrawPlayerWallclimbing( &rect, backColor, foreColor, shader );
+      break;
+    case CG_PLAYER_BOOSTED:
+      CG_DrawPlayerBoosted( &rect, backColor, foreColor, shader );
+      break;
+    case CG_PLAYER_BOOST_BOLT:
+      CG_DrawPlayerBoosterBolt( &rect, backColor, foreColor, shader );
+      break;
+    case CG_PLAYER_FBREATH:
+      CG_DrawPlayerFbreath( &rect, foreColor, shader );
+      break;
+    case CG_PLAYER_PRICKLES:
+      CG_DrawPlayerPrickles( &rect, foreColor, shader );
+      break;
+    case CG_PLAYER_BOMBS:
+      CG_DrawPlayerBombs( &rect, foreColor, shader );
+      break;
+    case CG_PLAYER_POISON_BARBS:
+      CG_DrawPlayerPoisonBarbs( &rect, foreColor, shader );
+      break;
+    case CG_DRAW_INVI_STAT:
+      CG_DrawInvisbleStatus( &rect, foreColor, shader );
+      break;
+    case CG_DRAW_INVI_OVERLAY:
+      CG_DrawInvisbleOverlay( &rect, foreColor, shader );
+      break;
+    case CG_PLAYER_ALIEN_SENSE:
+      CG_DrawAlienSense( &rect );
+      break;
+    case CG_PLAYER_HUMAN_SCANNER:
+      CG_DrawHumanScanner( &rect, shader, foreColor );
+      break;
+    case CG_PLAYER_USABLE_BUILDABLE:
+      CG_DrawUsableBuildable( &rect, shader, foreColor );
+      break;
+    case CG_KILLER:
+      CG_DrawKiller( &rect, scale, foreColor, shader, textStyle );
+      break;
+    case CG_PLAYER_SELECT:
+      CG_DrawItemSelect( &rect, foreColor );
+      break;
+    case CG_PLAYER_WEAPONICON:
+      CG_DrawWeaponIcon( &rect, foreColor );
+      break;
+    case CG_PLAYER_SELECTTEXT:
+      CG_DrawItemSelectText( &rect, scale, textStyle );
+      break;
+    case CG_SPECTATORS:
+      CG_DrawTeamSpectators( &rect, scale, textvalign, foreColor, shader );
+      break;
+    case CG_PLAYER_LOCATION:
+      CG_DrawLocation( &rect, scale, textalign, foreColor );
+      break;
+    case CG_FOLLOW:
+      CG_DrawFollow( &rect, text_x, text_y, foreColor, scale,
+                     textalign, textvalign, textStyle );
+      break;
+    case CG_PLAYER_CROSSHAIRNAMES:
+      CG_DrawCrosshairNames( &rect, scale, textStyle );
+      break;
+    case CG_PLAYER_CROSSHAIR:
+      CG_DrawCrosshair( &rect, foreColor );
+      break;
+    case CG_STAGE_REPORT_TEXT:
+      CG_DrawStageReport( &rect, text_x, text_y, foreColor, scale, textalign, textvalign, textStyle );
+      break;
+    case CG_ALIENS_SCORE_LABEL:
+      CG_DrawTeamLabel( &rect, TEAM_ALIENS, text_x, text_y, foreColor, scale, textalign, textvalign, textStyle );
+      break;
+    case CG_HUMANS_SCORE_LABEL:
+      CG_DrawTeamLabel( &rect, TEAM_HUMANS, text_x, text_y, foreColor, scale, textalign, textvalign, textStyle );
+      break;
+
+    //loading screen
+    case CG_LOAD_LEVELSHOT:
+      CG_DrawLevelShot( &rect );
+      break;
+    case CG_LOAD_EDGESHOT:
+      CG_DrawEdgeShot( &rect );
+      break;
+    case CG_LOAD_MEDIA:
+      CG_DrawMediaProgress( &rect, foreColor, scale, align, textalign, textStyle,
+                            borderSize );
+      break;
+    case CG_LOAD_MEDIA_LABEL:
+      CG_DrawMediaProgressLabel( &rect, text_x, text_y, foreColor, scale, textalign, textvalign );
+      break;
+    case CG_LOAD_BUILDABLES:
+      CG_DrawBuildablesProgress( &rect, foreColor, scale, align, textalign,
+                                 textStyle, borderSize );
+      break;
+    case CG_LOAD_BUILDABLES_LABEL:
+      CG_DrawBuildablesProgressLabel( &rect, text_x, text_y, foreColor, scale, textalign, textvalign );
+      break;
+    case CG_LOAD_CHARMODEL:
+      CG_DrawCharModelProgress( &rect, foreColor, scale, align, textalign,
+                                textStyle, borderSize );
+      break;
+    case CG_LOAD_CHARMODEL_LABEL:
+      CG_DrawCharModelProgressLabel( &rect, text_x, text_y, foreColor, scale, textalign, textvalign );
+      break;
+    case CG_LOAD_OVERALL:
+      CG_DrawOverallProgress( &rect, foreColor, scale, align, textalign, textStyle,
+                              borderSize );
+      break;
+    case CG_LOAD_LEVELNAME:
+      CG_DrawLevelName( &rect, text_x, text_y, foreColor, scale, textalign, textvalign, textStyle );
+      break;
+    case CG_LOAD_MOTD:
+      CG_DrawMOTD( &rect, text_x, text_y, foreColor, scale, textalign, textvalign, textStyle );
+      break;
+    case CG_LOAD_HOSTNAME:
+      CG_DrawHostname( &rect, text_x, text_y, foreColor, scale, textalign, textvalign, textStyle );
+      break;
+
+    case CG_FPS:
+      CG_DrawFPS( &rect, text_x, text_y, scale, foreColor, textalign, textvalign, textStyle, qtrue );
+      break;
+    case CG_FPS_FIXED:
+      CG_DrawFPS( &rect, text_x, text_y, scale, foreColor, textalign, textvalign, textStyle, qfalse );
+      break;
+    case CG_TIMER:
+      CG_DrawTimer( &rect, text_x, text_y, scale, foreColor, textalign, textvalign, textStyle );
+      break;
+    case CG_CLOCK:
+      CG_DrawClock( &rect, text_x, text_y, scale, foreColor, textalign, textvalign, textStyle );
+      break;
+    case CG_TIMER_MINS:
+      CG_DrawTimerMins( &rect, foreColor );
+      break;
+    case CG_TIMER_SECS:
+      CG_DrawTimerSecs( &rect, foreColor );
+      break;
+    case CG_SNAPSHOT:
+      CG_DrawSnapshot( &rect, text_x, text_y, scale, foreColor, textalign, textvalign, textStyle );
+      break;
+    case CG_LAGOMETER:
+      CG_DrawLagometer( &rect, text_x, text_y, scale, foreColor );
+      break;
+    case CG_TEAMOVERLAY:
+      CG_DrawTeamOverlay( &rect, scale, foreColor );
+      break;
+    case CG_SPEEDOMETER:
+      CG_DrawSpeed( &rect, text_x, text_y, scale, foreColor, backColor );
+      break;
+
+    case CG_DEMO_PLAYBACK:
+      CG_DrawDemoPlayback( &rect, foreColor, shader );
+      break;
+    case CG_DEMO_RECORDING:
+      CG_DrawDemoRecording( &rect, foreColor, shader );
+      break;
+
+    case CG_CONSOLE:
+      CG_DrawConsole( &rect, text_x, text_y, foreColor, scale, textalign, textvalign, textStyle );
+      break;
+
+    case CG_TUTORIAL:
+      CG_DrawTutorial( &rect, text_x, text_y, foreColor, scale, textalign, textvalign, textStyle );
+      break;
+
+    default:
+      break;
+  }
+}
+
+void CG_MouseEvent( int x, int y )
+{
+  int n;
+
+  if( ( cg.predictedPlayerState.pm_type == PM_NORMAL ||
+        cg.predictedPlayerState.pm_type == PM_SPECTATOR ) &&
+        cg.showScores == qfalse )
+  {
+    trap_Key_SetCatcher( 0 );
+    return;
+  }
+
+  cgs.cursorX += x;
+  if( cgs.cursorX < 0 )
+    cgs.cursorX = 0;
+  else if( cgs.cursorX > 640 )
+    cgs.cursorX = 640;
+
+  cgs.cursorY += y;
+  if( cgs.cursorY < 0 )
+    cgs.cursorY = 0;
+  else if( cgs.cursorY > 480 )
+    cgs.cursorY = 480;
+
+  n = Display_CursorType( cgs.cursorX, cgs.cursorY );
+  cgs.activeCursor = 0;
+  if( n == CURSOR_ARROW )
+    cgs.activeCursor = cgs.media.selectCursor;
+  else if( n == CURSOR_SIZER )
+    cgs.activeCursor = cgs.media.sizeCursor;
+
+  if( cgs.capturedItem )
+    Display_MouseMove( cgs.capturedItem, x, y );
+  else
+    Display_MouseMove( NULL, cgs.cursorX, cgs.cursorY );
+}
+
+/*
+==================
+CG_HideTeamMenus
+==================
+
+*/
+void CG_HideTeamMenu( void )
+{
+  Menus_CloseByName( "teamMenu" );
+  Menus_CloseByName( "getMenu" );
+}
+
+/*
+==================
+CG_ShowTeamMenus
+==================
+
+*/
+void CG_ShowTeamMenu( void )
+{
+  Menus_ActivateByName( "teamMenu" );
+}
+
+/*
+==================
+CG_EventHandling
+
+type 0 - no event handling
+     1 - team menu
+     2 - hud editor
+==================
+*/
+void CG_EventHandling( int type )
+{
+  cgs.eventHandling = type;
+
+  if( type == CGAME_EVENT_NONE )
+    CG_HideTeamMenu( );
+}
+
+
+
+void CG_KeyEvent( int key, qboolean down )
+{
+  if( !down )
+    return;
+
+  if( cg.predictedPlayerState.pm_type == PM_NORMAL ||
+      ( cg.predictedPlayerState.pm_type == PM_SPECTATOR &&
+        cg.showScores == qfalse ) )
+  {
+    CG_EventHandling( CGAME_EVENT_NONE );
+    trap_Key_SetCatcher( 0 );
+    return;
+  }
+
+  Display_HandleKey( key, down, cgs.cursorX, cgs.cursorY );
+
+  if( cgs.capturedItem )
+    cgs.capturedItem = NULL;
+  else
+  {
+    if( key == K_MOUSE2 && down )
+      cgs.capturedItem = Display_CaptureItem( cgs.cursorX, cgs.cursorY );
+  }
+}
+
+int CG_ClientNumFromName( const char *p )
+{
+  int i;
+
+  for( i = 0; i < cgs.maxclients; i++ )
+  {
+    if( cgs.clientinfo[ i ].infoValid &&
+        Q_stricmp( cgs.clientinfo[ i ].name, p ) == 0 )
+      return i;
+  }
+
+  return -1;
+}
+
+void CG_RunMenuScript( char **args )
+{
+}
+//END TA UI
+
+
+/*
+================
+CG_DrawLighting
+
+================
+*/
+static void CG_DrawLighting( void )
+{
+  centity_t   *cent;
+
+  cent = &cg_entities[ cg.snap->ps.clientNum ];
+
+  //fade to black if stamina is low
+  if( ( cg.snap->ps.stats[ STAT_STAMINA ] < STAMINA_BLACKOUT_LEVEL ) &&
+      ( cg.snap->ps.stats[ STAT_TEAM ] == TEAM_HUMANS ) )
+  {
+    vec4_t black = { 0, 0, 0, 0 };
+    black[ 3 ] = 1.0 - ( (float)( cg.snap->ps.stats[ STAT_STAMINA ] + 1000 ) / 200.0f );
+    trap_R_SetColor( black );
+    CG_DrawPic( 0, 0, 640, 480, cgs.media.whiteShader );
+    trap_R_SetColor( NULL );
+  }
+}
+
+/*
+===============================================================================
+
+CENTER PRINTING
+
+===============================================================================
+*/
+
+
+/*
+==============
+CG_CenterPrint
+
+Called for important messages that should stay in the center of the screen
+for a few moments
+==============
+*/
+void CG_CenterPrint( const char *str, int y, int charWidth )
+{
+  char  *s;
+  char newlineParsed[ MAX_STRING_CHARS ];
+  const char *wrapped;
+  static int maxWidth = (int)( ( 2.0f / 3.0f ) * (float)SCREEN_WIDTH );
+
+  Q_ParseNewlines( newlineParsed, str, sizeof( newlineParsed ) );
+
+  wrapped = Item_Text_Wrap( newlineParsed, 0.5f, maxWidth );
+
+  Q_strncpyz( cg.centerPrint, wrapped, sizeof( cg.centerPrint ) );
+
+  cg.centerPrintTime = cg.time;
+  cg.centerPrintY = y;
+  cg.centerPrintCharWidth = charWidth;
+
+  // count the number of lines for centering
+  cg.centerPrintLines = 1;
+  s = cg.centerPrint;
+  while( *s )
+  {
+    if( *s == '\n' )
+      cg.centerPrintLines++;
+
+    s++;
+  }
+}
+
+
+/*
+===================
+CG_DrawCenterString
+===================
+*/
+static void CG_DrawCenterString( void )
+{
+  char  *start;
+  int   l;
+  int   x, y, w;
+  int h;
+  float *color;
+
+  if( !cg.centerPrintTime )
+    return;
+
+  color = CG_FadeColor( cg.centerPrintTime, 1000 * cg_centertime.value );
+  if( !color )
+    return;
+
+  trap_R_SetColor( color );
+
+  start = cg.centerPrint;
+
+  y = cg.centerPrintY - cg.centerPrintLines * BIGCHAR_HEIGHT / 2;
+
+  while( 1 )
+  {
+    char linebuffer[ MAX_STRING_CHARS ];
+
+    for( l = 0; l < sizeof(linebuffer) - 1; l++ )
+    {
+      if( !start[ l ] || start[ l ] == '\n' )
+        break;
+
+      linebuffer[ l ] = start[ l ];
+    }
+
+    linebuffer[ l ] = 0;
+
+    w = UI_Text_Width( linebuffer, 0.5 );
+    h = UI_Text_Height( linebuffer, 0.5 );
+    x = ( SCREEN_WIDTH - w ) / 2;
+    UI_Text_Paint( x, y + h, 0.5, color, linebuffer, 0, 0, ITEM_TEXTSTYLE_SHADOWEDMORE );
+    y += h + 6;
+
+    while( *start && ( *start != '\n' ) )
+      start++;
+
+    if( !*start )
+      break;
+
+    start++;
+  }
+
+  trap_R_SetColor( NULL );
+}
+
+
+
+
+
+//==============================================================================
+
+//FIXME: both vote notes are hardcoded, change to ownerdrawn?
+
+/*
+=================
+CG_DrawVote
+=================
+*/
+static void CG_DrawVote( team_t team )
+{
+  char    *s;
+  int     sec;
+  int     offset = 0;
+  vec4_t  white = { 1.0f, 1.0f, 1.0f, 1.0f };
+  char    yeskey[ 32 ] = "", nokey[ 32 ] = "";
+
+	
+  if( !cgs.voteTime[ team ] )
+    return;
+	
+  sec = ( VOTE_TIME - ( cg.time - cgs.voteTime[ team ] ) ) / 1000;
+  
+  //play dong sound
+  if(sec > 28 )
+  {
+    trap_S_StartLocalSound( cgs.media.iniVote, CHAN_LOCAL_SOUND );
+  }
+
+ // if( cgs.voteModified[ team ] && (sec < 26 ) )
+ // {
+ //   cgs.voteModified[ team ] = qfalse;
+ //   trap_S_StartLocalSound( cgs.media.talkSound, CHAN_LOCAL_SOUND );
+  //}
+
+
+
+  if( sec < 0 )
+    sec = 0;
+
+  //if( cg_tutorial.integer )
+  //{
+  //  Com_sprintf( yeskey, sizeof( yeskey ), "[%s]", 
+  //    CG_KeyBinding( va( "^5%svote yes", team == TEAM_NONE ? "" : "team" ) ) );
+  //  Com_sprintf( nokey, sizeof( nokey ), "[%s]", 
+  //    CG_KeyBinding( va( "^5%svote no", team == TEAM_NONE ? "" : "team" ) ) );
+  //}
+
+  if( team != TEAM_NONE )
+    offset = -50;
+
+  s = va( "^5[yemf]%sVOTE [ ^7%i^5 | %sYes:%i - %sNo:%i ]: %s", 
+    team == TEAM_NONE ? "" : "TEAM", sec, yeskey, cgs.voteYes[ team ], nokey, cgs.voteNo[ team ], cgs.voteString[ team ] );
+
+  UI_Text_Paint( 8, 360 + offset, 0.4f, white, s, 0, 0,
+    ITEM_TEXTSTYLE_NORMAL );
+
+  s = va( "^5Called by: \"%s\"", cgs.voteCaller[ team ] );
+
+  UI_Text_Paint( 26, 376 + offset, 0.28f, white, s, 0, 0,
+    ITEM_TEXTSTYLE_NORMAL );
+
+  s = va( "^5(Vote: F1: Yes , F2: No | Teamvote: F3: Yes , F4: No) " );
+
+  UI_Text_Paint( 26, 390 + offset, 0.22f, white, s, 0, 0,
+    ITEM_TEXTSTYLE_NORMAL );
+ // s = va( "^5%sYes:%i %sNo:%i",
+//    yeskey, cgs.voteYes[ team ], nokey, cgs.voteNo[ team ] );
+
+//  UI_Text_Paint( 8, 340 + offset, 0.3f, white, s, 0, 0,
+//    ITEM_TEXTSTYLE_NORMAL );
+}
+
+
+static qboolean CG_DrawScoreboard( void )
+{
+  static qboolean firstTime = qtrue;
+  float fade, *fadeColor;
+
+  if( menuScoreboard )
+    menuScoreboard->window.flags &= ~WINDOW_FORCED;
+
+  if( cg_paused.integer )
+  {
+    cg.deferredPlayerLoading = 0;
+    firstTime = qtrue;
+    return qfalse;
+  }
+
+  if( cg.showScores ||
+      cg.predictedPlayerState.pm_type == PM_INTERMISSION )
+  {
+    fade = 1.0;
+    fadeColor = colorWhite;
+  }
+  else
+  {
+    cg.deferredPlayerLoading = 0;
+    cg.killerName[ 0 ] = 0;
+    firstTime = qtrue;
+    return qfalse;
+  }
+
+ // menuScoreboard == NULL 
+ if( cg.predictedPlayerState.pm_type == PM_INTERMISSION )
+ {
+          menuScoreboard = Menus_FindByName( "teamscore_menu" );
+ }
+ else
+ {
+          menuScoreboard = Menus_FindByName( "teamscore_menu" );
+ }
+ 
+ 
+  if( menuScoreboard )
+  {
+    if( firstTime )
+    {
+      cg.spectatorTime = trap_Milliseconds();
+      CG_SetScoreSelection( menuScoreboard );
+      firstTime = qfalse;
+    }
+
+    Menu_Update( menuScoreboard );
+    Menu_Paint( menuScoreboard, qtrue );
+  }
+
+  return qtrue;
+}
+
+/*
+=================
+CG_DrawIntermission
+=================
+*/
+static void CG_DrawIntermission( void )
+{
+  menuDef_t *menu = Menus_FindByName( "default_hud" );
+
+  Menu_Update( menu );
+  Menu_Paint( menu, qtrue );
+
+  cg.scoreFadeTime = cg.time;
+  cg.scoreBoardShowing = CG_DrawScoreboard( );
+}
+
+/*
+=================
+CG_DrawQueue
+=================
+*/
+static qboolean CG_DrawQueue( void )
+{
+  float       w;
+  vec4_t      color;
+  int         position;
+  char        *ordinal, buffer[ MAX_STRING_CHARS ];
+
+  if( !( cg.snap->ps.pm_flags & PMF_QUEUED ) )
+    return qfalse;
+
+  color[ 0 ] = 1;
+  color[ 1 ] = 1;
+  color[ 2 ] = 1;
+  color[ 3 ] = 1;
+
+  position = cg.snap->ps.persistant[ PERS_QUEUEPOS ] + 1;
+  if( position < 1 )
+    return qfalse;
+
+  switch( position % 100 )
+  {
+    case 11:
+    case 12:
+    case 13:
+      ordinal = "th";
+      break;
+    default:
+      switch( position % 10 )
+      {
+        case 1:  ordinal = "st"; break;
+        case 2:  ordinal = "nd"; break;
+        case 3:  ordinal = "rd"; break;
+        default: ordinal = "th"; break;
+      }
+      break;
+  }
+
+  Com_sprintf( buffer, MAX_STRING_CHARS, "You are %d%s in the spawn queue",
+               position, ordinal );
+
+  w = UI_Text_Width( buffer, 0.7f );
+  UI_Text_Paint( 320 - w / 2, 360, 0.7f, color, buffer, 0, 0, ITEM_TEXTSTYLE_SHADOWED );
+
+  if( cg.snap->ps.persistant[ PERS_SPAWNS ] == 0 )
+    Com_sprintf( buffer, MAX_STRING_CHARS, "There are no spawns remaining" );
+  else if( cg.snap->ps.persistant[ PERS_SPAWNS ] == 1 )
+    Com_sprintf( buffer, MAX_STRING_CHARS, "There is 1 spawn remaining" );
+  else
+    Com_sprintf( buffer, MAX_STRING_CHARS, "There are %d spawns remaining",
+                 cg.snap->ps.persistant[ PERS_SPAWNS ] );
+
+  w = UI_Text_Width( buffer, 0.7f );
+  UI_Text_Paint( 320 - w / 2, 400, 0.7f, color, buffer, 0, 0, ITEM_TEXTSTYLE_SHADOWED );
+
+  return qtrue;
+}
+
+
+/*
+=================
+CG_DrawWarmup
+=================
+*/
+static void CG_DrawWarmup( void )
+{
+  int    sec = 0;
+  int    w;
+  int    h;
+  float  size = 0.5f;
+  char   text[ MAX_STRING_CHARS ] = "Warmup Time:";
+
+  if( !cg.warmupTime )
+    return;
+
+  sec = ( cg.warmupTime - cg.time ) / 1000;
+
+  if( sec < 0 )
+    return;
+
+  w = UI_Text_Width( text, size );
+  h = UI_Text_Height( text, size );
+  UI_Text_Paint( 320 - w / 2, 200, size, colorWhite, text, 0, 0, ITEM_TEXTSTYLE_SHADOWED );
+
+  Com_sprintf( text, sizeof( text ), "%s", sec ? va( "%d", sec ) : "^5EDGE 7 | 2014" );
+  //  Com_sprintf( ,,,,"^5EDGE 5 | by yalt 2013" );
+  w = UI_Text_Width( text, size );
+  UI_Text_Paint( 320 - w / 2, 200 + 1.5f * h, size, colorWhite, text, 0, 0, ITEM_TEXTSTYLE_SHADOWED );
+}
+
+//==================================================================================
+
+/*
+=================
+CG_Draw2D
+=================
+*/
+static void CG_Draw2D( void )
+{
+  menuDef_t *menu = NULL;
+ 
+
+  
+
+  // if we are taking a levelshot for the menu, don't draw anything
+  if( cg.levelShot )
+    return;
+
+  // fading to black if stamina runs out
+  // (only 2D that can't be disabled)
+  CG_DrawLighting( );
+
+  if( cg_draw2D.integer == 0 )
+    return;
+
+  if( cg.snap->ps.pm_type == PM_INTERMISSION )
+  {
+    CG_DrawIntermission( );
+    return;
+  }
+
+  if( cg.snap->ps.persistant[ PERS_SPECSTATE ] == SPECTATOR_NOT &&
+      cg.snap->ps.stats[ STAT_HEALTH ] > 0 )
+  {
+    menu = Menus_FindByName( BG_ClassConfig(
+      cg.predictedPlayerState.stats[ STAT_CLASS ] )->hudName );
+
+    if (!(( cg.snap->ps.stats[ STAT_BUILDABLE ] & ~SB_VALID_TOGGLEBIT ) > BA_NONE ))
+      CG_DrawBuildableStatus( );
+  }
+
+  if( !menu )
+  {
+    menu = Menus_FindByName( "default_hud" );
+
+    if( !menu ) // still couldn't find it
+      CG_Error( "Default HUD could not be found" );
+  }
+
+  Menu_Update( menu );
+  Menu_Paint( menu, qtrue );
+
+
+
+
+  
+  
+  CG_DrawVote( TEAM_NONE );
+  CG_DrawVote( cg.predictedPlayerState.stats[ STAT_TEAM ] );
+  CG_DrawWarmup( );
+  CG_DrawQueue( );
+
+  // don't draw center string if scoreboard is up
+  cg.scoreBoardShowing = CG_DrawScoreboard( );
+  
+
+  if( !cg.scoreBoardShowing )
+    CG_DrawCenterString( );
+}
+
+/*
+===============
+CG_ScalePainBlendTCs
+===============
+*/
+static void CG_ScalePainBlendTCs( float* s1, float *t1, float *s2, float *t2 )
+{
+  *s1 -= 0.5f;
+  *t1 -= 0.5f;
+  *s2 -= 0.5f;
+  *t2 -= 0.5f;
+
+  *s1 *= cg_painBlendZoom.value;
+  *t1 *= cg_painBlendZoom.value;
+  *s2 *= cg_painBlendZoom.value;
+  *t2 *= cg_painBlendZoom.value;
+
+  *s1 += 0.5f;
+  *t1 += 0.5f;
+  *s2 += 0.5f;
+  *t2 += 0.5f;
+}
+
+#define PAINBLEND_BORDER    0.15f
+
+/*
+===============
+CG_PainBlend
+===============
+*/
+static void CG_PainBlend( void )
+{
+  vec4_t      color;
+  int         damage;
+  float       damageAsFracOfMax;
+  qhandle_t   shader = cgs.media.viewBloodShader;
+  float       x, y, w, h;
+  float       s1, t1, s2, t2;
+
+  if( cg.snap->ps.persistant[ PERS_SPECSTATE ] != SPECTATOR_NOT || cg.intermissionStarted )
+    return;
+
+  damage = cg.lastHealth - cg.snap->ps.stats[ STAT_HEALTH ];
+
+  if( damage < 0 )
+    damage = 0;
+
+  damageAsFracOfMax = (float)damage / cg.snap->ps.stats[ STAT_MAX_HEALTH ];
+  cg.lastHealth = cg.snap->ps.stats[ STAT_HEALTH ];
+
+  cg.painBlendValue += damageAsFracOfMax * cg_painBlendScale.value;
+
+  if( cg.painBlendValue > 0.0f )
+  {
+    cg.painBlendValue -= ( cg.frametime / 1000.0f ) *
+      cg_painBlendDownRate.value;
+  }
+
+  if( cg.painBlendValue > 1.0f )
+    cg.painBlendValue = 1.0f;
+  else if( cg.painBlendValue <= 0.0f )
+  {
+    cg.painBlendValue = 0.0f;
+    return;
+  }
+
+  if( cg.snap->ps.stats[ STAT_TEAM ] == TEAM_ALIENS )
+    VectorSet( color, 0.43f, 0.8f, 0.37f );
+  else if( cg.snap->ps.stats[ STAT_TEAM ] == TEAM_HUMANS )
+    VectorSet( color, 0.8f, 0.0f, 0.0f );
+
+  if( cg.painBlendValue > cg.painBlendTarget )
+  {
+    cg.painBlendTarget += ( cg.frametime / 1000.0f ) *
+      cg_painBlendUpRate.value;
+  }
+  else if( cg.painBlendValue < cg.painBlendTarget )
+    cg.painBlendTarget = cg.painBlendValue;
+
+  if( cg.painBlendTarget > cg_painBlendMax.value )
+    cg.painBlendTarget = cg_painBlendMax.value;
+
+  color[ 3 ] = cg.painBlendTarget;
+
+  trap_R_SetColor( color );
+
+  //left
+  x = 0.0f; y = 0.0f;
+  w = PAINBLEND_BORDER * 640.0f; h = 480.0f;
+  CG_AdjustFrom640( &x, &y, &w, &h );
+  s1 = 0.0f; t1 = 0.0f;
+  s2 = PAINBLEND_BORDER; t2 = 1.0f;
+  CG_ScalePainBlendTCs( &s1, &t1, &s2, &t2 );
+  trap_R_DrawStretchPic( x, y, w, h, s1, t1, s2, t2, shader );
+
+  //right
+  x = 640.0f - ( PAINBLEND_BORDER * 640.0f ); y = 0.0f;
+  w = PAINBLEND_BORDER * 640.0f; h = 480.0f;
+  CG_AdjustFrom640( &x, &y, &w, &h );
+  s1 = 1.0f - PAINBLEND_BORDER; t1 = 0.0f;
+  s2 = 1.0f; t2 =1.0f;
+  CG_ScalePainBlendTCs( &s1, &t1, &s2, &t2 );
+  trap_R_DrawStretchPic( x, y, w, h, s1, t1, s2, t2, shader );
+
+  //top
+  x = PAINBLEND_BORDER * 640.0f; y = 0.0f;
+  w = 640.0f - ( 2 * PAINBLEND_BORDER * 640.0f ); h = PAINBLEND_BORDER * 480.0f;
+  CG_AdjustFrom640( &x, &y, &w, &h );
+  s1 = PAINBLEND_BORDER; t1 = 0.0f;
+  s2 = 1.0f - PAINBLEND_BORDER; t2 = PAINBLEND_BORDER;
+  CG_ScalePainBlendTCs( &s1, &t1, &s2, &t2 );
+  trap_R_DrawStretchPic( x, y, w, h, s1, t1, s2, t2, shader );
+
+  //bottom
+  x = PAINBLEND_BORDER * 640.0f; y = 480.0f - ( PAINBLEND_BORDER * 480.0f );
+  w = 640.0f - ( 2 * PAINBLEND_BORDER * 640.0f ); h = PAINBLEND_BORDER * 480.0f;
+  CG_AdjustFrom640( &x, &y, &w, &h );
+  s1 = PAINBLEND_BORDER; t1 = 1.0f - PAINBLEND_BORDER;
+  s2 = 1.0f - PAINBLEND_BORDER; t2 = 1.0f;
+  CG_ScalePainBlendTCs( &s1, &t1, &s2, &t2 );
+  trap_R_DrawStretchPic( x, y, w, h, s1, t1, s2, t2, shader );
+
+  trap_R_SetColor( NULL );
+}
+
+/*
+=====================
+CG_ResetPainBlend
+=====================
+*/
+void CG_ResetPainBlend( void )
+{
+  cg.painBlendValue = 0.0f;
+  cg.painBlendTarget = 0.0f;
+  cg.lastHealth = cg.snap->ps.stats[ STAT_HEALTH ];
+}
+
+/*
+=====================
+CG_DrawActive
+
+Perform all drawing needed to completely fill the screen
+=====================
+*/
+void CG_DrawActive( stereoFrame_t stereoView )
+{
+  float   separation;
+  vec3_t    baseOrg;
+
+  // optionally draw the info screen instead
+  if( !cg.snap )
+    return;
+
+  switch ( stereoView )
+  {
+    case STEREO_CENTER:
+      separation = 0;
+      break;
+    case STEREO_LEFT:
+      separation = -cg_stereoSeparation.value / 2;
+      break;
+    case STEREO_RIGHT:
+      separation = cg_stereoSeparation.value / 2;
+      break;
+    default:
+      separation = 0;
+      CG_Error( "CG_DrawActive: Undefined stereoView" );
+  }
+
+  // clear around the rendered view if sized down
+  CG_TileClear( );
+
+  // offset vieworg appropriately if we're doing stereo separation
+  VectorCopy( cg.refdef.vieworg, baseOrg );
+
+  if( separation != 0 )
+    VectorMA( cg.refdef.vieworg, -separation, cg.refdef.viewaxis[ 1 ],
+              cg.refdef.vieworg );
+
+  // draw 3D view
+  trap_R_RenderScene( &cg.refdef );
+
+  // restore original viewpoint if running stereo
+  if( separation != 0 )
+    VectorCopy( baseOrg, cg.refdef.vieworg );
+
+  // first person blend blobs, done after AnglesToAxis
+  if( !cg.renderingThirdPerson )
+    CG_PainBlend( );
+
+  // draw status bar and other floating elements
+  CG_Draw2D( );
+}
+
diff --git a/src/cgame/cg_drawtools.c b/src/cgame/cg_drawtools.c
new file mode 100644
index 0000000..f35ed23
--- /dev/null
+++ b/src/cgame/cg_drawtools.c
@@ -0,0 +1,439 @@
+/*
+===========================================================================
+Copyright (C) 1999-2005 Id Software, Inc.
+Copyright (C) 2000-2009 Darklegion Development
+
+This file is part of Tremulous.
+
+Tremulous 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.
+
+Tremulous 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 Tremulous; if not, write to the Free Software
+Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
+===========================================================================
+*/
+
+// cg_drawtools.c -- helper functions called by cg_draw, cg_scoreboard, cg_info, etc
+
+
+#include "cg_local.h"
+
+/*
+===============
+CG_DrawPlane
+
+Draw a quad in 3 space - basically CG_DrawPic in 3 space
+===============
+*/
+void CG_DrawPlane( vec3_t origin, vec3_t down, vec3_t right, qhandle_t shader )
+{
+  polyVert_t  verts[ 4 ];
+  vec3_t      temp;
+
+  VectorCopy( origin, verts[ 0 ].xyz );
+  verts[ 0 ].st[ 0 ] = 0;
+  verts[ 0 ].st[ 1 ] = 0;
+  verts[ 0 ].modulate[ 0 ] = 255;
+  verts[ 0 ].modulate[ 1 ] = 255;
+  verts[ 0 ].modulate[ 2 ] = 255;
+  verts[ 0 ].modulate[ 3 ] = 255;
+
+  VectorAdd( origin, right, temp );
+  VectorCopy( temp, verts[ 1 ].xyz );
+  verts[ 1 ].st[ 0 ] = 1;
+  verts[ 1 ].st[ 1 ] = 0;
+  verts[ 1 ].modulate[ 0 ] = 255;
+  verts[ 1 ].modulate[ 1 ] = 255;
+  verts[ 1 ].modulate[ 2 ] = 255;
+  verts[ 1 ].modulate[ 3 ] = 255;
+
+  VectorAdd( origin, right, temp );
+  VectorAdd( temp, down, temp );
+  VectorCopy( temp, verts[ 2 ].xyz );
+  verts[ 2 ].st[ 0 ] = 1;
+  verts[ 2 ].st[ 1 ] = 1;
+  verts[ 2 ].modulate[ 0 ] = 255;
+  verts[ 2 ].modulate[ 1 ] = 255;
+  verts[ 2 ].modulate[ 2 ] = 255;
+  verts[ 2 ].modulate[ 3 ] = 255;
+
+  VectorAdd( origin, down, temp );
+  VectorCopy( temp, verts[ 3 ].xyz );
+  verts[ 3 ].st[ 0 ] = 0;
+  verts[ 3 ].st[ 1 ] = 1;
+  verts[ 3 ].modulate[ 0 ] = 255;
+  verts[ 3 ].modulate[ 1 ] = 255;
+  verts[ 3 ].modulate[ 2 ] = 255;
+  verts[ 3 ].modulate[ 3 ] = 255;
+
+  trap_R_AddPolyToScene( shader, 4, verts );
+}
+
+/*
+================
+CG_AdjustFrom640
+
+Adjusted for resolution and screen aspect ratio
+================
+*/
+void CG_AdjustFrom640( float *x, float *y, float *w, float *h )
+{
+#if 0
+  // adjust for wide screens
+  if ( cgs.glconfig.vidWidth * 480 > cgs.glconfig.vidHeight * 640 ) {
+    *x += 0.5 * ( cgs.glconfig.vidWidth - ( cgs.glconfig.vidHeight * 640 / 480 ) );
+  }
+#endif
+  // scale for screen sizes
+  *x *= cgs.screenXScale;
+  *y *= cgs.screenYScale;
+  *w *= cgs.screenXScale;
+  *h *= cgs.screenYScale;
+}
+
+/*
+================
+CG_FillRect
+
+Coordinates are 640*480 virtual values
+=================
+*/
+void CG_FillRect( float x, float y, float width, float height, const float *color )
+{
+  trap_R_SetColor( color );
+
+  CG_AdjustFrom640( &x, &y, &width, &height );
+  trap_R_DrawStretchPic( x, y, width, height, 0, 0, 0, 0, cgs.media.whiteShader );
+
+  trap_R_SetColor( NULL );
+}
+
+
+/*
+================
+CG_DrawSides
+
+Coords are virtual 640x480
+================
+*/
+void CG_DrawSides( float x, float y, float w, float h, float size )
+{
+  float sizeY;
+
+  CG_AdjustFrom640( &x, &y, &w, &h );
+  sizeY = size * cgs.screenYScale;
+  size *= cgs.screenXScale;
+
+  trap_R_DrawStretchPic( x, y + sizeY, size, h - ( sizeY * 2.0f ), 0, 0, 0, 0, cgs.media.whiteShader );
+  trap_R_DrawStretchPic( x + w - size, y + sizeY, size, h - ( sizeY * 2.0f ), 0, 0, 0, 0, cgs.media.whiteShader );
+}
+
+void CG_DrawTopBottom( float x, float y, float w, float h, float size )
+{
+  CG_AdjustFrom640( &x, &y, &w, &h );
+  size *= cgs.screenYScale;
+  trap_R_DrawStretchPic( x, y, w, size, 0, 0, 0, 0, cgs.media.whiteShader );
+  trap_R_DrawStretchPic( x, y + h - size, w, size, 0, 0, 0, 0, cgs.media.whiteShader );
+}
+
+
+/*
+================
+CG_DrawRect
+
+Coordinates are 640*480 virtual values
+=================
+*/
+void CG_DrawRect( float x, float y, float width, float height, float size, const float *color )
+{
+  trap_R_SetColor( color );
+
+  CG_DrawTopBottom( x, y, width, height, size );
+  CG_DrawSides( x, y, width, height, size );
+
+  trap_R_SetColor( NULL );
+}
+
+
+/*
+================
+CG_DrawPic
+
+Coordinates are 640*480 virtual values
+=================
+*/
+void CG_DrawPic( float x, float y, float width, float height, qhandle_t hShader )
+{
+  CG_AdjustFrom640( &x, &y, &width, &height );
+  trap_R_DrawStretchPic( x, y, width, height, 0, 0, 1, 1, hShader );
+}
+
+/*
+================
+CG_SetClipRegion
+=================
+*/
+void CG_SetClipRegion( float x, float y, float w, float h )
+{
+  vec4_t clip;
+
+  CG_AdjustFrom640( &x, &y, &w, &h );
+
+  clip[ 0 ] = x;
+  clip[ 1 ] = y;
+  clip[ 2 ] = x + w;
+  clip[ 3 ] = y + h;
+
+  trap_R_SetClipRegion( clip );
+}
+
+/*
+================
+CG_ClearClipRegion
+=================
+*/
+void CG_ClearClipRegion( void )
+{
+  trap_R_SetClipRegion( NULL );
+}
+
+/*
+================
+CG_DrawFadePic
+
+Coordinates are 640*480 virtual values
+=================
+*/
+void CG_DrawFadePic( float x, float y, float width, float height, vec4_t fcolor,
+                     vec4_t tcolor, float amount, qhandle_t hShader )
+{
+  vec4_t  finalcolor;
+  float   inverse;
+
+  inverse = 100 - amount;
+
+  CG_AdjustFrom640( &x, &y, &width, &height );
+
+  finalcolor[ 0 ] = ( ( inverse * fcolor[ 0 ] ) + ( amount * tcolor[ 0 ] ) ) / 100;
+  finalcolor[ 1 ] = ( ( inverse * fcolor[ 1 ] ) + ( amount * tcolor[ 1 ] ) ) / 100;
+  finalcolor[ 2 ] = ( ( inverse * fcolor[ 2 ] ) + ( amount * tcolor[ 2 ] ) ) / 100;
+  finalcolor[ 3 ] = ( ( inverse * fcolor[ 3 ] ) + ( amount * tcolor[ 3 ] ) ) / 100;
+
+  trap_R_SetColor( finalcolor );
+  trap_R_DrawStretchPic( x, y, width, height, 0, 0, 1, 1, hShader );
+  trap_R_SetColor( NULL );
+}
+
+/*
+=================
+CG_DrawStrlen
+
+Returns character count, skiping color escape codes
+=================
+*/
+int CG_DrawStrlen( const char *str )
+{
+  const char  *s = str;
+  int         count = 0;
+
+  while( *s )
+  {
+    if( Q_IsColorString( s ) )
+      s += 2;
+    else
+    {
+      count++;
+      s++;
+    }
+  }
+
+  return count;
+}
+
+/*
+=============
+CG_TileClearBox
+
+This repeats a 64*64 tile graphic to fill the screen around a sized down
+refresh window.
+=============
+*/
+static void CG_TileClearBox( int x, int y, int w, int h, qhandle_t hShader )
+{
+  float s1, t1, s2, t2;
+
+  s1 = x / 64.0;
+  t1 = y / 64.0;
+  s2 = ( x + w ) / 64.0;
+  t2 = ( y + h ) / 64.0;
+  trap_R_DrawStretchPic( x, y, w, h, s1, t1, s2, t2, hShader );
+}
+
+
+
+/*
+==============
+CG_TileClear
+
+Clear around a sized down screen
+==============
+*/
+void CG_TileClear( void )
+{
+  int   top, bottom, left, right;
+  int   w, h;
+
+  w = cgs.glconfig.vidWidth;
+  h = cgs.glconfig.vidHeight;
+
+  if( cg.refdef.x == 0 && cg.refdef.y == 0 &&
+      cg.refdef.width == w && cg.refdef.height == h )
+    return;   // full screen rendering
+
+  top = cg.refdef.y;
+  bottom = top + cg.refdef.height - 1;
+  left = cg.refdef.x;
+  right = left + cg.refdef.width - 1;
+
+  // clear above view screen
+  CG_TileClearBox( 0, 0, w, top, cgs.media.backTileShader );
+
+  // clear below view screen
+  CG_TileClearBox( 0, bottom, w, h - bottom, cgs.media.backTileShader );
+
+  // clear left of view screen
+  CG_TileClearBox( 0, top, left, bottom - top + 1, cgs.media.backTileShader );
+
+  // clear right of view screen
+  CG_TileClearBox( right, top, w - right, bottom - top + 1, cgs.media.backTileShader );
+}
+
+/*
+================
+CG_FadeColor
+================
+*/
+float *CG_FadeColor( int startMsec, int totalMsec )
+{
+  static vec4_t   color;
+  int     t;
+
+  if( startMsec == 0 )
+    return NULL;
+
+  t = cg.time - startMsec;
+
+  if( t >= totalMsec )
+    return NULL;
+
+  // fade out
+  if( totalMsec - t < FADE_TIME )
+    color[ 3 ] = ( totalMsec - t ) * 1.0 / FADE_TIME;
+  else
+    color[ 3 ] = 1.0;
+
+  color[ 0 ] = color[ 1 ] = color[ 2 ] = 1;
+
+  return color;
+}
+
+/*
+================
+CG_WorldToScreen
+================
+*/
+qboolean CG_WorldToScreen( vec3_t point, float *x, float *y )
+{
+  vec3_t  trans;
+  float   xc, yc;
+  float   px, py;
+  float   z;
+
+  px = tan( cg.refdef.fov_x * M_PI / 360.0f );
+  py = tan( cg.refdef.fov_y * M_PI / 360.0f );
+
+  VectorSubtract( point, cg.refdef.vieworg, trans );
+
+  xc = ( 640.0f * cg_viewsize.integer ) / 200.0f;
+  yc = ( 480.0f * cg_viewsize.integer ) / 200.0f;
+
+  z = DotProduct( trans, cg.refdef.viewaxis[ 0 ] );
+  if( z <= 0.001f )
+    return qfalse;
+
+  if( x )
+    *x = 320.0f - DotProduct( trans, cg.refdef.viewaxis[ 1 ] ) * xc / ( z * px );
+
+  if( y )
+    *y = 240.0f - DotProduct( trans, cg.refdef.viewaxis[ 2 ] ) * yc / ( z * py );
+
+  return qtrue;
+}
+
+/*
+================
+CG_KeyBinding
+================
+*/
+char *CG_KeyBinding( const char *bind )
+{
+  static char key[ 32 ];
+  char bindbuff[ MAX_CVAR_VALUE_STRING ];
+  int i;
+
+  key[ 0 ] = '\0';
+
+  // NOTE: change K_LAST_KEY to MAX_KEYS for full key support (eventually)
+  for( i = 0; i < K_LAST_KEY; i++ )
+  {
+    trap_Key_GetBindingBuf( i, bindbuff, sizeof( bindbuff ) );
+    if( !Q_stricmp( bindbuff, bind ) )
+    {
+      trap_Key_KeynumToStringBuf( i, key, sizeof( key ) );
+      break;
+    }
+  }
+
+  if( !key[ 0 ] )
+  {
+    Q_strncpyz( key, "\\", sizeof( key ) );
+    Q_strcat( key, sizeof( key ), bind );
+  }
+
+  return key;
+}
+
+/*
+=================
+CG_GetColorCharForHealth
+=================
+*/
+char CG_GetColorCharForHealth( int clientnum )
+{
+  char health_char = '2';
+  int  healthPercent;
+  int  maxHealth;
+  int  curWeaponClass = cgs.clientinfo[ clientnum ].curWeaponClass;
+
+  if( cgs.clientinfo[ clientnum ].team == TEAM_ALIENS )
+    maxHealth = BG_Class( curWeaponClass )->health;
+  else
+    maxHealth = BG_Class( PCL_HUMAN )->health;
+
+  healthPercent = (int) ( 100.0f * (float) cgs.clientinfo[ clientnum ].health /
+                        (float) maxHealth );
+
+  if( healthPercent < 33 )
+    health_char = '1';
+  else if( healthPercent < 67 )
+    health_char = '3';
+  return health_char;
+}
diff --git a/src/cgame/cg_ents.c b/src/cgame/cg_ents.c
new file mode 100644
index 0000000..7b27579
--- /dev/null
+++ b/src/cgame/cg_ents.c
@@ -0,0 +1,1253 @@
+/*
+===========================================================================
+Copyright (C) 1999-2005 Id Software, Inc.
+Copyright (C) 2000-2009 Darklegion Development
+
+This file is part of Tremulous.
+
+Tremulous 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.
+
+Tremulous 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 Tremulous; if not, write to the Free Software
+Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
+===========================================================================
+*/
+
+// cg_ents.c -- present snapshot entities, happens every single frame
+
+
+#include "cg_local.h"
+
+/*
+======================
+CG_DrawBoxFace
+
+Draws a bounding box face
+======================
+*/
+static void CG_DrawBoxFace( vec3_t a, vec3_t b, vec3_t c, vec3_t d )
+{
+  polyVert_t  verts[ 4 ];
+  vec4_t      color = { 255.0f, 0.0f, 0.0f, 128.0f };
+
+  VectorCopy( d, verts[ 0 ].xyz );
+  verts[ 0 ].st[ 0 ] = 1;
+  verts[ 0 ].st[ 1 ] = 1;
+  Vector4Copy( color, verts[ 0 ].modulate );
+
+  VectorCopy( c, verts[ 1 ].xyz );
+  verts[ 1 ].st[ 0 ] = 1;
+  verts[ 1 ].st[ 1 ] = 0;
+  Vector4Copy( color, verts[ 1 ].modulate );
+
+  VectorCopy( b, verts[ 2 ].xyz );
+  verts[ 2 ].st[ 0 ] = 0;
+  verts[ 2 ].st[ 1 ] = 0;
+  Vector4Copy( color, verts[ 2 ].modulate );
+
+  VectorCopy( a, verts[ 3 ].xyz );
+  verts[ 3 ].st[ 0 ] = 0;
+  verts[ 3 ].st[ 1 ] = 1;
+  Vector4Copy( color, verts[ 3 ].modulate );
+
+  trap_R_AddPolyToScene( cgs.media.outlineShader, 4, verts );
+}
+
+/*
+======================
+CG_DrawBoundingBox
+
+Draws a bounding box
+======================
+*/
+void CG_DrawBoundingBox( vec3_t origin, vec3_t mins, vec3_t maxs )
+{
+  vec3_t  ppp, mpp, mmp, pmp;
+  vec3_t  mmm, pmm, ppm, mpm;
+
+  ppp[ 0 ] = origin[ 0 ] + maxs[ 0 ];
+  ppp[ 1 ] = origin[ 1 ] + maxs[ 1 ];
+  ppp[ 2 ] = origin[ 2 ] + maxs[ 2 ];
+
+  mpp[ 0 ] = origin[ 0 ] + mins[ 0 ];
+  mpp[ 1 ] = origin[ 1 ] + maxs[ 1 ];
+  mpp[ 2 ] = origin[ 2 ] + maxs[ 2 ];
+
+  mmp[ 0 ] = origin[ 0 ] + mins[ 0 ];
+  mmp[ 1 ] = origin[ 1 ] + mins[ 1 ];
+  mmp[ 2 ] = origin[ 2 ] + maxs[ 2 ];
+
+  pmp[ 0 ] = origin[ 0 ] + maxs[ 0 ];
+  pmp[ 1 ] = origin[ 1 ] + mins[ 1 ];
+  pmp[ 2 ] = origin[ 2 ] + maxs[ 2 ];
+
+  ppm[ 0 ] = origin[ 0 ] + maxs[ 0 ];
+  ppm[ 1 ] = origin[ 1 ] + maxs[ 1 ];
+  ppm[ 2 ] = origin[ 2 ] + mins[ 2 ];
+
+  mpm[ 0 ] = origin[ 0 ] + mins[ 0 ];
+  mpm[ 1 ] = origin[ 1 ] + maxs[ 1 ];
+  mpm[ 2 ] = origin[ 2 ] + mins[ 2 ];
+
+  mmm[ 0 ] = origin[ 0 ] + mins[ 0 ];
+  mmm[ 1 ] = origin[ 1 ] + mins[ 1 ];
+  mmm[ 2 ] = origin[ 2 ] + mins[ 2 ];
+
+  pmm[ 0 ] = origin[ 0 ] + maxs[ 0 ];
+  pmm[ 1 ] = origin[ 1 ] + mins[ 1 ];
+  pmm[ 2 ] = origin[ 2 ] + mins[ 2 ];
+
+  //phew!
+
+  CG_DrawBoxFace( ppp, mpp, mmp, pmp );
+  CG_DrawBoxFace( ppp, pmp, pmm, ppm );
+  CG_DrawBoxFace( mpp, ppp, ppm, mpm );
+  CG_DrawBoxFace( mmp, mpp, mpm, mmm );
+  CG_DrawBoxFace( pmp, mmp, mmm, pmm );
+  CG_DrawBoxFace( mmm, mpm, ppm, pmm );
+}
+
+
+/*
+======================
+CG_PositionEntityOnTag
+
+Modifies the entities position and axis by the given
+tag location
+======================
+*/
+void CG_PositionEntityOnTag( refEntity_t *entity, const refEntity_t *parent,
+                             qhandle_t parentModel, char *tagName )
+{
+  int           i;
+  orientation_t lerped;
+
+  // lerp the tag
+  trap_R_LerpTag( &lerped, parentModel, parent->oldframe, parent->frame,
+                  1.0 - parent->backlerp, tagName );
+
+  // FIXME: allow origin offsets along tag?
+  VectorCopy( parent->origin, entity->origin );
+  for( i = 0; i < 3; i++ )
+    VectorMA( entity->origin, lerped.origin[ i ], parent->axis[ i ], entity->origin );
+
+  // had to cast away the const to avoid compiler problems...
+  MatrixMultiply( lerped.axis, ( (refEntity_t *)parent )->axis, entity->axis );
+  entity->backlerp = parent->backlerp;
+}
+
+
+/*
+======================
+CG_PositionRotatedEntityOnTag
+
+Modifies the entities position and axis by the given
+tag location
+======================
+*/
+void CG_PositionRotatedEntityOnTag( refEntity_t *entity, const refEntity_t *parent,
+                                    qhandle_t parentModel, char *tagName )
+{
+  int           i;
+  orientation_t lerped;
+  vec3_t        tempAxis[ 3 ];
+
+//AxisClear( entity->axis );
+  // lerp the tag
+  trap_R_LerpTag( &lerped, parentModel, parent->oldframe, parent->frame,
+                  1.0 - parent->backlerp, tagName );
+
+  // FIXME: allow origin offsets along tag?
+  VectorCopy( parent->origin, entity->origin );
+  for( i = 0; i < 3; i++ )
+    VectorMA( entity->origin, lerped.origin[ i ], parent->axis[ i ], entity->origin );
+
+  // had to cast away the const to avoid compiler problems...
+  MatrixMultiply( entity->axis, lerped.axis, tempAxis );
+  MatrixMultiply( tempAxis, ( (refEntity_t *)parent )->axis, entity->axis );
+}
+
+
+
+/*
+==========================================================================
+
+FUNCTIONS CALLED EACH FRAME
+
+==========================================================================
+*/
+
+/*
+======================
+CG_SetEntitySoundPosition
+
+Also called by event processing code
+======================
+*/
+void CG_SetEntitySoundPosition( centity_t *cent )
+{
+  if( cent->currentState.solid == SOLID_BMODEL )
+  {
+    vec3_t  origin;
+    float   *v;
+
+    v = cgs.inlineModelMidpoints[ cent->currentState.modelindex ];
+    VectorAdd( cent->lerpOrigin, v, origin );
+    trap_S_UpdateEntityPosition( cent->currentState.number, origin );
+  }
+  else
+    trap_S_UpdateEntityPosition( cent->currentState.number, cent->lerpOrigin );
+}
+
+/*
+==================
+CG_EntityEffects
+
+Add continuous entity effects, like local entity emission and lighting
+==================
+*/
+static void CG_EntityEffects( centity_t *cent )
+{
+  // update sound origins
+  CG_SetEntitySoundPosition( cent );
+
+  // add loop sound
+  if( cent->currentState.loopSound )
+  {
+    if( cent->currentState.eType != ET_SPEAKER )
+    {
+      trap_S_AddLoopingSound( cent->currentState.number, cent->lerpOrigin, vec3_origin,
+                              cgs.gameSounds[ cent->currentState.loopSound ] );
+    }
+    else
+    {
+      trap_S_AddRealLoopingSound( cent->currentState.number, cent->lerpOrigin, vec3_origin,
+                                  cgs.gameSounds[ cent->currentState.loopSound ] );
+    }
+  }
+
+
+  // constant light glow
+  if ( cent->currentState.constantLight )
+  {
+    int   cl;
+    int   i, r, g, b;
+
+    cl = cent->currentState.constantLight;
+    r = cl & 255;
+    g = ( cl >> 8 ) & 255;
+    b = ( cl >> 16 ) & 255;
+    i = ( ( cl >> 24 ) & 255 ) * 4;
+    trap_R_AddLightToScene( cent->lerpOrigin, i, r, g, b );
+  }
+
+  if( CG_IsTrailSystemValid( &cent->muzzleTS ) )
+  {
+    //FIXME hack to prevent tesla trails reaching too far
+    if( cent->currentState.eType == ET_BUILDABLE )
+    {
+      vec3_t  front, back;
+
+      CG_AttachmentPoint( &cent->muzzleTS->frontAttachment, front );
+      CG_AttachmentPoint( &cent->muzzleTS->backAttachment, back );
+
+      if( Distance( front, back ) > ( TESLAGEN_RANGE * M_ROOT3 ) )
+        CG_DestroyTrailSystem( &cent->muzzleTS );
+    }
+
+    if( cg.time > cent->muzzleTSDeathTime && CG_IsTrailSystemValid( &cent->muzzleTS ) )
+      CG_DestroyTrailSystem( &cent->muzzleTS );
+  }
+}
+
+
+/*
+==================
+CG_General
+==================
+*/
+static void CG_General( centity_t *cent )
+{
+  refEntity_t     ent;
+  entityState_t   *s1;
+
+  s1 = &cent->currentState;
+
+  // if set to invisible, skip
+  if( !s1->modelindex )
+    return;
+
+  memset( &ent, 0, sizeof( ent ) );
+
+  // set frame
+
+  ent.frame = s1->frame;
+  ent.oldframe = ent.frame;
+  ent.backlerp = 0;
+
+  VectorCopy( cent->lerpOrigin, ent.origin);
+  VectorCopy( cent->lerpOrigin, ent.oldorigin);
+
+  ent.hModel = cgs.gameModels[ s1->modelindex ];
+
+  // player model
+  if( s1->number == cg.snap->ps.clientNum )
+    ent.renderfx |= RF_THIRD_PERSON;  // only draw from mirrors
+
+  // convert angles to axis
+  AnglesToAxis( cent->lerpAngles, ent.axis );
+
+  // add to refresh list
+  trap_R_AddRefEntityToScene( &ent );
+}
+
+/*
+==================
+CG_Speaker
+
+Speaker entities can automatically play sounds
+==================
+*/
+static void CG_Speaker( centity_t *cent )
+{
+  if( ! cent->currentState.clientNum )
+  { // FIXME: use something other than clientNum...
+    return;   // not auto triggering
+  }
+
+  if( cg.time < cent->miscTime )
+    return;
+
+  trap_S_StartSound( NULL, cent->currentState.number, CHAN_ITEM, cgs.gameSounds[ cent->currentState.eventParm ] );
+
+  //  ent->s.frame = ent->wait * 10;
+  //  ent->s.clientNum = ent->random * 10;
+  cent->miscTime = cg.time + cent->currentState.frame * 100 + cent->currentState.clientNum * 100 * crandom( );
+}
+
+
+//============================================================================
+
+/*
+===============
+CG_LaunchMissile
+===============
+*/
+static void CG_LaunchMissile( centity_t *cent )
+{
+  entityState_t       *es;
+  const weaponInfo_t  *wi;
+  particleSystem_t    *ps;
+  trailSystem_t       *ts;
+  weapon_t            weapon;
+  weaponMode_t        weaponMode;
+
+  es = &cent->currentState;
+
+  weapon = es->weapon;
+  if( weapon > WP_NUM_WEAPONS )
+    weapon = WP_NONE;
+
+  wi = &cg_weapons[ weapon ];
+  weaponMode = es->generic1;
+
+  if( wi->wim[ weaponMode ].missileParticleSystem )
+  {
+    ps = CG_SpawnNewParticleSystem( wi->wim[ weaponMode ].missileParticleSystem );
+
+    if( CG_IsParticleSystemValid( &ps ) )
+    {
+      CG_SetAttachmentCent( &ps->attachment, cent );
+      CG_AttachToCent( &ps->attachment );
+      ps->charge = es->torsoAnim;
+    }
+  }
+
+  if( wi->wim[ weaponMode ].missileTrailSystem )
+  {
+    ts = CG_SpawnNewTrailSystem( wi->wim[ weaponMode ].missileTrailSystem );
+
+    if( CG_IsTrailSystemValid( &ts ) )
+    {
+      CG_SetAttachmentCent( &ts->frontAttachment, cent );
+      CG_AttachToCent( &ts->frontAttachment );
+    }
+  }
+}
+
+/*
+===============
+CG_Missile
+===============
+*/
+static void CG_Missile( centity_t *cent )
+{
+  refEntity_t             ent;
+  entityState_t           *es;
+  const weaponInfo_t      *wi;
+  weapon_t                weapon;
+  weaponMode_t            weaponMode;
+  const weaponInfoMode_t  *wim;
+
+  es = &cent->currentState;
+
+  weapon = es->weapon;
+  if( weapon > WP_NUM_WEAPONS )
+    weapon = WP_NONE;
+
+  wi = &cg_weapons[ weapon ];
+  weaponMode = es->generic1;
+
+  wim = &wi->wim[ weaponMode ];
+
+  // calculate the axis
+  VectorCopy( es->angles, cent->lerpAngles );
+
+  // add dynamic light
+  if( wim->missileDlight )
+  {
+    trap_R_AddLightToScene( cent->lerpOrigin, wim->missileDlight,
+      wim->missileDlightColor[ 0 ],
+      wim->missileDlightColor[ 1 ],
+      wim->missileDlightColor[ 2 ] );
+  }
+
+  // add missile sound
+  if( wim->missileSound )
+  {
+    vec3_t  velocity;
+
+    BG_EvaluateTrajectoryDelta( &cent->currentState.pos, cg.time, velocity );
+
+    trap_S_AddLoopingSound( cent->currentState.number, cent->lerpOrigin, velocity, wim->missileSound );
+  }
+
+  // create the render entity
+  memset( &ent, 0, sizeof( ent ) );
+  VectorCopy( cent->lerpOrigin, ent.origin );
+  VectorCopy( cent->lerpOrigin, ent.oldorigin );
+
+  if( wim->usesSpriteMissle )
+  {
+    ent.reType = RT_SPRITE;
+    ent.radius = wim->missileSpriteSize +
+                 wim->missileSpriteCharge * es->torsoAnim;
+    ent.rotation = 0;
+    ent.customShader = wim->missileSprite;
+    ent.shaderRGBA[ 0 ] = 0xFF;
+    ent.shaderRGBA[ 1 ] = 0xFF;
+    ent.shaderRGBA[ 2 ] = 0xFF;
+    ent.shaderRGBA[ 3 ] = 0xFF;
+  }
+  else
+  {
+    ent.hModel = wim->missileModel;
+    ent.renderfx = wim->missileRenderfx | RF_NOSHADOW;
+
+    // convert direction of travel into axis
+    if( VectorNormalize2( es->pos.trDelta, ent.axis[ 0 ] ) == 0 )
+      ent.axis[ 0 ][ 2 ] = 1;
+
+    // spin as it moves
+    if( es->pos.trType != TR_STATIONARY && wim->missileRotates )
+      RotateAroundDirection( ent.axis, cg.time / 4 );
+    else
+      RotateAroundDirection( ent.axis, es->time );
+
+    if( wim->missileAnimates )
+    {
+      int timeSinceStart = cg.time - es->time;
+
+      if( wim->missileAnimLooping )
+      {
+        ent.frame = wim->missileAnimStartFrame +
+          (int)( ( timeSinceStart / 1000.0f ) * wim->missileAnimFrameRate ) %
+          wim->missileAnimNumFrames;
+      }
+      else
+      {
+        ent.frame = wim->missileAnimStartFrame +
+          (int)( ( timeSinceStart / 1000.0f ) * wim->missileAnimFrameRate );
+
+        if( ent.frame > ( wim->missileAnimStartFrame + wim->missileAnimNumFrames ) )
+          ent.frame = wim->missileAnimStartFrame + wim->missileAnimNumFrames;
+      }
+    }
+  }
+
+  //only refresh if there is something to display
+  if( wim->missileSprite || wim->missileModel )
+    trap_R_AddRefEntityToScene( &ent );
+}
+
+/*
+===============
+CG_Mover
+===============
+*/
+static void CG_Mover( centity_t *cent )
+{
+  refEntity_t     ent;
+  entityState_t   *s1;
+
+  s1 = &cent->currentState;
+
+  // create the render entity
+  memset( &ent, 0, sizeof( ent ) );
+  VectorCopy( cent->lerpOrigin, ent.origin );
+  VectorCopy( cent->lerpOrigin, ent.oldorigin );
+  AnglesToAxis( cent->lerpAngles, ent.axis );
+
+  ent.renderfx = RF_NOSHADOW;
+
+  // flicker between two skins (FIXME?)
+  ent.skinNum = ( cg.time >> 6 ) & 1;
+
+  // get the model, either as a bmodel or a modelindex
+  if( s1->solid == SOLID_BMODEL )
+    ent.hModel = cgs.inlineDrawModel[ s1->modelindex ];
+  else
+    ent.hModel = cgs.gameModels[ s1->modelindex ];
+
+  // add to refresh list
+  trap_R_AddRefEntityToScene( &ent );
+
+  // add the secondary model
+  if( s1->modelindex2 )
+  {
+    ent.skinNum = 0;
+    ent.hModel = cgs.gameModels[ s1->modelindex2 ];
+    trap_R_AddRefEntityToScene( &ent );
+  }
+
+}
+
+/*
+===============
+CG_Beam
+
+Also called as an event
+===============
+*/
+void CG_Beam( centity_t *cent )
+{
+  refEntity_t     ent;
+  entityState_t   *s1;
+
+  s1 = &cent->currentState;
+
+  // create the render entity
+  memset( &ent, 0, sizeof( ent ) );
+  VectorCopy( s1->pos.trBase, ent.origin );
+  VectorCopy( s1->origin2, ent.oldorigin );
+  AxisClear( ent.axis );
+  ent.reType = RT_BEAM;
+
+  ent.renderfx = RF_NOSHADOW;
+
+  // add to refresh list
+  trap_R_AddRefEntityToScene( &ent );
+}
+
+
+/*
+===============
+CG_Portal
+===============
+*/
+static void CG_Portal( centity_t *cent )
+{
+  refEntity_t     ent;
+  entityState_t   *s1;
+
+  s1 = &cent->currentState;
+
+  // create the render entity
+  memset( &ent, 0, sizeof( ent ) );
+  VectorCopy( cent->lerpOrigin, ent.origin );
+  VectorCopy( s1->origin2, ent.oldorigin );
+  ByteToDir( s1->eventParm, ent.axis[ 0 ] );
+  PerpendicularVector( ent.axis[ 1 ], ent.axis[ 0 ] );
+
+  // negating this tends to get the directions like they want
+  // we really should have a camera roll value
+  VectorSubtract( vec3_origin, ent.axis[ 1 ], ent.axis[ 1 ] );
+
+  CrossProduct( ent.axis[ 0 ], ent.axis[ 1 ], ent.axis[ 2 ] );
+  ent.reType = RT_PORTALSURFACE;
+  ent.oldframe = s1->misc;
+  ent.frame = s1->frame;    // rotation speed
+  ent.skinNum = s1->clientNum / 256.0 * 360;  // roll offset
+
+  // add to refresh list
+  trap_R_AddRefEntityToScene( &ent );
+}
+
+//============================================================================
+
+#define SETBOUNDS(v1,v2,r)  ((v1)[0]=(-r/2),(v1)[1]=(-r/2),(v1)[2]=(-r/2),\
+                             (v2)[0]=(r/2),(v2)[1]=(r/2),(v2)[2]=(r/2))
+#define RADIUSSTEP          0.5f
+
+#define FLARE_OFF       0
+#define FLARE_NOFADE    1
+#define FLARE_TIMEFADE  2
+#define FLARE_REALFADE  3
+
+/*
+=========================
+CG_LightFlare
+=========================
+*/
+static void CG_LightFlare( centity_t *cent )
+{
+  refEntity_t   flare;
+  entityState_t *es;
+  vec3_t        forward, delta;
+  float         len;
+  trace_t       tr;
+  float         maxAngle;
+  vec3_t        mins, maxs, start, end;
+  float         srcRadius, srLocal, ratio = 1.0f;
+  int           entityNum;
+
+  es = &cent->currentState;
+
+  if( cg.renderingThirdPerson )
+    entityNum = MAGIC_TRACE_HACK;
+  else
+    entityNum = cg.predictedPlayerState.clientNum;
+
+  //don't draw light flares
+  if( cg_lightFlare.integer == FLARE_OFF )
+    return;
+
+  //flare is "off"
+  if( es->eFlags & EF_NODRAW )
+    return;
+
+  CG_Trace( &tr, cg.refdef.vieworg, NULL, NULL, es->angles2,
+            entityNum, MASK_SHOT );
+
+  //if there is no los between the view and the flare source
+  //it definately cannot be seen
+  if( tr.fraction < 1.0f || tr.allsolid )
+    return;
+
+  memset( &flare, 0, sizeof( flare ) );
+
+  flare.reType = RT_SPRITE;
+  flare.customShader = cgs.gameShaders[ es->modelindex ];
+  flare.shaderRGBA[ 0 ] = 0xFF;
+  flare.shaderRGBA[ 1 ] = 0xFF;
+  flare.shaderRGBA[ 2 ] = 0xFF;
+  flare.shaderRGBA[ 3 ] = 0xFF;
+
+  //flares always drawn before the rest of the scene
+  flare.renderfx |= RF_DEPTHHACK;
+
+  //bunch of geometry
+  AngleVectors( es->angles, forward, NULL, NULL );
+  VectorCopy( cent->lerpOrigin, flare.origin );
+  VectorSubtract( flare.origin, cg.refdef.vieworg, delta );
+  len = VectorLength( delta );
+  VectorNormalize( delta );
+
+  //flare is too close to camera to be drawn
+  if( len < es->generic1 )
+    return;
+
+  //don't bother for flares behind the view plane
+  if( DotProduct( delta, cg.refdef.viewaxis[ 0 ] ) < 0.0 )
+    return;
+
+  //only recalculate radius and ratio every three frames
+  if( !( cg.clientFrame % 2 ) )
+  {
+    //can only see the flare when in front of it
+    flare.radius = len / es->origin2[ 0 ];
+
+    if( es->origin2[ 2 ] == 0 )
+      srcRadius = srLocal = flare.radius / 2.0f;
+    else
+      srcRadius = srLocal = len / es->origin2[ 2 ];
+
+    maxAngle = es->origin2[ 1 ];
+
+    if( maxAngle > 0.0f )
+    {
+      float radiusMod = 1.0f - ( 180.0f - RAD2DEG(
+            acos( DotProduct( delta, forward ) ) ) ) / maxAngle;
+
+      if( radiusMod < 0.0f )
+        radiusMod = 0.0f;
+
+      flare.radius *= radiusMod;
+    }
+
+    if( flare.radius < 0.0f )
+      flare.radius = 0.0f;
+
+    VectorMA( flare.origin, -flare.radius, delta, end );
+    VectorMA( cg.refdef.vieworg, flare.radius, delta, start );
+
+    if( cg_lightFlare.integer == FLARE_REALFADE )
+    {
+      //"correct" flares
+      CG_BiSphereTrace( &tr, cg.refdef.vieworg, end,
+          1.0f, srcRadius, entityNum, MASK_SHOT );
+
+      if( tr.fraction < 1.0f )
+        ratio = tr.lateralFraction;
+      else
+        ratio = 1.0f;
+    }
+    else if( cg_lightFlare.integer == FLARE_TIMEFADE )
+    {
+      //draw timed flares
+      SETBOUNDS( mins, maxs, srcRadius );
+      CG_Trace( &tr, start, mins, maxs, end,
+                entityNum, MASK_SHOT );
+
+      if( ( tr.fraction < 1.0f || tr.startsolid ) && cent->lfs.status )
+      {
+        cent->lfs.status = qfalse;
+        cent->lfs.lastTime = cg.time;
+      }
+      else if( ( tr.fraction == 1.0f && !tr.startsolid ) && !cent->lfs.status )
+      {
+        cent->lfs.status = qtrue;
+        cent->lfs.lastTime = cg.time;
+      }
+
+      //fade flare up
+      if( cent->lfs.status )
+      {
+        if( cent->lfs.lastTime + es->time > cg.time )
+          ratio = (float)( cg.time - cent->lfs.lastTime ) / es->time;
+      }
+
+      //fade flare down
+      if( !cent->lfs.status )
+      {
+        if( cent->lfs.lastTime + es->time > cg.time )
+        {
+          ratio = (float)( cg.time - cent->lfs.lastTime ) / es->time;
+          ratio = 1.0f - ratio;
+        }
+        else
+          ratio = 0.0f;
+      }
+    }
+    else if( cg_lightFlare.integer == FLARE_NOFADE )
+    {
+      //draw nofade flares
+      SETBOUNDS( mins, maxs, srcRadius );
+      CG_Trace( &tr, start, mins, maxs, end,
+                entityNum, MASK_SHOT );
+
+      //flare source occluded
+      if( ( tr.fraction < 1.0f || tr.startsolid ) )
+        ratio = 0.0f;
+    }
+  }
+  else
+  {
+    ratio        = cent->lfs.lastRatio;
+    flare.radius = cent->lfs.lastRadius;
+  }
+
+  cent->lfs.lastRatio  = ratio;
+  cent->lfs.lastRadius = flare.radius;
+
+  if( ratio < 1.0f )
+  {
+    flare.radius *= ratio;
+    flare.shaderRGBA[ 3 ] = (byte)( (float)flare.shaderRGBA[ 3 ] * ratio );
+  }
+
+  if( flare.radius <= 0.0f )
+    return;
+
+  trap_R_AddRefEntityToScene( &flare );
+}
+
+/*
+=========================
+CG_Lev2ZapChain
+=========================
+*/
+static void CG_Lev2ZapChain( centity_t *cent )
+{
+  int           i;
+  entityState_t *es;
+  centity_t     *source = NULL, *target = NULL;
+  int           entityNums[ LEVEL2_AREAZAP_MAX_TARGETS + 1 ];
+  int           count;
+
+  es = &cent->currentState;
+
+  count = BG_UnpackEntityNumbers( es, entityNums, LEVEL2_AREAZAP_MAX_TARGETS + 1 );
+
+  for( i = 1; i < count; i++ )
+  {
+    if( i == 1 )
+    {
+      // First entity is the attacker
+      source = &cg_entities[ entityNums[ 0 ] ];
+    }
+    else
+    {
+      // Subsequent zaps come from the first target
+      source = &cg_entities[ entityNums[ 1 ] ];
+    }
+
+    target = &cg_entities[ entityNums[ i ] ];
+
+    if( !CG_IsTrailSystemValid( &cent->level2ZapTS[ i ] ) )
+      cent->level2ZapTS[ i ] = CG_SpawnNewTrailSystem( cgs.media.level2ZapTS );
+
+    if( CG_IsTrailSystemValid( &cent->level2ZapTS[ i ] ) )
+    {
+      CG_SetAttachmentCent( &cent->level2ZapTS[ i ]->frontAttachment, source );
+      CG_SetAttachmentCent( &cent->level2ZapTS[ i ]->backAttachment, target );
+      CG_AttachToCent( &cent->level2ZapTS[ i ]->frontAttachment );
+      CG_AttachToCent( &cent->level2ZapTS[ i ]->backAttachment );
+    }
+  }
+}
+
+/*
+=========================
+CG_AdjustPositionForMover
+
+Also called by client movement prediction code
+=========================
+*/
+void CG_AdjustPositionForMover( const vec3_t in, int moverNum, int fromTime, int toTime, vec3_t out )
+{
+  centity_t *cent;
+  vec3_t    oldOrigin, origin, deltaOrigin;
+  vec3_t    oldAngles, angles, deltaAngles;
+
+  if( moverNum <= 0 || moverNum >= ENTITYNUM_MAX_NORMAL )
+  {
+    VectorCopy( in, out );
+    return;
+  }
+
+  cent = &cg_entities[ moverNum ];
+
+  if( cent->currentState.eType != ET_MOVER )
+  {
+    VectorCopy( in, out );
+    return;
+  }
+
+  BG_EvaluateTrajectory( &cent->currentState.pos, fromTime, oldOrigin );
+  BG_EvaluateTrajectory( &cent->currentState.apos, fromTime, oldAngles );
+
+  BG_EvaluateTrajectory( &cent->currentState.pos, toTime, origin );
+  BG_EvaluateTrajectory( &cent->currentState.apos, toTime, angles );
+
+  VectorSubtract( origin, oldOrigin, deltaOrigin );
+  VectorSubtract( angles, oldAngles, deltaAngles );
+
+  VectorAdd( in, deltaOrigin, out );
+
+  // FIXME: origin change when on a rotating object
+}
+
+
+/*
+=============================
+CG_InterpolateEntityPosition
+=============================
+*/
+static void CG_InterpolateEntityPosition( centity_t *cent )
+{
+  vec3_t    current, next;
+  float     f;
+
+  // it would be an internal error to find an entity that interpolates without
+  // a snapshot ahead of the current one
+  if( cg.nextSnap == NULL )
+    CG_Error( "CG_InterpoateEntityPosition: cg.nextSnap == NULL" );
+
+  f = cg.frameInterpolation;
+
+  // this will linearize a sine or parabolic curve, but it is important
+  // to not extrapolate player positions if more recent data is available
+  BG_EvaluateTrajectory( &cent->currentState.pos, cg.snap->serverTime, current );
+  BG_EvaluateTrajectory( &cent->nextState.pos, cg.nextSnap->serverTime, next );
+
+  cent->lerpOrigin[ 0 ] = current[ 0 ] + f * ( next[ 0 ] - current[ 0 ] );
+  cent->lerpOrigin[ 1 ] = current[ 1 ] + f * ( next[ 1 ] - current[ 1 ] );
+  cent->lerpOrigin[ 2 ] = current[ 2 ] + f * ( next[ 2 ] - current[ 2 ] );
+
+  BG_EvaluateTrajectory( &cent->currentState.apos, cg.snap->serverTime, current );
+  BG_EvaluateTrajectory( &cent->nextState.apos, cg.nextSnap->serverTime, next );
+
+  cent->lerpAngles[ 0 ] = LerpAngle( current[ 0 ], next[ 0 ], f );
+  cent->lerpAngles[ 1 ] = LerpAngle( current[ 1 ], next[ 1 ], f );
+  cent->lerpAngles[ 2 ] = LerpAngle( current[ 2 ], next[ 2 ], f );
+
+}
+
+/*
+===============
+CG_CalcEntityLerpPositions
+
+===============
+*/
+static void CG_CalcEntityLerpPositions( centity_t *cent )
+{
+  // this will be set to how far forward projectiles will be extrapolated
+  int timeshift = 0;
+
+  // if this player does not want to see extrapolated players
+  if( !cg_smoothClients.integer )
+  {
+    // make sure the clients use TR_INTERPOLATE
+    if( cent->currentState.number < MAX_CLIENTS )
+    {
+      cent->currentState.pos.trType = TR_INTERPOLATE;
+      cent->nextState.pos.trType = TR_INTERPOLATE;
+    }
+  }
+
+  if( cent->interpolate && cent->currentState.pos.trType == TR_INTERPOLATE )
+  {
+    CG_InterpolateEntityPosition( cent );
+    return;
+  }
+
+  // first see if we can interpolate between two snaps for
+  // linear extrapolated clients
+  if( cent->interpolate && cent->currentState.pos.trType == TR_LINEAR_STOP &&
+      cent->currentState.number < MAX_CLIENTS )
+  {
+    CG_InterpolateEntityPosition( cent );
+    return;
+  }
+
+  if( cg_projectileNudge.integer &&
+      !cg.demoPlayback &&
+      cent->currentState.eType == ET_MISSILE &&
+      !( cg.snap->ps.pm_flags & PMF_FOLLOW ) )
+  {
+    timeshift = cg.ping;
+  }
+
+  // just use the current frame and evaluate as best we can
+  BG_EvaluateTrajectory( &cent->currentState.pos,
+    ( cg.time + timeshift ), cent->lerpOrigin );
+  BG_EvaluateTrajectory( &cent->currentState.apos,
+    ( cg.time + timeshift ), cent->lerpAngles );
+
+  if( timeshift )
+  {
+    trace_t tr;
+    vec3_t lastOrigin;
+	
+    BG_EvaluateTrajectory( &cent->currentState.pos, cg.time, lastOrigin );
+	
+    CG_Trace( &tr, lastOrigin, vec3_origin, vec3_origin, cent->lerpOrigin,
+      cent->currentState.number, MASK_SHOT );
+	
+    // don't let the projectile go through the floor
+    if( tr.fraction < 1.0f )
+      VectorLerp( tr.fraction, lastOrigin, cent->lerpOrigin, cent->lerpOrigin );
+  }
+
+  // adjust for riding a mover if it wasn't rolled into the predicted
+  // player state
+  if( cent != &cg.predictedPlayerEntity )
+  {
+    CG_AdjustPositionForMover( cent->lerpOrigin, cent->currentState.groundEntityNum,
+                               cg.snap->serverTime, cg.time, cent->lerpOrigin );
+  }
+}
+
+
+/*
+===============
+CG_CEntityPVSEnter
+
+===============
+*/
+static void CG_CEntityPVSEnter( centity_t *cent )
+{
+  entityState_t *es = &cent->currentState;
+
+  if( cg_debugPVS.integer )
+    CG_Printf( "Entity %d entered PVS\n", cent->currentState.number );
+
+  switch( es->eType )
+  {
+    case ET_MISSILE:
+      CG_LaunchMissile( cent );
+      break;
+
+    case ET_BUILDABLE:
+      cent->lastBuildableHealth = es->generic1;
+      break;
+  }
+
+  //clear any particle systems from previous uses of this centity_t
+  cent->muzzlePS = NULL;
+  cent->muzzlePsTrigger = qfalse;
+  cent->jetPackPS = NULL;
+  cent->jetPackState = JPS_OFF;
+  cent->buildablePS = NULL;
+  cent->entityPS = NULL;
+  cent->entityPSMissing = qfalse;
+
+  //make sure that the buildable animations are in a consistent state
+  //when a buildable enters the PVS
+  cent->buildableAnim = cent->lerpFrame.animationNumber = BANIM_NONE;
+  cent->oldBuildableAnim = es->legsAnim;
+}
+
+
+/*
+===============
+CG_CEntityPVSLeave
+
+===============
+*/
+static void CG_CEntityPVSLeave( centity_t *cent )
+{
+  int           i;
+  entityState_t *es = &cent->currentState;
+
+  if( cg_debugPVS.integer )
+    CG_Printf( "Entity %d left PVS\n", cent->currentState.number );
+  switch( es->eType )
+  {
+    case ET_LEV2_ZAP_CHAIN:
+      for( i = 0; i <= LEVEL2_AREAZAP_MAX_TARGETS; i++ )
+      {
+        if( CG_IsTrailSystemValid( &cent->level2ZapTS[ i ] ) )
+          CG_DestroyTrailSystem( &cent->level2ZapTS[ i ] );
+      }
+      break;
+  }
+}
+
+
+/*
+===============
+CG_AddCEntity
+
+===============
+*/
+static void CG_AddCEntity( centity_t *cent )
+{
+  // event-only entities will have been dealt with already
+  if( cent->currentState.eType >= ET_EVENTS )
+    return;
+
+  // calculate the current origin
+  CG_CalcEntityLerpPositions( cent );
+
+  // add automatic effects
+  CG_EntityEffects( cent );
+
+  switch( cent->currentState.eType )
+  {
+    default:
+      CG_Error( "Bad entity type: %i\n", cent->currentState.eType );
+      break;
+
+    case ET_INVISIBLE:
+    case ET_PUSH_TRIGGER:
+    case ET_TELEPORT_TRIGGER:
+    case ET_LOCATION:
+      break;
+
+    case ET_GENERAL:
+      CG_General( cent );
+      break;
+
+    case ET_CORPSE:
+      CG_Corpse( cent );
+      break;
+
+    case ET_PLAYER:
+      CG_Player( cent );
+      break;
+
+    case ET_BUILDABLE:
+      CG_Buildable( cent );
+      break;
+
+    case ET_MISSILE:
+      CG_Missile( cent );
+      break;
+
+    case ET_MOVER:
+      CG_Mover( cent );
+      break;
+
+    case ET_BEAM:
+      CG_Beam( cent );
+      break;
+
+    case ET_PORTAL:
+      CG_Portal( cent );
+      break;
+
+    case ET_SPEAKER:
+      CG_Speaker( cent );
+      break;
+
+    case ET_PARTICLE_SYSTEM:
+      CG_ParticleSystemEntity( cent );
+      break;
+
+    case ET_ANIMMAPOBJ:
+      CG_AnimMapObj( cent );
+      break;
+
+    case ET_MODELDOOR:
+      CG_ModelDoor( cent );
+      break;
+
+    case ET_LIGHTFLARE:
+      CG_LightFlare( cent );
+      break;
+
+    case ET_LEV2_ZAP_CHAIN:
+      CG_Lev2ZapChain( cent );
+      break;
+  }
+}
+
+/*
+===============
+CG_AddPacketEntities
+
+===============
+*/
+void CG_AddPacketEntities( void )
+{
+  int             num;
+  centity_t       *cent;
+  playerState_t   *ps;
+
+  // set cg.frameInterpolation
+  if( cg.nextSnap )
+  {
+    int   delta;
+
+    delta = ( cg.nextSnap->serverTime - cg.snap->serverTime );
+
+    if( delta == 0 )
+      cg.frameInterpolation = 0;
+    else
+      cg.frameInterpolation = (float)( cg.time - cg.snap->serverTime ) / delta;
+  }
+  else
+  {
+    cg.frameInterpolation = 0;  // actually, it should never be used, because
+                  // no entities should be marked as interpolating
+  }
+
+  // the auto-rotating items will all have the same axis
+  cg.autoAngles[ 0 ] = 0;
+  cg.autoAngles[ 1 ] = ( cg.time & 2047 ) * 360 / 2048.0;
+  cg.autoAngles[ 2 ] = 0;
+
+  cg.autoAnglesFast[ 0 ] = 0;
+  cg.autoAnglesFast[ 1 ] = ( cg.time & 1023 ) * 360 / 1024.0f;
+  cg.autoAnglesFast[ 2 ] = 0;
+
+  AnglesToAxis( cg.autoAngles, cg.autoAxis );
+  AnglesToAxis( cg.autoAnglesFast, cg.autoAxisFast );
+
+  // generate and add the entity from the playerstate
+  ps = &cg.predictedPlayerState;
+  BG_PlayerStateToEntityState( ps, &cg.predictedPlayerEntity.currentState, qfalse );
+  cg.predictedPlayerEntity.valid = qtrue;
+  CG_AddCEntity( &cg.predictedPlayerEntity );
+
+  // lerp the non-predicted value for lightning gun origins
+  CG_CalcEntityLerpPositions( &cg_entities[ cg.snap->ps.clientNum ] );
+
+  // scanner
+  CG_UpdateEntityPositions( );
+
+  for( num = 0; num < MAX_GENTITIES; num++ )
+    cg_entities[ num ].valid = qfalse;
+
+  // add each entity sent over by the server
+  for( num = 0; num < cg.snap->numEntities; num++ )
+  {
+    cent = &cg_entities[ cg.snap->entities[ num ].number ];
+    cent->valid = qtrue;
+  }
+
+  for( num = 0; num < MAX_GENTITIES; num++ )
+  {
+    cent = &cg_entities[ num ];
+
+    if( cent->valid && !cent->oldValid )
+      CG_CEntityPVSEnter( cent );
+    else if( !cent->valid && cent->oldValid )
+      CG_CEntityPVSLeave( cent );
+
+    cent->oldValid = cent->valid;
+  }
+
+  // add each entity sent over by the server
+  for( num = 0; num < cg.snap->numEntities; num++ )
+  {
+    cent = &cg_entities[ cg.snap->entities[ num ].number ];
+    CG_AddCEntity( cent );
+  }
+
+  //make an attempt at drawing bounding boxes of selected entity types
+  if( cg_drawBBOX.integer )
+  {
+    for( num = 0; num < cg.snap->numEntities; num++ )
+    {
+      float         x, zd, zu;
+      vec3_t        mins, maxs;
+      entityState_t *es;
+
+      cent = &cg_entities[ cg.snap->entities[ num ].number ];
+      es = &cent->currentState;
+
+      switch( es->eType )
+      {
+        case ET_BUILDABLE:
+        case ET_MISSILE:
+        case ET_CORPSE:
+          x = ( es->solid & 255 );
+          zd = ( ( es->solid >> 8 ) & 255 );
+          zu = ( ( es->solid >> 16 ) & 255 ) - 32;
+
+          mins[ 0 ] = mins[ 1 ] = -x;
+          maxs[ 0 ] = maxs[ 1 ] = x;
+          mins[ 2 ] = -zd;
+          maxs[ 2 ] = zu;
+
+          CG_DrawBoundingBox( cent->lerpOrigin, mins, maxs );
+          break;
+
+        default:
+          break;
+      }
+    }
+  }
+}
+
diff --git a/src/cgame/cg_event.c b/src/cgame/cg_event.c
new file mode 100644
index 0000000..ddce6a6
--- /dev/null
+++ b/src/cgame/cg_event.c
@@ -0,0 +1,1305 @@
+/*
+===========================================================================
+Copyright (C) 1999-2005 Id Software, Inc.
+Copyright (C) 2000-2009 Darklegion Development
+
+This file is part of Tremulous.
+
+Tremulous 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.
+
+Tremulous 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 Tremulous; if not, write to the Free Software
+Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
+===========================================================================
+*/
+
+// cg_event.c -- handle entity events at snapshot or playerstate transitions
+
+
+#include "cg_local.h"
+
+/*
+=============
+CG_Obituary
+=============
+*/
+static void CG_Obituary( entityState_t *ent )
+{
+  int           mod;
+  int           target, attacker;
+  char          *message;
+  char          *message2;
+  const char    *targetInfo;
+  const char    *attackerInfo;
+  char          targetName[ MAX_NAME_LENGTH ];
+  char          attackerName[ MAX_NAME_LENGTH ];
+  char          className[ 64 ];
+  gender_t      gender;
+  clientInfo_t  *ci;
+  qboolean      teamKill = qfalse;
+
+  target = ent->otherEntityNum;
+  attacker = ent->otherEntityNum2;
+  mod = ent->eventParm;
+
+  if( target < 0 || target >= MAX_CLIENTS )
+    CG_Error( "CG_Obituary: target out of range" );
+
+  ci = &cgs.clientinfo[ target ];
+  gender = ci->gender;
+
+  if( attacker < 0 || attacker >= MAX_CLIENTS )
+  {
+    attacker = ENTITYNUM_WORLD;
+    attackerInfo = NULL;
+  }
+  else
+  {
+    attackerInfo = CG_ConfigString( CS_PLAYERS + attacker );
+    if( ci && cgs.clientinfo[ attacker ].team == ci->team )
+      teamKill = qtrue;
+  }
+
+  targetInfo = CG_ConfigString( CS_PLAYERS + target );
+
+  if( !targetInfo )
+    return;
+
+  Q_strncpyz( targetName, Info_ValueForKey( targetInfo, "n" ), sizeof( targetName ));
+
+  message2 = "";
+
+  // check for single client messages
+
+  switch( mod )
+  {
+    case MOD_FALLING:
+      message = "^5fell fowl to gravity";
+      break;
+    case MOD_CRUSH:
+      message = "^5was squished";
+      break;
+    case MOD_WATER:
+      message = "^5forgot to pack a snorkel";
+      break;
+    case MOD_SLIME:
+      message = "^5was sucked by an infestation slime";
+      break;
+    case MOD_LAVA:
+      message = "^5did a back flip into the lava";
+      break;
+    case MOD_TARGET_LASER:
+      message = "^5saw the light";
+      break;
+    case MOD_TRIGGER_HURT:
+      message = "^5was in the wrong place";
+      break;
+    case MOD_HSPAWN:
+      message = "^5should have run further";
+      break;
+    case MOD_ASPAWN:
+      message = "^5shouldn't have trod in the acid";
+      break;
+    case MOD_MGTURRET:
+      message = "^5was gunned down by a turret";
+      break;
+    case MOD_MGTURRET2:
+      message = "^5was roasted by a flame turret";
+      break;
+    case MOD_TESLAGEN:
+      message = "^5was zapped by a tesla generator";
+      break;
+    case MOD_ATUBE:
+      message = "^5was melted by an acid tube";
+      break;
+    case MOD_OVERMIND:
+      message = "^5got too close to the overmind";
+      break;
+    case MOD_REACTOR:
+      message = "^5got too close to the reactor";
+      break;
+    case MOD_SLOWBLOB:
+      message = "^5should have visited a medical station";
+      break;
+    case MOD_SWARM:
+      message = "^5was hunted down by the swarm";
+      break;
+    case MOD_SPITEFUL_ABCESS:
+      message = "^5was raped by a Spiteful Abcess";
+      break;
+    default:
+      message = NULL;
+      break;
+  }
+
+  if( !message && attacker == target )
+  {
+    switch( mod )
+    {
+      case MOD_FLAMER_SPLASH:
+        if( gender == GENDER_FEMALE )
+          message = "^5toasted herself";
+        else if( gender == GENDER_NEUTER )
+          message = "^5toasted itself";
+        else
+          message = "^5toasted himself";
+        break;
+
+      case MOD_LCANNON_SPLASH:
+        if( gender == GENDER_FEMALE )
+          message = "^5irradiated herself";
+        else if( gender == GENDER_NEUTER )
+          message = "^5irradiated itself";
+        else
+          message = "^5irradiated himself";
+        break;
+
+      case MOD_PSAWBLADE:
+        if( gender == GENDER_FEMALE )
+          message = "^5sliced herself up";
+        else if( gender == GENDER_NEUTER )
+          message = "^5sliced itself up";
+        else
+          message = "^5sliced himself up";
+        break;
+		
+      case MOD_MD2:
+        if( gender == GENDER_FEMALE )
+          message = "^5 vaporized herself up";
+        else if( gender == GENDER_NEUTER )
+          message = "^vaporized itself up";
+        else
+          message = "^vaporized himself up";
+        break;	
+		
+      case MOD_GRENADE:
+        if( gender == GENDER_FEMALE )
+          message = "^5blew herself up";
+        else if( gender == GENDER_NEUTER )
+          message = "^5blew itself up";
+        else
+          message = "^5blew himself up";
+        break;
+
+      case MOD_LEVEL3_BOUNCEBALL:
+        if( gender == GENDER_FEMALE )
+          message = "^5sniped herself";
+        else if( gender == GENDER_NEUTER )
+          message = "^5sniped itself";
+        else
+          message = "^5sniped himself";
+        break;
+
+      case MOD_LEVEL5_PRICKLES:
+        if( gender == GENDER_FEMALE )
+          message = "^5spiked herself";
+        else if( gender == GENDER_NEUTER )
+          message = "^5spiked itself";
+        else
+          message = "^5spiked himself";
+        break;
+		
+      case MOD_PRIFLE:
+        if( gender == GENDER_FEMALE )
+          message = "^5pulse rifled herself";
+        else if( gender == GENDER_NEUTER )
+          message = "^5pulse rifled itself";
+        else
+          message = "^5pulse rifled himself";
+        break;
+        
+
+      case MOD_MINE:
+        if( gender == GENDER_FEMALE )
+          message = "^5was betrayed by own mine";
+        else if( gender == GENDER_NEUTER )
+          message = "^5it betrayed by own mine";
+        else
+          message = "^5was betrayed by own mine";
+        break;
+		
+		case MOD_FLAMES:
+        if( gender == GENDER_FEMALE )
+          message = "^5was terminated by own flames";
+        else if( gender == GENDER_NEUTER )
+          message = "^5it terminated by own flames";
+        else
+          message = "^5was terminated by own flames";
+        break;
+		
+    case MOD_SMOKE:
+        message = "^5smoked himself up";
+        break;
+      case MOD_ABOMB:
+        message = "^5bombed himself up";
+        break;
+
+      default:
+        if( gender == GENDER_FEMALE )
+          message = "^5killed herself";
+        else if( gender == GENDER_NEUTER )
+          message = "^5killed itself";
+        else
+          message = "^5killed himself";
+        break;
+    }
+  }
+
+  if( message )
+  {
+    CG_Printf( "%s" S_COLOR_WHITE " %s\n", targetName, message );
+    return;
+  }
+
+  // check for double client messages
+  if( !attackerInfo )
+  {
+    attacker = ENTITYNUM_WORLD;
+    strcpy( attackerName, "noname" );
+  }
+  else
+  {
+    Q_strncpyz( attackerName, Info_ValueForKey( attackerInfo, "n" ), sizeof( attackerName ));
+    // check for kill messages about the current clientNum
+    if( target == cg.snap->ps.clientNum )
+      Q_strncpyz( cg.killerName, attackerName, sizeof( cg.killerName ) );
+  }
+
+  if( attacker != ENTITYNUM_WORLD )
+  {
+    switch( mod )
+    {
+      case MOD_PAINSAW:
+        message = "^5was sawn by^7";
+        break;
+      case MOD_BLASTER:
+        message = "^5was blasted by^7";
+        break;
+      case MOD_MACHINEGUN:
+        message = "^5was machinegunned by^7";
+        break;
+      case MOD_CHAINGUN:
+        message = "^5was chaingunned by^7";
+        break;
+      case MOD_SHOTGUN:
+        message = "^5was gunned down by^7";
+        break;
+      case MOD_PRIFLE:
+        message = "^5was pulse rifled by^7";
+        break;
+      case MOD_MDRIVER:
+        message = "^5was mass driven by^7";
+        break;
+      case MOD_MD2:
+        message = "^5was vaporized by^7";
+        break;
+      case MOD_LASGUN:
+        message = "^5was lasgunned by^7";
+        break;
+      case MOD_FLAMER:
+        message = "^5was grilled by^7";
+        message2 = "^5's ^5flamer";
+        break;
+      case MOD_FLAMER_SPLASH:
+        message = "^5was toasted by^7";
+        message2 = "^5's ^5flamer";
+        break;
+      case MOD_LCANNON:
+        message = "^5felt the full force of^7";
+        message2 = "^5's ^5lucifer cannon";
+        break;
+      case MOD_LCANNON_SPLASH:
+        message = "^5was caught in the fallout of^7";
+        message2 = "^5's ^5lucifer cannon";
+        break;
+      case MOD_GRENADE:
+        message = "^5couldn't escape^7";
+        message2 = "^5's ^5grenade";
+        break;
+      case MOD_PSAWBLADE:
+        message = "^5was sliced by^7";
+        message2 = "^5's ^5blades";
+        break;
+      case MOD_MINE:
+        message = "^5found^7";
+        message2 = "^5's ^5mine";
+        break;  
+		
+	
+	   case MOD_FLAMES:
+        message = "^5tasted^7";
+        message2 = "^5's ^5flames";
+        break;
+		
+      case MOD_SMOKE:
+        message = "^5tasted^7";
+        message2 = "^5's ^5smoke";
+        break;
+		
+      case MOD_ABUILDER_CLAW:
+        message = "^5should leave^7";
+        message2 = "^5's ^5buildings alone";
+        break;
+      case MOD_LEVEL0_BITE:
+        message = "^5was bitten by^7";
+        break;
+      case MOD_LEVEL1_CLAW:
+        message = "^5was swiped by^7";
+        Com_sprintf( className, 64, "^5's %s",
+            BG_ClassConfig( PCL_ALIEN_LEVEL1 )->humanName );
+        message2 = className;
+        break;
+      case MOD_LEVEL2_CLAW:
+        message = "^5was clawed by^7";
+        Com_sprintf( className, 64, "^5's %s",
+            BG_ClassConfig( PCL_ALIEN_LEVEL2 )->humanName );
+        message2 = className;
+        break;
+      case MOD_LEVEL2_ZAP:
+        message = "^5was zapped by^7";
+        Com_sprintf( className, 64, "^5's %s",
+            BG_ClassConfig( PCL_ALIEN_LEVEL2 )->humanName );
+        message2 = className;
+        break;
+      case MOD_LEVEL2_BOUNCEBALL:
+        message = "^5was sniped by^7";
+        Com_sprintf( className, 64, "^5's %s",
+            BG_ClassConfig( PCL_ALIEN_LEVEL2 )->humanName );
+        message2 = className;
+        break;
+	  case MOD_LEVEL5_CLAW:
+        message = "^5was sliced by^7";
+        Com_sprintf( className, 64, "^5's %s",
+            BG_ClassConfig( PCL_ALIEN_LEVEL5 )->humanName );
+        message2 = className;
+        break;
+      case MOD_LEVEL5_ZAP:
+        message = "^5was zapped by^7";
+        Com_sprintf( className, 64, "^5's %s",
+            BG_ClassConfig( PCL_ALIEN_LEVEL5 )->humanName );
+        message2 = className;
+        break;
+		case MOD_LEVEL5_BOUNCEBALL:
+        message = "^5was sniped by^7";
+        Com_sprintf( className, 64, "^5's %s",
+            BG_ClassConfig( PCL_ALIEN_LEVEL5 )->humanName );
+        message2 = className;
+        break;
+      case MOD_LEVEL3_CLAW:
+        message = "^5was chomped by^7";
+        Com_sprintf( className, 64, "^5's %s",
+            BG_ClassConfig( PCL_ALIEN_LEVEL3 )->humanName );
+        message2 = className;
+        break;
+      case MOD_LEVEL3_POUNCE:
+        message = "^5was pounced upon by^7";
+        Com_sprintf( className, 64, "^5's %s",
+            BG_ClassConfig( PCL_ALIEN_LEVEL3 )->humanName );
+        message2 = className;
+        break;
+		
+		case MOD_LEVEL5_POUNCE:
+        message = "^5was air pounced upon by^7";
+        Com_sprintf( className, 64, "^5's %s",
+            BG_ClassConfig( PCL_ALIEN_LEVEL5 )->humanName );
+        message2 = className;
+        break;
+		
+      case MOD_LEVEL5_PRICKLES:
+        message = "^5was spiked by^7";
+        Com_sprintf( className, 64, "^5's %s",
+            BG_ClassConfig( PCL_ALIEN_LEVEL5 )->humanName );
+        message2 = className;
+        break;
+		
+      case MOD_LEVEL3_BOUNCEBALL:
+        message = "^5was sniped by^7";
+        Com_sprintf( className, 64, "^5's %s",
+            BG_ClassConfig( PCL_ALIEN_LEVEL3 )->humanName );
+        message2 = className;
+        break;
+      case MOD_LEVEL4_CLAW:
+        message = "^5was mauled by^7";
+        Com_sprintf( className, 64, "^5's %s",
+            BG_ClassConfig( PCL_ALIEN_LEVEL4 )->humanName );
+        message2 = className;
+        break;
+      case MOD_LEVEL4_TRAMPLE:
+        message = "^5should have gotten out of the way of^7";
+        Com_sprintf( className, 64, "^5's %s",
+            BG_ClassConfig( PCL_ALIEN_LEVEL4 )->humanName );
+        message2 = className;
+        break;
+      case MOD_LEVEL4_CRUSH:
+        message = "^5was crushed under^7";
+        message2 = "^5's weight";
+        break;
+
+      case MOD_POISON:
+        message = "^5should have used a medkit against^7";
+        message2 = "^5's poison";
+        break;
+      case MOD_INFECTION:
+        message = "^5got infected by^7";
+        break;
+      case MOD_LEVEL1_PCLOUD:
+        message = "^5was gassed by^7";
+        Com_sprintf( className, 64, "^5's %s",
+            BG_ClassConfig( PCL_ALIEN_LEVEL1 )->humanName );
+        message2 = className;
+        break;
+		
+
+      case MOD_TELEFRAG:
+        message = "^5tried to invade^7";
+        message2 = "^5's personal space";
+        break;
+        
+      case MOD_ABOMB:
+        message = "^5was bombed by^7";
+        Com_sprintf( className, 64, "^5's %s",
+            BG_ClassConfig( PCL_ALIEN_LEVEL1 )->humanName );
+        message2 = className;
+        break;
+        
+      default:
+        message = "^5was killed by^7";
+        break;
+    }
+
+    if( message )
+    {
+      CG_Printf( "%s" S_COLOR_WHITE " %s %s%s" S_COLOR_WHITE "%s\n",
+        targetName, message,
+        ( teamKill ) ? S_COLOR_RED "TEAMMATE " S_COLOR_WHITE : "",
+        attackerName, message2 );
+      if( teamKill && attacker == cg.clientNum )
+      {
+        CG_CenterPrint( va ( "You killed " S_COLOR_RED "TEAMMATE "
+          S_COLOR_WHITE "%s", targetName ),
+          SCREEN_HEIGHT * 0.30, BIGCHAR_WIDTH );
+      }
+      return;
+    }
+  }
+
+  // we don't know what it was
+  CG_Printf( "%s" S_COLOR_CYAN " ^5died\n", targetName );
+}
+
+
+
+//==========================================================================
+
+/*
+================
+CG_PainEvent
+
+Also called by playerstate transition
+================
+*/
+void CG_PainEvent( centity_t *cent, int health )
+{
+  char  *snd;
+
+  // don't do more than two pain sounds a second
+  if( cg.time - cent->pe.painTime < 500 )
+    return;
+
+  if( health < 25 )
+    snd = "*pain25_1.wav";
+  else if( health < 50 )
+    snd = "*pain50_1.wav";
+  else if( health < 75 )
+    snd = "*pain75_1.wav";
+  else
+    snd = "*pain100_1.wav";
+
+  trap_S_StartSound( NULL, cent->currentState.number, CHAN_VOICE,
+    CG_CustomSound( cent->currentState.number, snd ) );
+
+  // save pain time for programitic twitch animation
+  cent->pe.painTime = cg.time;
+  cent->pe.painDirection ^= 1;
+}
+
+/*
+=========================
+CG_Level2Zap
+=========================
+*/
+static void CG_Level2Zap( entityState_t *es )
+{
+  int           i;
+  centity_t     *source = NULL, *target = NULL;
+
+  if( es->misc < 0 || es->misc >= MAX_CLIENTS )
+    return;
+
+  source = &cg_entities[ es->misc ];
+  for( i = 0; i <= 2; i++ )
+  {
+    switch( i )
+    {
+      case 0:
+        if( es->time <= 0 )
+          continue;
+
+        target = &cg_entities[ es->time ];
+        break;
+
+      case 1:
+        if( es->time2 <= 0 )
+          continue;
+
+        target = &cg_entities[ es->time2 ];
+        break;
+
+      case 2:
+        if( es->constantLight <= 0 )
+          continue;
+
+        target = &cg_entities[ es->constantLight ];
+        break;
+    }
+
+    if( !CG_IsTrailSystemValid( &source->level2ZapTS[ i ] ) )
+      source->level2ZapTS[ i ] = CG_SpawnNewTrailSystem( cgs.media.level2ZapTS );
+
+    if( CG_IsTrailSystemValid( &source->level2ZapTS[ i ] ) )
+    {
+      CG_SetAttachmentCent( &source->level2ZapTS[ i ]->frontAttachment, source );
+      CG_SetAttachmentCent( &source->level2ZapTS[ i ]->backAttachment, target );
+      CG_AttachToCent( &source->level2ZapTS[ i ]->frontAttachment );
+      CG_AttachToCent( &source->level2ZapTS[ i ]->backAttachment );
+    }
+  }
+  source->level2ZapTime = cg.time;
+}
+
+/*
+==============
+CG_EntityEvent
+
+An entity has an event value
+also called by CG_CheckPlayerstateEvents
+==============
+*/
+void CG_EntityEvent( centity_t *cent, vec3_t position )
+{
+  entityState_t *es;
+  int           event;
+  vec3_t        dir;
+  const char    *s;
+  int           clientNum;
+  clientInfo_t  *ci;
+  int           steptime;
+
+  if( cg.snap->ps.persistant[ PERS_SPECSTATE ] != SPECTATOR_NOT )
+    steptime = 200;
+  else
+    steptime = BG_Class( cg.snap->ps.stats[ STAT_CLASS ] )->steptime;
+
+  es = &cent->currentState;
+  event = es->event & ~EV_EVENT_BITS;
+
+  if( cg_debugEvents.integer )
+    CG_Printf( "ent:%3i  event:%3i %s\n", es->number, event,
+               BG_EventName( event ) );
+
+  if( !event )
+    return;
+
+  clientNum = es->clientNum;
+  if( clientNum < 0 || clientNum >= MAX_CLIENTS )
+    clientNum = 0;
+
+  ci = &cgs.clientinfo[ clientNum ];
+
+  switch( event )
+  {
+    //
+    // movement generated events
+    //
+    case EV_FOOTSTEP:
+      if( cg_footsteps.integer && ci->footsteps != FOOTSTEP_NONE )
+      {
+        if( ci->footsteps == FOOTSTEP_CUSTOM )
+          trap_S_StartSound( NULL, es->number, CHAN_BODY,
+            ci->customFootsteps[ rand( ) & 3 ] );
+        else
+          trap_S_StartSound( NULL, es->number, CHAN_BODY,
+            cgs.media.footsteps[ ci->footsteps ][ rand( ) & 3 ] );
+      }
+      break;
+
+    case EV_FOOTSTEP_METAL:
+      if( cg_footsteps.integer && ci->footsteps != FOOTSTEP_NONE )
+      {
+        if( ci->footsteps == FOOTSTEP_CUSTOM )
+          trap_S_StartSound( NULL, es->number, CHAN_BODY,
+            ci->customMetalFootsteps[ rand( ) & 3 ] );
+        else
+          trap_S_StartSound( NULL, es->number, CHAN_BODY,
+            cgs.media.footsteps[ FOOTSTEP_METAL ][ rand( ) & 3 ] );
+      }
+      break;
+
+    case EV_FOOTSTEP_SQUELCH:
+      if( cg_footsteps.integer && ci->footsteps != FOOTSTEP_NONE )
+      {
+        trap_S_StartSound( NULL, es->number, CHAN_BODY,
+          cgs.media.footsteps[ FOOTSTEP_FLESH ][ rand( ) & 3 ] );
+      }
+      break;
+
+    case EV_FOOTSPLASH:
+      if( cg_footsteps.integer && ci->footsteps != FOOTSTEP_NONE )
+      {
+        trap_S_StartSound( NULL, es->number, CHAN_BODY,
+          cgs.media.footsteps[ FOOTSTEP_SPLASH ][ rand( ) & 3 ] );
+      }
+      break;
+
+    case EV_FOOTWADE:
+      if( cg_footsteps.integer && ci->footsteps != FOOTSTEP_NONE )
+      {
+        trap_S_StartSound( NULL, es->number, CHAN_BODY,
+          cgs.media.footsteps[ FOOTSTEP_SPLASH ][ rand( ) & 3 ] );
+      }
+      break;
+
+    case EV_SWIM:
+      if( cg_footsteps.integer && ci->footsteps != FOOTSTEP_NONE )
+      {
+        trap_S_StartSound( NULL, es->number, CHAN_BODY,
+          cgs.media.footsteps[ FOOTSTEP_SPLASH ][ rand( ) & 3 ] );
+      }
+      break;
+
+
+    case EV_FALL_SHORT:
+      trap_S_StartSound( NULL, es->number, CHAN_AUTO, cgs.media.landSound );
+
+      if( clientNum == cg.predictedPlayerState.clientNum )
+      {
+        // smooth landing z changes
+        cg.landChange = -8;
+        cg.landTime = cg.time;
+      }
+      break;
+
+    case EV_FALL_MEDIUM:
+      // use normal pain sound
+      trap_S_StartSound( NULL, es->number, CHAN_VOICE, CG_CustomSound( es->number, "*pain100_1.wav" ) );
+
+      if( clientNum == cg.predictedPlayerState.clientNum )
+      {
+        // smooth landing z changes
+        cg.landChange = -16;
+        cg.landTime = cg.time;
+      }
+      break;
+
+    case EV_FALL_FAR:
+      trap_S_StartSound (NULL, es->number, CHAN_AUTO, CG_CustomSound( es->number, "*fall1.wav" ) );
+      cent->pe.painTime = cg.time;  // don't play a pain sound right after this
+
+      if( clientNum == cg.predictedPlayerState.clientNum )
+      {
+        // smooth landing z changes
+        cg.landChange = -24;
+        cg.landTime = cg.time;
+      }
+      break;
+
+    case EV_FALLING:
+      trap_S_StartSound( NULL, es->number, CHAN_AUTO, CG_CustomSound( es->number, "*falling1.wav" ) );
+      break;
+
+    case EV_STEP_4:
+    case EV_STEP_8:
+    case EV_STEP_12:
+    case EV_STEP_16:    // smooth out step up transitions
+    case EV_STEPDN_4:
+    case EV_STEPDN_8:
+    case EV_STEPDN_12:
+    case EV_STEPDN_16:    // smooth out step down transitions
+      {
+        float  oldStep;
+        int    delta;
+        int    step;
+
+        if( clientNum != cg.predictedPlayerState.clientNum )
+          break;
+
+        // if we are interpolating, we don't need to smooth steps
+        if( cg.demoPlayback || ( cg.snap->ps.pm_flags & PMF_FOLLOW ) ||
+            cg_nopredict.integer || cg_synchronousClients.integer )
+          break;
+
+        // check for stepping up before a previous step is completed
+        delta = cg.time - cg.stepTime;
+
+        if( delta < steptime )
+          oldStep = cg.stepChange * ( steptime - delta ) / steptime;
+        else
+          oldStep = 0;
+
+        // add this amount
+        if( event >= EV_STEPDN_4 )
+        {
+          step = 4 * ( event - EV_STEPDN_4 + 1 );
+          cg.stepChange = oldStep - step;
+        }
+        else
+        {
+          step = 4 * ( event - EV_STEP_4 + 1 );
+          cg.stepChange = oldStep + step;
+        }
+
+        if( cg.stepChange > MAX_STEP_CHANGE )
+          cg.stepChange = MAX_STEP_CHANGE;
+        else if( cg.stepChange < -MAX_STEP_CHANGE )
+          cg.stepChange = -MAX_STEP_CHANGE;
+
+        cg.stepTime = cg.time;
+        break;
+      }
+
+    case EV_JUMP:
+      trap_S_StartSound( NULL, es->number, CHAN_VOICE, CG_CustomSound( es->number, "*jump1.wav" ) );
+
+      if( BG_ClassHasAbility( cg.predictedPlayerState.stats[ STAT_CLASS ], SCA_WALLJUMPER ) )
+      {
+        vec3_t  surfNormal, refNormal = { 0.0f, 0.0f, 1.0f };
+        vec3_t  is;
+
+        if( clientNum != cg.predictedPlayerState.clientNum )
+          break;
+
+        //set surfNormal
+        VectorCopy( cg.predictedPlayerState.grapplePoint, surfNormal );
+
+        //if we are moving from one surface to another smooth the transition
+        if( !VectorCompare( surfNormal, cg.lastNormal ) && surfNormal[ 2 ] != 1.0f )
+        {
+          CrossProduct( refNormal, surfNormal, is );
+          VectorNormalize( is );
+
+          //add the op
+          CG_addSmoothOp( is, 15.0f, 1.0f );
+        }
+
+        //copy the current normal to the lastNormal
+        VectorCopy( surfNormal, cg.lastNormal );
+      }
+
+      break;
+
+	  case EV_AIRPOUNCE:
+      trap_S_StartSound( NULL, es->number, CHAN_AUTO, cgs.media.airpounce);
+	  //airpounce gfx effect
+	  {
+        particleSystem_t *ps = CG_SpawnNewParticleSystem( cgs.media.airpounceblast );
+
+        if( CG_IsParticleSystemValid( &ps ) )
+        {
+          CG_SetAttachmentCent( &ps->attachment, cent );
+          CG_AttachToCent( &ps->attachment );
+        }
+      }
+	  
+      if( BG_ClassHasAbility( cg.predictedPlayerState.stats[ STAT_CLASS ], SCA_WALLJUMPER ) )
+      {
+        vec3_t  surfNormal, refNormal = { 0.0f, 0.0f, 1.0f };
+        vec3_t  is;
+
+        if( clientNum != cg.predictedPlayerState.clientNum )
+          break;
+
+        //set surfNormal
+        VectorCopy( cg.predictedPlayerState.grapplePoint, surfNormal );
+
+        //if we are moving from one surface to another smooth the transition
+        if( !VectorCompare( surfNormal, cg.lastNormal ) && surfNormal[ 2 ] != 1.0f )
+        {
+          CrossProduct( refNormal, surfNormal, is );
+          VectorNormalize( is );
+
+          //add the op
+          CG_addSmoothOp( is, 15.0f, 1.0f );
+        }
+
+        //copy the current normal to the lastNormal
+        VectorCopy( surfNormal, cg.lastNormal );
+      }
+
+      break;
+	  
+    case EV_LEV1_GRAB:
+      trap_S_StartSound( NULL, es->number, CHAN_VOICE, cgs.media.alienL1Grab );
+      break;
+
+    case EV_LEV4_TRAMPLE_PREPARE:
+      trap_S_StartSound( NULL, es->number, CHAN_VOICE, cgs.media.alienL4ChargePrepare );
+      break;
+
+    case EV_LEV4_TRAMPLE_START:
+      //FIXME: stop cgs.media.alienL4ChargePrepare playing here
+      trap_S_StartSound( NULL, es->number, CHAN_VOICE, cgs.media.alienL4ChargeStart );
+      break;
+
+    case EV_TAUNT:
+      if( !cg_noTaunt.integer )
+        trap_S_StartSound( NULL, es->number, CHAN_VOICE, CG_CustomSound( es->number, "*taunt.wav" ) );
+      break;
+
+	case EV_HUMMEL:
+        trap_S_StartSound( NULL, es->number, CHAN_AUTO, cgs.media.hummelSound );
+      break;
+	  
+    case EV_WATER_TOUCH:
+      trap_S_StartSound( NULL, es->number, CHAN_AUTO, cgs.media.watrInSound );
+      break;
+
+    case EV_WATER_LEAVE:
+      trap_S_StartSound( NULL, es->number, CHAN_AUTO, cgs.media.watrOutSound );
+      break;
+
+    case EV_WATER_UNDER:
+      trap_S_StartSound( NULL, es->number, CHAN_AUTO, cgs.media.watrUnSound );
+      break;
+
+    case EV_WATER_CLEAR:
+      trap_S_StartSound( NULL, es->number, CHAN_AUTO, CG_CustomSound( es->number, "*gasp.wav" ) );
+      break;
+
+    //
+    // weapon events
+    //
+    case EV_NOAMMO:
+      trap_S_StartSound( NULL, es->number, CHAN_WEAPON,
+                         cgs.media.weaponEmptyClick );
+      break;
+
+    case EV_CHANGE_WEAPON:
+      trap_S_StartSound( NULL, es->number, CHAN_AUTO, cgs.media.selectSound );
+      break;
+
+    case EV_FIRE_WEAPON:
+      CG_FireWeapon( cent, WPM_PRIMARY );
+      break;
+
+    case EV_FIRE_WEAPON2:
+      CG_FireWeapon( cent, WPM_SECONDARY );
+      break;
+
+    case EV_FIRE_WEAPON3:
+      CG_FireWeapon( cent, WPM_TERTIARY );
+      break;
+
+    //=================================================================
+
+    //
+    // other events
+    //
+    case EV_PLAYER_TELEPORT_IN:
+      //deprecated
+      break;
+
+    case EV_PLAYER_TELEPORT_OUT:
+      CG_PlayerDisconnect( position );
+      break;
+
+    case EV_BUILD_CONSTRUCT:
+      //do something useful here
+      break;
+
+    case EV_BUILD_DESTROY:
+      //do something useful here
+      break;
+
+    case EV_RPTUSE_SOUND:
+      trap_S_StartSound( NULL, es->number, CHAN_AUTO, cgs.media.repeaterUseSound );
+      break;
+
+    case EV_GRENADE_BOUNCE:
+      if( rand( ) & 1 )
+        trap_S_StartSound( NULL, es->number, CHAN_AUTO, cgs.media.hardBounceSound1 );
+      else
+        trap_S_StartSound( NULL, es->number, CHAN_AUTO, cgs.media.hardBounceSound2 );
+      break;
+
+	case EV_MINE_BOUNCE:
+      if( rand( ) & 1 )
+        trap_S_StartSound( NULL, es->number, CHAN_AUTO, cgs.media.mineBounceSound1 );
+		else
+        trap_S_StartSound( NULL, es->number, CHAN_AUTO, cgs.media.mineBounceSound1 );
+      break;
+      break;
+
+    case EV_ACIDBOMB_BOUNCE:
+      if( rand( ) & 1 )
+        trap_S_StartSound( NULL, es->number, CHAN_AUTO, cgs.media.acidBombBounceSound1 );
+      else
+        trap_S_StartSound( NULL, es->number, CHAN_AUTO, cgs.media.acidBombBounceSound2 );
+      break;
+
+    //
+    // missile impacts
+    //
+    case EV_MISSILE_HIT:
+      ByteToDir( es->eventParm, dir );
+      CG_MissileHitEntity( es->weapon, es->generic1, position, dir, es->otherEntityNum, es->torsoAnim );
+      break;
+
+    case EV_MISSILE_MISS:
+      ByteToDir( es->eventParm, dir );
+      CG_MissileHitWall( es->weapon, es->generic1, 0, position, dir, IMPACTSOUND_DEFAULT, es->torsoAnim );
+      break;
+
+    case EV_MISSILE_MISS_METAL:
+      ByteToDir( es->eventParm, dir );
+      CG_MissileHitWall( es->weapon, es->generic1, 0, position, dir, IMPACTSOUND_METAL, es->torsoAnim );
+      break;
+
+    case EV_HUMAN_BUILDABLE_EXPLOSION:
+      ByteToDir( es->eventParm, dir );
+      CG_HumanBuildableExplosion( position, dir, es->modelindex );
+      break;
+
+    case EV_ALIEN_BUILDABLE_EXPLOSION:
+      ByteToDir( es->eventParm, dir );
+      CG_AlienBuildableExplosion( position, dir, es->modelindex );
+      
+      if ( es->modelindex == BA_A_SPITEFUL_ABCESS )
+        CG_AlienSPITEFUL_ABCESSExplosion( position, dir );
+      break;
+	  
+//Scleim greifer schwanz f�r slime
+    case EV_SLIMETRAIL:
+      cent->currentState.weapon = WP_NONE;
+      {
+        centity_t *source = &cg_entities[ es->generic1 ];
+        centity_t *target = &cg_entities[ es->clientNum ];
+        vec3_t    sourceOffset = { 0.0f, 0.0f, 0.0f };
+
+        if( !CG_IsTrailSystemValid( &source->muzzleTS ) )
+        {
+		//trailsystem
+          source->muzzleTS = CG_SpawnNewTrailSystem( cgs.media.slimeTS );
+
+          if( CG_IsTrailSystemValid( &source->muzzleTS ) )
+          {
+            CG_SetAttachmentCent( &source->muzzleTS->frontAttachment, source );
+            CG_SetAttachmentCent( &source->muzzleTS->backAttachment, target );
+            CG_AttachToCent( &source->muzzleTS->frontAttachment );
+            CG_AttachToCent( &source->muzzleTS->backAttachment );
+            CG_SetAttachmentOffset( &source->muzzleTS->frontAttachment, sourceOffset );
+
+            source->muzzleTSDeathTime = cg.time + cg_teslaTrailTime.integer;
+          }
+        }
+      }
+      break;
+	  
+
+
+
+    case EV_TESLATRAIL:
+      cent->currentState.weapon = WP_TESLAGEN;
+      {
+        centity_t *source = &cg_entities[ es->generic1 ];
+        centity_t *target = &cg_entities[ es->clientNum ];
+        vec3_t    sourceOffset = { 0.0f, 0.0f, 28.0f };
+
+        if( !CG_IsTrailSystemValid( &source->muzzleTS ) )
+        {
+          source->muzzleTS = CG_SpawnNewTrailSystem( cgs.media.teslaZapTS );
+
+          if( CG_IsTrailSystemValid( &source->muzzleTS ) )
+          {
+            CG_SetAttachmentCent( &source->muzzleTS->frontAttachment, source );
+            CG_SetAttachmentCent( &source->muzzleTS->backAttachment, target );
+            CG_AttachToCent( &source->muzzleTS->frontAttachment );
+            CG_AttachToCent( &source->muzzleTS->backAttachment );
+            CG_SetAttachmentOffset( &source->muzzleTS->frontAttachment, sourceOffset );
+
+            source->muzzleTSDeathTime = cg.time + cg_teslaTrailTime.integer;
+          }
+        }
+      }
+      break;
+
+    case EV_BULLET_HIT_WALL:
+      ByteToDir( es->eventParm, dir );
+      CG_Bullet( es->pos.trBase, es->otherEntityNum, dir, qfalse, ENTITYNUM_WORLD );
+      break;
+
+    case EV_BULLET_HIT_FLESH:
+      CG_Bullet( es->pos.trBase, es->otherEntityNum, dir, qtrue, es->eventParm );
+      break;
+
+    case EV_SHOTGUN:
+      CG_ShotgunFire( es );
+      break;
+
+    case EV_GENERAL_SOUND:
+      if( cgs.gameSounds[ es->eventParm ] )
+        trap_S_StartSound( NULL, es->number, CHAN_VOICE, cgs.gameSounds[ es->eventParm ] );
+      else
+      {
+        s = CG_ConfigString( CS_SOUNDS + es->eventParm );
+        trap_S_StartSound( NULL, es->number, CHAN_VOICE, CG_CustomSound( es->number, s ) );
+      }
+      break;
+
+    case EV_GLOBAL_SOUND: // play from the player's head so it never diminishes
+      if( cgs.gameSounds[ es->eventParm ] )
+        trap_S_StartSound( NULL, cg.snap->ps.clientNum, CHAN_AUTO, cgs.gameSounds[ es->eventParm ] );
+      else
+      {
+        s = CG_ConfigString( CS_SOUNDS + es->eventParm );
+        trap_S_StartSound( NULL, cg.snap->ps.clientNum, CHAN_AUTO, CG_CustomSound( es->number, s ) );
+      }
+      break;
+
+    case EV_PAIN:
+      // local player sounds are triggered in CG_CheckLocalSounds,
+      // so ignore events on the player
+      if( cent->currentState.number != cg.snap->ps.clientNum )
+        CG_PainEvent( cent, es->eventParm );
+      break;
+
+    case EV_DEATH1:
+    case EV_DEATH2:
+    case EV_DEATH3:
+      trap_S_StartSound( NULL, es->number, CHAN_VOICE,
+          CG_CustomSound( es->number, va( "*death%i.wav", event - EV_DEATH1 + 1 ) ) );
+      break;
+
+    case EV_OBITUARY:
+      CG_Obituary( es );
+      break;
+
+    case EV_GIB_PLAYER:
+      // no gibbing
+      break;
+
+    case EV_STOPLOOPINGSOUND:
+      trap_S_StopLoopingSound( es->number );
+      es->loopSound = 0;
+      break;
+
+    case EV_DEBUG_LINE:
+      CG_Beam( cent );
+      break;
+
+    case EV_BUILD_DELAY:
+      if( clientNum == cg.predictedPlayerState.clientNum )
+      {
+        trap_S_StartLocalSound( cgs.media.buildableRepairedSound, CHAN_LOCAL_SOUND );
+        cg.lastBuildAttempt = cg.time;
+      }
+      break;
+
+    case EV_BUILD_REPAIR:
+      trap_S_StartSound( NULL, es->number, CHAN_AUTO, cgs.media.buildableRepairSound );
+      break;
+
+    case EV_BUILD_REPAIRED:
+      trap_S_StartSound( NULL, es->number, CHAN_AUTO, cgs.media.buildableRepairedSound );
+      break;
+
+    case EV_OVERMIND_ATTACK:
+      if( cg.predictedPlayerState.stats[ STAT_TEAM ] == TEAM_ALIENS )
+      {
+        trap_S_StartLocalSound( cgs.media.alienOvermindAttack, CHAN_ANNOUNCER );
+        CG_CenterPrint( "The Overmind is under attack!", 200, GIANTCHAR_WIDTH * 4 );
+      }
+      break;
+
+    case EV_OVERMIND_DYING:
+      if( cg.predictedPlayerState.stats[ STAT_TEAM ] == TEAM_ALIENS )
+      {
+        trap_S_StartLocalSound( cgs.media.alienOvermindDying, CHAN_ANNOUNCER );
+        CG_CenterPrint( "The Overmind is dying!", 200, GIANTCHAR_WIDTH * 4 );
+      }
+      break;
+
+    case EV_DCC_ATTACK:
+      if( cg.predictedPlayerState.stats[ STAT_TEAM ] == TEAM_HUMANS )
+      {
+        CG_CenterPrint( "Our base is under attack!", 200, GIANTCHAR_WIDTH * 4 );
+		trap_S_StartLocalSound( cgs.media.humanbaseunderatt, CHAN_ANNOUNCER );
+      }
+      break;
+
+    case EV_MGTURRET_SPINUP:
+      trap_S_StartSound( NULL, es->number, CHAN_AUTO, cgs.media.turretSpinupSound );
+      break;
+
+    case EV_OVERMIND_SPAWNS:
+      if( cg.predictedPlayerState.stats[ STAT_TEAM ] == TEAM_ALIENS )
+      {
+        trap_S_StartLocalSound( cgs.media.alienOvermindSpawns, CHAN_ANNOUNCER );
+        CG_CenterPrint( "^5The Overmind needs spawns!", 200, GIANTCHAR_WIDTH * 4 );
+      }
+      break;
+
+    case EV_ALIEN_EVOLVE:
+      trap_S_StartSound( NULL, es->number, CHAN_BODY, cgs.media.alienEvolveSound );
+      {
+        particleSystem_t *ps = CG_SpawnNewParticleSystem( cgs.media.alienEvolvePS );
+
+        if( CG_IsParticleSystemValid( &ps ) )
+        {
+          CG_SetAttachmentCent( &ps->attachment, cent );
+          CG_AttachToCent( &ps->attachment );
+        }
+      }
+
+      if( es->number == cg.clientNum )
+      {
+        CG_ResetPainBlend( );
+        cg.spawnTime = cg.time;
+      }
+      break;
+
+    case EV_ALIEN_EVOLVE_FAILED:
+      if( clientNum == cg.predictedPlayerState.clientNum )
+      {
+        //FIXME: change to "negative" sound
+        trap_S_StartLocalSound( cgs.media.buildableRepairedSound, CHAN_LOCAL_SOUND );
+        cg.lastEvolveAttempt = cg.time;
+      }
+      break;
+
+    case EV_ALIEN_ACIDTUBE:
+      {
+        particleSystem_t *ps = CG_SpawnNewParticleSystem( cgs.media.alienAcidTubePS );
+
+        if( CG_IsParticleSystemValid( &ps ) )
+        {
+          CG_SetAttachmentCent( &ps->attachment, cent );
+          ByteToDir( es->eventParm, dir );
+          CG_SetParticleSystemNormal( ps, dir );
+          CG_AttachToCent( &ps->attachment );
+        }
+      }
+      break;
+	  
+	      case EV_FORCE_FIELD:
+      {
+        particleSystem_t *ps = CG_SpawnNewParticleSystem( cgs.media.forcefieldPS );
+        trap_S_StartSound( NULL, es->number, CHAN_AUTO, cgs.media.forcefieldSound );
+
+        if( CG_IsParticleSystemValid( &ps ) )
+        {
+          CG_SetAttachmentCent( &ps->attachment, cent );
+          ByteToDir( es->eventParm, dir );
+          CG_SetParticleSystemNormal( ps, dir );
+          CG_AttachToCent( &ps->attachment );
+        }
+      }
+	        break;
+	      case EV_ALIEN_SLIME:
+      {
+        particleSystem_t *ps = CG_SpawnNewParticleSystem( cgs.media.alienSlimePS );
+
+
+        if( CG_IsParticleSystemValid( &ps ) )
+        {
+          CG_SetAttachmentCent( &ps->attachment, cent );
+          ByteToDir( es->eventParm, dir );
+          CG_SetParticleSystemNormal( ps, dir );
+          CG_AttachToCent( &ps->attachment );
+        }
+      }
+      break;
+
+    case EV_MEDKIT_USED:
+      trap_S_StartSound( NULL, es->number, CHAN_AUTO, cgs.media.medkitUseSound );
+      break;
+
+    case EV_PLAYER_RESPAWN:
+      if( es->number == cg.clientNum )
+        cg.spawnTime = cg.time;
+      break;
+
+    case EV_LEV2_ZAP:
+      CG_Level2Zap( es );
+      break;
+
+    default:
+      CG_Error( "Unknown event: %i", event );
+      break;
+  }
+}
+
+
+/*
+==============
+CG_CheckEvents
+
+==============
+*/
+void CG_CheckEvents( centity_t *cent )
+{
+  entity_event_t event;
+  entity_event_t oldEvent = EV_NONE;
+
+
+  // check for event-only entities
+  if( cent->currentState.eType > ET_EVENTS )
+  {
+    event = cent->currentState.eType - ET_EVENTS;
+
+    if( cent->previousEvent )
+      return; // already fired
+
+    cent->previousEvent = 1;
+
+    cent->currentState.event = cent->currentState.eType - ET_EVENTS;
+    
+    // Move the pointer to the entity that the
+    // event was originally attached to
+    if( cent->currentState.eFlags & EF_PLAYER_EVENT )
+    {
+      cent = &cg_entities[ cent->currentState.otherEntityNum ];
+      oldEvent = cent->currentState.event;
+      cent->currentState.event = event;
+    }
+
+  }
+  else
+  {
+    // check for events riding with another entity
+    if( cent->currentState.event == cent->previousEvent )
+      return;
+
+    cent->previousEvent = cent->currentState.event;
+    if( ( cent->currentState.event & ~EV_EVENT_BITS ) == 0 )
+      return;
+  }
+
+  // calculate the position at exactly the frame time
+  BG_EvaluateTrajectory( &cent->currentState.pos, cg.snap->serverTime, cent->lerpOrigin );
+  CG_SetEntitySoundPosition( cent );
+
+  CG_EntityEvent( cent, cent->lerpOrigin );
+  
+  // If this was a reattached spilled event, restore the original event
+  if( oldEvent != EV_NONE )
+    cent->currentState.event = oldEvent;
+}
+
diff --git a/src/cgame/cg_local.h b/src/cgame/cg_local.h
new file mode 100644
index 0000000..1498af9
--- /dev/null
+++ b/src/cgame/cg_local.h
@@ -0,0 +1,2124 @@
+/*
+===========================================================================
+Copyright (C) 1999-2005 Id Software, Inc.
+Copyright (C) 2000-2009 Darklegion Development
+
+This file is part of Tremulous.
+
+Tremulous 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.
+
+Tremulous 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 Tremulous; if not, write to the Free Software
+Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
+===========================================================================
+*/
+
+
+#include "../qcommon/q_shared.h"
+#include "../renderer/tr_types.h"
+#include "../game/bg_public.h"
+#include "cg_public.h"
+#include "../ui/ui_shared.h"
+
+// The entire cgame module is unloaded and reloaded on each level change,
+// so there is NO persistant data between levels on the client side.
+// If you absolutely need something stored, it can either be kept
+// by the server in the server stored userinfos, or stashed in a cvar.
+
+#define CG_FONT_THRESHOLD 0.1
+
+#define POWERUP_BLINKS      5
+
+
+#define POWERUP_BLINK_TIME  1000
+#define FADE_TIME           200
+#define PULSE_TIME          200
+#define DAMAGE_DEFLECT_TIME 100
+#define DAMAGE_RETURN_TIME  400
+#define DAMAGE_TIME         500
+#define LAND_DEFLECT_TIME   150
+#define LAND_RETURN_TIME    300
+#define DUCK_TIME           100
+#define PAIN_TWITCH_TIME    200
+#define WEAPON_SELECT_TIME  1400
+#define ITEM_SCALEUP_TIME   1000
+#define ZOOM_TIME           150
+#define ITEM_BLOB_TIME      200
+#define MUZZLE_FLASH_TIME   20
+#define SINK_TIME           1000    // time for fragments to sink into ground before going away
+#define ATTACKER_HEAD_TIME  10000
+#define REWARD_TIME         3000
+
+#define PULSE_SCALE         1.5     // amount to scale up the icons when activating
+
+#define MAX_STEP_CHANGE     32
+
+#define MAX_VERTS_ON_POLY   80
+#define MAX_MARK_POLYS      2048
+
+#define STAT_MINUS          10  // num frame for '-' stats digit
+
+#define ICON_SIZE           48
+#define CHAR_WIDTH          32
+#define CHAR_HEIGHT         48
+#define TEXT_ICON_SPACE     4
+
+// very large characters
+#define GIANT_WIDTH         32
+#define GIANT_HEIGHT        48
+
+#define NUM_CROSSHAIRS      10
+
+#define TEAM_OVERLAY_MAXNAME_WIDTH  12
+#define TEAM_OVERLAY_MAXLOCATION_WIDTH  16
+
+typedef enum
+{
+  FOOTSTEP_NORMAL,
+  FOOTSTEP_FLESH,
+  FOOTSTEP_METAL,
+  FOOTSTEP_SPLASH,
+  FOOTSTEP_CUSTOM,
+  FOOTSTEP_NONE,
+
+  FOOTSTEP_TOTAL
+} footstep_t;
+
+typedef enum
+{
+  IMPACTSOUND_DEFAULT,
+  IMPACTSOUND_METAL,
+  IMPACTSOUND_FLESH
+} impactSound_t;
+
+typedef enum
+{
+  JPS_OFF,
+  JPS_DESCENDING,
+  JPS_HOVERING,
+  JPS_ASCENDING
+} jetPackState_t;
+
+//======================================================================
+
+// when changing animation, set animationTime to frameTime + lerping time
+// The current lerp will finish out, then it will lerp to the new animation
+typedef struct
+{
+  int         oldFrame;
+  int         oldFrameTime;     // time when ->oldFrame was exactly on
+
+  int         frame;
+  int         frameTime;        // time when ->frame will be exactly on
+
+  float       backlerp;
+
+  float       yawAngle;
+  qboolean    yawing;
+  float       pitchAngle;
+  qboolean    pitching;
+
+  int         animationNumber;  // may include ANIM_TOGGLEBIT
+  animation_t *animation;
+  int         animationTime;    // time when the first frame of the animation will be exact
+} lerpFrame_t;
+
+//======================================================================
+
+//attachment system
+typedef enum
+{
+  AT_STATIC,
+  AT_TAG,
+  AT_CENT,
+  AT_PARTICLE
+} attachmentType_t;
+
+//forward declaration for particle_t
+struct particle_s;
+
+typedef struct attachment_s
+{
+  attachmentType_t  type;
+  qboolean          attached;
+
+  qboolean          staticValid;
+  qboolean          tagValid;
+  qboolean          centValid;
+  qboolean          particleValid;
+
+  qboolean          hasOffset;
+  vec3_t            offset;
+
+  vec3_t            lastValidAttachmentPoint;
+
+  //AT_STATIC
+  vec3_t            origin;
+
+  //AT_TAG
+  refEntity_t       re;     //FIXME: should be pointers?
+  refEntity_t       parent; //
+  qhandle_t         model;
+  char              tagName[ MAX_STRING_CHARS ];
+
+  //AT_CENT
+  int               centNum;
+
+  //AT_PARTICLE
+  struct particle_s *particle;
+} attachment_t;
+
+//======================================================================
+
+//particle system stuff
+#define MAX_PARTICLE_FILES        128
+
+#define MAX_PS_SHADER_FRAMES      32
+#define MAX_PS_MODELS             8
+#define MAX_EJECTORS_PER_SYSTEM   5
+#define MAX_PARTICLES_PER_EJECTOR 5
+
+#define MAX_BASEPARTICLE_SYSTEMS  384
+#define MAX_BASEPARTICLE_EJECTORS MAX_BASEPARTICLE_SYSTEMS*MAX_EJECTORS_PER_SYSTEM
+#define MAX_BASEPARTICLES         MAX_BASEPARTICLE_EJECTORS*MAX_PARTICLES_PER_EJECTOR
+
+#define MAX_PARTICLE_SYSTEMS      96
+#define MAX_PARTICLE_EJECTORS     MAX_PARTICLE_SYSTEMS*MAX_EJECTORS_PER_SYSTEM
+#define MAX_PARTICLES             MAX_PARTICLE_EJECTORS*5
+
+#define PARTICLES_INFINITE        -1
+#define PARTICLES_SAME_AS_INITIAL -2
+
+//COMPILE TIME STRUCTURES
+typedef enum
+{
+  PMT_STATIC,
+  PMT_STATIC_TRANSFORM,
+  PMT_TAG,
+  PMT_CENT_ANGLES,
+  PMT_NORMAL
+} pMoveType_t;
+
+typedef enum
+{
+  PMD_LINEAR,
+  PMD_POINT
+} pDirType_t;
+
+typedef struct pMoveValues_u
+{
+  pDirType_t  dirType;
+
+  //PMD_LINEAR
+  vec3_t      dir;
+  float       dirRandAngle;
+
+  //PMD_POINT
+  vec3_t      point;
+  float       pointRandAngle;
+
+  float       mag;
+  float       magRandFrac;
+
+  float       parentVelFrac;
+  float       parentVelFracRandFrac;
+} pMoveValues_t;
+
+typedef struct pLerpValues_s
+{
+  int   delay;
+  float delayRandFrac;
+
+  float initial;
+  float initialRandFrac;
+
+  float final;
+  float finalRandFrac;
+
+  float randFrac;
+} pLerpValues_t;
+
+//particle template
+typedef struct baseParticle_s
+{
+  vec3_t          displacement;
+  vec3_t          randDisplacement;
+  float           normalDisplacement;
+
+  pMoveType_t     velMoveType;
+  pMoveValues_t   velMoveValues;
+
+  pMoveType_t     accMoveType;
+  pMoveValues_t   accMoveValues;
+
+  int             lifeTime;
+  float           lifeTimeRandFrac;
+
+  float           bounceFrac;
+  float           bounceFracRandFrac;
+  qboolean        bounceCull;
+
+  char            bounceMarkName[ MAX_QPATH ];
+  qhandle_t       bounceMark;
+  float           bounceMarkRadius;
+  float           bounceMarkRadiusRandFrac;
+  float           bounceMarkCount;
+  float           bounceMarkCountRandFrac;
+
+  char            bounceSoundName[ MAX_QPATH ];
+  qhandle_t       bounceSound;
+  float           bounceSoundCount;
+  float           bounceSoundCountRandFrac;
+
+  pLerpValues_t   radius;
+  int             physicsRadius;
+  pLerpValues_t   alpha;
+  pLerpValues_t   rotation;
+
+  qboolean        dynamicLight;
+  pLerpValues_t   dLightRadius;
+  byte            dLightColor[ 3 ];
+
+  int             colorDelay;
+  float           colorDelayRandFrac;
+  byte            initialColor[ 3 ];
+  byte            finalColor[ 3 ];
+
+  char            childSystemName[ MAX_QPATH ];
+  qhandle_t       childSystemHandle;
+
+  char            onDeathSystemName[ MAX_QPATH ];
+  qhandle_t       onDeathSystemHandle;
+
+  char            childTrailSystemName[ MAX_QPATH ];
+  qhandle_t       childTrailSystemHandle;
+
+  //particle invariant stuff
+  char            shaderNames[ MAX_PS_SHADER_FRAMES ][ MAX_QPATH ];
+  qhandle_t       shaders[ MAX_PS_SHADER_FRAMES ];
+  int             numFrames;
+  float           framerate;
+
+  char            modelNames[ MAX_PS_MODELS ][ MAX_QPATH ];
+  qhandle_t       models[ MAX_PS_MODELS ];
+  int             numModels;
+  animation_t     modelAnimation;
+
+  qboolean        overdrawProtection;
+  qboolean        realLight;
+  qboolean        cullOnStartSolid;
+  
+  float           scaleWithCharge;
+} baseParticle_t;
+
+
+//ejector template
+typedef struct baseParticleEjector_s
+{
+  baseParticle_t  *particles[ MAX_PARTICLES_PER_EJECTOR ];
+  int             numParticles;
+
+  pLerpValues_t   eject;          //zero period indicates creation of all particles at once
+
+  int             totalParticles;         //can be infinite
+  float           totalParticlesRandFrac;
+} baseParticleEjector_t;
+
+
+//particle system template
+typedef struct baseParticleSystem_s
+{
+  char                  name[ MAX_QPATH ];
+  baseParticleEjector_t *ejectors[ MAX_EJECTORS_PER_SYSTEM ];
+  int                   numEjectors;
+
+  qboolean              thirdPersonOnly;
+  qboolean              registered; //whether or not the assets for this particle have been loaded
+} baseParticleSystem_t;
+
+
+//RUN TIME STRUCTURES
+typedef struct particleSystem_s
+{
+  baseParticleSystem_t  *class;
+
+  attachment_t          attachment;
+
+  qboolean              valid;
+  qboolean              lazyRemove; //mark this system for later removal
+
+  //for PMT_NORMAL
+  qboolean              normalValid;
+  vec3_t                normal;
+  
+  int                   charge;
+} particleSystem_t;
+
+
+typedef struct particleEjector_s
+{
+  baseParticleEjector_t *class;
+  particleSystem_t      *parent;
+
+  pLerpValues_t         ejectPeriod;
+
+  int                   count;
+  int                   totalParticles;
+
+  int                   nextEjectionTime;
+
+  qboolean              valid;
+} particleEjector_t;
+
+
+//used for actual particle evaluation
+typedef struct particle_s
+{
+  baseParticle_t    *class;
+  particleEjector_t *parent;
+
+  int               birthTime;
+  int               lifeTime;
+
+  float             bounceMarkRadius;
+  int               bounceMarkCount;
+  int               bounceSoundCount;
+  qboolean          atRest;
+
+  vec3_t            origin;
+  vec3_t            velocity;
+
+  pMoveType_t       accMoveType;
+  pMoveValues_t     accMoveValues;
+
+  int               lastEvalTime;
+
+  int               nextChildTime;
+
+  pLerpValues_t     radius;
+  pLerpValues_t     alpha;
+  pLerpValues_t     rotation;
+
+  pLerpValues_t     dLightRadius;
+
+  int               colorDelay;
+
+  qhandle_t         model;
+  lerpFrame_t       lf;
+  vec3_t            lastAxis[ 3 ];
+
+  qboolean          valid;
+  int               frameWhenInvalidated;
+
+  int               sortKey;
+} particle_t;
+
+//======================================================================
+
+//trail system stuff
+#define MAX_TRAIL_FILES           128
+
+#define MAX_BEAMS_PER_SYSTEM      4
+
+#define MAX_BASETRAIL_SYSTEMS     64
+#define MAX_BASETRAIL_BEAMS       MAX_BASETRAIL_SYSTEMS*MAX_BEAMS_PER_SYSTEM
+
+#define MAX_TRAIL_SYSTEMS         32
+#define MAX_TRAIL_BEAMS           MAX_TRAIL_SYSTEMS*MAX_BEAMS_PER_SYSTEM
+#define MAX_TRAIL_BEAM_NODES      128
+
+#define MAX_TRAIL_BEAM_JITTERS    4
+
+typedef enum
+{
+  TBTT_STRETCH,
+  TBTT_REPEAT
+} trailBeamTextureType_t;
+
+typedef struct baseTrailJitter_s
+{
+  float   magnitude;
+  int     period;
+} baseTrailJitter_t;
+
+//beam template
+typedef struct baseTrailBeam_s
+{
+  int                     numSegments;
+  float                   frontWidth;
+  float                   backWidth;
+  float                   frontAlpha;
+  float                   backAlpha;
+  byte                    frontColor[ 3 ];
+  byte                    backColor[ 3 ];
+
+  // the time it takes for a segment to vanish (single attached only)
+  int                     segmentTime;
+
+  // the time it takes for a beam to fade out (double attached only)
+  int                     fadeOutTime;
+  
+  char                    shaderName[ MAX_QPATH ];
+  qhandle_t               shader;
+
+  trailBeamTextureType_t  textureType;
+
+  //TBTT_STRETCH
+  float                   frontTextureCoord;
+  float                   backTextureCoord;
+
+  //TBTT_REPEAT
+  float                   repeatLength;
+  qboolean                clampToBack;
+
+  qboolean                realLight;
+
+  int                     numJitters;
+  baseTrailJitter_t       jitters[ MAX_TRAIL_BEAM_JITTERS ];
+  qboolean                jitterAttachments;
+} baseTrailBeam_t;
+
+
+//trail system template
+typedef struct baseTrailSystem_s
+{
+  char            name[ MAX_QPATH ];
+  baseTrailBeam_t *beams[ MAX_BEAMS_PER_SYSTEM ];
+  int             numBeams;
+
+  int             lifeTime;
+  qboolean        thirdPersonOnly;
+  qboolean        registered; //whether or not the assets for this trail have been loaded
+} baseTrailSystem_t;
+
+typedef struct trailSystem_s
+{
+  baseTrailSystem_t   *class;
+
+  attachment_t        frontAttachment;
+  attachment_t        backAttachment;
+
+  int                 birthTime;
+  int                 destroyTime;
+  qboolean            valid;
+} trailSystem_t;
+
+typedef struct trailBeamNode_s
+{
+  vec3_t                  refPosition;
+  vec3_t                  position;
+
+  int                     timeLeft;
+
+  float                   textureCoord;
+  float                   halfWidth;
+  byte                    alpha;
+  byte                    color[ 3 ];
+
+  vec2_t                  jitters[ MAX_TRAIL_BEAM_JITTERS ];
+
+  struct trailBeamNode_s  *prev;
+  struct trailBeamNode_s  *next;
+
+  qboolean                used;
+} trailBeamNode_t;
+
+typedef struct trailBeam_s
+{
+  baseTrailBeam_t   *class;
+  trailSystem_t     *parent;
+
+  trailBeamNode_t   nodePool[ MAX_TRAIL_BEAM_NODES ];
+  trailBeamNode_t   *nodes;
+
+  int               lastEvalTime;
+
+  qboolean          valid;
+
+  int               nextJitterTimes[ MAX_TRAIL_BEAM_JITTERS ];
+} trailBeam_t;
+
+//======================================================================
+
+// player entities need to track more information
+// than any other type of entity.
+
+// note that not every player entity is a client entity,
+// because corpses after respawn are outside the normal
+// client numbering range
+
+// smoothing of view and model for WW transitions
+#define   MAXSMOOTHS          32
+
+typedef struct
+{
+  float     time;
+  float     timeMod;
+
+  vec3_t    rotAxis;
+  float     rotAngle;
+} smooth_t;
+
+
+typedef struct
+{
+  lerpFrame_t legs, torso, nonseg, weapon;
+  int         painTime;
+  int         painDirection;  // flip from 0 to 1
+
+  // machinegun spinning
+  float       barrelAngle;
+  int         barrelTime;
+  qboolean    barrelSpinning;
+
+  vec3_t      lastNormal;
+  vec3_t      lastAxis[ 3 ];
+  smooth_t    sList[ MAXSMOOTHS ];
+} playerEntity_t;
+
+typedef struct lightFlareStatus_s
+{
+  float     lastRadius;    //caching of likely flare radius
+  float     lastRatio;     //caching of likely flare ratio
+  int       lastTime;      //last time flare was visible/occluded
+  qboolean  status;        //flare is visble?
+} lightFlareStatus_t;
+
+typedef struct buildableStatus_s
+{
+  int       lastTime;      // Last time status was visible
+  qboolean  visible;       // Status is visble?
+} buildableStatus_t;
+
+typedef struct buildableCache_s
+{
+  vec3_t   cachedOrigin;   // If either the cached entity origin or the
+  vec3_t   cachedNormal;   // cached surfNormal change the cache is invalid
+  vec3_t   axis[ 3 ];
+  vec3_t   origin;
+} buildableCache_t;
+
+//=================================================
+
+// centity_t have a direct corespondence with gentity_t in the game, but
+// only the entityState_t is directly communicated to the cgame
+typedef struct centity_s
+{
+  entityState_t         currentState;     // from cg.frame
+  entityState_t         nextState;        // from cg.nextFrame, if available
+  qboolean              interpolate;      // true if next is valid to interpolate to
+  qboolean              currentValid;     // true if cg.frame holds this entity
+
+  int                   muzzleFlashTime;  // move to playerEntity?
+  int                   muzzleFlashTime2; // move to playerEntity?
+  int                   muzzleFlashTime3; // move to playerEntity?
+  int                   previousEvent;
+  int                   teleportFlag;
+
+  int                   trailTime;        // so missile trails can handle dropped initial packets
+  int                   dustTrailTime;
+  int                   miscTime;
+  int                   snapShotTime;     // last time this entity was found in a snapshot
+
+  playerEntity_t        pe;
+
+  int                   errorTime;        // decay the error from this time
+  vec3_t                errorOrigin;
+  vec3_t                errorAngles;
+
+  qboolean              extrapolated;     // false if origin / angles is an interpolation
+  vec3_t                rawOrigin;
+  vec3_t                rawAngles;
+
+  vec3_t                beamEnd;
+
+  // exact interpolated position of entity on this frame
+  vec3_t                lerpOrigin;
+  vec3_t                lerpAngles;
+
+  lerpFrame_t           lerpFrame;
+
+  buildableAnimNumber_t buildableAnim;    //persistant anim number
+  buildableAnimNumber_t oldBuildableAnim; //to detect when new anims are set
+  qboolean              buildableIdleAnim; //to check if new idle anim
+  particleSystem_t      *buildablePS;
+  buildableStatus_t     buildableStatus;
+  buildableCache_t      buildableCache;   // so we don't recalculate things
+  float                 lastBuildableHealth;
+  int                   lastBuildableDamageSoundTime;
+
+  lightFlareStatus_t    lfs;
+
+  qboolean              doorState;
+
+  qboolean              animInit;
+  qboolean              animPlaying;
+  qboolean              animLastState;
+
+  particleSystem_t      *muzzlePS;
+  qboolean              muzzlePsTrigger;
+
+  particleSystem_t      *jetPackPS;
+  jetPackState_t        jetPackState;
+
+  particleSystem_t      *poisonCloudedPS;
+
+  particleSystem_t      *entityPS;
+  qboolean              entityPSMissing;
+
+  trailSystem_t         *level2ZapTS[ LEVEL2_AREAZAP_MAX_TARGETS ];
+  int                   level2ZapTime;
+
+  trailSystem_t         *muzzleTS; //used for the tesla and reactor
+  int                   muzzleTSDeathTime;
+
+  qboolean              valid;
+  qboolean              oldValid;  
+
+  int                   invisibleTime;
+  qboolean              invisible;
+  struct centity_s      *nextLocation;
+} centity_t;
+
+
+//======================================================================
+
+typedef struct markPoly_s
+{
+  struct markPoly_s *prevMark, *nextMark;
+  int               time;
+  qhandle_t         markShader;
+  qboolean          alphaFade;    // fade alpha instead of rgb
+  float             color[ 4 ];
+  poly_t            poly;
+  polyVert_t        verts[ MAX_VERTS_ON_POLY ];
+} markPoly_t;
+
+//======================================================================
+
+
+typedef struct
+{
+  int       client;
+  int       score;
+  int       ping;
+  int       time;
+  int       team;
+  weapon_t  weapon;
+  upgrade_t upgrade;
+} score_t;
+
+// each client has an associated clientInfo_t
+// that contains media references necessary to present the
+// client model and other color coded effects
+// this is regenerated each time a client's configstring changes,
+// usually as a result of a userinfo (name, model, etc) change
+#define MAX_CUSTOM_SOUNDS 32
+typedef struct
+{
+  qboolean    infoValid;
+
+  char        name[ MAX_QPATH ];
+  team_t      team;
+
+  int         score;                      // updated by score servercmds
+  int         location;                   // location index for team mode
+  int         health;                     // you only get this info about your teammates
+  int         upgrade; 
+  int         curWeaponClass;             // sends current weapon for H, current class for A
+
+  // when clientinfo is changed, the loading of models/skins/sounds
+  // can be deferred until you are dead, to prevent hitches in
+  // gameplay
+  char        modelName[ MAX_QPATH ];
+  char        skinName[ MAX_QPATH ];
+
+  qboolean    newAnims;                   // true if using the new mission pack animations
+  qboolean    fixedlegs;                  // true if legs yaw is always the same as torso yaw
+  qboolean    fixedtorso;                 // true if torso never changes yaw
+  qboolean    nonsegmented;               // true if model is Q2 style nonsegmented
+
+  vec3_t      headOffset;                 // move head in icon views
+  footstep_t  footsteps;
+  gender_t    gender;                     // from model
+
+  qhandle_t   legsModel;
+  qhandle_t   legsSkin;
+
+  qhandle_t   torsoModel;
+  qhandle_t   torsoSkin;
+
+  qhandle_t   headModel;
+  qhandle_t   headSkin;
+
+  qhandle_t   nonSegModel;                //non-segmented model system
+  qhandle_t   nonSegSkin;                 //non-segmented model system
+
+  qhandle_t   modelIcon;
+
+  animation_t animations[ MAX_PLAYER_TOTALANIMATIONS ];
+
+  sfxHandle_t sounds[ MAX_CUSTOM_SOUNDS ];
+
+  sfxHandle_t customFootsteps[ 4 ];
+  sfxHandle_t customMetalFootsteps[ 4 ];
+
+  char        voice[ MAX_VOICE_NAME_LEN ];
+  int         voiceTime;
+  
+
+  char                  chatText[ MAX_SAY_TEXT ];
+  int                   chatDurationTime;
+  int                   chatTeam;
+} clientInfo_t;
+
+
+typedef struct weaponInfoMode_s
+{
+  float       flashDlight;
+  vec3_t      flashDlightColor;
+  sfxHandle_t flashSound[ 4 ];  // fast firing weapons randomly choose
+  qboolean    continuousFlash;
+
+  qhandle_t   missileModel;
+  sfxHandle_t missileSound;
+  float       missileDlight;
+  vec3_t      missileDlightColor;
+  int         missileRenderfx;
+  qboolean    usesSpriteMissle;
+  qhandle_t   missileSprite;
+  int         missileSpriteSize;
+  float       missileSpriteCharge;
+  qhandle_t   missileParticleSystem;
+  qhandle_t   missileTrailSystem;
+  qboolean    missileRotates;
+  qboolean    missileAnimates;
+  int         missileAnimStartFrame;
+  int         missileAnimNumFrames;
+  int         missileAnimFrameRate;
+  int         missileAnimLooping;
+
+  sfxHandle_t firingSound;
+
+  qhandle_t   muzzleParticleSystem;
+
+  qboolean    alwaysImpact;
+  qhandle_t   impactParticleSystem;
+  qhandle_t   impactMark;
+  qhandle_t   impactMarkSize;
+  sfxHandle_t impactSound[ 4 ]; //random impact sound
+  sfxHandle_t impactFleshSound[ 4 ]; //random impact sound
+} weaponInfoMode_t;
+
+// each WP_* weapon enum has an associated weaponInfo_t
+// that contains media references necessary to present the
+// weapon and its effects
+typedef struct weaponInfo_s
+{
+  qboolean          registered;
+  char              *humanName;
+
+  qhandle_t         handsModel;       // the hands don't actually draw, they just position the weapon
+  qhandle_t         weaponModel;
+  qhandle_t         barrelModel;
+  qhandle_t         flashModel;
+
+  qhandle_t         weaponModel3rdPerson;
+  qhandle_t         barrelModel3rdPerson;
+  qhandle_t         flashModel3rdPerson;
+
+  animation_t       animations[ MAX_WEAPON_ANIMATIONS ];
+  qboolean          noDrift;
+
+  vec3_t            weaponMidpoint;   // so it will rotate centered instead of by tag
+
+  qhandle_t         weaponIcon;
+  qhandle_t         ammoIcon;
+
+  qhandle_t         crossHair;
+  int               crossHairSize;
+
+  sfxHandle_t       readySound;
+
+  qboolean          disableIn3rdPerson;
+
+  weaponInfoMode_t  wim[ WPM_NUM_WEAPONMODES ];
+} weaponInfo_t;
+
+typedef struct upgradeInfo_s
+{
+  qboolean    registered;
+  char        *humanName;
+
+  qhandle_t   upgradeIcon;
+} upgradeInfo_t;
+
+typedef struct
+{
+  qboolean    looped;
+  qboolean    enabled;
+
+  sfxHandle_t sound;
+} sound_t;
+
+typedef struct
+{
+  qhandle_t   models[ MAX_BUILDABLE_MODELS ];
+  animation_t animations[ MAX_BUILDABLE_ANIMATIONS ];
+
+  //same number of sounds as animations
+  sound_t     sounds[ MAX_BUILDABLE_ANIMATIONS ];
+} buildableInfo_t;
+
+#define MAX_REWARDSTACK   10
+#define MAX_SOUNDBUFFER   20
+
+//======================================================================
+
+typedef struct
+{
+  vec3_t    alienBuildablePos[ MAX_GENTITIES ];
+  int       alienBuildableTimes[ MAX_GENTITIES ];
+  int       numAlienBuildables;
+
+  vec3_t    humanBuildablePos[ MAX_GENTITIES ];
+  int       numHumanBuildables;
+
+  vec3_t    alienClientPos[ MAX_CLIENTS ];
+  int       numAlienClients;
+
+  vec3_t    humanClientPos[ MAX_CLIENTS ];
+  int       numHumanClients;
+
+  int       lastUpdateTime;
+  vec3_t    origin;
+  vec3_t    vangles;
+} entityPos_t;
+
+typedef struct
+{
+  int time;
+  int length;
+} consoleLine_t;
+
+#define MAX_CONSOLE_TEXT  16384
+#define MAX_CONSOLE_LINES 64
+
+// all cg.stepTime, cg.duckTime, cg.landTime, etc are set to cg.time when the action
+// occurs, and they will have visible effects for #define STEP_TIME or whatever msec after
+
+#define MAX_PREDICTED_EVENTS  16
+
+#define NUM_SAVED_STATES ( CMD_BACKUP + 2 )
+
+// After this many msec the crosshair name fades out completely
+#define CROSSHAIR_CLIENT_TIMEOUT 1000
+
+typedef struct
+{
+  int           clientFrame;                        // incremented each frame
+
+  int           clientNum;
+
+  qboolean      demoPlayback;
+  qboolean      levelShot;                          // taking a level menu screenshot
+  int           deferredPlayerLoading;
+  qboolean      loading;                            // don't defer players at initial startup
+  qboolean      intermissionStarted;                // don't play voice rewards, because game will end shortly
+
+  // there are only one or two snapshot_t that are relevent at a time
+  int           latestSnapshotNum;                  // the number of snapshots the client system has received
+  int           latestSnapshotTime;                 // the time from latestSnapshotNum, so we don't need to read the snapshot yet
+
+  snapshot_t    *snap;                              // cg.snap->serverTime <= cg.time
+  snapshot_t    *nextSnap;                          // cg.nextSnap->serverTime > cg.time, or NULL
+  snapshot_t    activeSnapshots[ 2 ];
+
+  float         frameInterpolation;                 // (float)( cg.time - cg.frame->serverTime ) /
+                                                    // (cg.nextFrame->serverTime - cg.frame->serverTime)
+
+  qboolean      thisFrameTeleport;
+  qboolean      nextFrameTeleport;
+
+  int           frametime;                          // cg.time - cg.oldTime
+
+  int           time;                               // this is the time value that the client
+                                                    // is rendering at.
+  int           oldTime;                            // time at last frame, used for missile trails and prediction checking
+
+  int           physicsTime;                        // either cg.snap->time or cg.nextSnap->time
+
+  int           timelimitWarnings;                  // 5 min, 1 min, overtime
+  int           fraglimitWarnings;
+
+  qboolean      mapRestart;                         // set on a map restart to set back the weapon
+
+  qboolean      renderingThirdPerson;               // during deaths, chasecams, etc
+
+  // prediction state
+  qboolean      hyperspace;                         // true if prediction has hit a trigger_teleport
+  playerState_t predictedPlayerState;
+  pmoveExt_t    pmext;
+  centity_t     predictedPlayerEntity;
+  qboolean      validPPS;                           // clear until the first call to CG_PredictPlayerState
+  int           predictedErrorTime;
+  vec3_t        predictedError;
+
+  int           eventSequence;
+  int           predictableEvents[MAX_PREDICTED_EVENTS];
+
+  float         stepChange;                         // for stair up smoothing
+  int           stepTime;
+
+  float         duckChange;                         // for duck viewheight smoothing
+  int           duckTime;
+
+  float         landChange;                         // for landing hard
+  int           landTime;
+
+  // input state sent to server
+  int           weaponSelect;
+
+  // auto rotating items
+  vec3_t        autoAngles;
+  vec3_t        autoAxis[ 3 ];
+  vec3_t        autoAnglesFast;
+  vec3_t        autoAxisFast[ 3 ];
+
+  // view rendering
+  refdef_t      refdef;
+  vec3_t        refdefViewAngles;                   // will be converted to refdef.viewaxis
+
+  // zoom key
+  qboolean      zoomed;
+  int           zoomTime;
+  float         zoomSensitivity;
+
+  // information screen text during loading
+  char          infoScreenText[ MAX_STRING_CHARS ];
+
+  // scoreboard
+  int           scoresRequestTime;
+  int           numScores;
+  int           selectedScore;
+  int           teamScores[ 2 ];
+  score_t       scores[MAX_CLIENTS];
+  qboolean      showScores;
+  qboolean      scoreBoardShowing;
+  int           scoreFadeTime;
+  char          killerName[ MAX_NAME_LENGTH ];
+  char          spectatorList[ MAX_STRING_CHARS ];  // list of names
+  int           spectatorTime;                      // next time to offset
+  float         spectatorOffset;                    // current offset from start
+
+  // centerprinting
+  int           centerPrintTime;
+  int           centerPrintCharWidth;
+  int           centerPrintY;
+  char          centerPrint[ MAX_STRING_CHARS ];
+  int           centerPrintLines;
+
+  // low ammo warning state
+  int           lowAmmoWarning;   // 1 = low, 2 = empty
+
+  // kill timers for carnage reward
+  int           lastKillTime;
+
+  // crosshair client ID
+  int           crosshairBuildable;
+  int           crosshairClientNum;
+  int           crosshairClientTime;
+
+  // powerup active flashing
+  int           powerupActive;
+  int           powerupTime;
+
+  // attacking player
+  int           attackerTime;
+
+  // reward medals
+  int           rewardStack;
+  int           rewardTime;
+  int           rewardCount[ MAX_REWARDSTACK ];
+  qhandle_t     rewardShader[ MAX_REWARDSTACK ];
+  qhandle_t     rewardSound[ MAX_REWARDSTACK ];
+
+  // sound buffer mainly for announcer sounds
+  int           soundBufferIn;
+  int           soundBufferOut;
+  int           soundTime;
+  qhandle_t     soundBuffer[ MAX_SOUNDBUFFER ];
+
+  // for voice chat buffer
+  int           voiceChatTime;
+  int           voiceChatBufferIn;
+  int           voiceChatBufferOut;
+
+  // warmup countdown
+  int           warmupTime;
+
+  //==========================
+
+  int           itemPickup;
+  int           itemPickupTime;
+  int           itemPickupBlendTime;                // the pulse around the crosshair is timed seperately
+
+  int           weaponSelectTime;
+  int           weaponAnimation;
+  int           weaponAnimationTime;
+
+  // blend blobs
+  float         damageTime;
+  float         damageX, damageY, damageValue;
+
+  // status bar head
+  float         headYaw;
+  float         headEndPitch;
+  float         headEndYaw;
+  int           headEndTime;
+  float         headStartPitch;
+  float         headStartYaw;
+  int           headStartTime;
+
+  // view movement
+  float         v_dmg_time;
+  float         v_dmg_pitch;
+  float         v_dmg_roll;
+
+  qboolean      chaseFollow;
+
+  // temp working variables for player view
+  float         bobfracsin;
+  int           bobcycle;
+  float         xyspeed;
+  int           nextOrbitTime;
+
+  // development tool
+  refEntity_t   testModelEntity;
+  refEntity_t   testModelBarrelEntity;
+  char          testModelName[MAX_QPATH];
+  char          testModelBarrelName[MAX_QPATH];
+  qboolean      testGun;
+
+  int           spawnTime;                          // fovwarp
+  int           weapon1Time;                        // time when BUTTON_ATTACK went t->f f->t
+  int           weapon2Time;                        // time when BUTTON_ATTACK2 went t->f f->t
+  int           weapon3Time;                        // time when BUTTON_USE_HOLDABLE went t->f f->t
+  qboolean      weapon1Firing;
+  qboolean      weapon2Firing;
+  qboolean      weapon3Firing;
+
+  int           poisonedTime;
+
+  vec3_t        lastNormal;                         // view smoothage
+  vec3_t        lastVangles;                        // view smoothage
+  smooth_t      sList[ MAXSMOOTHS ];                // WW smoothing
+
+  int           forwardMoveTime;                    // for struggling
+  int           rightMoveTime;
+  int           upMoveTime;
+
+  float         charModelFraction;                  // loading percentages
+  float         mediaFraction;
+  float         buildablesFraction;
+
+  int           lastBuildAttempt;
+  int           lastEvolveAttempt;
+
+  char          consoleText[ MAX_CONSOLE_TEXT ];
+  consoleLine_t consoleLines[ MAX_CONSOLE_LINES ];
+  int           numConsoleLines;
+
+  particleSystem_t  *poisonCloudPS;
+  particleSystem_t  *poisonCloudedPS;
+
+  float         painBlendValue;
+  float         painBlendTarget;
+  float         healBlendValue;
+  int           lastHealth;
+  qboolean      wasDeadLastFrame;
+
+  int           lastPredictedCommand;
+  int           lastServerTime;
+  playerState_t savedPmoveStates[ NUM_SAVED_STATES ];
+  int           stateHead, stateTail;
+  int           ping;
+  
+  float         chargeMeterAlpha;
+  float         chargeMeterValue;
+  qhandle_t     lastHealthCross;
+  float         healthCrossFade;
+  int           nearUsableBuildable;
+  
+  int           nextWeaponClickTime;
+} cg_t;
+
+
+// all of the model, shader, and sound references that are
+// loaded at gamestate time are stored in cgMedia_t
+// Other media that can be tied to clients, weapons, or items are
+// stored in the clientInfo_t, itemInfo_t, weaponInfo_t, and powerupInfo_t
+typedef struct
+{
+  qhandle_t   charsetShader;
+  qhandle_t   whiteShader;
+  qhandle_t   outlineShader;
+
+  qhandle_t   level2ZapTS;
+
+
+  qhandle_t   connectionShader;
+
+  qhandle_t   viewBloodShader;
+  qhandle_t   tracerShader;
+  qhandle_t   crosshairShader[ WP_NUM_WEAPONS ];
+  qhandle_t   backTileShader;
+
+  qhandle_t   creepShader;
+
+  qhandle_t   scannerShader;
+  qhandle_t   scannerBlipShader;
+  qhandle_t   scannerBlipShaderHealing;
+  qhandle_t   scannerBlipShaderPlayer;
+  qhandle_t   scannerLineShader;
+
+
+  qhandle_t   teamOverlayShader;
+
+  qhandle_t   numberShaders[ 11 ];
+
+  qhandle_t   shadowMarkShader;
+  qhandle_t   wakeMarkShader;
+
+  // cloak shaders
+  qhandle_t   invisibleShader;
+  qhandle_t   invisibleShaderTeam;
+  qhandle_t   invisibleFadeShader;
+
+  // buildable shaders
+  qhandle_t   greenBuildShader;
+  qhandle_t   redBuildShader;
+  qhandle_t   humanSpawningShader;
+
+  // disconnect
+  qhandle_t   disconnectPS;
+  qhandle_t   disconnectSound;
+
+  // sounds
+  sfxHandle_t tracerSound;
+  sfxHandle_t weaponEmptyClick;
+  sfxHandle_t selectSound;
+  sfxHandle_t footsteps[ FOOTSTEP_TOTAL ][ 4 ];
+  sfxHandle_t talkSound;
+  sfxHandle_t alienTalkSound;
+  sfxHandle_t humanTalkSound;
+  sfxHandle_t landSound;
+  sfxHandle_t fallSound;
+  sfxHandle_t turretSpinupSound;
+  sfxHandle_t hardBounceSound1;
+  sfxHandle_t hardBounceSound2;
+  sfxHandle_t mineBounceSound1;
+
+  sfxHandle_t airpounce;
+  sfxHandle_t acidBombBounceSound1;
+  sfxHandle_t acidBombBounceSound2;
+  sfxHandle_t voteNow;
+  sfxHandle_t votePassed;
+  sfxHandle_t voteFailed;
+  sfxHandle_t hummelSound;
+  sfxHandle_t watrInSound;
+  sfxHandle_t watrOutSound;
+  sfxHandle_t watrUnSound;
+
+  sfxHandle_t jetpackDescendSound;
+  sfxHandle_t jetpackIdleSound;
+  sfxHandle_t jetpackAscendSound;
+
+  qhandle_t   jetPackDescendPS;
+  qhandle_t   jetPackHoverPS;
+  qhandle_t   jetPackAscendPS;
+
+  sfxHandle_t medkitUseSound;
+  
+  sfxHandle_t forcefieldSound;
+
+  sfxHandle_t alienStageTransition;
+  sfxHandle_t humanStageTransition;
+  
+  sfxHandle_t iniVote;
+
+  sfxHandle_t alienOvermindAttack;
+  sfxHandle_t alienOvermindDying;
+  sfxHandle_t alienOvermindSpawns;
+  sfxHandle_t humanbaseunderatt;
+  
+  sfxHandle_t alienBuildableExplosion;
+  sfxHandle_t alienBuildableDamage;
+  sfxHandle_t alienBuildablePrebuild;
+  sfxHandle_t humanBuildableExplosion;
+  sfxHandle_t humanBuildablePrebuild;
+  sfxHandle_t humanBuildableDamage[ 4 ];
+
+  sfxHandle_t alienL1Grab;
+  sfxHandle_t alienL4ChargePrepare;
+  sfxHandle_t alienL4ChargeStart;
+
+  qhandle_t   cursor;
+  qhandle_t   selectCursor;
+  qhandle_t   sizeCursor;
+
+  //light armour
+  qhandle_t larmourHeadSkin;
+  qhandle_t larmourLegsSkin;
+  qhandle_t larmourTorsoSkin;
+
+  qhandle_t jetpackModel;
+  qhandle_t jetpackFlashModel;
+  qhandle_t battpackModel;
+
+  sfxHandle_t repeaterUseSound;
+
+  sfxHandle_t buildableRepairSound;
+  sfxHandle_t buildableRepairedSound;
+
+  qhandle_t   poisonCloudPS;
+  qhandle_t   fireCloudPS;
+  qhandle_t   poisonCloudedPS;
+  qhandle_t   alienEvolvePS;
+  qhandle_t   airpounceblast;
+  qhandle_t   alienAcidTubePS;
+  qhandle_t   alienSlimePS;
+  
+  qhandle_t   forcefieldPS;
+
+  sfxHandle_t alienEvolveSound;
+
+  qhandle_t   humanBuildableDamagedPS;
+  qhandle_t   organicbulbPS;
+  qhandle_t   humanBuildableDestroyedPS;
+  qhandle_t   alienBuildableDamagedPS;
+  qhandle_t   alienBuildableDestroyedPS;
+  
+  qhandle_t   alienSpiteful_AbcessDestroyedPS;
+
+  qhandle_t   alienBleedPS;
+  qhandle_t   humanBleedPS;
+  qhandle_t   alienBuildableBleedPS;
+  qhandle_t   humanBuildableBleedPS;
+
+
+  qhandle_t   teslaZapTS;
+  qhandle_t   slimeTS;
+  qhandle_t   netzTS;
+  
+  sfxHandle_t lCannonWarningSound;
+  sfxHandle_t lCannonWarningSound2;
+  sfxHandle_t FlamerWarningSound;
+  sfxHandle_t FlamerWarningSound2;
+  
+  qhandle_t   buildWeaponTimerPie[ 8 ];
+  qhandle_t   upgradeClassIconShader;
+  qhandle_t   healthCross;
+  qhandle_t   healthCross2X;
+  qhandle_t   healthCross3X;
+  qhandle_t   healthCrossMedkit;
+  qhandle_t   healthCrossPoisoned;
+} cgMedia_t;
+
+typedef struct
+{
+  qhandle_t     frameShader;
+  qhandle_t     overlayShader;
+  qhandle_t     noPowerShader;
+  qhandle_t     markedShader;
+  vec4_t        healthSevereColor;
+  vec4_t        healthHighColor;
+  vec4_t        healthElevatedColor;
+  vec4_t        healthGuardedColor;
+  vec4_t        healthLowColor;
+  int           frameHeight;
+  int           frameWidth;
+  int           healthPadding;
+  int           overlayHeight;
+  int           overlayWidth;
+  float         verticalMargin;
+  float         horizontalMargin;
+  vec4_t        foreColor;
+  vec4_t        backColor;
+  qboolean      loaded;
+} buildStat_t;
+
+
+// The client game static (cgs) structure hold everything
+// loaded or calculated from the gamestate.  It will NOT
+// be cleared when a tournement restart is done, allowing
+// all clients to begin playing instantly
+typedef struct
+{
+  gameState_t   gameState;              // gamestate from server
+  glconfig_t    glconfig;               // rendering configuration
+  float         screenXScale;           // derived from glconfig
+  float         screenYScale;
+  float         screenXBias;
+
+  int           serverCommandSequence;  // reliable command stream counter
+  int           processedSnapshotNum;   // the number of snapshots cgame has requested
+
+  qboolean      localServer;            // detected on startup by checking sv_running
+
+  // parsed from serverinfo
+  int           timelimit;
+  int           maxclients;
+  char          mapname[ MAX_QPATH ];
+  qboolean      markDeconstruct;        // Whether or not buildables are marked
+
+  int           voteTime[ NUM_TEAMS ];
+  int           voteYes[ NUM_TEAMS ];
+  int           voteNo[ NUM_TEAMS ];
+  char          voteCaller[ NUM_TEAMS ][ MAX_NAME_LENGTH ];
+  qboolean      voteModified[ NUM_TEAMS ];// beep whenever changed
+  char          voteString[ NUM_TEAMS ][ MAX_STRING_TOKENS ];
+
+  int           levelStartTime;
+
+  int           scores1, scores2;   // from configstrings
+
+  qboolean      newHud;
+
+  int           alienStage;
+  int           humanStage;
+  int           alienCredits;
+  int           humanCredits;
+  int           alienNextStageThreshold;
+  int           humanNextStageThreshold;
+
+  //
+  // locally derived information from gamestate
+  //
+  qhandle_t     gameModels[ MAX_MODELS ];
+  qhandle_t     gameShaders[ MAX_GAME_SHADERS ];
+  qhandle_t     gameParticleSystems[ MAX_GAME_PARTICLE_SYSTEMS ];
+  sfxHandle_t   gameSounds[ MAX_SOUNDS ];
+
+  int           numInlineModels;
+  qhandle_t     inlineDrawModel[ MAX_MODELS ];
+  vec3_t        inlineModelMidpoints[ MAX_MODELS ];
+
+  clientInfo_t  clientinfo[ MAX_CLIENTS ];
+
+  int teaminfoReceievedTime;
+
+  // corpse info
+  clientInfo_t  corpseinfo[ MAX_CLIENTS ];
+
+  int           cursorX;
+  int           cursorY;
+  qboolean      eventHandling;
+  qboolean      mouseCaptured;
+  qboolean      sizingHud;
+  void          *capturedItem;
+  qhandle_t     activeCursor;
+
+  buildStat_t   alienBuildStat;
+  buildStat_t   humanBuildStat;
+
+  // media
+  cgMedia_t           media;
+
+  voice_t       *voices;
+  clientList_t  ignoreList;
+} cgs_t;
+
+typedef struct
+{
+  char *cmd;
+  void ( *function )( void );
+} consoleCommand_t;
+
+//==============================================================================
+
+extern  cgs_t     cgs;
+extern  cg_t      cg;
+extern  centity_t cg_entities[ MAX_GENTITIES ];
+extern  displayContextDef_t  cgDC;
+
+extern  weaponInfo_t    cg_weapons[ 40 ];
+extern  upgradeInfo_t   cg_upgrades[ 40 ];
+
+extern  buildableInfo_t cg_buildables[ BA_NUM_BUILDABLES ];
+
+extern  markPoly_t      cg_markPolys[ MAX_MARK_POLYS ];
+
+extern  vmCvar_t    cg_teslaTrailTime;
+extern  vmCvar_t    cg_centertime;
+extern  vmCvar_t    cg_runpitch;
+extern  vmCvar_t    cg_runroll;
+extern  vmCvar_t    cg_swingSpeed;
+extern  vmCvar_t    cg_shadows;
+extern  vmCvar_t    cg_drawTimer;
+extern  vmCvar_t    cg_drawClock;
+extern  vmCvar_t    cg_drawFPS;
+extern  vmCvar_t    cg_drawDemoState;
+extern  vmCvar_t    cg_drawSnapshot;
+extern  vmCvar_t    cg_drawChargeBar;
+extern  vmCvar_t    cg_drawCrosshair;
+extern  vmCvar_t    cg_drawCrosshairNames;
+extern  vmCvar_t    cg_crosshairSize;
+extern  vmCvar_t    cg_drawTeamOverlay;
+extern  vmCvar_t    cg_teamOverlaySortMode;
+extern  vmCvar_t    cg_teamOverlayMaxPlayers;
+extern  vmCvar_t    cg_teamOverlayUserinfo;
+extern  vmCvar_t    cg_draw2D;
+extern  vmCvar_t    cg_animSpeed;
+extern  vmCvar_t    cg_debugAnim;
+extern  vmCvar_t    cg_debugPosition;
+extern  vmCvar_t    cg_debugEvents;
+extern  vmCvar_t    cg_errorDecay;
+extern  vmCvar_t    cg_nopredict;
+extern  vmCvar_t    cg_debugMove;
+extern  vmCvar_t    cg_noPlayerAnims;
+extern  vmCvar_t    cg_showmiss;
+extern  vmCvar_t    cg_footsteps;
+extern  vmCvar_t    cg_addMarks;
+extern  vmCvar_t    cg_viewsize;
+extern  vmCvar_t    cg_drawGun;
+extern  vmCvar_t    cg_gun_frame;
+extern  vmCvar_t    cg_gun_x;
+extern  vmCvar_t    cg_gun_y;
+extern  vmCvar_t    cg_gun_z;
+extern  vmCvar_t    cg_tracerChance;
+extern  vmCvar_t    cg_tracerWidth;
+extern  vmCvar_t    cg_tracerLength;
+extern  vmCvar_t    cg_thirdPerson;
+extern  vmCvar_t    cg_thirdPersonAngle;
+extern  vmCvar_t    cg_thirdPersonShoulderViewMode;
+extern  vmCvar_t    cg_staticDeathCam;
+extern  vmCvar_t    cg_thirdPersonPitchFollow;
+extern  vmCvar_t    cg_thirdPersonRange;
+extern  vmCvar_t    cg_stereoSeparation;
+extern  vmCvar_t    cg_lagometer;
+extern  vmCvar_t    cg_drawSpeed;
+extern  vmCvar_t    cg_synchronousClients;
+extern  vmCvar_t    cg_stats;
+extern  vmCvar_t    cg_paused;
+extern  vmCvar_t    cg_blood;
+extern  vmCvar_t    cg_teamOverlayUserinfo;
+extern  vmCvar_t    cg_teamChatsOnly;
+extern  vmCvar_t    cg_noVoiceChats;
+extern  vmCvar_t    cg_noVoiceText;
+extern  vmCvar_t    cg_hudFiles;
+extern  vmCvar_t    cg_smoothClients;
+extern  vmCvar_t    pmove_fixed;
+extern  vmCvar_t    pmove_msec;
+extern  vmCvar_t    cg_cameraMode;
+extern  vmCvar_t    cg_timescaleFadeEnd;
+extern  vmCvar_t    cg_timescaleFadeSpeed;
+extern  vmCvar_t    cg_timescale;
+extern  vmCvar_t    cg_noTaunt;
+extern  vmCvar_t    cg_drawSurfNormal;
+extern  vmCvar_t    cg_drawBBOX;
+extern  vmCvar_t    cg_wwSmoothTime;
+extern  vmCvar_t    cg_disableBlueprintErrors;
+extern  vmCvar_t    cg_depthSortParticles;
+extern  vmCvar_t    cg_bounceParticles;
+extern  vmCvar_t    cg_consoleLatency;
+extern  vmCvar_t    cg_lightFlare;
+extern  vmCvar_t    cg_debugParticles;
+extern  vmCvar_t    cg_debugTrails;
+extern  vmCvar_t    cg_debugPVS;
+extern  vmCvar_t    cg_disableWarningDialogs;
+extern  vmCvar_t    cg_disableUpgradeDialogs;
+extern  vmCvar_t    cg_disableBuildDialogs;
+extern  vmCvar_t    cg_disableCommandDialogs;
+extern  vmCvar_t    cg_disableScannerPlane;
+extern  vmCvar_t    cg_tutorial;
+
+extern  vmCvar_t    cg_painBlendUpRate;
+extern  vmCvar_t    cg_painBlendDownRate;
+extern  vmCvar_t    cg_painBlendMax;
+extern  vmCvar_t    cg_painBlendScale;
+extern  vmCvar_t    cg_painBlendZoom;
+
+extern  vmCvar_t    cg_stickySpec;
+extern  vmCvar_t    cg_sprintToggle;
+extern  vmCvar_t    cg_unlagged;
+
+extern  vmCvar_t    cg_debugVoices;
+
+extern  vmCvar_t    ui_currentClass;
+extern  vmCvar_t    ui_carriage;
+extern  vmCvar_t    ui_stages;
+extern  vmCvar_t    ui_dialog;
+extern  vmCvar_t    ui_voteActive;
+extern  vmCvar_t    ui_alienTeamVoteActive;
+extern  vmCvar_t    ui_humanTeamVoteActive;
+
+extern  vmCvar_t    cg_debugRandom;
+
+extern  vmCvar_t    cg_optimizePrediction;
+extern  vmCvar_t    cg_projectileNudge;
+
+extern  vmCvar_t    cg_voice;
+
+extern  vmCvar_t    cg_emoticons;
+
+extern  vmCvar_t    cg_chatTeamPrefix;
+
+
+extern  vmCvar_t    cg_drawBubble;
+extern  vmCvar_t    cg_BubbleZoom;
+
+//
+// cg_main.c
+//
+const char  *CG_ConfigString( int index );
+const char  *CG_Argv( int arg );
+
+void QDECL  CG_Printf( const char *msg, ... );
+void QDECL  CG_Error( const char *msg, ... );
+
+void        CG_StartMusic( void );
+int         CG_PlayerCount( void );
+
+void        CG_UpdateCvars( void );
+
+int         CG_CrosshairPlayer( void );
+int         CG_LastAttacker( void );
+void        CG_LoadMenus( const char *menuFile );
+void        CG_KeyEvent( int key, qboolean down );
+void        CG_MouseEvent( int x, int y );
+void        CG_EventHandling( int type );
+void        CG_SetScoreSelection( void *menu );
+qboolean    CG_ClientIsReady( int clientNum );
+void        CG_BuildSpectatorString( void );
+
+qboolean    CG_FileExists( char *filename );
+void        CG_RemoveNotifyLine( void );
+void        CG_AddNotifyText( void );
+
+
+//
+// cg_view.c
+//
+void        CG_addSmoothOp( vec3_t rotAxis, float rotAngle, float timeMod );
+void        CG_TestModel_f( void );
+void        CG_TestGun_f( void );
+void        CG_TestModelNextFrame_f( void );
+void        CG_TestModelPrevFrame_f( void );
+void        CG_TestModelNextSkin_f( void );
+void        CG_TestModelPrevSkin_f( void );
+void        CG_AddBufferedSound( sfxHandle_t sfx );
+void        CG_DrawActiveFrame( int serverTime, stereoFrame_t stereoView, qboolean demoPlayback );
+void        CG_OffsetFirstPersonView( void );
+void        CG_OffsetThirdPersonView( void );
+void        CG_OffsetShoulderView( void );
+
+
+//
+// cg_drawtools.c
+//
+void        CG_DrawPlane( vec3_t origin, vec3_t down, vec3_t right, qhandle_t shader );
+void        CG_AdjustFrom640( float *x, float *y, float *w, float *h );
+void        CG_FillRect( float x, float y, float width, float height, const float *color );
+void        CG_DrawPic( float x, float y, float width, float height, qhandle_t hShader );
+void        CG_DrawFadePic( float x, float y, float width, float height, vec4_t fcolor,
+                            vec4_t tcolor, float amount, qhandle_t hShader );
+void        CG_SetClipRegion( float x, float y, float w, float h );
+void        CG_ClearClipRegion( void );
+
+int         CG_DrawStrlen( const char *str );
+
+float       *CG_FadeColor( int startMsec, int totalMsec );
+void        CG_TileClear( void );
+void        CG_ColorForHealth( vec4_t hcolor );
+void        CG_DrawRect( float x, float y, float width, float height, float size, const float *color );
+void        CG_DrawSides(float x, float y, float w, float h, float size);
+void        CG_DrawTopBottom(float x, float y, float w, float h, float size);
+qboolean    CG_WorldToScreen( vec3_t point, float *x, float *y );
+char        *CG_KeyBinding( const char *bind );
+char        CG_GetColorCharForHealth( int clientnum );
+
+//
+// cg_draw.c
+//
+
+void        CG_AddLagometerFrameInfo( void );
+void        CG_AddLagometerSnapshotInfo( snapshot_t *snap );
+void        CG_AddSpeed( void );
+void        CG_CenterPrint( const char *str, int y, int charWidth );
+void        CG_DrawActive( stereoFrame_t stereoView );
+void        CG_OwnerDraw( float x, float y, float w, float h, float text_x,
+                          float text_y, int ownerDraw, int ownerDrawFlags,
+                          int align, int textalign, int textvalign,
+                          float borderSize, float scale, vec4_t foreColor,
+                          vec4_t backColor, qhandle_t shader, int textStyle );
+float       CG_GetValue(int ownerDraw);
+void        CG_RunMenuScript(char **args);
+void        CG_SetPrintString( int type, const char *p );
+void        CG_GetTeamColor( vec4_t *color );
+const char  *CG_GetKillerText( void );
+void        CG_Text_PaintChar( float x, float y, float width, float height, float scale,
+                               float s, float t, float s2, float t2, qhandle_t hShader );
+void        CG_DrawLoadingScreen( void );
+void        CG_UpdateMediaFraction( float newFract );
+void        CG_ResetPainBlend( void );
+void        CG_DrawField( float x, float y, int width, float cw, float ch, int value );
+
+//
+// cg_players.c
+//
+void        CG_Player( centity_t *cent );
+void        CG_Corpse( centity_t *cent );
+void        CG_ResetPlayerEntity( centity_t *cent );
+void        CG_NewClientInfo( int clientNum );
+void        CG_PrecacheClientInfo( class_t class, char *model, char *skin );
+sfxHandle_t CG_CustomSound( int clientNum, const char *soundName );
+void        CG_PlayerDisconnect( vec3_t org );
+void        CG_Bleed( vec3_t origin, vec3_t normal, int entityNum );
+centity_t   *CG_GetPlayerLocation( void );
+
+//
+// cg_buildable.c
+//
+void        CG_GhostBuildable( buildable_t buildable );
+void        CG_Buildable( centity_t *cent );
+void        CG_BuildableStatusParse( const char *filename, buildStat_t *bs );
+void        CG_DrawBuildableStatus( void );
+void        CG_InitBuildables( void );
+void        CG_HumanBuildableExplosion( vec3_t origin, vec3_t dir, int buildable );
+void        CG_AlienBuildableExplosion( vec3_t origin, vec3_t dir, int buildable );
+
+
+void        CG_DrawChatBubble( void );
+int CG_SortDistance( const void *a, const void *b );
+
+//
+// cg_animation.c
+//
+void        CG_RunLerpFrame( lerpFrame_t *lf, float scale );
+
+//
+// cg_animmapobj.c
+//
+void        CG_AnimMapObj( centity_t *cent );
+void        CG_ModelDoor( centity_t *cent );
+
+//
+// cg_predict.c
+//
+
+#define MAGIC_TRACE_HACK -2
+
+void        CG_BuildSolidList( void );
+int         CG_PointContents( const vec3_t point, int passEntityNum );
+void        CG_Trace( trace_t *result, const vec3_t start, const vec3_t mins, const vec3_t maxs,
+                const vec3_t end, int skipNumber, int mask );
+void        CG_CapTrace( trace_t *result, const vec3_t start, const vec3_t mins, const vec3_t maxs,
+                const vec3_t end, int skipNumber, int mask );
+void        CG_BiSphereTrace( trace_t *result, const vec3_t start, const vec3_t end,
+                const float startRadius, const float endRadius, int skipNumber, int mask );
+void        CG_PredictPlayerState( void );
+
+
+//
+// cg_events.c
+//
+void        CG_CheckEvents( centity_t *cent );
+void        CG_EntityEvent( centity_t *cent, vec3_t position );
+void        CG_PainEvent( centity_t *cent, int health );
+
+
+//
+// cg_ents.c
+//
+void        CG_DrawBoundingBox( vec3_t origin, vec3_t mins, vec3_t maxs );
+void        CG_SetEntitySoundPosition( centity_t *cent );
+void        CG_AddPacketEntities( void );
+void        CG_Beam( centity_t *cent );
+void        CG_AdjustPositionForMover( const vec3_t in, int moverNum, int fromTime, int toTime, vec3_t out );
+
+void        CG_PositionEntityOnTag( refEntity_t *entity, const refEntity_t *parent,
+                                    qhandle_t parentModel, char *tagName );
+void        CG_PositionRotatedEntityOnTag( refEntity_t *entity, const refEntity_t *parent,
+                                           qhandle_t parentModel, char *tagName );
+
+
+//
+// cg_weapons.c
+//
+void        CG_NextWeapon_f( void );
+void        CG_PrevWeapon_f( void );
+void        CG_Weapon_f( void );
+
+void        CG_InitUpgrades( void );
+void        CG_RegisterUpgrade( int upgradeNum );
+void        CG_InitWeapons( void );
+void        CG_RegisterWeapon( int weaponNum );
+
+void        CG_FireWeapon( centity_t *cent, weaponMode_t weaponMode );
+void        CG_MissileHitWall( weapon_t weapon, weaponMode_t weaponMode, int clientNum,
+                               vec3_t origin, vec3_t dir, impactSound_t soundType, int charge );
+void        CG_MissileHitEntity( weapon_t weaponNum, weaponMode_t weaponMode,
+                                 vec3_t origin, vec3_t dir, int entityNum, int charge );
+void        CG_Bullet( vec3_t origin, int sourceEntityNum, vec3_t normal, qboolean flesh, int fleshEntityNum );
+void        CG_ShotgunFire( entityState_t *es );
+
+void        CG_AddViewWeapon (playerState_t *ps);
+void        CG_AddPlayerWeapon( refEntity_t *parent, playerState_t *ps, centity_t *cent );
+void        CG_DrawItemSelect( rectDef_t *rect, vec4_t color );
+void        CG_DrawItemSelectText( rectDef_t *rect, float scale, int textStyle );
+
+
+//
+// cg_scanner.c
+//
+void        CG_UpdateEntityPositions( void );
+void        CG_Scanner( rectDef_t *rect, qhandle_t shader, vec4_t color );
+void        CG_AlienSense( rectDef_t *rect );
+
+//
+// cg_marks.c
+//
+void        CG_InitMarkPolys( void );
+void        CG_AddMarks( void );
+void        CG_ImpactMark( qhandle_t markShader,
+                           const vec3_t origin, const vec3_t dir,
+                           float orientation,
+                           float r, float g, float b, float a,
+                           qboolean alphaFade,
+                           float radius, qboolean temporary );
+						   
+//
+// cg_snapshot.c
+//
+void          CG_ProcessSnapshots( void );
+
+//
+// cg_consolecmds.c
+//
+qboolean      CG_ConsoleCommand( void );
+void          CG_InitConsoleCommands( void );
+qboolean      CG_RequestScores( void );
+
+//
+// cg_servercmds.c
+//
+void          CG_ExecuteNewServerCommands( int latestSequence );
+void          CG_ParseServerinfo( void );
+void          CG_SetConfigValues( void );
+void          CG_ShaderStateChanged(void);
+
+//
+// cg_playerstate.c
+//
+void          CG_Respawn( void );
+void          CG_TransitionPlayerState( playerState_t *ps, playerState_t *ops );
+void          CG_CheckChangedPredictableEvents( playerState_t *ps );
+
+//
+// cg_attachment.c
+//
+qboolean    CG_AttachmentPoint( attachment_t *a, vec3_t v );
+qboolean    CG_AttachmentDir( attachment_t *a, vec3_t v );
+qboolean    CG_AttachmentAxis( attachment_t *a, vec3_t axis[ 3 ] );
+qboolean    CG_AttachmentVelocity( attachment_t *a, vec3_t v );
+int         CG_AttachmentCentNum( attachment_t *a );
+
+qboolean    CG_Attached( attachment_t *a );
+
+void        CG_AttachToPoint( attachment_t *a );
+void        CG_AttachToCent( attachment_t *a );
+void        CG_AttachToTag( attachment_t *a );
+void        CG_AttachToParticle( attachment_t *a );
+void        CG_SetAttachmentPoint( attachment_t *a, vec3_t v );
+void        CG_SetAttachmentCent( attachment_t *a, centity_t *cent );
+void        CG_SetAttachmentTag( attachment_t *a, refEntity_t parent,
+                qhandle_t model, char *tagName );
+void        CG_SetAttachmentParticle( attachment_t *a, particle_t *p );
+
+void        CG_SetAttachmentOffset( attachment_t *a, vec3_t v );
+
+//
+// cg_particles.c
+//
+void                CG_LoadParticleSystems( void );
+qhandle_t           CG_RegisterParticleSystem( char *name );
+
+particleSystem_t    *CG_SpawnNewParticleSystem( qhandle_t psHandle );
+void                CG_DestroyParticleSystem( particleSystem_t **ps );
+
+qboolean            CG_IsParticleSystemInfinite( particleSystem_t *ps );
+qboolean            CG_IsParticleSystemValid( particleSystem_t **ps );
+
+void                CG_SetParticleSystemNormal( particleSystem_t *ps, vec3_t normal );
+
+void                CG_AddParticles( void );
+
+void                CG_ParticleSystemEntity( centity_t *cent );
+
+void                CG_TestPS_f( void );
+void                CG_DestroyTestPS_f( void );
+
+//
+// cg_trails.c
+//
+void                CG_LoadTrailSystems( void );
+qhandle_t           CG_RegisterTrailSystem( char *name );
+
+trailSystem_t       *CG_SpawnNewTrailSystem( qhandle_t psHandle );
+void                CG_DestroyTrailSystem( trailSystem_t **ts );
+
+qboolean            CG_IsTrailSystemValid( trailSystem_t **ts );
+
+void                CG_AddTrails( void );
+
+void                CG_TestTS_f( void );
+void                CG_DestroyTestTS_f( void );
+
+//
+// cg_ptr.c
+//
+int   CG_ReadPTRCode( void );
+void  CG_WritePTRCode( int code );
+
+//
+// cg_tutorial.c
+//
+const char *CG_TutorialText( void );
+
+//
+//===============================================
+
+//
+// system traps
+// These functions are how the cgame communicates with the main game system
+//
+
+
+// print message on the local console
+void          trap_Print( const char *fmt );
+
+// abort the game
+void          trap_Error( const char *fmt );
+
+// milliseconds should only be used for performance tuning, never
+// for anything game related.  Get time from the CG_DrawActiveFrame parameter
+int           trap_Milliseconds( void );
+
+// console variable interaction
+void          trap_Cvar_Register( vmCvar_t *vmCvar, const char *varName, const char *defaultValue, int flags );
+void          trap_Cvar_Update( vmCvar_t *vmCvar );
+void          trap_Cvar_Set( const char *var_name, const char *value );
+void          trap_Cvar_VariableStringBuffer( const char *var_name, char *buffer, int bufsize );
+
+// ServerCommand and ConsoleCommand parameter access
+int           trap_Argc( void );
+void          trap_Argv( int n, char *buffer, int bufferLength );
+void          trap_Args( char *buffer, int bufferLength );
+void          trap_LiteralArgs( char *buffer, int bufferLength );
+
+// filesystem access
+// returns length of file
+int           trap_FS_FOpenFile( const char *qpath, fileHandle_t *f, fsMode_t mode );
+void          trap_FS_Read( void *buffer, int len, fileHandle_t f );
+void          trap_FS_Write( const void *buffer, int len, fileHandle_t f );
+void          trap_FS_FCloseFile( fileHandle_t f );
+void          trap_FS_Seek( fileHandle_t f, long offset, fsOrigin_t origin ); // fsOrigin_t
+int           trap_FS_GetFileList( const char *path, const char *extension,
+                                   char *listbuf, int bufsize );
+
+// add commands to the local console as if they were typed in
+// for map changing, etc.  The command is not executed immediately,
+// but will be executed in order the next time console commands
+// are processed
+void          trap_SendConsoleCommand( const char *text );
+
+// register a command name so the console can perform command completion.
+// FIXME: replace this with a normal console command "defineCommand"?
+void          trap_AddCommand( const char *cmdName );
+
+// send a string to the server over the network
+void          trap_SendClientCommand( const char *s );
+
+// force a screen update, only used during gamestate load
+void          trap_UpdateScreen( void );
+
+// model collision
+void          trap_CM_LoadMap( const char *mapname );
+int           trap_CM_NumInlineModels( void );
+clipHandle_t  trap_CM_InlineModel( int index );    // 0 = world, 1+ = bmodels
+clipHandle_t  trap_CM_TempBoxModel( const vec3_t mins, const vec3_t maxs );
+int           trap_CM_PointContents( const vec3_t p, clipHandle_t model );
+int           trap_CM_TransformedPointContents( const vec3_t p, clipHandle_t model, const vec3_t origin, const vec3_t angles );
+void          trap_CM_BoxTrace( trace_t *results, const vec3_t start, const vec3_t end,
+                                const vec3_t mins, const vec3_t maxs,
+                                clipHandle_t model, int brushmask );
+void          trap_CM_TransformedBoxTrace( trace_t *results, const vec3_t start, const vec3_t end,
+                                           const vec3_t mins, const vec3_t maxs,
+                                           clipHandle_t model, int brushmask,
+                                           const vec3_t origin, const vec3_t angles );
+void          trap_CM_CapsuleTrace( trace_t *results, const vec3_t start, const vec3_t end,
+                const vec3_t mins, const vec3_t maxs,
+                clipHandle_t model, int brushmask );
+void          trap_CM_TransformedCapsuleTrace( trace_t *results, const vec3_t start, const vec3_t end,
+                const vec3_t mins, const vec3_t maxs,
+                clipHandle_t model, int brushmask,
+                const vec3_t origin, const vec3_t angles );
+void          trap_CM_BiSphereTrace( trace_t *results, const vec3_t start,
+                const vec3_t end, float startRad, float endRad,
+                clipHandle_t model, int mask );
+void          trap_CM_TransformedBiSphereTrace( trace_t *results, const vec3_t start,
+                const vec3_t end, float startRad, float endRad,
+                clipHandle_t model, int mask,
+                const vec3_t origin );
+
+// Returns the projection of a polygon onto the solid brushes in the world
+int           trap_CM_MarkFragments( int numPoints, const vec3_t *points,
+                                     const vec3_t projection,
+                                     int maxPoints, vec3_t pointBuffer,
+                                     int maxFragments, markFragment_t *fragmentBuffer );
+
+// normal sounds will have their volume dynamically changed as their entity
+// moves and the listener moves
+void          trap_S_StartSound( vec3_t origin, int entityNum, int entchannel, sfxHandle_t sfx );
+void          trap_S_StopLoopingSound( int entnum );
+
+// a local sound is always played full volume
+void          trap_S_StartLocalSound( sfxHandle_t sfx, int channelNum );
+void          trap_S_ClearLoopingSounds( qboolean killall );
+void          trap_S_AddLoopingSound( int entityNum, const vec3_t origin, const vec3_t velocity, sfxHandle_t sfx );
+void          trap_S_AddRealLoopingSound( int entityNum, const vec3_t origin, const vec3_t velocity, sfxHandle_t sfx );
+void          trap_S_UpdateEntityPosition( int entityNum, const vec3_t origin );
+
+// respatialize recalculates the volumes of sound as they should be heard by the
+// given entityNum and position
+void          trap_S_Respatialize( int entityNum, const vec3_t origin, vec3_t axis[3], int inwater );
+sfxHandle_t   trap_S_RegisterSound( const char *sample, qboolean compressed );    // returns buzz if not found
+void          trap_S_StartBackgroundTrack( const char *intro, const char *loop ); // empty name stops music
+void          trap_S_StopBackgroundTrack( void );
+
+
+void          trap_R_LoadWorldMap( const char *mapname );
+
+// all media should be registered during level startup to prevent
+// hitches during gameplay
+qhandle_t     trap_R_RegisterModel( const char *name );     // returns rgb axis if not found
+qhandle_t     trap_R_RegisterSkin( const char *name );      // returns all white if not found
+qhandle_t     trap_R_RegisterShader( const char *name );      // returns all white if not found
+qhandle_t     trap_R_RegisterShaderNoMip( const char *name );     // returns all white if not found
+
+// a scene is built up by calls to R_ClearScene and the various R_Add functions.
+// Nothing is drawn until R_RenderScene is called.
+void          trap_R_ClearScene( void );
+void          trap_R_AddRefEntityToScene( const refEntity_t *re );
+
+// polys are intended for simple wall marks, not really for doing
+// significant construction
+void          trap_R_AddPolyToScene( qhandle_t hShader , int numVerts, const polyVert_t *verts );
+void          trap_R_AddPolysToScene( qhandle_t hShader , int numVerts, const polyVert_t *verts, int numPolys );
+qboolean      trap_R_inPVS( const vec3_t p1, const vec3_t p2 );
+void          trap_R_AddLightToScene( const vec3_t org, float intensity, float r, float g, float b );
+void          trap_R_AddAdditiveLightToScene( const vec3_t org, float intensity, float r, float g, float b );
+int           trap_R_LightForPoint( vec3_t point, vec3_t ambientLight, vec3_t directedLight, vec3_t lightDir );
+void          trap_R_RenderScene( const refdef_t *fd );
+void          trap_R_SetColor( const float *rgba ); // NULL = 1,1,1,1
+void          trap_R_SetClipRegion( const float *region );
+void          trap_R_DrawStretchPic( float x, float y, float w, float h,
+                                     float s1, float t1, float s2, float t2, qhandle_t hShader );
+void          trap_R_ModelBounds( clipHandle_t model, vec3_t mins, vec3_t maxs );
+int           trap_R_LerpTag( orientation_t *tag, clipHandle_t mod, int startFrame, int endFrame,
+                              float frac, const char *tagName );
+void          trap_R_RemapShader( const char *oldShader, const char *newShader, const char *timeOffset );
+
+// The glconfig_t will not change during the life of a cgame.
+// If it needs to change, the entire cgame will be restarted, because
+// all the qhandle_t are then invalid.
+void          trap_GetGlconfig( glconfig_t *glconfig );
+
+// the gamestate should be grabbed at startup, and whenever a
+// configstring changes
+void          trap_GetGameState( gameState_t *gamestate );
+
+// cgame will poll each frame to see if a newer snapshot has arrived
+// that it is interested in.  The time is returned seperately so that
+// snapshot latency can be calculated.
+void          trap_GetCurrentSnapshotNumber( int *snapshotNumber, int *serverTime );
+
+// a snapshot get can fail if the snapshot (or the entties it holds) is so
+// old that it has fallen out of the client system queue
+qboolean      trap_GetSnapshot( int snapshotNumber, snapshot_t *snapshot );
+
+// retrieve a text command from the server stream
+// the current snapshot will hold the number of the most recent command
+// qfalse can be returned if the client system handled the command
+// argc() / argv() can be used to examine the parameters of the command
+qboolean      trap_GetServerCommand( int serverCommandNumber );
+
+// returns the most recent command number that can be passed to GetUserCmd
+// this will always be at least one higher than the number in the current
+// snapshot, and it may be quite a few higher if it is a fast computer on
+// a lagged connection
+int           trap_GetCurrentCmdNumber( void );
+
+qboolean      trap_GetUserCmd( int cmdNumber, usercmd_t *ucmd );
+
+// used for the weapon select and zoom
+void          trap_SetUserCmdValue( int stateValue, float sensitivityScale );
+
+// aids for VM testing
+void          testPrintInt( char *string, int i );
+void          testPrintFloat( char *string, float f );
+
+int           trap_MemoryRemaining( void );
+void          trap_R_RegisterFont(const char *fontName, int pointSize, fontInfo_t *font);
+qboolean      trap_Key_IsDown( int keynum );
+int           trap_Key_GetCatcher( void );
+void          trap_Key_SetCatcher( int catcher );
+int           trap_Key_GetKey( const char *binding );
+void          trap_Key_KeynumToStringBuf( int keynum, char *buf, int buflen );
+void          trap_Key_GetBindingBuf( int keynum, char *buf, int buflen );
+void          trap_Key_SetBinding( int keynum, const char *binding );
+void          trap_Key_SetOverstrikeMode( qboolean state );
+qboolean      trap_Key_GetOverstrikeMode( void );
+
+int           trap_CIN_PlayCinematic( const char *arg0, int xpos, int ypos, int width, int height, int bits );
+e_status      trap_CIN_StopCinematic( int handle );
+e_status      trap_CIN_RunCinematic( int handle );
+void          trap_CIN_DrawCinematic( int handle );
+void          trap_CIN_SetExtents( int handle, int x, int y, int w, int h );
+
+void          trap_SnapVector( float *v );
+int           trap_RealTime( qtime_t *tm );
+
+qboolean      trap_loadCamera( const char *name );
+void          trap_startCamera( int time );
+qboolean      trap_getCameraInfo( int time, vec3_t *origin, vec3_t *angles );
+
+qboolean      trap_GetEntityToken( char *buffer, int bufferSize );
+
+int           trap_GetDemoState( void );
+int           trap_GetDemoPos( void );
+void          trap_GetDemoName( char *buffer, int size );
+
+// cg_drawCrosshair settings
+#define CROSSHAIR_ALWAYSOFF       0
+#define CROSSHAIR_RANGEDONLY      1
+#define CROSSHAIR_ALWAYSON        2
+
+// menu types for cg_disable*Dialogs
+typedef enum
+{
+  DT_INTERACTIVE, // team, class, armoury
+  DT_ARMOURYEVOLVE, // Insufficient funds et al
+  DT_BUILD, // build errors
+  DT_COMMAND, // You must be living/human/spec etc.
+
+} dialogType_t;
diff --git a/src/cgame/cg_main.c b/src/cgame/cg_main.c
new file mode 100644
index 0000000..1bd5c7f
--- /dev/null
+++ b/src/cgame/cg_main.c
@@ -0,0 +1,1963 @@
+/*
+===========================================================================
+Copyright (C) 1999-2005 Id Software, Inc.
+Copyright (C) 2000-2009 Darklegion Development
+
+This file is part of Tremulous.
+
+Tremulous 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.
+
+Tremulous 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 Tremulous; if not, write to the Free Software
+Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
+===========================================================================
+*/
+
+// cg_main.c -- initialization and primary entry point for cgame
+
+
+#include "cg_local.h"
+
+#include "../ui/ui_shared.h"
+// display context for new ui stuff
+displayContextDef_t cgDC;
+
+void CG_Init( int serverMessageNum, int serverCommandSequence, int clientNum );
+void CG_Shutdown( void );
+static char *CG_VoIPString( void );
+
+/*
+================
+vmMain
+
+This is the only way control passes into the module.
+This must be the very first function compiled into the .q3vm file
+================
+*/
+Q_EXPORT intptr_t vmMain( int command, int arg0, int arg1, int arg2, int arg3,
+                                       int arg4, int arg5, int arg6, int arg7,
+                                       int arg8, int arg9, int arg10, int arg11 )
+{
+  switch( command )
+  {
+    case CG_INIT:
+      CG_Init( arg0, arg1, arg2 );
+      return 0;
+
+    case CG_SHUTDOWN:
+      CG_Shutdown( );
+      return 0;
+
+    case CG_CONSOLE_COMMAND:
+      return CG_ConsoleCommand( );
+
+    case CG_CONSOLE_TEXT:
+      CG_AddNotifyText( );
+      return 0;
+
+    case CG_DRAW_ACTIVE_FRAME:
+      CG_DrawActiveFrame( arg0, arg1, arg2 );
+      return 0;
+
+    case CG_CROSSHAIR_PLAYER:
+      return CG_CrosshairPlayer( );
+
+    case CG_LAST_ATTACKER:
+      return CG_LastAttacker( );
+
+    case CG_KEY_EVENT:
+      CG_KeyEvent( arg0, arg1 );
+      return 0;
+
+    case CG_MOUSE_EVENT:
+      // cgame doesn't care where the cursor is
+      return 0;
+
+    case CG_EVENT_HANDLING:
+      CG_EventHandling( arg0 );
+      return 0;
+
+    case CG_VOIP_STRING:
+      return (intptr_t)CG_VoIPString( );
+
+    default:
+      CG_Error( "vmMain: unknown command %i", command );
+      break;
+  }
+
+  return -1;
+}
+
+
+cg_t        cg;
+cgs_t       cgs;
+centity_t   cg_entities[ MAX_GENTITIES ];
+
+weaponInfo_t    cg_weapons[ 40 ];
+upgradeInfo_t   cg_upgrades[ 40 ];
+
+buildableInfo_t cg_buildables[ BA_NUM_BUILDABLES ];
+
+vmCvar_t  cg_teslaTrailTime;
+vmCvar_t  cg_centertime;
+vmCvar_t  cg_runpitch;
+vmCvar_t  cg_runroll;
+vmCvar_t  cg_swingSpeed;
+vmCvar_t  cg_shadows;
+vmCvar_t  cg_drawTimer;
+vmCvar_t  cg_drawClock;
+vmCvar_t  cg_drawFPS;
+vmCvar_t  cg_drawDemoState;
+vmCvar_t  cg_drawSnapshot;
+vmCvar_t  cg_drawChargeBar;
+vmCvar_t  cg_drawCrosshair;
+vmCvar_t  cg_drawCrosshairNames;
+vmCvar_t  cg_crosshairSize;
+vmCvar_t  cg_draw2D;
+vmCvar_t  cg_animSpeed;
+vmCvar_t  cg_debugAnim;
+vmCvar_t  cg_debugPosition;
+vmCvar_t  cg_debugEvents;
+vmCvar_t  cg_errorDecay;
+vmCvar_t  cg_nopredict;
+vmCvar_t  cg_debugMove;
+vmCvar_t  cg_noPlayerAnims;
+vmCvar_t  cg_showmiss;
+vmCvar_t  cg_footsteps;
+vmCvar_t  cg_addMarks;
+vmCvar_t  cg_viewsize;
+vmCvar_t  cg_drawGun;
+vmCvar_t  cg_gun_frame;
+vmCvar_t  cg_gun_x;
+vmCvar_t  cg_gun_y;
+vmCvar_t  cg_gun_z;
+vmCvar_t  cg_tracerChance;
+vmCvar_t  cg_tracerWidth;
+vmCvar_t  cg_tracerLength;
+vmCvar_t  cg_thirdPerson;
+vmCvar_t  cg_thirdPersonAngle;
+vmCvar_t  cg_thirdPersonShoulderViewMode;
+vmCvar_t  cg_staticDeathCam;
+vmCvar_t  cg_thirdPersonPitchFollow;
+vmCvar_t  cg_thirdPersonRange;
+vmCvar_t  cg_stereoSeparation;
+vmCvar_t  cg_lagometer;
+vmCvar_t  cg_drawSpeed;
+vmCvar_t  cg_synchronousClients;
+vmCvar_t  cg_stats;
+vmCvar_t  cg_paused;
+vmCvar_t  cg_blood;
+vmCvar_t  cg_teamChatsOnly;
+vmCvar_t  cg_drawTeamOverlay;
+vmCvar_t  cg_teamOverlaySortMode;
+vmCvar_t  cg_teamOverlayMaxPlayers;
+vmCvar_t  cg_teamOverlayUserinfo;
+vmCvar_t  cg_noPrintDuplicate;
+vmCvar_t  cg_noVoiceChats;
+vmCvar_t  cg_noVoiceText;
+vmCvar_t  cg_hudFiles;
+vmCvar_t  cg_hudFilesEnable;
+vmCvar_t  cg_smoothClients;
+vmCvar_t  pmove_fixed;
+vmCvar_t  pmove_msec;
+vmCvar_t  cg_cameraMode;
+vmCvar_t  cg_timescaleFadeEnd;
+vmCvar_t  cg_timescaleFadeSpeed;
+vmCvar_t  cg_timescale;
+vmCvar_t  cg_noTaunt;
+vmCvar_t  cg_drawSurfNormal;
+vmCvar_t  cg_drawBBOX;
+vmCvar_t  cg_wwSmoothTime;
+vmCvar_t  cg_disableBlueprintErrors;
+vmCvar_t  cg_depthSortParticles;
+vmCvar_t  cg_bounceParticles;
+vmCvar_t  cg_consoleLatency;
+vmCvar_t  cg_lightFlare;
+vmCvar_t  cg_debugParticles;
+vmCvar_t  cg_debugTrails;
+vmCvar_t  cg_debugPVS;
+vmCvar_t  cg_disableWarningDialogs;
+vmCvar_t  cg_disableUpgradeDialogs;
+vmCvar_t  cg_disableBuildDialogs;
+vmCvar_t  cg_disableCommandDialogs;
+vmCvar_t  cg_disableScannerPlane;
+vmCvar_t  cg_tutorial;
+
+vmCvar_t  cg_painBlendUpRate;
+vmCvar_t  cg_painBlendDownRate;
+vmCvar_t  cg_painBlendMax;
+vmCvar_t  cg_painBlendScale;
+vmCvar_t  cg_painBlendZoom;
+
+vmCvar_t  cg_stickySpec;
+vmCvar_t  cg_sprintToggle;
+vmCvar_t  cg_unlagged;
+
+vmCvar_t  cg_debugVoices;
+
+vmCvar_t  ui_currentClass;
+vmCvar_t  ui_carriage;
+vmCvar_t  ui_stages;
+vmCvar_t  ui_dialog;
+vmCvar_t  ui_voteActive;
+vmCvar_t  ui_alienTeamVoteActive;
+vmCvar_t  ui_humanTeamVoteActive;
+
+vmCvar_t  cg_debugRandom;
+
+vmCvar_t  cg_optimizePrediction;
+vmCvar_t  cg_projectileNudge;
+
+vmCvar_t  cg_voice;
+
+vmCvar_t  cg_emoticons;
+
+vmCvar_t  cg_chatTeamPrefix;
+
+vmCvar_t  cg_drawBubble;
+vmCvar_t  cg_BubbleZoom;
+vmCvar_t  cg_EDGEFPSFIX; 
+
+typedef struct
+{
+  vmCvar_t  *vmCvar;
+  char      *cvarName;
+  char      *defaultString;
+  int       cvarFlags;
+} cvarTable_t;
+
+static cvarTable_t cvarTable[ ] =
+{
+  { &cg_drawGun, "cg_drawGun", "1", CVAR_ARCHIVE },
+  { &cg_viewsize, "cg_viewsize", "100", CVAR_ARCHIVE },
+  { &cg_stereoSeparation, "cg_stereoSeparation", "0.4", CVAR_ARCHIVE  },
+  { &cg_shadows, "cg_shadows", "1", CVAR_ARCHIVE  },
+  { &cg_draw2D, "cg_draw2D", "1", CVAR_ARCHIVE  },
+  { &cg_drawTimer, "cg_drawTimer", "1", CVAR_ARCHIVE  },
+  { &cg_drawClock, "cg_drawClock", "0", CVAR_ARCHIVE  },
+  { &cg_drawFPS, "cg_drawFPS", "1", CVAR_ARCHIVE  },
+  { &cg_drawDemoState, "cg_drawDemoState", "1", CVAR_ARCHIVE  },
+  { &cg_drawSnapshot, "cg_drawSnapshot", "0", CVAR_ARCHIVE  },
+  { &cg_drawChargeBar, "cg_drawChargeBar", "1", CVAR_ARCHIVE  },
+  { &cg_drawCrosshair, "cg_drawCrosshair", "2", CVAR_ARCHIVE },
+  { &cg_drawCrosshairNames, "cg_drawCrosshairNames", "1", CVAR_ARCHIVE },
+  { &cg_crosshairSize, "cg_crosshairSize", "1", CVAR_ARCHIVE },
+  { &cg_addMarks, "cg_marks", "1", CVAR_ARCHIVE },
+  { &cg_lagometer, "cg_lagometer", "0", CVAR_ARCHIVE },
+  { &cg_drawSpeed, "cg_drawSpeed", "0", CVAR_ARCHIVE },
+  { &cg_teslaTrailTime, "cg_teslaTrailTime", "250", CVAR_ARCHIVE  },
+  { &cg_gun_x, "cg_gunX", "0", CVAR_CHEAT },
+  { &cg_gun_y, "cg_gunY", "0", CVAR_CHEAT },
+  { &cg_gun_z, "cg_gunZ", "0", CVAR_CHEAT },
+  { &cg_centertime, "cg_centertime", "3", CVAR_CHEAT },
+  { &cg_runpitch, "cg_runpitch", "0.002", CVAR_ARCHIVE},
+  { &cg_runroll, "cg_runroll", "0.005", CVAR_ARCHIVE },
+  { &cg_swingSpeed, "cg_swingSpeed", "0.3", CVAR_CHEAT },
+  { &cg_animSpeed, "cg_animspeed", "1", CVAR_CHEAT },
+  { &cg_debugAnim, "cg_debuganim", "0", CVAR_CHEAT },
+  { &cg_debugPosition, "cg_debugposition", "0", CVAR_CHEAT },
+  { &cg_debugEvents, "cg_debugevents", "0", CVAR_CHEAT },
+  { &cg_errorDecay, "cg_errordecay", "100", 0 },
+  { &cg_nopredict, "cg_nopredict", "0", 0 },
+  { &cg_debugMove, "cg_debugMove", "0", 0 },
+  { &cg_noPlayerAnims, "cg_noplayeranims", "0", CVAR_CHEAT },
+  { &cg_showmiss, "cg_showmiss", "0", 0 },
+  { &cg_footsteps, "cg_footsteps", "1", CVAR_CHEAT },
+  { &cg_tracerChance, "cg_tracerchance", "0.4", CVAR_CHEAT },
+  { &cg_tracerWidth, "cg_tracerwidth", "1", CVAR_CHEAT },
+  { &cg_tracerLength, "cg_tracerlength", "100", CVAR_CHEAT },
+  { &cg_thirdPersonRange, "cg_thirdPersonRange", "75", CVAR_ARCHIVE },
+  { &cg_thirdPerson, "cg_thirdPerson", "0", CVAR_CHEAT },
+  { &cg_thirdPersonAngle, "cg_thirdPersonAngle", "0", CVAR_CHEAT },
+  { &cg_thirdPersonPitchFollow, "cg_thirdPersonPitchFollow", "0", 0 },
+  { &cg_thirdPersonShoulderViewMode, "cg_thirdPersonShoulderViewMode", "1", CVAR_ARCHIVE },
+  { &cg_staticDeathCam, "cg_staticDeathCam", "0", CVAR_ARCHIVE },
+  { &cg_stats, "cg_stats", "0", 0 },
+  { &cg_drawTeamOverlay, "cg_drawTeamOverlay", "1", CVAR_ARCHIVE },
+  { &cg_teamOverlaySortMode, "cg_teamOverlaySortMode", "1", CVAR_ARCHIVE },
+  { &cg_teamOverlayMaxPlayers, "cg_teamOverlayMaxPlayers", "8", CVAR_ARCHIVE },
+  { &cg_teamOverlayUserinfo, "teamoverlay", "1", CVAR_ARCHIVE|CVAR_USERINFO },
+  { &cg_teamChatsOnly, "cg_teamChatsOnly", "0", CVAR_ARCHIVE },
+  { &cg_noPrintDuplicate, "cg_noPrintDuplicate", "0", CVAR_ARCHIVE },
+  { &cg_noVoiceChats, "cg_noVoiceChats", "0", CVAR_ARCHIVE },
+  { &cg_noVoiceText, "cg_noVoiceText", "0", CVAR_ARCHIVE },
+  { &cg_drawSurfNormal, "cg_drawSurfNormal", "0", CVAR_CHEAT },
+  { &cg_drawBBOX, "cg_drawBBOX", "0", CVAR_CHEAT },
+  { &cg_wwSmoothTime, "cg_wwSmoothTime", "300", CVAR_ARCHIVE },
+  { NULL, "cg_wwFollow", "1", CVAR_ARCHIVE|CVAR_USERINFO },
+  { NULL, "cg_wwToggle", "1", CVAR_ARCHIVE|CVAR_USERINFO },
+  { NULL, "cg_disableBlueprintErrors", "0", CVAR_ARCHIVE|CVAR_USERINFO },
+  { &cg_stickySpec, "cg_stickySpec", "1", CVAR_ARCHIVE|CVAR_USERINFO },
+  { &cg_sprintToggle, "cg_sprintToggle", "0", CVAR_ARCHIVE|CVAR_USERINFO },
+  { &cg_unlagged, "cg_unlagged", "1", CVAR_ARCHIVE|CVAR_USERINFO },
+  
+
+  { &cg_drawBubble, "cg_drawBubble", "0", CVAR_ARCHIVE|CVAR_USERINFO },
+  { &cg_BubbleZoom, "cg_BubbleZoom", "0", CVAR_ARCHIVE|CVAR_USERINFO },
+  
+  { NULL, "cg_flySpeed", "600", CVAR_ARCHIVE|CVAR_USERINFO },
+  { &cg_depthSortParticles, "cg_depthSortParticles", "1", CVAR_ARCHIVE },
+  { &cg_bounceParticles, "cg_bounceParticles", "0", CVAR_ARCHIVE },
+  { &cg_consoleLatency, "cg_consoleLatency", "3000", CVAR_ARCHIVE },
+  { &cg_lightFlare, "cg_lightFlare", "3", CVAR_ARCHIVE },
+  { &cg_debugParticles, "cg_debugParticles", "0", CVAR_CHEAT },
+  { &cg_debugTrails, "cg_debugTrails", "0", CVAR_CHEAT },
+  { &cg_debugPVS, "cg_debugPVS", "0", CVAR_CHEAT },
+  { &cg_disableWarningDialogs, "cg_disableWarningDialogs", "0", CVAR_ARCHIVE },
+  { &cg_disableUpgradeDialogs, "cg_disableUpgradeDialogs", "0", CVAR_ARCHIVE },
+  { &cg_disableBuildDialogs, "cg_disableBuildDialogs", "0", CVAR_ARCHIVE },
+  { &cg_disableCommandDialogs, "cg_disableCommandDialogs", "0", CVAR_ARCHIVE },
+  { &cg_disableScannerPlane, "cg_disableScannerPlane", "0", CVAR_ARCHIVE },
+  { &cg_tutorial, "cg_tutorial", "1", CVAR_ARCHIVE },
+  { &cg_hudFiles, "cg_hudFiles", "ui/hud.txt", CVAR_ARCHIVE},
+  { &cg_hudFilesEnable, "cg_hudFilesEnable", "0", CVAR_ARCHIVE},
+  { NULL, "cg_alienConfig", "", CVAR_ARCHIVE },
+  { NULL, "cg_humanConfig", "", CVAR_ARCHIVE },
+  { NULL, "cg_spectatorConfig", "", CVAR_ARCHIVE },
+
+  { &cg_painBlendUpRate, "cg_painBlendUpRate", "10.0", 0 },
+  { &cg_painBlendDownRate, "cg_painBlendDownRate", "0.5", 0 },
+  { &cg_painBlendMax, "cg_painBlendMax", "0.7", 0 },
+  { &cg_painBlendScale, "cg_painBlendScale", "7.0", 0 },
+  { &cg_painBlendZoom, "cg_painBlendZoom", "0.65", 0 },
+  
+  { &cg_debugVoices, "cg_debugVoices", "0", 0 },
+
+  // communication cvars set by the cgame to be read by ui
+  { &ui_currentClass, "ui_currentClass", "0", CVAR_ROM },
+  { &ui_carriage, "ui_carriage", "", CVAR_ROM },
+  { &ui_stages, "ui_stages", "0 0", CVAR_ROM },
+  { &ui_dialog, "ui_dialog", "Text not set", CVAR_ROM },
+  { &ui_voteActive, "ui_voteActive", "0", CVAR_ROM },
+  { &ui_humanTeamVoteActive, "ui_humanTeamVoteActive", "0", CVAR_ROM },
+  { &ui_alienTeamVoteActive, "ui_alienTeamVoteActive", "0", CVAR_ROM },
+
+  { &cg_debugRandom, "cg_debugRandom", "0", 0 },
+  
+  { &cg_optimizePrediction, "cg_optimizePrediction", "1", CVAR_ARCHIVE },
+  { &cg_projectileNudge, "cg_projectileNudge", "1", CVAR_ARCHIVE },
+
+  // the following variables are created in other parts of the system,
+  // but we also reference them here
+
+  { &cg_paused, "cl_paused", "0", CVAR_ROM },
+  { &cg_blood, "com_blood", "1", CVAR_ARCHIVE },
+  { &cg_synchronousClients, "g_synchronousClients", "0", 0 }, // communicated by systeminfo
+  { &cg_timescaleFadeEnd, "cg_timescaleFadeEnd", "1", CVAR_CHEAT },
+  { &cg_timescaleFadeSpeed, "cg_timescaleFadeSpeed", "0", CVAR_CHEAT },
+  { &cg_timescale, "timescale", "1", 0},
+  { &cg_smoothClients, "cg_smoothClients", "0", CVAR_USERINFO | CVAR_ARCHIVE},
+  { &cg_cameraMode, "com_cameraMode", "0", CVAR_CHEAT},
+
+  { &pmove_fixed, "pmove_fixed", "0", 0},
+  { &pmove_msec, "pmove_msec", "8", 0},
+  { &cg_noTaunt, "cg_noTaunt", "0", CVAR_ARCHIVE},
+  
+  { &cg_voice, "voice", "default", CVAR_USERINFO|CVAR_ARCHIVE},
+
+  { &cg_emoticons, "cg_emoticons", "1", CVAR_LATCH|CVAR_ARCHIVE},
+
+//  { &cg_chatTeamPrefix, "cg_chatTeamPrefix", "1", CVAR_ARCHIVE}
+  { &cg_chatTeamPrefix, "cg_chatTeamPrefix", "1", CVAR_ARCHIVE},
+  { &cg_EDGEFPSFIX, "cg_EDGEFPSFIX", "0", CVAR_ARCHIVE|CVAR_USERINFO } 
+};
+
+static int   cvarTableSize = sizeof( cvarTable ) / sizeof( cvarTable[0] );
+
+/*
+=================
+CG_RegisterCvars
+=================
+*/
+void CG_RegisterCvars( void )
+{
+  int         i;
+  cvarTable_t *cv;
+  char        var[ MAX_TOKEN_CHARS ];
+
+  for( i = 0, cv = cvarTable; i < cvarTableSize; i++, cv++ )
+  {
+    trap_Cvar_Register( cv->vmCvar, cv->cvarName,
+      cv->defaultString, cv->cvarFlags );
+  }
+
+  // see if we are also running the server on this machine
+  trap_Cvar_VariableStringBuffer( "sv_running", var, sizeof( var ) );
+  cgs.localServer = atoi( var );
+}
+
+
+/*
+===============
+CG_SetUIVars
+
+Set some cvars used by the UI
+===============
+*/
+static void CG_SetUIVars( void )
+{
+  int   i;
+  char  carriageCvar[ MAX_TOKEN_CHARS ];
+
+  if( !cg.snap )
+    return;
+
+  *carriageCvar = 0;
+
+  //determine what the player is carrying
+  for( i = WP_NONE + 1; i < WP_NUM_WEAPONS; i++ )
+  {
+    if( BG_InventoryContainsWeapon( i, cg.snap->ps.stats ) &&
+        BG_Weapon( i )->purchasable )
+      strcat( carriageCvar, va( "W%d ", i ) );
+  }
+  for( i = UP_NONE + 1; i < UP_NUM_UPGRADES; i++ )
+  {
+    if( BG_InventoryContainsUpgrade( i, cg.snap->ps.stats ) &&
+        BG_Upgrade( i )->purchasable )
+      strcat( carriageCvar, va( "U%d ", i ) );
+  }
+  strcat( carriageCvar, "$" );
+
+  trap_Cvar_Set( "ui_carriage", carriageCvar );
+
+  trap_Cvar_Set( "ui_stages", va( "%d %d", cgs.alienStage, cgs.humanStage ) );
+}
+
+/*
+=================
+CG_UpdateCvars
+=================
+*/
+void CG_UpdateCvars( void )
+{
+  int         i;
+  cvarTable_t *cv;
+
+  for( i = 0, cv = cvarTable; i < cvarTableSize; i++, cv++ )
+    if( cv->vmCvar )
+      trap_Cvar_Update( cv->vmCvar );
+
+  // check for modications here
+
+  CG_SetUIVars( );
+
+}
+
+
+int CG_CrosshairPlayer( void )
+{
+  if( cg.time > ( cg.crosshairClientTime + 1000 ) )
+    return -1;
+
+  return cg.crosshairClientNum;
+}
+
+
+int CG_LastAttacker( void )
+{
+  if( !cg.attackerTime )
+    return -1;
+
+  return cg.snap->ps.persistant[ PERS_ATTACKER ];
+}
+
+
+/*
+=================
+CG_RemoveNotifyLine
+=================
+*/
+void CG_RemoveNotifyLine( void )
+{
+  int i, offset, totalLength;
+
+  if( cg.numConsoleLines == 0 )
+    return;
+
+  offset = cg.consoleLines[ 0 ].length;
+  totalLength = strlen( cg.consoleText ) - offset;
+
+  //slide up consoleText
+  for( i = 0; i <= totalLength; i++ )
+    cg.consoleText[ i ] = cg.consoleText[ i + offset ];
+
+  //pop up the first consoleLine
+  for( i = 0; i < cg.numConsoleLines; i++ )
+    cg.consoleLines[ i ] = cg.consoleLines[ i + 1 ];
+
+  cg.numConsoleLines--;
+}
+
+/*
+=================
+CG_AddNotifyText
+=================
+*/
+void CG_AddNotifyText( void )
+{
+  char buffer[ BIG_INFO_STRING ];
+  int bufferLen, textLen;
+
+  trap_LiteralArgs( buffer, BIG_INFO_STRING );
+
+  if( !buffer[ 0 ] )
+  {
+    cg.consoleText[ 0 ] = '\0';
+    cg.numConsoleLines = 0;
+    return;
+  }
+
+  bufferLen = strlen( buffer );
+  textLen = strlen( cg.consoleText );
+  
+  // Ignore console messages that were just printed
+  if( cg_noPrintDuplicate.integer && textLen >= bufferLen &&
+      !strcmp( cg.consoleText + textLen - bufferLen, buffer ) )
+    return;
+      
+  if( cg.numConsoleLines == MAX_CONSOLE_LINES )
+    CG_RemoveNotifyLine( );
+
+  Q_strcat( cg.consoleText, MAX_CONSOLE_TEXT, buffer );
+  cg.consoleLines[ cg.numConsoleLines ].time = cg.time;
+  cg.consoleLines[ cg.numConsoleLines ].length = bufferLen;
+  cg.numConsoleLines++;
+}
+
+void QDECL CG_Printf( const char *msg, ... )
+{
+  va_list argptr;
+  char    text[ 1024 ];
+
+  va_start( argptr, msg );
+  Q_vsnprintf( text, sizeof( text ), msg, argptr );
+  va_end( argptr );
+
+  trap_Print( text );
+}
+
+void QDECL CG_Error( const char *msg, ... )
+{
+  va_list argptr;
+  char    text[ 1024 ];
+
+  va_start( argptr, msg );
+  Q_vsnprintf( text, sizeof( text ), msg, argptr );
+  va_end( argptr );
+
+  trap_Error( text );
+}
+
+void QDECL Com_Error( int level, const char *error, ... )
+{
+  va_list argptr;
+  char    text[1024];
+
+  va_start( argptr, error );
+  Q_vsnprintf( text, sizeof( text ), error, argptr );
+  va_end( argptr );
+
+  CG_Error( "%s", text );
+}
+
+void QDECL Com_Printf( const char *msg, ... ) {
+  va_list   argptr;
+  char    text[1024];
+
+  va_start( argptr, msg );
+  Q_vsnprintf( text, sizeof( text ), msg, argptr );
+  va_end( argptr );
+
+  CG_Printf ("%s", text);
+}
+
+
+
+/*
+================
+CG_Argv
+================
+*/
+const char *CG_Argv( int arg )
+{
+  static char buffer[ MAX_STRING_CHARS ];
+
+  trap_Argv( arg, buffer, sizeof( buffer ) );
+
+  return buffer;
+}
+
+
+//========================================================================
+
+/*
+=================
+CG_FileExists
+
+Test if a specific file exists or not
+=================
+*/
+qboolean CG_FileExists( char *filename )
+{
+  return trap_FS_FOpenFile( filename, NULL, FS_READ );
+}
+
+/*
+=================
+CG_RegisterSounds
+
+called during a precache command
+=================
+*/
+static void CG_RegisterSounds( void )
+{
+  int         i;
+  char        name[ MAX_QPATH ];
+  const char  *soundName;
+
+  
+  
+  cgs.media.alienStageTransition  = trap_S_RegisterSound( "sound/announcements/overmindevolved.wav", qtrue );
+  cgs.media.humanStageTransition  = trap_S_RegisterSound( "sound/announcements/reinforcement.wav", qtrue );
+
+  cgs.media.iniVote  = trap_S_RegisterSound( "sound/ye/votebing.wav", qtrue );
+  
+  cgs.media.alienOvermindAttack   = trap_S_RegisterSound( "sound/announcements/overmindattack.wav", qtrue );
+  cgs.media.alienOvermindDying    = trap_S_RegisterSound( "sound/announcements/overminddying.wav", qtrue );
+  cgs.media.alienOvermindSpawns   = trap_S_RegisterSound( "sound/announcements/overmindspawns.wav", qtrue );
+  cgs.media.humanbaseunderatt   = trap_S_RegisterSound( "sound/ye/humanbaseunderatt.wav", qtrue );
+  
+  cgs.media.alienL1Grab           = trap_S_RegisterSound( "sound/player/level1/grab.wav", qtrue );
+  cgs.media.alienL4ChargePrepare  = trap_S_RegisterSound( "sound/player/level4/charge_prepare.wav", qtrue );
+  cgs.media.alienL4ChargeStart    = trap_S_RegisterSound( "sound/player/level4/charge_start.wav", qtrue );
+
+  cgs.media.tracerSound           = trap_S_RegisterSound( "sound/weapons/tracer.wav", qfalse );
+  cgs.media.selectSound           = trap_S_RegisterSound( "sound/weapons/change.wav", qfalse );
+  cgs.media.turretSpinupSound     = trap_S_RegisterSound( "sound/buildables/mgturret/spinup.wav", qfalse );
+  cgs.media.weaponEmptyClick      = trap_S_RegisterSound( "sound/weapons/click.wav", qfalse );
+
+  cgs.media.talkSound             = trap_S_RegisterSound( "sound/misc/talk.wav", qfalse );
+  cgs.media.alienTalkSound        = trap_S_RegisterSound( "sound/misc/alien_talk.wav", qfalse );
+  cgs.media.humanTalkSound        = trap_S_RegisterSound( "sound/misc/human_talk.wav", qfalse );
+  cgs.media.landSound             = trap_S_RegisterSound( "sound/player/land1.wav", qfalse );
+
+  cgs.media.hummelSound           = trap_S_RegisterSound( "sound/player/hummel.wav", qfalse );
+
+  cgs.media.watrInSound           = trap_S_RegisterSound( "sound/player/watr_in.wav", qfalse );
+  cgs.media.watrOutSound          = trap_S_RegisterSound( "sound/player/watr_out.wav", qfalse );
+  cgs.media.watrUnSound           = trap_S_RegisterSound( "sound/player/watr_un.wav", qfalse );
+
+  cgs.media.disconnectSound       = trap_S_RegisterSound( "sound/misc/disconnect.wav", qfalse );
+
+  for( i = 0; i < 4; i++ )
+  {
+    Com_sprintf( name, sizeof( name ), "sound/player/footsteps/step%i.wav", i + 1 );
+    cgs.media.footsteps[ FOOTSTEP_NORMAL ][ i ] = trap_S_RegisterSound( name, qfalse );
+
+    Com_sprintf( name, sizeof( name ), "sound/player/footsteps/flesh%i.wav", i + 1 );
+    cgs.media.footsteps[ FOOTSTEP_FLESH ][ i ] = trap_S_RegisterSound( name, qfalse );
+
+    Com_sprintf( name, sizeof( name ), "sound/player/footsteps/splash%i.wav", i + 1 );
+    cgs.media.footsteps[ FOOTSTEP_SPLASH ][ i ] = trap_S_RegisterSound( name, qfalse );
+
+    Com_sprintf( name, sizeof( name ), "sound/player/footsteps/clank%i.wav", i + 1 );
+    cgs.media.footsteps[ FOOTSTEP_METAL ][ i ] = trap_S_RegisterSound( name, qfalse );
+  }
+
+  for( i = 1 ; i < MAX_SOUNDS ; i++ )
+  {
+    soundName = CG_ConfigString( CS_SOUNDS + i );
+
+    if( !soundName[ 0 ] )
+      break;
+
+    if( soundName[ 0 ] == '*' )
+      continue; // custom sound
+
+    cgs.gameSounds[ i ] = trap_S_RegisterSound( soundName, qfalse );
+  }
+
+  cgs.media.jetpackDescendSound     = trap_S_RegisterSound( "sound/upgrades/jetpack/low.wav", qfalse );
+  cgs.media.jetpackIdleSound        = trap_S_RegisterSound( "sound/upgrades/jetpack/idle.wav", qfalse );
+  cgs.media.jetpackAscendSound      = trap_S_RegisterSound( "sound/upgrades/jetpack/hi.wav", qfalse );
+
+  cgs.media.medkitUseSound          = trap_S_RegisterSound( "sound/upgrades/medkit/medkit.wav", qfalse );
+  
+  cgs.media.forcefieldSound          = trap_S_RegisterSound( "sound/buildables/light/emp.wav", qfalse );
+  
+  cgs.media.alienEvolveSound        = trap_S_RegisterSound( "sound/player/alienevolve.wav", qfalse );
+
+  cgs.media.alienBuildableExplosion = trap_S_RegisterSound( "sound/buildables/alien/explosion.wav", qfalse );
+  cgs.media.alienBuildableDamage    = trap_S_RegisterSound( "sound/buildables/alien/damage.wav", qfalse );
+  cgs.media.alienBuildablePrebuild  = trap_S_RegisterSound( "sound/buildables/alien/prebuild.wav", qfalse );
+
+  cgs.media.humanBuildableExplosion = trap_S_RegisterSound( "sound/buildables/human/explosion.wav", qfalse );
+  cgs.media.humanBuildablePrebuild  = trap_S_RegisterSound( "sound/buildables/human/prebuild.wav", qfalse );
+
+  for( i = 0; i < 4; i++ )
+    cgs.media.humanBuildableDamage[ i ] = trap_S_RegisterSound(
+        va( "sound/buildables/human/damage%d.wav", i ), qfalse );
+
+  cgs.media.hardBounceSound1        = trap_S_RegisterSound( "sound/misc/hard_bounce1.wav", qfalse );
+  cgs.media.hardBounceSound2        = trap_S_RegisterSound( "sound/misc/hard_bounce2.wav", qfalse );
+  
+  cgs.media.mineBounceSound1        = trap_S_RegisterSound( "sound/misc/mine_bounce1.wav", qfalse );
+  
+  cgs.media.airpounce               = trap_S_RegisterSound( "models/weapons/level5/airpounce.wav", qfalse );
+  
+  cgs.media.acidBombBounceSound1    = trap_S_RegisterSound( "sound/misc/abomb_bounce1.wav", qfalse );
+  cgs.media.acidBombBounceSound2    = trap_S_RegisterSound( "sound/misc/abomb_bounce2.wav", qfalse );
+
+  cgs.media.repeaterUseSound        = trap_S_RegisterSound( "sound/buildables/repeater/use.wav", qfalse );
+
+  cgs.media.buildableRepairSound    = trap_S_RegisterSound( "sound/buildables/human/repair.wav", qfalse );
+  cgs.media.buildableRepairedSound  = trap_S_RegisterSound( "sound/buildables/human/repaired.wav", qfalse );
+
+  cgs.media.lCannonWarningSound     = trap_S_RegisterSound( "models/weapons/lcannon/warning.wav", qfalse );
+  cgs.media.lCannonWarningSound2    = trap_S_RegisterSound( "models/weapons/lcannon/warning2.wav", qfalse );
+  
+  cgs.media.FlamerWarningSound     = trap_S_RegisterSound( "models/weapons/flamer/warning.wav", qfalse );
+  cgs.media.FlamerWarningSound2    = trap_S_RegisterSound( "models/weapons/flamer/warning2.wav", qfalse );
+}
+
+
+//===================================================================================
+
+
+/*
+=================
+CG_RegisterGraphics
+
+This function may execute for a couple of minutes with a slow disk.
+=================
+*/
+static void CG_RegisterGraphics( void )
+{
+  int         i;
+  static char *sb_nums[ 11 ] =
+  {
+    "gfx/2d/numbers/zero_32b",
+    "gfx/2d/numbers/one_32b",
+    "gfx/2d/numbers/two_32b",
+    "gfx/2d/numbers/three_32b",
+    "gfx/2d/numbers/four_32b",
+    "gfx/2d/numbers/five_32b",
+    "gfx/2d/numbers/six_32b",
+    "gfx/2d/numbers/seven_32b",
+    "gfx/2d/numbers/eight_32b",
+    "gfx/2d/numbers/nine_32b",
+    "gfx/2d/numbers/minus_32b",
+  };
+  static char *buildWeaponTimerPieShaders[ 8 ] =
+  {
+    "ui/assets/neutral/1_5pie",
+    "ui/assets/neutral/3_0pie",
+    "ui/assets/neutral/4_5pie",
+    "ui/assets/neutral/6_0pie",
+    "ui/assets/neutral/7_5pie",
+    "ui/assets/neutral/9_0pie",
+    "ui/assets/neutral/10_5pie",
+    "ui/assets/neutral/12_0pie",
+  };
+
+  // clear any references to old media
+  memset( &cg.refdef, 0, sizeof( cg.refdef ) );
+  trap_R_ClearScene( );
+
+  trap_R_LoadWorldMap( cgs.mapname );
+  CG_UpdateMediaFraction( 0.66f );
+
+  for( i = 0; i < 11; i++ )
+    cgs.media.numberShaders[ i ] = trap_R_RegisterShader( sb_nums[ i ] );
+
+  cgs.media.viewBloodShader           = trap_R_RegisterShader( "gfx/damage/fullscreen_painblend" );
+
+  cgs.media.connectionShader          = trap_R_RegisterShader( "gfx/2d/net" );
+
+  cgs.media.creepShader               = trap_R_RegisterShader( "creep" );
+
+  cgs.media.scannerBlipShader         = trap_R_RegisterShader( "gfx/2d/blip" );
+  cgs.media.scannerBlipShaderPlayer   = trap_R_RegisterShader( "gfx/2d/blipPlayer" );
+  cgs.media.scannerBlipShaderHealing   = trap_R_RegisterShader( "gfx/2d/blipHealing" );
+  cgs.media.scannerLineShader         = trap_R_RegisterShader( "gfx/2d/stalk" );
+
+  cgs.media.teamOverlayShader         = trap_R_RegisterShader( "ui/assets/teamoverlay" );
+  
+  cgs.media.tracerShader              = trap_R_RegisterShader( "gfx/misc/tracer" );
+
+  cgs.media.backTileShader            = trap_R_RegisterShader( "console" );
+
+  // cloak shaders
+  cgs.media.invisibleShader           = trap_R_RegisterShader( "gfx/invisible" );
+  cgs.media.invisibleShaderTeam       = trap_R_RegisterShader( "gfx/invisible_team" );
+  cgs.media.invisibleFadeShader       = trap_R_RegisterShader( "gfx/invisible_fade" );
+
+  // building shaders
+  cgs.media.greenBuildShader          = trap_R_RegisterShader("gfx/misc/greenbuild" );
+  cgs.media.redBuildShader            = trap_R_RegisterShader("gfx/misc/redbuild" );
+  cgs.media.humanSpawningShader       = trap_R_RegisterShader("models/buildables/telenode/rep_cyl" );
+
+  for( i = 0; i < 8; i++ )
+    cgs.media.buildWeaponTimerPie[ i ] = trap_R_RegisterShader( buildWeaponTimerPieShaders[ i ] );
+
+  // player health cross shaders
+  cgs.media.healthCross               = trap_R_RegisterShader( "ui/assets/neutral/cross.tga" );
+  cgs.media.healthCross2X             = trap_R_RegisterShader( "ui/assets/neutral/cross2.tga" );
+  cgs.media.healthCross3X             = trap_R_RegisterShader( "ui/assets/neutral/cross3.tga" );
+  cgs.media.healthCrossMedkit         = trap_R_RegisterShader( "ui/assets/neutral/cross_medkit.tga" );
+  cgs.media.healthCrossPoisoned       = trap_R_RegisterShader( "ui/assets/neutral/cross_poison.tga" );
+  
+  cgs.media.upgradeClassIconShader    = trap_R_RegisterShader( "icons/icona_upgrade.tga" );
+  
+
+  cgs.media.disconnectPS              = CG_RegisterParticleSystem( "disconnectPS" );
+
+  CG_UpdateMediaFraction( 0.7f );
+
+  memset( cg_weapons, 0, sizeof( cg_weapons ) );
+  memset( cg_upgrades, 0, sizeof( cg_upgrades ) );
+
+  cgs.media.shadowMarkShader          = trap_R_RegisterShader( "gfx/marks/shadow" );
+  cgs.media.wakeMarkShader            = trap_R_RegisterShader( "gfx/marks/wake" );
+
+  cgs.media.poisonCloudPS             = CG_RegisterParticleSystem( "firstPersonPoisonCloudPS" );
+  cgs.media.fireCloudPS             = CG_RegisterParticleSystem( "fireCloudPS" );
+  cgs.media.poisonCloudedPS           = CG_RegisterParticleSystem( "poisonCloudedPS" );
+  cgs.media.alienEvolvePS             = CG_RegisterParticleSystem( "alienEvolvePS" );
+  cgs.media.airpounceblast            = CG_RegisterParticleSystem( "airpounceblast" );
+  cgs.media.alienAcidTubePS           = CG_RegisterParticleSystem( "alienAcidTubePS" );
+  cgs.media.alienSlimePS           = CG_RegisterParticleSystem( "alienSlimePS" );
+
+  cgs.media.forcefieldPS           = CG_RegisterParticleSystem( "forcefieldPS" );
+ 
+  cgs.media.jetPackDescendPS          = CG_RegisterParticleSystem( "jetPackDescendPS" );
+  cgs.media.jetPackHoverPS            = CG_RegisterParticleSystem( "jetPackHoverPS" );
+  cgs.media.jetPackAscendPS           = CG_RegisterParticleSystem( "jetPackAscendPS" );
+
+  cgs.media.organicbulbPS = CG_RegisterParticleSystem( "organicbulbPS" );
+  cgs.media.humanBuildableDamagedPS   = CG_RegisterParticleSystem( "humanBuildableDamagedPS" );
+  cgs.media.alienBuildableDamagedPS   = CG_RegisterParticleSystem( "alienBuildableDamagedPS" );
+  cgs.media.humanBuildableDestroyedPS = CG_RegisterParticleSystem( "humanBuildableDestroyedPS" );
+  cgs.media.alienBuildableDestroyedPS = CG_RegisterParticleSystem( "alienBuildableDestroyedPS" );
+
+  cgs.media.humanBuildableBleedPS     = CG_RegisterParticleSystem( "humanBuildableBleedPS");  
+  cgs.media.alienBuildableBleedPS     = CG_RegisterParticleSystem( "alienBuildableBleedPS" );
+
+  cgs.media.alienBleedPS              = CG_RegisterParticleSystem( "alienBleedPS" );
+  cgs.media.humanBleedPS              = CG_RegisterParticleSystem( "humanBleedPS" );
+
+  cgs.media.alienSpiteful_AbcessDestroyedPS = CG_RegisterParticleSystem( "alienSpiteful_AbcessDestroyedPS" );
+
+
+  CG_BuildableStatusParse( "ui/assets/human/buildstat.cfg", &cgs.humanBuildStat );
+  CG_BuildableStatusParse( "ui/assets/alien/buildstat.cfg", &cgs.alienBuildStat );
+ 
+  // register the inline models
+  cgs.numInlineModels = trap_CM_NumInlineModels( );
+
+  for( i = 1; i < cgs.numInlineModels; i++ )
+  {
+    char    name[ 10 ];
+    vec3_t  mins, maxs;
+    int     j;
+
+    Com_sprintf( name, sizeof( name ), "*%i", i );
+
+    cgs.inlineDrawModel[ i ] = trap_R_RegisterModel( name );
+    trap_R_ModelBounds( cgs.inlineDrawModel[ i ], mins, maxs );
+
+    for( j = 0 ; j < 3 ; j++ )
+      cgs.inlineModelMidpoints[ i ][ j ] = mins[ j ] + 0.5 * ( maxs[ j ] - mins[ j ] );
+  }
+
+  // register all the server specified models
+  for( i = 1; i < MAX_MODELS; i++ )
+  {
+    const char *modelName;
+
+    modelName = CG_ConfigString( CS_MODELS + i );
+
+    if( !modelName[ 0 ] )
+      break;
+
+    cgs.gameModels[ i ] = trap_R_RegisterModel( modelName );
+  }
+
+  CG_UpdateMediaFraction( 0.8f );
+
+  // register all the server specified shaders
+  for( i = 1; i < MAX_GAME_SHADERS; i++ )
+  {
+    const char *shaderName;
+
+    shaderName = CG_ConfigString( CS_SHADERS + i );
+
+    if( !shaderName[ 0 ] )
+      break;
+
+    cgs.gameShaders[ i ] = trap_R_RegisterShader( shaderName );
+  }
+
+  CG_UpdateMediaFraction( 0.9f );
+
+  // register all the server specified particle systems
+  for( i = 1; i < MAX_GAME_PARTICLE_SYSTEMS; i++ )
+  {
+    const char *psName;
+
+    psName = CG_ConfigString( CS_PARTICLE_SYSTEMS + i );
+
+    if( !psName[ 0 ] )
+      break;
+
+    cgs.gameParticleSystems[ i ] = CG_RegisterParticleSystem( (char *)psName );
+  }
+}
+
+
+/*
+=======================
+CG_BuildSpectatorString
+
+=======================
+*/
+void CG_BuildSpectatorString( void )
+{
+  int i;
+
+  cg.spectatorList[ 0 ] = 0;
+
+  for( i = 0; i < MAX_CLIENTS; i++ )
+  {
+    if( cgs.clientinfo[ i ].infoValid && cgs.clientinfo[ i ].team == TEAM_NONE )
+    {
+      Q_strcat( cg.spectatorList, sizeof( cg.spectatorList ),
+          va( S_COLOR_WHITE "%s     ", cgs.clientinfo[ i ].name ) );
+    }
+  }
+}
+
+
+
+/*
+===================
+CG_RegisterClients
+
+===================
+*/
+static void CG_RegisterClients( void )
+{
+  int   i;
+
+  cg.charModelFraction = 0.0f;
+
+  //precache all the models/sounds/etc
+  for( i = PCL_NONE + 1; i < PCL_NUM_CLASSES;  i++ )
+  {
+    CG_PrecacheClientInfo( i, BG_ClassConfig( i )->modelName,
+                              BG_ClassConfig( i )->skinName );
+
+    cg.charModelFraction = (float)i / (float)PCL_NUM_CLASSES;
+    trap_UpdateScreen( );
+  }
+
+  cgs.media.larmourHeadSkin    = trap_R_RegisterSkin( "models/players/human_base/head_light.skin" );
+  cgs.media.larmourLegsSkin    = trap_R_RegisterSkin( "models/players/human_base/lower_light.skin" );
+  cgs.media.larmourTorsoSkin   = trap_R_RegisterSkin( "models/players/human_base/upper_light.skin" );
+
+  cgs.media.jetpackModel       = trap_R_RegisterModel( "models/players/human_base/jetpack.md3" );
+  cgs.media.jetpackFlashModel  = trap_R_RegisterModel( "models/players/human_base/jetpack_flash.md3" );
+  cgs.media.battpackModel      = trap_R_RegisterModel( "models/players/human_base/battpack.md3" );
+
+  cg.charModelFraction = 1.0f;
+  trap_UpdateScreen( );
+
+  //load all the clientinfos of clients already connected to the server
+  for( i = 0; i < MAX_CLIENTS; i++ )
+  {
+    const char  *clientInfo;
+
+    clientInfo = CG_ConfigString( CS_PLAYERS + i );
+    if( !clientInfo[ 0 ] )
+      continue;
+
+    CG_NewClientInfo( i );
+  }
+
+  CG_BuildSpectatorString( );
+}
+
+//===========================================================================
+
+/*
+=================
+CG_ConfigString
+=================
+*/
+const char *CG_ConfigString( int index )
+{
+  if( index < 0 || index >= MAX_CONFIGSTRINGS )
+    CG_Error( "CG_ConfigString: bad index: %i", index );
+
+  return cgs.gameState.stringData + cgs.gameState.stringOffsets[ index ];
+}
+
+//==================================================================
+
+/*
+======================
+CG_StartMusic
+
+======================
+*/
+void CG_StartMusic( void )
+{
+  char  *s;
+  char  parm1[ MAX_QPATH ], parm2[ MAX_QPATH ];
+
+  // start the background music
+  s = (char *)CG_ConfigString( CS_MUSIC );
+  Q_strncpyz( parm1, COM_Parse( &s ), sizeof( parm1 ) );
+  Q_strncpyz( parm2, COM_Parse( &s ), sizeof( parm2 ) );
+
+  trap_S_StartBackgroundTrack( parm1, parm2 );
+}
+
+/*
+======================
+CG_PlayerCount
+======================
+*/
+int CG_PlayerCount( void )
+{
+  int i, count = 0;
+
+  CG_RequestScores( );
+
+  for( i = 0; i < cg.numScores; i++ )
+  {
+    if( cg.scores[ i ].team == TEAM_ALIENS ||
+        cg.scores[ i ].team == TEAM_HUMANS )
+      count++;
+  }
+
+  return count;
+}
+
+//
+// ==============================
+// new hud stuff ( mission pack )
+// ==============================
+//
+char *CG_GetMenuBuffer( const char *filename )
+{
+  int           len;
+  fileHandle_t  f;
+  static char   buf[ MAX_MENUFILE ];
+
+  len = trap_FS_FOpenFile( filename, &f, FS_READ );
+
+  if( !f )
+  {
+    trap_Print( va( S_COLOR_RED "menu file not found: %s, using default\n", filename ) );
+    return NULL;
+  }
+
+  if( len >= MAX_MENUFILE )
+  {
+    trap_Print( va( S_COLOR_RED "menu file too large: %s is %i, max allowed is %i",
+                    filename, len, MAX_MENUFILE ) );
+    trap_FS_FCloseFile( f );
+    return NULL;
+  }
+
+  trap_FS_Read( buf, len, f );
+  buf[len] = 0;
+  trap_FS_FCloseFile( f );
+
+  return buf;
+}
+
+qboolean CG_Asset_Parse( int handle )
+{
+  pc_token_t token;
+  const char *tempStr;
+
+  if( !trap_Parse_ReadToken( handle, &token ) )
+    return qfalse;
+
+  if( Q_stricmp( token.string, "{" ) != 0 )
+    return qfalse;
+
+  while( 1 )
+  {
+    if( !trap_Parse_ReadToken( handle, &token ) )
+      return qfalse;
+
+    if( Q_stricmp( token.string, "}" ) == 0 )
+      return qtrue;
+
+    // font
+    if( Q_stricmp( token.string, "font" ) == 0 )
+    {
+      int pointSize;
+
+      if( !PC_String_Parse( handle, &tempStr ) || !PC_Int_Parse( handle, &pointSize ) )
+        return qfalse;
+
+      cgDC.registerFont( tempStr, pointSize, &cgDC.Assets.textFont );
+      continue;
+    }
+
+    // smallFont
+    if( Q_stricmp( token.string, "smallFont" ) == 0 )
+    {
+      int pointSize;
+
+      if( !PC_String_Parse( handle, &tempStr ) || !PC_Int_Parse( handle, &pointSize ) )
+        return qfalse;
+
+      cgDC.registerFont( tempStr, pointSize, &cgDC.Assets.smallFont );
+      continue;
+    }
+
+    // font
+    if( Q_stricmp( token.string, "bigfont" ) == 0 )
+    {
+      int pointSize;
+
+      if( !PC_String_Parse( handle, &tempStr ) || !PC_Int_Parse( handle, &pointSize ) )
+        return qfalse;
+
+      cgDC.registerFont( tempStr, pointSize, &cgDC.Assets.bigFont );
+      continue;
+    }
+
+    // gradientbar
+    if( Q_stricmp( token.string, "gradientbar" ) == 0 )
+    {
+      if( !PC_String_Parse( handle, &tempStr ) )
+        return qfalse;
+
+      cgDC.Assets.gradientBar = trap_R_RegisterShaderNoMip( tempStr );
+      continue;
+    }
+
+    // enterMenuSound
+    if( Q_stricmp( token.string, "menuEnterSound" ) == 0 )
+    {
+      if( !PC_String_Parse( handle, &tempStr ) )
+        return qfalse;
+
+      cgDC.Assets.menuEnterSound = trap_S_RegisterSound( tempStr, qfalse );
+      continue;
+    }
+
+    // exitMenuSound
+    if( Q_stricmp( token.string, "menuExitSound" ) == 0 )
+    {
+      if( !PC_String_Parse( handle, &tempStr ) )
+        return qfalse;
+
+      cgDC.Assets.menuExitSound = trap_S_RegisterSound( tempStr, qfalse );
+      continue;
+    }
+
+    // itemFocusSound
+    if( Q_stricmp( token.string, "itemFocusSound" ) == 0 )
+    {
+      if( !PC_String_Parse( handle, &tempStr ) )
+        return qfalse;
+
+      cgDC.Assets.itemFocusSound = trap_S_RegisterSound( tempStr, qfalse );
+      continue;
+    }
+
+    // menuBuzzSound
+    if( Q_stricmp( token.string, "menuBuzzSound" ) == 0 )
+    {
+      if( !PC_String_Parse( handle, &tempStr ) )
+        return qfalse;
+
+      cgDC.Assets.menuBuzzSound = trap_S_RegisterSound( tempStr, qfalse );
+      continue;
+    }
+
+    if( Q_stricmp( token.string, "cursor" ) == 0 )
+    {
+      if( !PC_String_Parse( handle, &cgDC.Assets.cursorStr ) )
+        return qfalse;
+
+      cgDC.Assets.cursor = trap_R_RegisterShaderNoMip( cgDC.Assets.cursorStr );
+      continue;
+    }
+
+    if( Q_stricmp( token.string, "fadeClamp" ) == 0 )
+    {
+      if( !PC_Float_Parse( handle, &cgDC.Assets.fadeClamp ) )
+        return qfalse;
+
+      continue;
+    }
+
+    if( Q_stricmp( token.string, "fadeCycle" ) == 0 )
+    {
+      if( !PC_Int_Parse( handle, &cgDC.Assets.fadeCycle ) )
+        return qfalse;
+
+      continue;
+    }
+
+    if( Q_stricmp( token.string, "fadeAmount" ) == 0 )
+    {
+      if( !PC_Float_Parse( handle, &cgDC.Assets.fadeAmount ) )
+        return qfalse;
+
+      continue;
+    }
+
+    if( Q_stricmp( token.string, "shadowX" ) == 0 )
+    {
+      if( !PC_Float_Parse( handle, &cgDC.Assets.shadowX ) )
+        return qfalse;
+
+      continue;
+    }
+
+    if( Q_stricmp( token.string, "shadowY" ) == 0 )
+    {
+      if( !PC_Float_Parse( handle, &cgDC.Assets.shadowY ) )
+        return qfalse;
+
+      continue;
+    }
+
+    if( Q_stricmp( token.string, "shadowColor" ) == 0 )
+    {
+      if( !PC_Color_Parse( handle, &cgDC.Assets.shadowColor ) )
+        return qfalse;
+
+      cgDC.Assets.shadowFadeClamp = cgDC.Assets.shadowColor[ 3 ];
+      continue;
+    }
+  }
+
+  return qfalse;
+}
+
+void CG_ParseMenu( const char *menuFile )
+{
+  pc_token_t  token;
+  int         handle;
+
+  handle = trap_Parse_LoadSource( menuFile );
+
+  if( !handle )
+    handle = trap_Parse_LoadSource( "ui/testhud.menu" );
+
+  if( !handle )
+    return;
+
+  while( 1 )
+  {
+    if( !trap_Parse_ReadToken( handle, &token ) )
+      break;
+
+    //if ( Q_stricmp( token, "{" ) ) {
+    //  Com_Printf( "Missing { in menu file\n" );
+    //  break;
+    //}
+
+    //if ( menuCount == MAX_MENUS ) {
+    //  Com_Printf( "Too many menus!\n" );
+    //  break;
+    //}
+
+    if( token.string[ 0 ] == '}' )
+      break;
+
+    if( Q_stricmp( token.string, "assetGlobalDef" ) == 0 )
+    {
+      if( CG_Asset_Parse( handle ) )
+        continue;
+      else
+        break;
+    }
+
+
+    if( Q_stricmp( token.string, "menudef" ) == 0 )
+    {
+      // start a new menu
+      Menu_New( handle );
+    }
+  }
+
+  trap_Parse_FreeSource( handle );
+}
+
+qboolean CG_Load_Menu( char **p )
+{
+  char *token;
+
+  token = COM_ParseExt( p, qtrue );
+
+  if( token[ 0 ] != '{' )
+    return qfalse;
+
+  while( 1 )
+  {
+    token = COM_ParseExt( p, qtrue );
+
+    if( Q_stricmp( token, "}" ) == 0 )
+      return qtrue;
+
+    if( !token || token[ 0 ] == 0 )
+      return qfalse;
+
+    CG_ParseMenu( token );
+  }
+  return qfalse;
+}
+
+
+
+void CG_LoadMenus( const char *menuFile )
+{
+  char          *token;
+  char          *p;
+  int           len, start;
+  fileHandle_t  f;
+  static char   buf[ MAX_MENUDEFFILE ];
+
+  start = trap_Milliseconds( );
+
+  len = trap_FS_FOpenFile( menuFile, &f, FS_READ );
+
+  if( !f )
+  {
+    trap_Error( va( S_COLOR_YELLOW "menu file not found: %s, using default\n", menuFile ) );
+    len = trap_FS_FOpenFile( "ui/hud.txt", &f, FS_READ );
+
+    if( !f )
+      trap_Error( va( S_COLOR_RED "default menu file not found: ui/hud.txt, unable to continue!\n" ) );
+  }
+
+  if( len >= MAX_MENUDEFFILE )
+  {
+    trap_Error( va( S_COLOR_RED "menu file too large: %s is %i, max allowed is %i",
+                menuFile, len, MAX_MENUDEFFILE ) );
+    trap_FS_FCloseFile( f );
+    return;
+  }
+
+  trap_FS_Read( buf, len, f );
+  buf[ len ] = 0;
+  trap_FS_FCloseFile( f );
+
+  COM_Compress( buf );
+
+  Menu_Reset( );
+
+  p = buf;
+
+  while( 1 )
+  {
+    token = COM_ParseExt( &p, qtrue );
+
+    if( !token || token[ 0 ] == 0 || token[ 0 ] == '}' )
+      break;
+
+    if( Q_stricmp( token, "}" ) == 0 )
+      break;
+
+    if( Q_stricmp( token, "loadmenu" ) == 0 )
+    {
+      if( CG_Load_Menu( &p ) )
+        continue;
+      else
+        break;
+    }
+  }
+
+  Com_Printf( "UI menu load time = %d milli seconds\n", trap_Milliseconds( ) - start );
+}
+
+
+
+static qboolean CG_OwnerDrawHandleKey( int ownerDraw, int key )
+{
+  return qfalse;
+}
+
+
+static int CG_FeederCount( int feederID )
+{
+  int i, count = 0;
+
+  if( feederID == FEEDER_ALIENTEAM_LIST )
+  {
+    for( i = 0; i < cg.numScores; i++ )
+    {
+      if( cg.scores[ i ].team == TEAM_ALIENS )
+        count++;
+    }
+  }
+  else if( feederID == FEEDER_HUMANTEAM_LIST )
+  {
+    for( i = 0; i < cg.numScores; i++ )
+    {
+      if( cg.scores[ i ].team == TEAM_HUMANS )
+        count++;
+    }
+  }
+
+  return count;
+}
+
+
+void CG_SetScoreSelection( void *p )
+{
+  menuDef_t     *menu = (menuDef_t*)p;
+  playerState_t *ps = &cg.snap->ps;
+  int           i, alien, human;
+  int           feeder;
+
+  alien = human = 0;
+
+  for( i = 0; i < cg.numScores; i++ )
+  {
+    if( cg.scores[ i ].team == TEAM_ALIENS )
+      alien++;
+    else if( cg.scores[ i ].team == TEAM_HUMANS )
+      human++;
+
+    if( ps->clientNum == cg.scores[ i ].client )
+      cg.selectedScore = i;
+  }
+
+  if( menu == NULL )
+    // just interested in setting the selected score
+    return;
+
+  feeder = FEEDER_ALIENTEAM_LIST;
+  i = alien;
+
+  if( cg.scores[ cg.selectedScore ].team == TEAM_HUMANS )
+  {
+    feeder = FEEDER_HUMANTEAM_LIST;
+    i = human;
+  }
+
+  Menu_SetFeederSelection(menu, feeder, i, NULL);
+}
+
+// FIXME: might need to cache this info
+static clientInfo_t * CG_InfoFromScoreIndex( int index, int team, int *scoreIndex )
+{
+  int i, count;
+  count = 0;
+
+  for( i = 0; i < cg.numScores; i++ )
+  {
+    if( cg.scores[ i ].team == team )
+    {
+      if( count == index )
+      {
+        *scoreIndex = i;
+        return &cgs.clientinfo[ cg.scores[ i ].client ];
+      }
+      count++;
+    }
+  }
+
+  *scoreIndex = index;
+  return &cgs.clientinfo[ cg.scores[ index ].client ];
+}
+
+qboolean CG_ClientIsReady( int clientNum )
+{
+  clientList_t ready;
+
+  Com_ClientListParse( &ready, CG_ConfigString( CS_CLIENTS_READY ) );
+
+  return Com_ClientListContains( &ready, clientNum );
+}
+
+static const char *CG_FeederItemText( int feederID, int index, int column, qhandle_t *handle )
+{
+  int           scoreIndex = 0;
+  clientInfo_t  *info = NULL;
+  int           team = -1;
+  score_t       *sp = NULL;
+  qboolean      showIcons = qfalse;
+
+  *handle = -1;
+
+  if( feederID == FEEDER_ALIENTEAM_LIST )
+    team = TEAM_ALIENS;
+  else if( feederID == FEEDER_HUMANTEAM_LIST )
+    team = TEAM_HUMANS;
+
+  info = CG_InfoFromScoreIndex( index, team, &scoreIndex );
+  sp = &cg.scores[ scoreIndex ];
+
+  if( cg.intermissionStarted && CG_ClientIsReady( sp->client ) )
+    showIcons = qfalse;
+  else if( cg.snap->ps.pm_type == PM_SPECTATOR ||
+           cg.snap->ps.pm_type == PM_NOCLIP ||
+           cg.snap->ps.pm_flags & PMF_FOLLOW ||
+           team == cg.snap->ps.stats[ STAT_TEAM ] ||
+           cg.intermissionStarted )
+  {
+    showIcons = qtrue;
+  }
+
+  if( info && info->infoValid )
+  {
+    switch( column )
+    {
+      case 0:
+        if( showIcons )
+        {
+          if( sp->weapon != WP_NONE )
+            *handle = cg_weapons[ sp->weapon ].weaponIcon;
+        }
+        break;
+
+      case 1:
+        if( showIcons )
+        {
+          if( sp->team == TEAM_HUMANS && sp->upgrade != UP_NONE )
+            *handle = cg_upgrades[ sp->upgrade ].upgradeIcon;
+          else if( sp->team == TEAM_ALIENS )
+          {
+            switch( sp->weapon )
+            {
+              case WP_ABUILD2:
+              case WP_ALEVEL0_UPG:
+              case WP_ALEVEL1_UPG:
+              case WP_ALEVEL2_UPG:
+              case WP_ALEVEL3_UPG:
+                *handle = cgs.media.upgradeClassIconShader;
+                break;
+
+              default:
+                break;
+            }
+          }
+        }
+        break;
+
+      case 2:
+        if( cg.intermissionStarted && CG_ClientIsReady( sp->client ) )
+          return "Ready";
+        break;
+
+      case 3:
+        return va( S_COLOR_WHITE "%s", info->name );
+        break;
+
+      case 4:
+        return va( "%d", sp->score );
+        break;
+
+      case 5:
+        return va( "%4d", sp->time );
+        break;
+
+      case 6:
+        if( sp->ping == -1 )
+          return "";
+
+        return va( "%4d", sp->ping );
+        break;
+    }
+  }
+
+  return "";
+}
+
+static qhandle_t CG_FeederItemImage( int feederID, int index )
+{
+  return 0;
+}
+
+static void CG_FeederSelection( int feederID, int index )
+{
+  int i, count;
+  int team = ( feederID == FEEDER_ALIENTEAM_LIST ) ? TEAM_ALIENS : TEAM_HUMANS;
+  count = 0;
+
+  for( i = 0; i < cg.numScores; i++ )
+  {
+    if( cg.scores[ i ].team == team )
+    {
+      if( index == count )
+        cg.selectedScore = i;
+
+      count++;
+    }
+  }
+}
+
+static float CG_Cvar_Get( const char *cvar )
+{
+  char buff[ 128 ];
+
+  memset( buff, 0, sizeof( buff ) );
+  trap_Cvar_VariableStringBuffer( cvar, buff, sizeof( buff ) );
+  return atof( buff );
+}
+
+void CG_Text_PaintWithCursor( float x, float y, float scale, vec4_t color, const char *text,
+                              int cursorPos, char cursor, int limit, int style )
+{
+  UI_Text_Paint( x, y, scale, color, text, 0, limit, style );
+}
+
+static int CG_OwnerDrawWidth( int ownerDraw, float scale )
+{
+  switch( ownerDraw )
+  {
+    case CG_KILLER:
+      return UI_Text_Width( CG_GetKillerText( ), scale );
+      break;
+  }
+
+  return 0;
+}
+
+static int CG_PlayCinematic( const char *name, float x, float y, float w, float h )
+{
+  return trap_CIN_PlayCinematic( name, x, y, w, h, CIN_loop );
+}
+
+static void CG_StopCinematic( int handle )
+{
+  trap_CIN_StopCinematic( handle );
+}
+
+static void CG_DrawCinematic( int handle, float x, float y, float w, float h )
+{
+  trap_CIN_SetExtents( handle, x, y, w, h );
+  trap_CIN_DrawCinematic( handle );
+}
+
+static void CG_RunCinematicFrame( int handle )
+{
+  trap_CIN_RunCinematic( handle );
+}
+
+// hack to prevent warning
+static qboolean CG_OwnerDrawVisible( int parameter )
+{
+  return qfalse;
+}
+
+/*
+=================
+CG_LoadHudMenu
+=================
+*/
+void CG_LoadHudMenu( void )
+{
+  char        buff[ 1024 ];
+  const char  *hudSet;
+
+  cgDC.aspectScale = ( ( 640.0f * cgs.glconfig.vidHeight ) /
+                       ( 480.0f * cgs.glconfig.vidWidth ) );
+  cgDC.xscale = cgs.glconfig.vidWidth / 640.0f;
+  cgDC.yscale = cgs.glconfig.vidHeight / 480.0f;
+
+  cgDC.smallFontScale = CG_Cvar_Get( "ui_smallFont" );
+  cgDC.bigFontScale = CG_Cvar_Get( "ui_bigFont" );
+
+  cgDC.registerShaderNoMip  = &trap_R_RegisterShaderNoMip;
+  cgDC.setColor             = &trap_R_SetColor;
+  cgDC.drawHandlePic        = &CG_DrawPic;
+  cgDC.drawStretchPic       = &trap_R_DrawStretchPic;
+  cgDC.registerModel        = &trap_R_RegisterModel;
+  cgDC.modelBounds          = &trap_R_ModelBounds;
+  cgDC.fillRect             = &CG_FillRect;
+  cgDC.drawRect             = &CG_DrawRect;
+  cgDC.drawSides            = &CG_DrawSides;
+  cgDC.drawTopBottom        = &CG_DrawTopBottom;
+  cgDC.clearScene           = &trap_R_ClearScene;
+  cgDC.addRefEntityToScene  = &trap_R_AddRefEntityToScene;
+  cgDC.renderScene          = &trap_R_RenderScene;
+  cgDC.registerFont         = &trap_R_RegisterFont;
+  cgDC.ownerDrawItem        = &CG_OwnerDraw;
+  cgDC.getValue             = &CG_GetValue;
+  cgDC.ownerDrawVisible     = &CG_OwnerDrawVisible;
+  cgDC.runScript            = &CG_RunMenuScript;
+  cgDC.setCVar              = trap_Cvar_Set;
+  cgDC.getCVarString        = trap_Cvar_VariableStringBuffer;
+  cgDC.getCVarValue         = CG_Cvar_Get;
+  cgDC.setOverstrikeMode    = &trap_Key_SetOverstrikeMode;
+  cgDC.getOverstrikeMode    = &trap_Key_GetOverstrikeMode;
+  cgDC.startLocalSound      = &trap_S_StartLocalSound;
+  cgDC.ownerDrawHandleKey   = &CG_OwnerDrawHandleKey;
+  cgDC.feederCount          = &CG_FeederCount;
+  cgDC.feederItemImage      = &CG_FeederItemImage;
+  cgDC.feederItemText       = &CG_FeederItemText;
+  cgDC.feederSelection      = &CG_FeederSelection;
+  //cgDC.setBinding           = &trap_Key_SetBinding;
+  //cgDC.getBindingBuf        = &trap_Key_GetBindingBuf;
+  //cgDC.keynumToStringBuf    = &trap_Key_KeynumToStringBuf;
+  //cgDC.executeText          = &trap_Cmd_ExecuteText;
+  cgDC.Error                = &Com_Error;
+  cgDC.Print                = &Com_Printf;
+  cgDC.ownerDrawWidth       = &CG_OwnerDrawWidth;
+  //cgDC.ownerDrawText        = &CG_OwnerDrawText;
+  //cgDC.Pause                = &CG_Pause;
+  cgDC.registerSound        = &trap_S_RegisterSound;
+  cgDC.startBackgroundTrack = &trap_S_StartBackgroundTrack;
+  cgDC.stopBackgroundTrack  = &trap_S_StopBackgroundTrack;
+  cgDC.playCinematic        = &CG_PlayCinematic;
+  cgDC.stopCinematic        = &CG_StopCinematic;
+  cgDC.drawCinematic        = &CG_DrawCinematic;
+  cgDC.runCinematicFrame    = &CG_RunCinematicFrame;
+
+  Init_Display( &cgDC );
+
+  Menu_Reset( );
+
+  trap_Cvar_VariableStringBuffer( "cg_hudFiles", buff, sizeof( buff ) );
+  hudSet = buff;
+
+  if( !cg_hudFilesEnable.integer || hudSet[ 0 ] == '\0' )
+    hudSet = "ui/hud.txt";
+
+  CG_LoadMenus( hudSet );
+}
+
+void CG_AssetCache( void )
+{
+  int i;
+
+  cgDC.Assets.gradientBar         = trap_R_RegisterShaderNoMip( ASSET_GRADIENTBAR );
+  cgDC.Assets.scrollBar           = trap_R_RegisterShaderNoMip( ASSET_SCROLLBAR );
+  cgDC.Assets.scrollBarArrowDown  = trap_R_RegisterShaderNoMip( ASSET_SCROLLBAR_ARROWDOWN );
+  cgDC.Assets.scrollBarArrowUp    = trap_R_RegisterShaderNoMip( ASSET_SCROLLBAR_ARROWUP );
+  cgDC.Assets.scrollBarArrowLeft  = trap_R_RegisterShaderNoMip( ASSET_SCROLLBAR_ARROWLEFT );
+  cgDC.Assets.scrollBarArrowRight = trap_R_RegisterShaderNoMip( ASSET_SCROLLBAR_ARROWRIGHT );
+  cgDC.Assets.scrollBarThumb      = trap_R_RegisterShaderNoMip( ASSET_SCROLL_THUMB );
+  cgDC.Assets.sliderBar           = trap_R_RegisterShaderNoMip( ASSET_SLIDER_BAR );
+  cgDC.Assets.sliderThumb         = trap_R_RegisterShaderNoMip( ASSET_SLIDER_THUMB );
+
+  if( cg_emoticons.integer )
+  {
+    cgDC.Assets.emoticonCount = BG_LoadEmoticons( cgDC.Assets.emoticons,
+      MAX_EMOTICONS );
+  }
+  else
+    cgDC.Assets.emoticonCount = 0;
+
+  for( i = 0; i < cgDC.Assets.emoticonCount; i++ )
+  {
+    cgDC.Assets.emoticons[ i ].shader = trap_R_RegisterShaderNoMip(
+      va( "emoticons/%s_%dx1.tga", cgDC.Assets.emoticons[ i ].name,
+          cgDC.Assets.emoticons[ i ].width ) );
+  }
+}
+
+/*
+=================
+CG_Init
+
+Called after every level change or subsystem restart
+Will perform callbacks to make the loading info screen update.
+=================
+*/
+void CG_Init( int serverMessageNum, int serverCommandSequence, int clientNum )
+{
+  const char  *s;
+
+  // clear everything
+  memset( &cgs, 0, sizeof( cgs ) );
+  memset( &cg, 0, sizeof( cg ) );
+  memset( &cg.pmext, 0, sizeof( cg.pmext ) );
+  memset( cg_entities, 0, sizeof( cg_entities ) );
+
+  cg.clientNum = clientNum;
+
+  cgs.processedSnapshotNum = serverMessageNum;
+  cgs.serverCommandSequence = serverCommandSequence;
+
+  // get the rendering configuration from the client system
+  trap_GetGlconfig( &cgs.glconfig );
+  cgs.screenXScale = cgs.glconfig.vidWidth / 640.0f;
+  cgs.screenYScale = cgs.glconfig.vidHeight / 480.0f;
+
+  // load a few needed things before we do any screen updates
+  cgs.media.whiteShader     = trap_R_RegisterShader( "white" );
+  cgs.media.charsetShader   = trap_R_RegisterShader( "gfx/2d/bigchars" );
+  cgs.media.outlineShader   = trap_R_RegisterShader( "outline" );
+
+  // load overrides
+  BG_InitClassConfigs( );
+  BG_InitBuildableConfigs( );
+  BG_InitAllowedGameElements( );
+
+  // Dynamic memory
+  BG_InitMemory( );
+
+  CG_RegisterCvars( );
+
+  CG_InitConsoleCommands( );
+
+  String_Init( );
+
+  CG_AssetCache( );
+  CG_LoadHudMenu( );
+
+  cg.weaponSelect = WP_NONE;
+
+  // old servers
+
+  // get the gamestate from the client system
+  trap_GetGameState( &cgs.gameState );
+
+  // copy vote display strings so they don't show up blank if we see
+  // the same one directly after connecting
+  Q_strncpyz( cgs.voteString[ TEAM_NONE ],
+      CG_ConfigString( CS_VOTE_STRING + TEAM_NONE ),
+      sizeof( cgs.voteString ) );
+  Q_strncpyz( cgs.voteString[ TEAM_ALIENS ],
+      CG_ConfigString( CS_VOTE_STRING + TEAM_ALIENS ),
+      sizeof( cgs.voteString[ TEAM_ALIENS ] ) );
+  Q_strncpyz( cgs.voteString[ TEAM_HUMANS ],
+      CG_ConfigString( CS_VOTE_STRING + TEAM_ALIENS ),
+      sizeof( cgs.voteString[ TEAM_HUMANS ] ) );
+
+  // check version
+  s = CG_ConfigString( CS_GAME_VERSION );
+
+  if( strcmp( s, GAME_VERSION ) )
+    CG_Error( "Client/Server game mismatch: %s/%s", GAME_VERSION, s );
+
+  s = CG_ConfigString( CS_LEVEL_START_TIME );
+  cgs.levelStartTime = atoi( s );
+
+  CG_ParseServerinfo( );
+
+  // load the new map
+  trap_CM_LoadMap( cgs.mapname );
+
+  cg.loading = qtrue;   // force players to load instead of defer
+
+  CG_LoadTrailSystems( );
+  CG_UpdateMediaFraction( 0.05f );
+
+  CG_LoadParticleSystems( );
+  CG_UpdateMediaFraction( 0.05f );
+
+  CG_RegisterSounds( );
+  CG_UpdateMediaFraction( 0.60f );
+
+  CG_RegisterGraphics( );
+  CG_UpdateMediaFraction( 0.90f );
+
+  CG_InitWeapons( );
+  CG_UpdateMediaFraction( 0.95f );
+
+  CG_InitUpgrades( );
+  CG_UpdateMediaFraction( 1.0f );
+
+  CG_InitBuildables( );
+ 
+  cgs.voices = BG_VoiceInit( );
+  BG_PrintVoices( cgs.voices, cg_debugVoices.integer );
+
+  CG_RegisterClients( );   // if low on memory, some clients will be deferred
+
+  cg.loading = qfalse;  // future players will be deferred
+
+  CG_InitMarkPolys( );
+
+  // remove the last loading update
+  cg.infoScreenText[ 0 ] = 0;
+
+  // Make sure we have update values (scores)
+  CG_SetConfigValues( );
+
+  CG_StartMusic( );
+
+  CG_ShaderStateChanged( );
+
+  trap_S_ClearLoopingSounds( qtrue );
+}
+
+/*
+=================
+CG_Shutdown
+
+Called before every level change or subsystem restart
+=================
+*/
+void CG_Shutdown( void )
+{
+  // some mods may need to do cleanup work here,
+  // like closing files or archiving session data
+}
+
+/*
+================
+CG_VoIPString
+================
+*/
+static char *CG_VoIPString( void )
+{
+  // a generous overestimate of the space needed for 0,1,2...61,62,63
+  static char voipString[ MAX_CLIENTS * 4 ];
+  char voipSendTarget[ MAX_CVAR_VALUE_STRING ];
+
+  trap_Cvar_VariableStringBuffer( "cl_voipSendTarget", voipSendTarget,
+                                  sizeof( voipSendTarget ) );
+
+  if( Q_stricmp( voipSendTarget, "team" ) == 0 )
+  {
+    int i, slen, nlen;
+    for( slen = i = 0; i < cgs.maxclients; i++ )
+    {
+      if( !cgs.clientinfo[ i ].infoValid || i == cg.clientNum )
+        continue;
+      if( cgs.clientinfo[ i ].team != cgs.clientinfo[ cg.clientNum ].team )
+        continue;
+
+      nlen = Q_snprintf( &voipString[ slen ], sizeof( voipString ) - slen,
+                         "%s%d", ( slen > 0 ) ? "," : "", i );
+      if( slen + nlen + 1 >= sizeof( voipString ) )
+      {
+        CG_Printf( S_COLOR_YELLOW "WARNING: voipString overflowed\n" );
+        break;
+      }
+
+      slen += nlen;
+    }
+
+    // Notice that if the snprintf was truncated, slen was not updated
+    // so this will remove any trailing commas or partially-completed numbers
+    voipString[ slen ] = '\0';
+  }
+  else if( Q_stricmp( voipSendTarget, "crosshair" ) == 0 )
+    Com_sprintf( voipString, sizeof( voipString ), "%d",
+                 CG_CrosshairPlayer( ) );
+  else if( Q_stricmp( voipSendTarget, "attacker" ) == 0 )
+    Com_sprintf( voipString, sizeof( voipString ), "%d",
+                 CG_LastAttacker( ) );
+  else
+    return NULL;
+
+  return voipString;
+}
+
diff --git a/src/cgame/cg_marks.c b/src/cgame/cg_marks.c
new file mode 100644
index 0000000..953eb80
--- /dev/null
+++ b/src/cgame/cg_marks.c
@@ -0,0 +1,287 @@
+/*
+===========================================================================
+Copyright (C) 1999-2005 Id Software, Inc.
+Copyright (C) 2000-2009 Darklegion Development
+
+This file is part of Tremulous.
+
+Tremulous 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.
+
+Tremulous 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 Tremulous; if not, write to the Free Software
+Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
+===========================================================================
+*/
+
+// cg_marks.c -- wall marks
+
+
+#include "cg_local.h"
+
+/*
+===================================================================
+
+MARK POLYS
+
+===================================================================
+*/
+
+
+markPoly_t  cg_activeMarkPolys;     // double linked list
+markPoly_t  *cg_freeMarkPolys;      // single linked list
+markPoly_t  cg_markPolys[ MAX_MARK_POLYS ];
+static int  markTotal;
+
+/*
+===================
+CG_InitMarkPolys
+
+This is called at startup and for tournement restarts
+===================
+*/
+void CG_InitMarkPolys( void )
+{
+  int   i;
+
+  memset( cg_markPolys, 0, sizeof( cg_markPolys ) );
+
+  cg_activeMarkPolys.nextMark = &cg_activeMarkPolys;
+  cg_activeMarkPolys.prevMark = &cg_activeMarkPolys;
+  cg_freeMarkPolys = cg_markPolys;
+
+  for( i = 0; i < MAX_MARK_POLYS - 1; i++ )
+    cg_markPolys[ i ].nextMark = &cg_markPolys[ i + 1 ];
+}
+
+
+/*
+==================
+CG_FreeMarkPoly
+==================
+*/
+void CG_FreeMarkPoly( markPoly_t *le )
+{
+  if( !le->prevMark )
+    CG_Error( "CG_FreeLocalEntity: not active" );
+
+  // remove from the doubly linked active list
+  le->prevMark->nextMark = le->nextMark;
+  le->nextMark->prevMark = le->prevMark;
+
+  // the free list is only singly linked
+  le->nextMark = cg_freeMarkPolys;
+  cg_freeMarkPolys = le;
+}
+
+/*
+===================
+CG_AllocMark
+
+Will allways succeed, even if it requires freeing an old active mark
+===================
+*/
+markPoly_t *CG_AllocMark( void )
+{
+  markPoly_t  *le;
+  int         time;
+
+  if( !cg_freeMarkPolys )
+  {
+    // no free entities, so free the one at the end of the chain
+    // remove the oldest active entity
+    time = cg_activeMarkPolys.prevMark->time;
+
+    while( cg_activeMarkPolys.prevMark && time == cg_activeMarkPolys.prevMark->time )
+      CG_FreeMarkPoly( cg_activeMarkPolys.prevMark );
+  }
+
+  le = cg_freeMarkPolys;
+  cg_freeMarkPolys = cg_freeMarkPolys->nextMark;
+
+  memset( le, 0, sizeof( *le ) );
+
+  // link into the active list
+  le->nextMark = cg_activeMarkPolys.nextMark;
+  le->prevMark = &cg_activeMarkPolys;
+  cg_activeMarkPolys.nextMark->prevMark = le;
+  cg_activeMarkPolys.nextMark = le;
+  return le;
+}
+
+/*
+=================
+CG_ImpactMark
+
+origin should be a point within a unit of the plane
+dir should be the plane normal
+
+temporary marks will not be stored or randomly oriented, but immediately
+passed to the renderer.
+=================
+*/
+#define MAX_MARK_FRAGMENTS  700
+#define MAX_MARK_POINTS   400
+#define MAX_VERTS_ON_POLY2 64
+
+void CG_ImpactMark( qhandle_t markShader, const vec3_t origin, const vec3_t dir,
+                    float orientation, float red, float green, float blue, float alpha,
+                    qboolean alphaFade, float radius, qboolean temporary )
+{
+  vec3_t          axis[ 3 ];
+  float           texCoordScale;
+  vec3_t          originalPoints[ 4 ];
+  byte            colors[ 4 ];
+  int             i, j;
+  int             numFragments;
+  markFragment_t  markFragments[ MAX_MARK_FRAGMENTS ], *mf;
+  vec3_t          markPoints[ MAX_MARK_POINTS ];
+  vec3_t          projection;
+
+  
+  if( !cg_addMarks.integer )
+    return;
+
+  if( radius <= 0 )
+    CG_Error( "CG_ImpactMark called with <= 0 radius" );
+
+  // create the texture axis
+  VectorNormalize2( dir, axis[ 0 ] );
+  PerpendicularVector( axis[ 1 ], axis[ 0 ] );
+  RotatePointAroundVector( axis[ 2 ], axis[ 0 ], axis[ 1 ], orientation );
+  CrossProduct( axis[ 0 ], axis[ 2 ], axis[ 1 ] );
+  texCoordScale = 0.5 * 1.0 / radius;
+
+//
+        /* make rotated polygon axis */
+
+//
+  // create the full polygon
+  for( i = 0; i < 3; i++ )
+  {
+    originalPoints[ 0 ][ i ] = origin[ i ] - radius * axis[ 1 ][ i ] - radius * axis[ 2 ][ i ];
+    originalPoints[ 1 ][ i ] = origin[ i ] + radius * axis[ 1 ][ i ] - radius * axis[ 2 ][ i ];
+    originalPoints[ 2 ][ i ] = origin[ i ] + radius * axis[ 1 ][ i ] + radius * axis[ 2 ][ i ];
+    originalPoints[ 3 ][ i ] = origin[ i ] - radius * axis[ 1 ][ i ] + radius * axis[ 2 ][ i ];
+  
+  }
+   // get the fragments
+  VectorScale( dir, -20, projection );
+  numFragments = trap_CM_MarkFragments( 4, (void *)originalPoints,
+          projection, MAX_MARK_POINTS, markPoints[ 0 ],
+          MAX_MARK_FRAGMENTS, markFragments );
+
+  colors[ 0 ] = red * 255;
+  colors[ 1 ] = green * 255;
+  colors[ 2 ] = blue * 255;
+  colors[ 3 ] = alpha * 255;
+
+  for( i = 0, mf = markFragments; i < numFragments; i++, mf++ )
+  {
+    polyVert_t  *v;
+    polyVert_t  verts[ MAX_VERTS_ON_POLY2 ];
+    markPoly_t  *mark;
+
+    // we have an upper limit on the complexity of polygons
+    // that we store persistantly
+    if( mf->numPoints > MAX_VERTS_ON_POLY2 )
+      mf->numPoints = MAX_VERTS_ON_POLY2;
+
+    for( j = 0, v = verts; j < mf->numPoints; j++, v++ )
+    {
+      vec3_t    delta;
+
+      VectorCopy( markPoints[ mf->firstPoint + j ], v->xyz );
+
+      VectorSubtract( v->xyz, origin, delta );
+      v->st[ 0 ] = 0.5 + DotProduct( delta, axis[ 1 ] ) * texCoordScale;
+      v->st[ 1 ] = 0.5 + DotProduct( delta, axis[ 2 ] ) * texCoordScale;
+      *(int *)v->modulate = *(int *)colors;
+    }
+
+    // if it is a temporary (shadow) mark, add it immediately and forget about it
+    if( temporary )
+    {
+      trap_R_AddPolyToScene( markShader, mf->numPoints, verts );
+      continue;
+    }
+
+    // otherwise save it persistantly
+    mark = CG_AllocMark( );
+    mark->time = cg.time;
+    mark->alphaFade = alphaFade;
+    mark->markShader = markShader;
+    mark->poly.numVerts = mf->numPoints;
+    mark->color[ 0 ] = red;
+    mark->color[ 1 ] = green;
+    mark->color[ 2 ] = blue;
+    mark->color[ 3 ] = alpha;
+    memcpy( mark->verts, verts, mf->numPoints * sizeof( verts[ 0 ] ) );
+    markTotal++;
+  }
+}
+
+
+/*
+===============
+CG_AddMarks
+===============
+*/
+#define MARK_TOTAL_TIME   8000
+#define MARK_FADE_TIME     500
+
+void CG_AddMarks( void )
+{
+  int         j;
+  markPoly_t  *mp, *next;
+  int         t;
+  int         fade;
+
+  if( !cg_addMarks.integer )
+    return;
+
+  mp = cg_activeMarkPolys.nextMark;
+  for ( ; mp != &cg_activeMarkPolys; mp = next )
+  {
+    // grab next now, so if the local entity is freed we
+    // still have it
+    next = mp->nextMark;
+
+    // see if it is time to completely remove it
+    if( cg.time > mp->time + MARK_TOTAL_TIME )
+    {
+      CG_FreeMarkPoly( mp );
+      continue;
+    }
+
+    // fade all marks out with time
+    t = mp->time + MARK_TOTAL_TIME - cg.time;
+    if( t < MARK_FADE_TIME )
+    {
+      fade = 255 * t / MARK_FADE_TIME;
+      if( mp->alphaFade )
+      {
+        for( j = 0; j < mp->poly.numVerts; j++ )
+          mp->verts[ j ].modulate[ 3 ] = fade;
+      }
+      else
+      {
+        for( j = 0; j < mp->poly.numVerts; j++ )
+        {
+          mp->verts[ j ].modulate[ 0 ] = mp->color[ 0 ] * fade;
+          mp->verts[ j ].modulate[ 1 ] = mp->color[ 1 ] * fade;
+          mp->verts[ j ].modulate[ 2 ] = mp->color[ 2 ] * fade;
+        }
+      }
+    }
+    trap_R_AddPolyToScene( mp->markShader, mp->poly.numVerts, mp->verts );
+  }
+}
+
diff --git a/src/cgame/cg_particles.c b/src/cgame/cg_particles.c
new file mode 100644
index 0000000..c2374c7
--- /dev/null
+++ b/src/cgame/cg_particles.c
@@ -0,0 +1,2599 @@
+/*
+===========================================================================
+Copyright (C) 2000-2009 Darklegion Development
+
+This file is part of Tremulous.
+
+Tremulous 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.
+
+Tremulous 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 Tremulous; if not, write to the Free Software
+Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
+===========================================================================
+*/
+
+// cg_particles.c -- the particle system
+
+
+#include "cg_local.h"
+
+static baseParticleSystem_t   baseParticleSystems[ MAX_BASEPARTICLE_SYSTEMS ];
+static baseParticleEjector_t  baseParticleEjectors[ MAX_BASEPARTICLE_EJECTORS ];
+static baseParticle_t         baseParticles[ MAX_BASEPARTICLES ];
+static int                    numBaseParticleSystems = 0;
+static int                    numBaseParticleEjectors = 0;
+static int                    numBaseParticles = 0;
+
+static particleSystem_t     particleSystems[ MAX_PARTICLE_SYSTEMS ];
+static particleEjector_t    particleEjectors[ MAX_PARTICLE_EJECTORS ];
+static particle_t           particles[ MAX_PARTICLES ];
+static particle_t           *sortedParticles[ MAX_PARTICLES ];
+static particle_t           *radixBuffer[ MAX_PARTICLES ];
+
+/*
+===============
+CG_LerpValues
+
+Lerp between two values
+===============
+*/
+static float CG_LerpValues( float a, float b, float f )
+{
+  if( b == PARTICLES_SAME_AS_INITIAL )
+    return a;
+  else
+    return ( (a) + (f) * ( (b) - (a) ) );
+}
+
+/*
+===============
+CG_RandomiseValue
+
+Randomise some value by some variance
+===============
+*/
+static float CG_RandomiseValue( float value, float variance )
+{
+  if( value != 0.0f )
+    return value * ( 1.0f + ( random( ) * variance ) );
+  else
+    return random( ) * variance;
+}
+
+/*
+===============
+CG_SpreadVector
+
+Randomly spread a vector by some amount
+===============
+*/
+static void CG_SpreadVector( vec3_t v, float spread )
+{
+  vec3_t  p, r1, r2;
+  float   randomSpread = crandom( ) * spread;
+  float   randomRotation = random( ) * 360.0f;
+
+  PerpendicularVector( p, v );
+
+  RotatePointAroundVector( r1, p, v, randomSpread );
+  RotatePointAroundVector( r2, v, r1, randomRotation );
+
+  VectorCopy( r2, v );
+}
+
+/*
+===============
+CG_DestroyParticle
+
+Destroy an individual particle
+===============
+*/
+static void CG_DestroyParticle( particle_t *p, vec3_t impactNormal )
+{
+  //this particle has an onDeath particle system attached
+  if( p->class->onDeathSystemName[ 0 ] != '\0' )
+  {
+    particleSystem_t  *ps;
+
+    ps = CG_SpawnNewParticleSystem( p->class->onDeathSystemHandle );
+
+    if( CG_IsParticleSystemValid( &ps ) )
+    {
+      if( impactNormal )
+        CG_SetParticleSystemNormal( ps, impactNormal );
+
+      CG_SetAttachmentPoint( &ps->attachment, p->origin );
+      CG_AttachToPoint( &ps->attachment );
+    }
+  }
+
+  p->valid = qfalse;
+
+  //this gives other systems a couple of
+  //frames to realise the particle is gone
+  p->frameWhenInvalidated = cg.clientFrame;
+}
+
+/*
+===============
+CG_SpawnNewParticle
+
+Introduce a new particle into the world
+===============
+*/
+static particle_t *CG_SpawnNewParticle( baseParticle_t *bp, particleEjector_t *parent )
+{
+  int                     i, j;
+  particle_t              *p = NULL;
+  particleEjector_t       *pe = parent;
+  particleSystem_t        *ps = parent->parent;
+  vec3_t                  attachmentPoint, attachmentVelocity;
+  vec3_t                  transform[ 3 ];
+
+  for( i = 0; i < MAX_PARTICLES; i++ )
+  {
+    p = &particles[ i ];
+
+    //FIXME: the + 1 may be unnecessary
+    if( !p->valid && cg.clientFrame > p->frameWhenInvalidated + 1 )
+    {
+      memset( p, 0, sizeof( particle_t ) );
+
+      //found a free slot
+      p->class = bp;
+      p->parent = pe;
+
+      p->birthTime = cg.time;
+      p->lifeTime = (int)CG_RandomiseValue( (float)bp->lifeTime, bp->lifeTimeRandFrac );
+
+      p->radius.delay = (int)CG_RandomiseValue( (float)bp->radius.delay, bp->radius.delayRandFrac );
+      p->radius.initial = CG_RandomiseValue( bp->radius.initial, bp->radius.initialRandFrac );
+      p->radius.final = CG_RandomiseValue( bp->radius.final, bp->radius.finalRandFrac );
+      
+      p->radius.initial += bp->scaleWithCharge * pe->parent->charge;
+
+      p->alpha.delay = (int)CG_RandomiseValue( (float)bp->alpha.delay, bp->alpha.delayRandFrac );
+      p->alpha.initial = CG_RandomiseValue( bp->alpha.initial, bp->alpha.initialRandFrac );
+      p->alpha.final = CG_RandomiseValue( bp->alpha.final, bp->alpha.finalRandFrac );
+
+      p->rotation.delay = (int)CG_RandomiseValue( (float)bp->rotation.delay, bp->rotation.delayRandFrac );
+      p->rotation.initial = CG_RandomiseValue( bp->rotation.initial, bp->rotation.initialRandFrac );
+      p->rotation.final = CG_RandomiseValue( bp->rotation.final, bp->rotation.finalRandFrac );
+
+      p->dLightRadius.delay =
+        (int)CG_RandomiseValue( (float)bp->dLightRadius.delay, bp->dLightRadius.delayRandFrac );
+      p->dLightRadius.initial =
+        CG_RandomiseValue( bp->dLightRadius.initial, bp->dLightRadius.initialRandFrac );
+      p->dLightRadius.final =
+        CG_RandomiseValue( bp->dLightRadius.final, bp->dLightRadius.finalRandFrac );
+
+      p->colorDelay = CG_RandomiseValue( bp->colorDelay, bp->colorDelayRandFrac );
+
+      p->bounceMarkRadius = CG_RandomiseValue( bp->bounceMarkRadius, bp->bounceMarkRadiusRandFrac );
+      p->bounceMarkCount =
+        rint( CG_RandomiseValue( (float)bp->bounceMarkCount, bp->bounceMarkCountRandFrac ) );
+      p->bounceSoundCount =
+        rint( CG_RandomiseValue( (float)bp->bounceSoundCount, bp->bounceSoundCountRandFrac ) );
+
+      if( bp->numModels )
+      {
+        p->model = bp->models[ rand( ) % bp->numModels ];
+
+        if( bp->modelAnimation.frameLerp < 0 )
+        {
+          bp->modelAnimation.frameLerp = p->lifeTime / bp->modelAnimation.numFrames;
+          bp->modelAnimation.initialLerp = p->lifeTime / bp->modelAnimation.numFrames;
+        }
+      }
+
+      if( !CG_AttachmentPoint( &ps->attachment, attachmentPoint ) )
+        return NULL;
+
+      VectorCopy( attachmentPoint, p->origin );
+
+      if( CG_AttachmentAxis( &ps->attachment, transform ) )
+      {
+        vec3_t  transDisplacement;
+
+        VectorMatrixMultiply( bp->displacement, transform, transDisplacement );
+        VectorAdd( p->origin, transDisplacement, p->origin );
+      }
+      else
+        VectorAdd( p->origin, bp->displacement, p->origin );
+
+      for( j = 0; j <= 2; j++ )
+        p->origin[ j ] += ( crandom( ) * bp->randDisplacement[ j ] );
+
+      switch( bp->velMoveType )
+      {
+        case PMT_STATIC:
+          if( bp->velMoveValues.dirType == PMD_POINT )
+            VectorSubtract( bp->velMoveValues.point, p->origin, p->velocity );
+          else if( bp->velMoveValues.dirType == PMD_LINEAR )
+            VectorCopy( bp->velMoveValues.dir, p->velocity );
+          break;
+
+        case PMT_STATIC_TRANSFORM:
+          if( !CG_AttachmentAxis( &ps->attachment, transform ) )
+            return NULL;
+
+          if( bp->velMoveValues.dirType == PMD_POINT )
+          {
+            vec3_t transPoint;
+
+            VectorMatrixMultiply( bp->velMoveValues.point, transform, transPoint );
+            VectorSubtract( transPoint, p->origin, p->velocity );
+          }
+          else if( bp->velMoveValues.dirType == PMD_LINEAR )
+            VectorMatrixMultiply( bp->velMoveValues.dir, transform, p->velocity );
+          break;
+
+        case PMT_TAG:
+        case PMT_CENT_ANGLES:
+          if( bp->velMoveValues.dirType == PMD_POINT )
+            VectorSubtract( attachmentPoint, p->origin, p->velocity );
+          else if( bp->velMoveValues.dirType == PMD_LINEAR )
+          {
+            if( !CG_AttachmentDir( &ps->attachment, p->velocity ) )
+              return NULL;
+          }
+          break;
+
+        case PMT_NORMAL:
+          if( !ps->normalValid )
+          {
+            CG_Printf( S_COLOR_RED "ERROR: a particle with velocityType "
+                "normal has no normal\n" );
+            return NULL;
+          }
+
+          VectorCopy( ps->normal, p->velocity );
+
+          //normal displacement
+          VectorNormalize( p->velocity );
+          VectorMA( p->origin, bp->normalDisplacement, p->velocity, p->origin );
+          break;
+      }
+
+      VectorNormalize( p->velocity );
+      CG_SpreadVector( p->velocity, bp->velMoveValues.dirRandAngle );
+      VectorScale( p->velocity,
+                   CG_RandomiseValue( bp->velMoveValues.mag, bp->velMoveValues.magRandFrac ),
+                   p->velocity );
+
+      if( CG_AttachmentVelocity( &ps->attachment, attachmentVelocity ) )
+      {
+        VectorMA( p->velocity,
+            CG_RandomiseValue( bp->velMoveValues.parentVelFrac,
+              bp->velMoveValues.parentVelFracRandFrac ), attachmentVelocity, p->velocity );
+      }
+
+      p->lastEvalTime = cg.time;
+
+      p->valid = qtrue;
+
+      //this particle has a child particle system attached
+      if( bp->childSystemName[ 0 ] != '\0' )
+      {
+        particleSystem_t  *ps = CG_SpawnNewParticleSystem( bp->childSystemHandle );
+
+        if( CG_IsParticleSystemValid( &ps ) )
+        {
+          CG_SetAttachmentParticle( &ps->attachment, p );
+          CG_AttachToParticle( &ps->attachment );
+        }
+      }
+
+      //this particle has a child trail system attached
+      if( bp->childTrailSystemName[ 0 ] != '\0' )
+      {
+        trailSystem_t *ts = CG_SpawnNewTrailSystem( bp->childTrailSystemHandle );
+
+        if( CG_IsTrailSystemValid( &ts ) )
+        {
+          CG_SetAttachmentParticle( &ts->frontAttachment, p );
+          CG_AttachToParticle( &ts->frontAttachment );
+        }
+      }
+
+      break;
+    }
+  }
+
+  return p;
+}
+
+
+/*
+===============
+CG_SpawnNewParticles
+
+Check if there are any ejectors that should be
+introducing new particles
+===============
+*/
+static void CG_SpawnNewParticles( void )
+{
+  int                   i, j;
+  particle_t            *p;
+  particleSystem_t      *ps;
+  particleEjector_t     *pe;
+  baseParticleEjector_t *bpe;
+  float                 lerpFrac;
+  int                   count;
+
+  for( i = 0; i < MAX_PARTICLE_EJECTORS; i++ )
+  {
+    pe = &particleEjectors[ i ];
+    ps = pe->parent;
+
+    if( pe->valid )
+    {
+      //a non attached particle system can't make particles
+      if( !CG_Attached( &ps->attachment ) )
+        continue;
+
+      bpe = particleEjectors[ i ].class;
+
+      //if this system is scheduled for removal don't make any new particles
+      if( !ps->lazyRemove )
+      {
+        while( pe->nextEjectionTime <= cg.time &&
+               ( pe->count > 0 || pe->totalParticles == PARTICLES_INFINITE ) )
+        {
+          for( j = 0; j < bpe->numParticles; j++ )
+            CG_SpawnNewParticle( bpe->particles[ j ], pe );
+
+          if( pe->count > 0 )
+            pe->count--;
+
+          //calculate next ejection time
+          lerpFrac = 1.0 - ( (float)pe->count / (float)pe->totalParticles );
+          pe->nextEjectionTime = cg.time + (int)CG_RandomiseValue(
+              CG_LerpValues( pe->ejectPeriod.initial,
+                             pe->ejectPeriod.final,
+                             lerpFrac ),
+              pe->ejectPeriod.randFrac );
+        }
+      }
+
+      if( pe->count == 0 || ps->lazyRemove )
+      {
+        count = 0;
+
+        //wait for child particles to die before declaring this pe invalid
+        for( j = 0; j < MAX_PARTICLES; j++ )
+        {
+          p = &particles[ j ];
+
+          if( p->valid && p->parent == pe )
+            count++;
+        }
+
+        if( !count )
+          pe->valid = qfalse;
+      }
+    }
+  }
+}
+
+
+/*
+===============
+CG_SpawnNewParticleEjector
+
+Allocate a new particle ejector
+===============
+*/
+static particleEjector_t *CG_SpawnNewParticleEjector( baseParticleEjector_t *bpe,
+                                                      particleSystem_t *parent )
+{
+  int                     i;
+  particleEjector_t       *pe = NULL;
+  particleSystem_t        *ps = parent;
+
+  for( i = 0; i < MAX_PARTICLE_EJECTORS; i++ )
+  {
+    pe = &particleEjectors[ i ];
+
+    if( !pe->valid )
+    {
+      memset( pe, 0, sizeof( particleEjector_t ) );
+
+      //found a free slot
+      pe->class = bpe;
+      pe->parent = ps;
+
+      pe->ejectPeriod.initial = bpe->eject.initial;
+      pe->ejectPeriod.final = bpe->eject.final;
+      pe->ejectPeriod.randFrac = bpe->eject.randFrac;
+
+      pe->nextEjectionTime = cg.time +
+        (int)CG_RandomiseValue( (float)bpe->eject.delay, bpe->eject.delayRandFrac );
+      pe->count = pe->totalParticles =
+        (int)rint( CG_RandomiseValue( (float)bpe->totalParticles, bpe->totalParticlesRandFrac ) );
+
+      pe->valid = qtrue;
+
+      if( cg_debugParticles.integer >= 1 )
+        CG_Printf( "PE %s created\n", ps->class->name );
+
+      break;
+    }
+  }
+
+  return pe;
+}
+
+
+/*
+===============
+CG_SpawnNewParticleSystem
+
+Allocate a new particle system
+===============
+*/
+particleSystem_t *CG_SpawnNewParticleSystem( qhandle_t psHandle )
+{
+  int                   i, j;
+  particleSystem_t      *ps = NULL;
+  baseParticleSystem_t  *bps = &baseParticleSystems[ psHandle - 1 ];
+
+  if( !bps->registered )
+  {
+    CG_Printf( S_COLOR_RED "ERROR: a particle system has not been registered yet\n" );
+    return NULL;
+  }
+
+  for( i = 0; i < MAX_PARTICLE_SYSTEMS; i++ )
+  {
+    ps = &particleSystems[ i ];
+
+    if( !ps->valid )
+    {
+      memset( ps, 0, sizeof( particleSystem_t ) );
+
+      //found a free slot
+      ps->class = bps;
+
+      ps->valid = qtrue;
+      ps->lazyRemove = qfalse;
+
+      for( j = 0; j < bps->numEjectors; j++ )
+        CG_SpawnNewParticleEjector( bps->ejectors[ j ], ps );
+
+      if( cg_debugParticles.integer >= 1 )
+        CG_Printf( "PS %s created\n", bps->name );
+
+      break;
+    }
+  }
+
+  return ps;
+}
+
+/*
+===============
+CG_RegisterParticleSystem
+
+Load the shaders required for a particle system
+===============
+*/
+qhandle_t CG_RegisterParticleSystem( char *name )
+{
+  int                   i, j, k, l;
+  baseParticleSystem_t  *bps;
+  baseParticleEjector_t *bpe;
+  baseParticle_t        *bp;
+
+  for( i = 0; i < MAX_BASEPARTICLE_SYSTEMS; i++ )
+  {
+    bps = &baseParticleSystems[ i ];
+
+    if( !Q_stricmpn( bps->name, name, MAX_QPATH ) )
+    {
+      //already registered
+      if( bps->registered )
+        return i + 1;
+
+      for( j = 0; j < bps->numEjectors; j++ )
+      {
+        bpe = bps->ejectors[ j ];
+
+        for( l = 0; l < bpe->numParticles; l++ )
+        {
+          bp = bpe->particles[ l ];
+
+          for( k = 0; k < bp->numFrames; k++ )
+            bp->shaders[ k ] = trap_R_RegisterShader( bp->shaderNames[ k ] );
+
+          for( k = 0; k < bp->numModels; k++ )
+            bp->models[ k ] = trap_R_RegisterModel( bp->modelNames[ k ] );
+
+          if( bp->bounceMarkName[ 0 ] != '\0' )
+            bp->bounceMark = trap_R_RegisterShader( bp->bounceMarkName );
+
+          if( bp->bounceSoundName[ 0 ] != '\0' )
+            bp->bounceSound = trap_S_RegisterSound( bp->bounceSoundName, qfalse );
+
+          //recursively register any children
+          if( bp->childSystemName[ 0 ] != '\0' )
+          {
+            //don't care about a handle for children since
+            //the system deals with it
+            CG_RegisterParticleSystem( bp->childSystemName );
+          }
+
+          if( bp->onDeathSystemName[ 0 ] != '\0' )
+          {
+            //don't care about a handle for children since
+            //the system deals with it
+            CG_RegisterParticleSystem( bp->onDeathSystemName );
+          }
+
+          if( bp->childTrailSystemName[ 0 ] != '\0' )
+            bp->childTrailSystemHandle = CG_RegisterTrailSystem( bp->childTrailSystemName );
+        }
+      }
+
+      if( cg_debugParticles.integer >= 1 )
+        CG_Printf( "Registered particle system %s\n", name );
+
+      bps->registered = qtrue;
+
+      //avoid returning 0
+      return i + 1;
+    }
+  }
+
+  CG_Printf( S_COLOR_RED "ERROR: failed to register particle system %s\n", name );
+  return 0;
+}
+
+
+/*
+===============
+CG_ParseValueAndVariance
+
+Parse a value and its random variance
+===============
+*/
+static void CG_ParseValueAndVariance( char *token, float *value, float *variance, qboolean allowNegative )
+{
+  char  valueBuffer[ 16 ];
+  char  *variancePtr = NULL, *varEndPointer = NULL;
+  float localValue = 0.0f;
+  float localVariance = 0.0f;
+
+  Q_strncpyz( valueBuffer, token, sizeof( valueBuffer ) );
+
+  variancePtr = strchr( valueBuffer, '~' );
+
+  //variance included
+  if( variancePtr )
+  {
+    variancePtr[ 0 ] = '\0';
+    variancePtr++;
+
+    localValue = atof_neg( valueBuffer, allowNegative );
+
+    varEndPointer = strchr( variancePtr, '%' );
+
+    if( varEndPointer )
+    {
+      varEndPointer[ 0 ] = '\0';
+      localVariance = atof_neg( variancePtr, qfalse ) / 100.0f;
+    }
+    else
+    {
+      if( localValue != 0.0f )
+        localVariance = atof_neg( variancePtr, qfalse ) / localValue;
+      else
+        localVariance = atof_neg( variancePtr, qfalse );
+    }
+  }
+  else
+    localValue = atof_neg( valueBuffer, allowNegative );
+
+  if( value != NULL )
+    *value = localValue;
+
+  if( variance != NULL )
+    *variance = localVariance;
+}
+
+/*
+===============
+CG_ParseColor
+===============
+*/
+static qboolean CG_ParseColor( byte *c, char **text_p )
+{
+  char  *token;
+  int   i;
+
+  for( i = 0; i <= 2; i++ )
+  {
+    token = COM_Parse( text_p );
+
+    if( !Q_stricmp( token, "" ) )
+      return qfalse;
+
+    c[ i ] = (int)( (float)0xFF * atof_neg( token, qfalse ) );
+  }
+
+  return qtrue;
+}
+
+/*
+===============
+CG_ParseParticle
+
+Parse a particle section
+===============
+*/
+static qboolean CG_ParseParticle( baseParticle_t *bp, char **text_p )
+{
+  char  *token;
+  float number, randFrac;
+  int   i;
+
+  // read optional parameters
+  while( 1 )
+  {
+    token = COM_Parse( text_p );
+
+    if( !token )
+      break;
+
+    if( !Q_stricmp( token, "" ) )
+      return qfalse;
+
+    if( !Q_stricmp( token, "bounce" ) )
+    {
+      token = COM_Parse( text_p );
+      if( !token )
+        break;
+
+      if( !Q_stricmp( token, "cull" ) )
+      {
+        bp->bounceCull = qtrue;
+
+        bp->bounceFrac = -1.0f;
+        bp->bounceFracRandFrac = 0.0f;
+      }
+      else
+      {
+        CG_ParseValueAndVariance( token, &number, &randFrac, qfalse );
+
+        bp->bounceFrac = number;
+        bp->bounceFracRandFrac = randFrac;
+      }
+
+      continue;
+    }
+    else if( !Q_stricmp( token, "bounceMark" ) )
+    {
+      token = COM_Parse( text_p );
+      if( !*token )
+        break;
+
+      CG_ParseValueAndVariance( token, &number, &randFrac, qfalse );
+
+      bp->bounceMarkCount = number;
+      bp->bounceMarkCountRandFrac = randFrac;
+
+      token = COM_Parse( text_p );
+      if( !*token )
+        break;
+
+      CG_ParseValueAndVariance( token, &number, &randFrac, qfalse );
+
+      bp->bounceMarkRadius = number;
+      bp->bounceMarkRadiusRandFrac = randFrac;
+
+      token = COM_ParseExt( text_p, qfalse );
+      if( !*token )
+        break;
+
+      Q_strncpyz( bp->bounceMarkName, token, MAX_QPATH );
+
+      continue;
+    }
+    else if( !Q_stricmp( token, "bounceSound" ) )
+    {
+      token = COM_Parse( text_p );
+      if( !*token )
+        break;
+
+      CG_ParseValueAndVariance( token, &number, &randFrac, qfalse );
+
+      bp->bounceSoundCount = number;
+      bp->bounceSoundCountRandFrac = randFrac;
+
+      token = COM_Parse( text_p );
+      if( !*token )
+        break;
+
+      Q_strncpyz( bp->bounceSoundName, token, MAX_QPATH );
+
+      continue;
+    }
+    else if( !Q_stricmp( token, "shader" ) )
+    {
+      if( bp->numModels > 0 )
+      {
+        CG_Printf( S_COLOR_RED "ERROR: 'shader' not allowed in "
+            "conjunction with 'model'\n", token );
+        break;
+      }
+
+      token = COM_Parse( text_p );
+      if( !token )
+        break;
+
+      if( !Q_stricmp( token, "sync" ) )
+        bp->framerate = 0.0f;
+      else
+        bp->framerate = atof_neg( token, qfalse );
+
+      token = COM_ParseExt( text_p, qfalse );
+      if( !*token )
+        break;
+
+      while( *token && bp->numFrames < MAX_PS_SHADER_FRAMES )
+      {
+        Q_strncpyz( bp->shaderNames[ bp->numFrames++ ], token, MAX_QPATH );
+        token = COM_ParseExt( text_p, qfalse );
+      }
+
+      continue;
+    }
+    else if( !Q_stricmp( token, "model" ) )
+    {
+      if( bp->numFrames > 0 )
+      {
+        CG_Printf( S_COLOR_RED "ERROR: 'model' not allowed in "
+            "conjunction with 'shader'\n", token );
+        break;
+      }
+
+      token = COM_ParseExt( text_p, qfalse );
+      if( !*token )
+        break;
+
+      while( *token && bp->numModels < MAX_PS_MODELS )
+      {
+        Q_strncpyz( bp->modelNames[ bp->numModels++ ], token, MAX_QPATH );
+        token = COM_ParseExt( text_p, qfalse );
+      }
+
+      continue;
+    }
+    else if( !Q_stricmp( token, "modelAnimation" ) )
+    {
+      token = COM_Parse( text_p );
+      if( !*token )
+        break;
+
+      bp->modelAnimation.firstFrame = atoi_neg( token, qfalse );
+
+      token = COM_Parse( text_p );
+      if( !*token )
+        break;
+
+      bp->modelAnimation.numFrames = atoi( token );
+      bp->modelAnimation.reversed = qfalse;
+      bp->modelAnimation.flipflop = qfalse;
+
+      // if numFrames is negative the animation is reversed
+      if( bp->modelAnimation.numFrames < 0 )
+      {
+        bp->modelAnimation.numFrames = -bp->modelAnimation.numFrames;
+        bp->modelAnimation.reversed = qtrue;
+      }
+
+      token = COM_Parse( text_p );
+      if( !*token )
+        break;
+
+      bp->modelAnimation.loopFrames = atoi( token );
+
+      token = COM_Parse( text_p );
+      if( !*token )
+        break;
+
+      if( !Q_stricmp( token, "sync" ) )
+      {
+        bp->modelAnimation.frameLerp = -1;
+        bp->modelAnimation.initialLerp = -1;
+      }
+      else
+      {
+        float fps = atof_neg( token, qfalse );
+
+        if( fps == 0.0f )
+          fps = 1.0f;
+
+        bp->modelAnimation.frameLerp = 1000 / fps;
+        bp->modelAnimation.initialLerp = 1000 / fps;
+      }
+
+      continue;
+    }
+    ///
+    else if( !Q_stricmp( token, "velocityType" ) )
+    {
+      token = COM_Parse( text_p );
+      if( !token )
+        break;
+
+      if( !Q_stricmp( token, "static" ) )
+        bp->velMoveType = PMT_STATIC;
+      else if( !Q_stricmp( token, "static_transform" ) )
+        bp->velMoveType = PMT_STATIC_TRANSFORM;
+      else if( !Q_stricmp( token, "tag" ) )
+        bp->velMoveType = PMT_TAG;
+      else if( !Q_stricmp( token, "cent" ) )
+        bp->velMoveType = PMT_CENT_ANGLES;
+      else if( !Q_stricmp( token, "normal" ) )
+        bp->velMoveType = PMT_NORMAL;
+
+      continue;
+    }
+    else if( !Q_stricmp( token, "velocityDir" ) )
+    {
+      token = COM_Parse( text_p );
+      if( !token )
+        break;
+
+      if( !Q_stricmp( token, "linear" ) )
+        bp->velMoveValues.dirType = PMD_LINEAR;
+      else if( !Q_stricmp( token, "point" ) )
+        bp->velMoveValues.dirType = PMD_POINT;
+
+      continue;
+    }
+    else if( !Q_stricmp( token, "velocityMagnitude" ) )
+    {
+      token = COM_Parse( text_p );
+      if( !token )
+        break;
+
+      CG_ParseValueAndVariance( token, &number, &randFrac, qfalse );
+
+      bp->velMoveValues.mag = number;
+      bp->velMoveValues.magRandFrac = randFrac;
+
+      continue;
+    }
+    else if( !Q_stricmp( token, "parentVelocityFraction" ) )
+    {
+      token = COM_Parse( text_p );
+      if( !token )
+        break;
+
+      CG_ParseValueAndVariance( token, &number, &randFrac, qfalse );
+
+      bp->velMoveValues.parentVelFrac = number;
+      bp->velMoveValues.parentVelFracRandFrac = randFrac;
+
+      continue;
+    }
+    else if( !Q_stricmp( token, "velocity" ) )
+    {
+      for( i = 0; i <= 2; i++ )
+      {
+        token = COM_Parse( text_p );
+        if( !token )
+          break;
+
+        bp->velMoveValues.dir[ i ] = atof_neg( token, qtrue );
+      }
+
+      token = COM_Parse( text_p );
+      if( !token )
+        break;
+
+      CG_ParseValueAndVariance( token, NULL, &randFrac, qfalse );
+
+      bp->velMoveValues.dirRandAngle = randFrac;
+
+      continue;
+    }
+    else if( !Q_stricmp( token, "velocityPoint" ) )
+    {
+      for( i = 0; i <= 2; i++ )
+      {
+        token = COM_Parse( text_p );
+        if( !token )
+          break;
+
+        bp->velMoveValues.point[ i ] = atof_neg( token, qtrue );
+      }
+
+      token = COM_Parse( text_p );
+      if( !token )
+        break;
+
+      CG_ParseValueAndVariance( token, NULL, &randFrac, qfalse );
+
+      bp->velMoveValues.pointRandAngle = randFrac;
+
+      continue;
+    }
+    ///
+    else if( !Q_stricmp( token, "accelerationType" ) )
+    {
+      token = COM_Parse( text_p );
+      if( !token )
+        break;
+
+      if( !Q_stricmp( token, "static" ) )
+        bp->accMoveType = PMT_STATIC;
+      else if( !Q_stricmp( token, "static_transform" ) )
+        bp->accMoveType = PMT_STATIC_TRANSFORM;
+      else if( !Q_stricmp( token, "tag" ) )
+        bp->accMoveType = PMT_TAG;
+      else if( !Q_stricmp( token, "cent" ) )
+        bp->accMoveType = PMT_CENT_ANGLES;
+      else if( !Q_stricmp( token, "normal" ) )
+        bp->accMoveType = PMT_NORMAL;
+
+      continue;
+    }
+    else if( !Q_stricmp( token, "accelerationDir" ) )
+    {
+      token = COM_Parse( text_p );
+      if( !token )
+        break;
+
+      if( !Q_stricmp( token, "linear" ) )
+        bp->accMoveValues.dirType = PMD_LINEAR;
+      else if( !Q_stricmp( token, "point" ) )
+        bp->accMoveValues.dirType = PMD_POINT;
+
+      continue;
+    }
+    else if( !Q_stricmp( token, "accelerationMagnitude" ) )
+    {
+      token = COM_Parse( text_p );
+      if( !token )
+        break;
+
+      CG_ParseValueAndVariance( token, &number, &randFrac, qfalse );
+
+      bp->accMoveValues.mag = number;
+      bp->accMoveValues.magRandFrac = randFrac;
+
+      continue;
+    }
+    else if( !Q_stricmp( token, "acceleration" ) )
+    {
+      for( i = 0; i <= 2; i++ )
+      {
+        token = COM_Parse( text_p );
+        if( !token )
+          break;
+
+        bp->accMoveValues.dir[ i ] = atof_neg( token, qtrue );
+      }
+
+      token = COM_Parse( text_p );
+      if( !token )
+        break;
+
+      CG_ParseValueAndVariance( token, NULL, &randFrac, qfalse );
+
+      bp->accMoveValues.dirRandAngle = randFrac;
+
+      continue;
+    }
+    else if( !Q_stricmp( token, "accelerationPoint" ) )
+    {
+      for( i = 0; i <= 2; i++ )
+      {
+        token = COM_Parse( text_p );
+        if( !token )
+          break;
+
+        bp->accMoveValues.point[ i ] = atof_neg( token, qtrue );
+      }
+
+      token = COM_Parse( text_p );
+      if( !token )
+        break;
+
+      CG_ParseValueAndVariance( token, NULL, &randFrac, qfalse );
+
+      bp->accMoveValues.pointRandAngle = randFrac;
+
+      continue;
+    }
+    ///
+    else if( !Q_stricmp( token, "displacement" ) )
+    {
+      for( i = 0; i <= 2; i++ )
+      {
+        token = COM_Parse( text_p );
+        if( !token )
+          break;
+
+        CG_ParseValueAndVariance( token, &bp->displacement[ i ],
+                                  &bp->randDisplacement[ i ], qtrue );
+      }
+
+      // if there is another token on the same line interpret it as an
+      // additional displacement in all three directions, for compatibility
+      // with the old scripts where this was the only option
+      randFrac = 0;
+      token = COM_ParseExt( text_p, qfalse );
+      if( token )
+        CG_ParseValueAndVariance( token, NULL, &randFrac, qtrue );
+
+      for( i = 0; i < 3; i++ )
+      {
+        // convert randDisplacement from proportions to absolute values
+        if( bp->displacement[ i ] != 0 )
+          bp->randDisplacement[ i ] *= bp->displacement[ i ];
+
+        bp->randDisplacement[ i ] += randFrac;
+      }
+
+      continue;
+    }
+    else if( !Q_stricmp( token, "normalDisplacement" ) )
+    {
+      token = COM_Parse( text_p );
+      if( !token )
+        break;
+
+      bp->normalDisplacement = atof_neg( token, qtrue );
+
+      continue;
+    }
+    else if( !Q_stricmp( token, "overdrawProtection" ) )
+    {
+      bp->overdrawProtection = qtrue;
+
+      continue;
+    }
+    else if( !Q_stricmp( token, "realLight" ) )
+    {
+      bp->realLight = qtrue;
+
+      continue;
+    }
+    else if( !Q_stricmp( token, "dynamicLight" ) )
+    {
+      bp->dynamicLight = qtrue;
+
+      token = COM_Parse( text_p );
+      if( !*token )
+        break;
+
+      CG_ParseValueAndVariance( token, &number, &randFrac, qfalse );
+
+      bp->dLightRadius.delay = (int)number;
+      bp->dLightRadius.delayRandFrac = randFrac;
+
+      token = COM_Parse( text_p );
+      if( !*token )
+        break;
+
+      CG_ParseValueAndVariance( token, &number, &randFrac, qfalse );
+
+      bp->dLightRadius.initial = number;
+      bp->dLightRadius.initialRandFrac = randFrac;
+
+      token = COM_Parse( text_p );
+      if( !*token )
+        break;
+
+      if( !Q_stricmp( token, "-" ) )
+      {
+        bp->dLightRadius.final = PARTICLES_SAME_AS_INITIAL;
+        bp->dLightRadius.finalRandFrac = 0.0f;
+      }
+      else
+      {
+        CG_ParseValueAndVariance( token, &number, &randFrac, qfalse );
+
+        bp->dLightRadius.final = number;
+        bp->dLightRadius.finalRandFrac = randFrac;
+      }
+
+      token = COM_Parse( text_p );
+      if( !*token )
+        break;
+
+      if( !Q_stricmp( token, "{" ) )
+      {
+        if( !CG_ParseColor( bp->dLightColor, text_p ) )
+          break;
+
+        token = COM_Parse( text_p );
+        if( Q_stricmp( token, "}" ) )
+        {
+          CG_Printf( S_COLOR_RED "ERROR: missing '}'\n" );
+          break;
+        }
+      }
+
+      continue;
+    }
+    else if( !Q_stricmp( token, "cullOnStartSolid" ) )
+    {
+      bp->cullOnStartSolid = qtrue;
+
+      continue;
+    }
+    else if( !Q_stricmp( token, "radius" ) )
+    {
+      token = COM_Parse( text_p );
+      if( !token )
+        break;
+
+      CG_ParseValueAndVariance( token, &number, &randFrac, qfalse );
+
+      bp->radius.delay = (int)number;
+      bp->radius.delayRandFrac = randFrac;
+
+      token = COM_Parse( text_p );
+      if( !token )
+        break;
+
+      CG_ParseValueAndVariance( token, &number, &randFrac, qfalse );
+
+      bp->radius.initial = number;
+      bp->radius.initialRandFrac = randFrac;
+
+      token = COM_Parse( text_p );
+      if( !token )
+        break;
+
+      if( !Q_stricmp( token, "-" ) )
+      {
+        bp->radius.final = PARTICLES_SAME_AS_INITIAL;
+        bp->radius.finalRandFrac = 0.0f;
+      }
+      else
+      {
+        CG_ParseValueAndVariance( token, &number, &randFrac, qfalse );
+
+        bp->radius.final = number;
+        bp->radius.finalRandFrac = randFrac;
+      }
+
+      continue;
+    }
+    else if( !Q_stricmp( token, "physicsRadius" ) )
+    {
+      token = COM_Parse( text_p );
+      if( !token )
+        break;
+      
+      bp->physicsRadius = atoi( token );      
+    }
+    else if( !Q_stricmp( token, "alpha" ) )
+    {
+      token = COM_Parse( text_p );
+      if( !token )
+        break;
+
+      CG_ParseValueAndVariance( token, &number, &randFrac, qfalse );
+
+      bp->alpha.delay = (int)number;
+      bp->alpha.delayRandFrac = randFrac;
+
+      token = COM_Parse( text_p );
+      if( !token )
+        break;
+
+      CG_ParseValueAndVariance( token, &number, &randFrac, qfalse );
+
+      bp->alpha.initial = number;
+      bp->alpha.initialRandFrac = randFrac;
+
+      token = COM_Parse( text_p );
+      if( !token )
+        break;
+
+      if( !Q_stricmp( token, "-" ) )
+      {
+        bp->alpha.final = PARTICLES_SAME_AS_INITIAL;
+        bp->alpha.finalRandFrac = 0.0f;
+      }
+      else
+      {
+        CG_ParseValueAndVariance( token, &number, &randFrac, qfalse );
+
+        bp->alpha.final = number;
+        bp->alpha.finalRandFrac = randFrac;
+      }
+
+      continue;
+    }
+    else if( !Q_stricmp( token, "color" ) )
+    {
+      token = COM_Parse( text_p );
+      if( !*token )
+        break;
+
+      CG_ParseValueAndVariance( token, &number, &randFrac, qfalse );
+
+      bp->colorDelay = (int)number;
+      bp->colorDelayRandFrac = randFrac;
+
+      token = COM_Parse( text_p );
+      if( !*token )
+        break;
+
+      if( !Q_stricmp( token, "{" ) )
+      {
+        if( !CG_ParseColor( bp->initialColor, text_p ) )
+          break;
+
+        token = COM_Parse( text_p );
+        if( Q_stricmp( token, "}" ) )
+        {
+          CG_Printf( S_COLOR_RED "ERROR: missing '}'\n" );
+          break;
+        }
+
+        token = COM_Parse( text_p );
+        if( !*token )
+          break;
+
+        if( !Q_stricmp( token, "-" ) )
+        {
+          bp->finalColor[ 0 ] = bp->initialColor[ 0 ];
+          bp->finalColor[ 1 ] = bp->initialColor[ 1 ];
+          bp->finalColor[ 2 ] = bp->initialColor[ 2 ];
+        }
+        else if( !Q_stricmp( token, "{" ) )
+        {
+          if( !CG_ParseColor( bp->finalColor, text_p ) )
+            break;
+
+          token = COM_Parse( text_p );
+          if( Q_stricmp( token, "}" ) )
+          {
+            CG_Printf( S_COLOR_RED "ERROR: missing '}'\n" );
+            break;
+          }
+        }
+        else
+        {
+          CG_Printf( S_COLOR_RED "ERROR: missing '{'\n" );
+          break;
+        }
+      }
+      else
+      {
+        CG_Printf( S_COLOR_RED "ERROR: missing '{'\n" );
+        break;
+      }
+
+      continue;
+    }
+    else if( !Q_stricmp( token, "rotation" ) )
+    {
+      token = COM_Parse( text_p );
+      if( !token )
+        break;
+
+      CG_ParseValueAndVariance( token, &number, &randFrac, qfalse );
+
+      bp->rotation.delay = (int)number;
+      bp->rotation.delayRandFrac = randFrac;
+
+      token = COM_Parse( text_p );
+      if( !token )
+        break;
+
+      CG_ParseValueAndVariance( token, &number, &randFrac, qtrue );
+
+      bp->rotation.initial = number;
+      bp->rotation.initialRandFrac = randFrac;
+
+      token = COM_Parse( text_p );
+      if( !token )
+        break;
+
+      if( !Q_stricmp( token, "-" ) )
+      {
+        bp->rotation.final = PARTICLES_SAME_AS_INITIAL;
+        bp->rotation.finalRandFrac = 0.0f;
+      }
+      else
+      {
+        CG_ParseValueAndVariance( token, &number, &randFrac, qtrue );
+
+        bp->rotation.final = number;
+        bp->rotation.finalRandFrac = randFrac;
+      }
+
+      continue;
+    }
+    else if( !Q_stricmp( token, "lifeTime" ) )
+    {
+      token = COM_Parse( text_p );
+      if( !token )
+        break;
+
+      CG_ParseValueAndVariance( token, &number, &randFrac, qfalse );
+
+      bp->lifeTime = (int)number;
+      bp->lifeTimeRandFrac = randFrac;
+
+      continue;
+    }
+    else if( !Q_stricmp( token, "childSystem" ) )
+    {
+      token = COM_Parse( text_p );
+      if( !token )
+        break;
+
+      Q_strncpyz( bp->childSystemName, token, MAX_QPATH );
+
+      continue;
+    }
+    else if( !Q_stricmp( token, "onDeathSystem" ) )
+    {
+      token = COM_Parse( text_p );
+      if( !token )
+        break;
+
+      Q_strncpyz( bp->onDeathSystemName, token, MAX_QPATH );
+
+      continue;
+    }
+    else if( !Q_stricmp( token, "childTrailSystem" ) )
+    {
+      token = COM_Parse( text_p );
+      if( !token )
+        break;
+
+      Q_strncpyz( bp->childTrailSystemName, token, MAX_QPATH );
+
+      continue;
+    }
+    else if( !Q_stricmp( token, "scaleWithCharge" ) )
+    {
+      token = COM_Parse( text_p );
+      if( !token )
+        break;
+
+      bp->scaleWithCharge = atof( token );
+
+      continue;
+    }
+    else if( !Q_stricmp( token, "}" ) )
+      return qtrue; //reached the end of this particle
+    else
+    {
+      CG_Printf( S_COLOR_RED "ERROR: unknown token '%s' in particle\n", token );
+      return qfalse;
+    }
+  }
+
+  return qfalse;
+}
+
+/*
+===============
+CG_InitialiseBaseParticle
+===============
+*/
+static void CG_InitialiseBaseParticle( baseParticle_t *bp )
+{
+  memset( bp, 0, sizeof( baseParticle_t ) );
+
+  memset( bp->initialColor, 0xFF, sizeof( bp->initialColor ) );
+  memset( bp->finalColor, 0xFF, sizeof( bp->finalColor ) );
+}
+
+/*
+===============
+CG_ParseParticleEjector
+
+Parse a particle ejector section
+===============
+*/
+static qboolean CG_ParseParticleEjector( baseParticleEjector_t *bpe, char **text_p )
+{
+  char  *token;
+  float number, randFrac;
+
+  // read optional parameters
+  while( 1 )
+  {
+    token = COM_Parse( text_p );
+
+    if( !token )
+      break;
+
+    if( !Q_stricmp( token, "" ) )
+      return qfalse;
+
+    if( !Q_stricmp( token, "{" ) )
+    {
+      CG_InitialiseBaseParticle( &baseParticles[ numBaseParticles ] );
+
+      if( !CG_ParseParticle( &baseParticles[ numBaseParticles ], text_p ) )
+      {
+        CG_Printf( S_COLOR_RED "ERROR: failed to parse particle\n" );
+        return qfalse;
+      }
+
+      if( bpe->numParticles == MAX_PARTICLES_PER_EJECTOR )
+      {
+        CG_Printf( S_COLOR_RED "ERROR: ejector has > %d particles\n", MAX_PARTICLES_PER_EJECTOR );
+        return qfalse;
+      }
+      else if( numBaseParticles == MAX_BASEPARTICLES )
+      {
+        CG_Printf( S_COLOR_RED "ERROR: maximum number of particles (%d) reached\n", MAX_BASEPARTICLES );
+        return qfalse;
+      }
+      else
+      {
+        //start parsing particles again
+        bpe->particles[ bpe->numParticles ] = &baseParticles[ numBaseParticles ];
+        bpe->numParticles++;
+        numBaseParticles++;
+      }
+      continue;
+    }
+    else if( !Q_stricmp( token, "delay" ) )
+    {
+      token = COM_Parse( text_p );
+      if( !token )
+        break;
+
+      CG_ParseValueAndVariance( token, &number, &randFrac, qfalse );
+
+      bpe->eject.delay = (int)number;
+      bpe->eject.delayRandFrac = randFrac;
+
+      continue;
+    }
+    else if( !Q_stricmp( token, "period" ) )
+    {
+      token = COM_Parse( text_p );
+      if( !token )
+        break;
+
+      bpe->eject.initial = atoi_neg( token, qfalse );
+
+      token = COM_Parse( text_p );
+      if( !token )
+        break;
+
+      if( !Q_stricmp( token, "-" ) )
+        bpe->eject.final = PARTICLES_SAME_AS_INITIAL;
+      else
+        bpe->eject.final = atoi_neg( token, qfalse );
+
+      token = COM_Parse( text_p );
+      if( !token )
+        break;
+
+      CG_ParseValueAndVariance( token, NULL, &bpe->eject.randFrac, qfalse );
+
+      continue;
+    }
+    else if( !Q_stricmp( token, "count" ) )
+    {
+      token = COM_Parse( text_p );
+      if( !token )
+        break;
+
+      if( !Q_stricmp( token, "infinite" ) )
+      {
+        bpe->totalParticles = PARTICLES_INFINITE;
+        bpe->totalParticlesRandFrac = 0.0f;
+      }
+      else
+      {
+        CG_ParseValueAndVariance( token, &number, &randFrac, qfalse );
+
+        bpe->totalParticles = (int)number;
+        bpe->totalParticlesRandFrac = randFrac;
+      }
+
+      continue;
+    }
+    else if( !Q_stricmp( token, "particle" ) ) //acceptable text
+      continue;
+    else if( !Q_stricmp( token, "}" ) )
+      return qtrue; //reached the end of this particle ejector
+    else
+    {
+      CG_Printf( S_COLOR_RED "ERROR: unknown token '%s' in particle ejector\n", token );
+      return qfalse;
+    }
+  }
+
+  return qfalse;
+}
+
+
+/*
+===============
+CG_ParseParticleSystem
+
+Parse a particle system section
+===============
+*/
+static qboolean CG_ParseParticleSystem( baseParticleSystem_t *bps, char **text_p, const char *name )
+{
+  char                  *token;
+  baseParticleEjector_t *bpe;
+
+  // read optional parameters
+  while( 1 )
+  {
+    token = COM_Parse( text_p );
+
+    if( !token )
+      break;
+
+    if( !Q_stricmp( token, "" ) )
+      return qfalse;
+
+    if( !Q_stricmp( token, "{" ) )
+    {
+      if( !CG_ParseParticleEjector( &baseParticleEjectors[ numBaseParticleEjectors ], text_p ) )
+      {
+        CG_Printf( S_COLOR_RED "ERROR: failed to parse particle ejector\n" );
+        return qfalse;
+      }
+
+      bpe = &baseParticleEjectors[ numBaseParticleEjectors ];
+
+      //check for infinite count + zero period
+      if( bpe->totalParticles == PARTICLES_INFINITE &&
+          ( bpe->eject.initial == 0.0f || bpe->eject.final == 0.0f ) )
+      {
+        CG_Printf( S_COLOR_RED "ERROR: ejector with 'count infinite' potentially has zero period\n" );
+        return qfalse;
+      }
+
+      if( bps->numEjectors == MAX_EJECTORS_PER_SYSTEM )
+      {
+        CG_Printf( S_COLOR_RED "ERROR: particle system has > %d ejectors\n", MAX_EJECTORS_PER_SYSTEM );
+        return qfalse;
+      }
+      else if( numBaseParticleEjectors == MAX_BASEPARTICLE_EJECTORS )
+      {
+        CG_Printf( S_COLOR_RED "ERROR: maximum number of particle ejectors (%d) reached\n",
+            MAX_BASEPARTICLE_EJECTORS );
+        return qfalse;
+      }
+      else
+      {
+        //start parsing ejectors again
+        bps->ejectors[ bps->numEjectors ] = &baseParticleEjectors[ numBaseParticleEjectors ];
+        bps->numEjectors++;
+        numBaseParticleEjectors++;
+      }
+      continue;
+    }
+    else if( !Q_stricmp( token, "thirdPersonOnly" ) )
+      bps->thirdPersonOnly = qtrue;
+    else if( !Q_stricmp( token, "ejector" ) ) //acceptable text
+      continue;
+    else if( !Q_stricmp( token, "}" ) )
+    {
+      if( cg_debugParticles.integer >= 1 )
+        CG_Printf( "Parsed particle system %s\n", name );
+
+      return qtrue; //reached the end of this particle system
+    }
+    else
+    {
+      CG_Printf( S_COLOR_RED "ERROR: unknown token '%s' in particle system %s\n", token, bps->name );
+      return qfalse;
+    }
+  }
+
+  return qfalse;
+}
+
+/*
+===============
+CG_ParseParticleFile
+
+Load the particle systems from a particle file
+===============
+*/
+static qboolean CG_ParseParticleFile( const char *fileName )
+{
+  char          *text_p;
+  int           i;
+  int           len;
+  char          *token;
+  char          text[ 32000 ];
+  char          psName[ MAX_QPATH ];
+  qboolean      psNameSet = qfalse;
+  fileHandle_t  f;
+
+  // load the file
+  len = trap_FS_FOpenFile( fileName, &f, FS_READ );
+  if( len < 0 )
+    return qfalse;
+
+  if( len == 0 || len >= sizeof( text ) - 1 )
+  {
+    trap_FS_FCloseFile( f );
+    CG_Printf( S_COLOR_RED "ERROR: particle file %s is %s\n", fileName,
+      len == 0 ? "empty" : "too long" );
+    return qfalse;
+  }
+
+  trap_FS_Read( text, len, f );
+  text[ len ] = 0;
+  trap_FS_FCloseFile( f );
+
+  // parse the text
+  text_p = text;
+
+  // read optional parameters
+  while( 1 )
+  {
+    token = COM_Parse( &text_p );
+
+    if( !Q_stricmp( token, "" ) )
+      break;
+
+    if( !Q_stricmp( token, "{" ) )
+    {
+      if( psNameSet )
+      {
+        //check for name space clashes
+        for( i = 0; i < numBaseParticleSystems; i++ )
+        {
+          if( !Q_stricmp( baseParticleSystems[ i ].name, psName ) )
+          {
+            CG_Printf( S_COLOR_RED "ERROR: a particle system is already named %s\n", psName );
+            return qfalse;
+          }
+        }
+
+        Q_strncpyz( baseParticleSystems[ numBaseParticleSystems ].name, psName, MAX_QPATH );
+
+        if( !CG_ParseParticleSystem( &baseParticleSystems[ numBaseParticleSystems ], &text_p, psName ) )
+        {
+          CG_Printf( S_COLOR_RED "ERROR: %s: failed to parse particle system %s\n", fileName, psName );
+          return qfalse;
+        }
+
+        //start parsing particle systems again
+        psNameSet = qfalse;
+
+        if( numBaseParticleSystems == MAX_BASEPARTICLE_SYSTEMS )
+        {
+          CG_Printf( S_COLOR_RED "ERROR: maximum number of particle systems (%d) reached\n",
+              MAX_BASEPARTICLE_SYSTEMS );
+          return qfalse;
+        }
+        else
+          numBaseParticleSystems++;
+
+        continue;
+      }
+      else
+      {
+        CG_Printf( S_COLOR_RED "ERROR: unamed particle system\n" );
+        return qfalse;
+      }
+    }
+
+    if( !psNameSet )
+    {
+      Q_strncpyz( psName, token, sizeof( psName ) );
+      psNameSet = qtrue;
+    }
+    else
+    {
+      CG_Printf( S_COLOR_RED "ERROR: particle system already named\n" );
+      return qfalse;
+    }
+  }
+
+  return qtrue;
+}
+
+
+/*
+===============
+CG_LoadParticleSystems
+
+Load particle systems from .particle files
+===============
+*/
+void CG_LoadParticleSystems( void )
+{
+  int   i, j, numFiles, fileLen;
+  char  fileList[ MAX_PARTICLE_FILES * MAX_QPATH ];
+  char  fileName[ MAX_QPATH ];
+  char  *filePtr;
+
+  //clear out the old
+  numBaseParticleSystems = 0;
+  numBaseParticleEjectors = 0;
+  numBaseParticles = 0;
+
+  for( i = 0; i < MAX_BASEPARTICLE_SYSTEMS; i++ )
+  {
+    baseParticleSystem_t  *bps = &baseParticleSystems[ i ];
+    memset( bps, 0, sizeof( baseParticleSystem_t ) );
+  }
+
+  for( i = 0; i < MAX_BASEPARTICLE_EJECTORS; i++ )
+  {
+    baseParticleEjector_t  *bpe = &baseParticleEjectors[ i ];
+    memset( bpe, 0, sizeof( baseParticleEjector_t ) );
+  }
+
+  for( i = 0; i < MAX_BASEPARTICLES; i++ )
+  {
+    baseParticle_t  *bp = &baseParticles[ i ];
+    memset( bp, 0, sizeof( baseParticle_t ) );
+  }
+
+
+  //and bring in the new
+  numFiles = trap_FS_GetFileList( "scripts", ".particle",
+      fileList, MAX_PARTICLE_FILES * MAX_QPATH );
+  filePtr = fileList;
+
+  for( i = 0; i < numFiles; i++, filePtr += fileLen + 1 )
+  {
+    fileLen = strlen( filePtr );
+    strcpy( fileName, "scripts/" );
+    strcat( fileName, filePtr );
+    //CG_Printf( "...loading '%s'\n", fileName );
+    CG_ParseParticleFile( fileName );
+  }
+
+  //connect any child systems to their psHandle
+  for( i = 0; i < numBaseParticles; i++ )
+  {
+    baseParticle_t  *bp = &baseParticles[ i ];
+
+    if( bp->childSystemName[ 0 ] )
+    {
+      //particle class has a child, resolve the name
+      for( j = 0; j < numBaseParticleSystems; j++ )
+      {
+        baseParticleSystem_t  *bps = &baseParticleSystems[ j ];
+
+        if( !Q_stricmp( bps->name, bp->childSystemName ) )
+        {
+          //FIXME: add checks for cycles and infinite children
+
+          bp->childSystemHandle = j + 1;
+
+          break;
+        }
+      }
+
+      if( j == numBaseParticleSystems )
+      {
+        //couldn't find named particle system
+        CG_Printf( S_COLOR_YELLOW "WARNING: failed to find child %s\n", bp->childSystemName );
+        bp->childSystemName[ 0 ] = '\0';
+      }
+    }
+
+    if( bp->onDeathSystemName[ 0 ] )
+    {
+      //particle class has a child, resolve the name
+      for( j = 0; j < numBaseParticleSystems; j++ )
+      {
+        baseParticleSystem_t  *bps = &baseParticleSystems[ j ];
+
+        if( !Q_stricmp( bps->name, bp->onDeathSystemName ) )
+        {
+          //FIXME: add checks for cycles and infinite children
+
+          bp->onDeathSystemHandle = j + 1;
+
+          break;
+        }
+      }
+
+      if( j == numBaseParticleSystems )
+      {
+        //couldn't find named particle system
+        CG_Printf( S_COLOR_YELLOW "WARNING: failed to find onDeath system %s\n", bp->onDeathSystemName );
+        bp->onDeathSystemName[ 0 ] = '\0';
+      }
+    }
+  }
+}
+
+/*
+===============
+CG_SetParticleSystemNormal
+===============
+*/
+void CG_SetParticleSystemNormal( particleSystem_t *ps, vec3_t normal )
+{
+  if( ps == NULL || !ps->valid )
+  {
+    CG_Printf( S_COLOR_YELLOW "WARNING: tried to modify a NULL particle system\n" );
+    return;
+  }
+
+  ps->normalValid = qtrue;
+  VectorCopy( normal, ps->normal );
+  VectorNormalize( ps->normal );
+}
+
+
+/*
+===============
+CG_DestroyParticleSystem
+
+Destroy a particle system
+
+This doesn't actually invalidate anything, it just stops
+particle ejectors from producing new particles so the
+garbage collector will eventually remove this system.
+However is does set the pointer to NULL so the user is
+unable to manipulate this particle system any longer.
+===============
+*/
+void CG_DestroyParticleSystem( particleSystem_t **ps )
+{
+  int               i;
+  particleEjector_t *pe;
+
+  if( *ps == NULL || !(*ps)->valid )
+  {
+    CG_Printf( S_COLOR_YELLOW "WARNING: tried to destroy a NULL particle system\n" );
+    return;
+  }
+
+  if( cg_debugParticles.integer >= 1 )
+    CG_Printf( "PS destroyed\n" );
+
+  for( i = 0; i < MAX_PARTICLE_EJECTORS; i++ )
+  {
+    pe = &particleEjectors[ i ];
+
+    if( pe->valid && pe->parent == *ps )
+      pe->totalParticles = pe->count = 0;
+  }
+
+  *ps = NULL;
+}
+
+/*
+===============
+CG_IsParticleSystemInfinite
+
+Test a particle system for 'count infinite' ejectors
+===============
+*/
+qboolean CG_IsParticleSystemInfinite( particleSystem_t *ps )
+{
+  int               i;
+  particleEjector_t *pe;
+
+  if( ps == NULL )
+  {
+    CG_Printf( S_COLOR_YELLOW "WARNING: tried to test a NULL particle system\n" );
+    return qfalse;
+  }
+
+  if( !ps->valid )
+  {
+    CG_Printf( S_COLOR_YELLOW "WARNING: tried to test an invalid particle system\n" );
+    return qfalse;
+  }
+
+  //don't bother checking already invalid systems
+  if( !ps->valid )
+    return qfalse;
+
+  for( i = 0; i < MAX_PARTICLE_EJECTORS; i++ )
+  {
+    pe = &particleEjectors[ i ];
+
+    if( pe->valid && pe->parent == ps )
+    {
+      if( pe->totalParticles == PARTICLES_INFINITE )
+        return qtrue;
+    }
+  }
+
+  return qfalse;
+}
+
+/*
+===============
+CG_IsParticleSystemValid
+
+Test a particle system for validity
+===============
+*/
+qboolean CG_IsParticleSystemValid( particleSystem_t **ps )
+{
+  if( *ps == NULL || ( *ps && !(*ps)->valid ) )
+  {
+    if( *ps && !(*ps)->valid )
+      *ps = NULL;
+
+    return qfalse;
+  }
+
+  return qtrue;
+}
+
+/*
+===============
+CG_GarbageCollectParticleSystems
+
+Destroy inactive particle systems
+===============
+*/
+static void CG_GarbageCollectParticleSystems( void )
+{
+  int               i, j, count;
+  particleSystem_t  *ps;
+  particleEjector_t *pe;
+  int               centNum;
+
+  for( i = 0; i < MAX_PARTICLE_SYSTEMS; i++ )
+  {
+    ps = &particleSystems[ i ];
+    count = 0;
+
+    //don't bother checking already invalid systems
+    if( !ps->valid )
+      continue;
+
+    for( j = 0; j < MAX_PARTICLE_EJECTORS; j++ )
+    {
+      pe = &particleEjectors[ j ];
+
+      if( pe->valid && pe->parent == ps )
+        count++;
+    }
+
+    if( !count )
+      ps->valid = qfalse;
+
+    //check systems where the parent cent has left the PVS
+    //( local player entity is always valid )
+    if( ( centNum = CG_AttachmentCentNum( &ps->attachment ) ) >= 0 &&
+        centNum != cg.snap->ps.clientNum )
+    {
+      if( !cg_entities[ centNum ].valid )
+        ps->lazyRemove = qtrue;
+    }
+
+    if( cg_debugParticles.integer >= 1 && !ps->valid )
+      CG_Printf( "PS %s garbage collected\n", ps->class->name );
+  }
+}
+
+
+/*
+===============
+CG_CalculateTimeFrac
+
+Calculate the fraction of time passed
+===============
+*/
+static float CG_CalculateTimeFrac( int birth, int life, int delay )
+{
+  float frac;
+
+  frac = ( (float)cg.time - (float)( birth + delay ) ) / (float)( life - delay );
+
+  if( frac < 0.0f )
+    frac = 0.0f;
+  else if( frac > 1.0f )
+    frac = 1.0f;
+
+  return frac;
+}
+
+/*
+===============
+CG_EvaluateParticlePhysics
+
+Compute the physics on a specific particle
+===============
+*/
+static void CG_EvaluateParticlePhysics( particle_t *p )
+{
+  particleSystem_t  *ps = p->parent->parent;
+  baseParticle_t    *bp = p->class;
+  vec3_t            acceleration, newOrigin;
+  vec3_t            mins, maxs;
+  float             deltaTime, bounce, radius, dot;
+  trace_t           trace;
+  vec3_t            transform[ 3 ];
+
+  if( p->atRest )
+  {
+    VectorClear( p->velocity );
+    return;
+  }
+
+  switch( bp->accMoveType )
+  {
+    case PMT_STATIC:
+      if( bp->accMoveValues.dirType == PMD_POINT )
+        VectorSubtract( bp->accMoveValues.point, p->origin, acceleration );
+      else if( bp->accMoveValues.dirType == PMD_LINEAR )
+        VectorCopy( bp->accMoveValues.dir, acceleration );
+
+      break;
+
+    case PMT_STATIC_TRANSFORM:
+      if( !CG_AttachmentAxis( &ps->attachment, transform ) )
+        return;
+
+      if( bp->accMoveValues.dirType == PMD_POINT )
+      {
+        vec3_t transPoint;
+
+        VectorMatrixMultiply( bp->accMoveValues.point, transform, transPoint );
+        VectorSubtract( transPoint, p->origin, acceleration );
+      }
+      else if( bp->accMoveValues.dirType == PMD_LINEAR )
+        VectorMatrixMultiply( bp->accMoveValues.dir, transform, acceleration );
+      break;
+
+    case PMT_TAG:
+    case PMT_CENT_ANGLES:
+      if( bp->accMoveValues.dirType == PMD_POINT )
+      {
+        vec3_t point;
+
+        if( !CG_AttachmentPoint( &ps->attachment, point ) )
+          return;
+
+        VectorSubtract( point, p->origin, acceleration );
+      }
+      else if( bp->accMoveValues.dirType == PMD_LINEAR )
+      {
+        if( !CG_AttachmentDir( &ps->attachment, acceleration ) )
+          return;
+      }
+      break;
+
+    case PMT_NORMAL:
+      if( !ps->normalValid )
+        return;
+
+      VectorCopy( ps->normal, acceleration );
+
+      break;
+  }
+
+#define MAX_ACC_RADIUS 1000.0f
+
+  if( bp->accMoveValues.dirType == PMD_POINT )
+  {
+    //FIXME: so this fall off is a bit... odd -- it works..
+    float r2 = DotProduct( acceleration, acceleration ); // = radius^2
+    float scale = ( MAX_ACC_RADIUS - r2 ) / MAX_ACC_RADIUS;
+
+    if( scale > 1.0f )
+      scale = 1.0f;
+    else if( scale < 0.1f )
+      scale = 0.1f;
+
+    scale *= CG_RandomiseValue( bp->accMoveValues.mag, bp->accMoveValues.magRandFrac );
+
+    VectorNormalize( acceleration );
+    CG_SpreadVector( acceleration, bp->accMoveValues.dirRandAngle );
+    VectorScale( acceleration, scale, acceleration );
+  }
+  else if( bp->accMoveValues.dirType == PMD_LINEAR )
+  {
+    VectorNormalize( acceleration );
+    CG_SpreadVector( acceleration, bp->accMoveValues.dirRandAngle );
+    VectorScale( acceleration,
+                 CG_RandomiseValue( bp->accMoveValues.mag, bp->accMoveValues.magRandFrac ),
+                 acceleration );
+  }
+
+  // Some particles have a visual radius that differs from their collision radius
+  if( bp->physicsRadius )
+    radius = bp->physicsRadius;
+  else
+    radius = CG_LerpValues( p->radius.initial, p->radius.final,
+                            CG_CalculateTimeFrac( p->birthTime, p->lifeTime,
+                                                  p->radius.delay ) );
+
+  VectorSet( mins, -radius, -radius, -radius );
+  VectorSet( maxs, radius, radius, radius );
+
+  bounce = CG_RandomiseValue( bp->bounceFrac, bp->bounceFracRandFrac );
+
+  deltaTime = (float)( cg.time - p->lastEvalTime ) * 0.001;
+  VectorMA( p->velocity, deltaTime, acceleration, p->velocity );
+  VectorMA( p->origin, deltaTime, p->velocity, newOrigin );
+  p->lastEvalTime = cg.time;
+
+  // we're not doing particle physics, but at least cull them in solids
+  if( !cg_bounceParticles.integer )
+  {
+    int contents = trap_CM_PointContents( newOrigin, 0 ); 
+
+    if( ( contents & CONTENTS_SOLID ) || ( contents & CONTENTS_NODROP ) )
+      CG_DestroyParticle( p, NULL );
+    else 
+      VectorCopy( newOrigin, p->origin );
+    return;
+  }
+
+  CG_Trace( &trace, p->origin, mins, maxs, newOrigin,
+      CG_AttachmentCentNum( &ps->attachment ), CONTENTS_SOLID );
+
+  //not hit anything or not a collider
+  if( trace.fraction == 1.0f || bounce == 0.0f )
+  {
+    VectorCopy( newOrigin, p->origin );
+    return;
+  }
+
+  //remove particles that get into a CONTENTS_NODROP brush
+  if( ( trap_CM_PointContents( trace.endpos, 0 ) & CONTENTS_NODROP ) ||
+      ( bp->cullOnStartSolid && trace.startsolid ) )
+  {
+    CG_DestroyParticle( p, NULL );
+    return;
+  }
+  else if( bp->bounceCull )
+  {
+    CG_DestroyParticle( p, trace.plane.normal );
+    return;
+  }
+
+  //reflect the velocity on the trace plane
+  dot = DotProduct( p->velocity, trace.plane.normal );
+  VectorMA( p->velocity, -2.0f * dot, trace.plane.normal, p->velocity );
+
+  VectorScale( p->velocity, bounce, p->velocity );
+
+  if( trace.plane.normal[ 2 ] > 0.5f &&
+      ( p->velocity[ 2 ] < 40.0f ||
+        p->velocity[ 2 ] < -cg.frametime * p->velocity[ 2 ] ) )
+    p->atRest = qtrue;
+
+  if( bp->bounceMarkName[ 0 ] && p->bounceMarkCount > 0 )
+  {
+    CG_ImpactMark( bp->bounceMark, trace.endpos, trace.plane.normal,
+        random( ) * 360, 1.0f, 1.0f, 1.0f, 1.0f, qtrue, bp->bounceMarkRadius, qfalse );
+    p->bounceMarkCount--;
+  }
+
+  if( bp->bounceSoundName[ 0 ] && p->bounceSoundCount > 0 )
+  {
+    trap_S_StartSound( trace.endpos, ENTITYNUM_WORLD, CHAN_AUTO, bp->bounceSound );
+    p->bounceSoundCount--;
+  }
+
+  VectorCopy( trace.endpos, p->origin );
+}
+
+
+#define GETKEY(x,y) (((x)>>y)&0xFF)
+
+/*
+===============
+CG_Radix
+===============
+*/
+static void CG_Radix( int bits, int size, particle_t **source, particle_t **dest )
+{
+  int count[ 256 ];
+  int index[ 256 ];
+  int i;
+
+  memset( count, 0, sizeof( count ) );
+
+  for( i = 0; i < size; i++ )
+    count[ GETKEY( source[ i ]->sortKey, bits ) ]++;
+
+  index[ 0 ] = 0;
+
+  for( i = 1; i < 256; i++ )
+    index[ i ] = index[ i - 1 ] + count[ i - 1 ];
+
+  for( i = 0; i < size; i++ )
+    dest[ index[ GETKEY( source[ i ]->sortKey, bits ) ]++ ] = source[ i ];
+}
+
+/*
+===============
+CG_RadixSort
+
+Radix sort with 4 byte size buckets
+===============
+*/
+static void CG_RadixSort( particle_t **source, particle_t **temp, int size )
+{
+  CG_Radix( 0,   size, source, temp );
+  CG_Radix( 8,   size, temp, source );
+  CG_Radix( 16,  size, source, temp );
+  CG_Radix( 24,  size, temp, source );
+}
+
+/*
+===============
+CG_CompactAndSortParticles
+
+Depth sort the particles
+===============
+*/
+static void CG_CompactAndSortParticles( void )
+{
+  int     i, j = 0;
+  int     numParticles;
+  vec3_t  delta;
+
+  for( i = 0; i < MAX_PARTICLES; i++ )
+    sortedParticles[ i ] = &particles[ i ];
+
+  if( !cg_depthSortParticles.integer )
+    return;
+
+  for( i = MAX_PARTICLES - 1; i >= 0; i-- )
+  {
+    if( sortedParticles[ i ]->valid )
+    {
+      //find the first hole
+      while( j < MAX_PARTICLES && sortedParticles[ j ]->valid )
+        j++;
+
+      //no more holes
+      if( j >= i )
+        break;
+
+      sortedParticles[ j ] = sortedParticles[ i ];
+    }
+  }
+
+  numParticles = i;
+
+  //set sort keys
+  for( i = 0; i < numParticles; i++ )
+  {
+    VectorSubtract( sortedParticles[ i ]->origin, cg.refdef.vieworg, delta );
+    sortedParticles[ i ]->sortKey = (int)DotProduct( delta, delta );
+  }
+
+  CG_RadixSort( sortedParticles, radixBuffer, numParticles );
+
+  //FIXME: wtf?
+  //reverse order of particles array
+  for( i = 0; i < numParticles; i++ )
+    radixBuffer[ i ] = sortedParticles[ numParticles - i - 1 ];
+
+  for( i = 0; i < numParticles; i++ )
+    sortedParticles[ i ] = radixBuffer[ i ];
+}
+
+/*
+===============
+CG_RenderParticle
+
+Actually render a particle
+===============
+*/
+static void CG_RenderParticle( particle_t *p )
+{
+  refEntity_t           re;
+  float                 timeFrac, scale;
+  int                   index;
+  baseParticle_t        *bp = p->class;
+  particleSystem_t      *ps = p->parent->parent;
+  baseParticleSystem_t  *bps = ps->class;
+  vec3_t                alight, dlight, lightdir;
+  int                   i;
+  vec3_t                up = { 0.0f, 0.0f, 1.0f };
+
+  memset( &re, 0, sizeof( refEntity_t ) );
+
+  timeFrac = CG_CalculateTimeFrac( p->birthTime, p->lifeTime, 0 );
+
+  scale = CG_LerpValues( p->radius.initial,
+                    p->radius.final,
+                    CG_CalculateTimeFrac( p->birthTime,
+                                          p->lifeTime,
+                                          p->radius.delay ) );
+
+  re.shaderTime = p->birthTime / 1000.0f;
+
+  if( bp->numFrames )       //shader based
+  {
+    re.reType = RT_SPRITE;
+
+    //apply environmental lighting to the particle
+    if( bp->realLight )
+    {
+      trap_R_LightForPoint( p->origin, alight, dlight, lightdir );
+      for( i = 0; i <= 2; i++ )
+        re.shaderRGBA[ i ] = (byte)alight[ i ];
+    }
+    else
+    {
+      vec3_t  colorRange;
+
+      VectorSubtract( bp->finalColor,
+          bp->initialColor, colorRange );
+
+      VectorMA( bp->initialColor,
+          CG_CalculateTimeFrac( p->birthTime,
+            p->lifeTime,
+            p->colorDelay ),
+          colorRange, re.shaderRGBA );
+    }
+
+    re.shaderRGBA[ 3 ] = (byte)( (float)0xFF *
+                         CG_LerpValues( p->alpha.initial,
+                               p->alpha.final,
+                               CG_CalculateTimeFrac( p->birthTime,
+                                                     p->lifeTime,
+                                                     p->alpha.delay ) ) );
+
+    re.radius = scale;
+
+    re.rotation = CG_LerpValues( p->rotation.initial,
+                        p->rotation.final,
+                        CG_CalculateTimeFrac( p->birthTime,
+                                              p->lifeTime,
+                                              p->rotation.delay ) );
+
+    // if the view would be "inside" the sprite, kill the sprite
+    // so it doesn't add too much overdraw
+    if( Distance( p->origin, cg.refdef.vieworg ) < re.radius && bp->overdrawProtection )
+      return;
+
+    if( bp->framerate == 0.0f )
+    {
+      //sync animation time to lifeTime of particle
+      index = (int)( timeFrac * ( bp->numFrames + 1 ) );
+
+      if( index >= bp->numFrames )
+        index = bp->numFrames - 1;
+
+      re.customShader = bp->shaders[ index ];
+    }
+    else
+    {
+      //looping animation
+      index = (int)( bp->framerate * timeFrac * p->lifeTime * 0.001 ) % bp->numFrames;
+      re.customShader = bp->shaders[ index ];
+    }
+
+  }
+  else if( bp->numModels )  //model based
+  {
+    re.reType = RT_MODEL;
+
+    re.hModel = p->model;
+
+    if( p->atRest )
+      AxisCopy( p->lastAxis, re.axis );
+    else
+    {
+      // convert direction of travel into axis
+      VectorNormalize2( p->velocity, re.axis[ 0 ] );
+
+      if( re.axis[ 0 ][ 0 ] == 0.0f && re.axis[ 0 ][ 1 ] == 0.0f )
+        AxisCopy( axisDefault, re.axis );
+      else
+      {
+        ProjectPointOnPlane( re.axis[ 2 ], up, re.axis[ 0 ] );
+        VectorNormalize( re.axis[ 2 ] );
+        CrossProduct( re.axis[ 2 ], re.axis[ 0 ], re.axis[ 1 ] );
+      }
+
+      AxisCopy( re.axis, p->lastAxis );
+    }
+
+    if( scale != 1.0f )
+    {
+      VectorScale( re.axis[ 0 ], scale, re.axis[ 0 ] );
+      VectorScale( re.axis[ 1 ], scale, re.axis[ 1 ] );
+      VectorScale( re.axis[ 2 ], scale, re.axis[ 2 ] );
+      re.nonNormalizedAxes = qtrue;
+    }
+    else
+      re.nonNormalizedAxes = qfalse;
+
+    p->lf.animation = &bp->modelAnimation;
+
+    //run animation
+    CG_RunLerpFrame( &p->lf, 1.0f );
+
+    re.oldframe = p->lf.oldFrame;
+    re.frame    = p->lf.frame;
+    re.backlerp = p->lf.backlerp;
+  }
+
+  if( bps->thirdPersonOnly &&
+      CG_AttachmentCentNum( &ps->attachment ) == cg.snap->ps.clientNum &&
+      !cg.renderingThirdPerson )
+    re.renderfx |= RF_THIRD_PERSON;
+
+  if( bp->dynamicLight && !( re.renderfx & RF_THIRD_PERSON ) )
+  {
+    trap_R_AddLightToScene( p->origin,
+      CG_LerpValues( p->dLightRadius.initial, p->dLightRadius.final,
+        CG_CalculateTimeFrac( p->birthTime, p->lifeTime, p->dLightRadius.delay ) ),
+        (float)bp->dLightColor[ 0 ] / (float)0xFF,
+        (float)bp->dLightColor[ 1 ] / (float)0xFF,
+        (float)bp->dLightColor[ 2 ] / (float)0xFF );
+  }
+
+  VectorCopy( p->origin, re.origin );
+
+  trap_R_AddRefEntityToScene( &re );
+}
+
+/*
+===============
+CG_AddParticles
+
+Add particles to the scene
+===============
+*/
+void CG_AddParticles( void )
+{
+  int           i;
+  particle_t    *p;
+  int           numPS = 0, numPE = 0, numP = 0;
+
+  //remove expired particle systems
+  CG_GarbageCollectParticleSystems( );
+
+  //check each ejector and introduce any new particles
+  CG_SpawnNewParticles( );
+
+  //sorting
+  CG_CompactAndSortParticles( );
+
+  for( i = 0; i < MAX_PARTICLES; i++ )
+  {
+    p = sortedParticles[ i ];
+
+    if( p->valid )
+    {
+      if( p->birthTime + p->lifeTime > cg.time )
+      {
+        //particle is active
+        CG_EvaluateParticlePhysics( p );
+        CG_RenderParticle( p );
+      }
+      else
+        CG_DestroyParticle( p, NULL );
+    }
+  }
+
+  if( cg_debugParticles.integer >= 2 )
+  {
+    for( i = 0; i < MAX_PARTICLE_SYSTEMS; i++ )
+      if( particleSystems[ i ].valid )
+        numPS++;
+
+    for( i = 0; i < MAX_PARTICLE_EJECTORS; i++ )
+      if( particleEjectors[ i ].valid )
+        numPE++;
+
+    for( i = 0; i < MAX_PARTICLES; i++ )
+      if( particles[ i ].valid )
+        numP++;
+
+    CG_Printf( "PS: %d  PE: %d  P: %d\n", numPS, numPE, numP );
+  }
+}
+
+/*
+===============
+CG_ParticleSystemEntity
+
+Particle system entity client code
+===============
+*/
+void CG_ParticleSystemEntity( centity_t *cent )
+{
+  entityState_t *es;
+
+  es = &cent->currentState;
+
+  if( es->eFlags & EF_NODRAW )
+  {
+    if( CG_IsParticleSystemValid( &cent->entityPS ) && CG_IsParticleSystemInfinite( cent->entityPS ) )
+      CG_DestroyParticleSystem( &cent->entityPS );
+
+    return;
+  }
+
+  if( !CG_IsParticleSystemValid( &cent->entityPS ) && !cent->entityPSMissing )
+  {
+    cent->entityPS = CG_SpawnNewParticleSystem( cgs.gameParticleSystems[ es->modelindex ] );
+
+    if( CG_IsParticleSystemValid( &cent->entityPS ) )
+    {
+      CG_SetAttachmentPoint( &cent->entityPS->attachment, cent->lerpOrigin );
+      CG_SetAttachmentCent( &cent->entityPS->attachment, cent );
+      CG_AttachToPoint( &cent->entityPS->attachment );
+    }
+    else
+      cent->entityPSMissing = qtrue;
+  }
+}
+
+static particleSystem_t *testPS;
+static qhandle_t        testPSHandle;
+
+/*
+===============
+CG_DestroyTestPS_f
+
+Destroy the test a particle system
+===============
+*/
+void CG_DestroyTestPS_f( void )
+{
+  if( CG_IsParticleSystemValid( &testPS ) )
+    CG_DestroyParticleSystem( &testPS );
+}
+
+/*
+===============
+CG_TestPS_f
+
+Test a particle system
+===============
+*/
+void CG_TestPS_f( void )
+{
+  vec3_t  origin;
+  vec3_t  up = { 0.0f, 0.0f, 1.0f };
+  char    psName[ MAX_QPATH ];
+
+  if( trap_Argc( ) < 2 )
+    return;
+
+  Q_strncpyz( psName, CG_Argv( 1 ), MAX_QPATH );
+  testPSHandle = CG_RegisterParticleSystem( psName );
+
+  if( testPSHandle )
+  {
+    CG_DestroyTestPS_f( );
+
+    testPS = CG_SpawnNewParticleSystem( testPSHandle );
+
+    VectorMA( cg.refdef.vieworg, 100, cg.refdef.viewaxis[ 0 ], origin );
+
+    if( CG_IsParticleSystemValid( &testPS ) )
+    {
+      CG_SetAttachmentPoint( &testPS->attachment, origin );
+      CG_SetParticleSystemNormal( testPS, up );
+      CG_AttachToPoint( &testPS->attachment );
+    }
+  }
+}
diff --git a/src/cgame/cg_players.c b/src/cgame/cg_players.c
new file mode 100644
index 0000000..32f3fb8
--- /dev/null
+++ b/src/cgame/cg_players.c
@@ -0,0 +1,2512 @@
+/*
+===========================================================================
+Copyright (C) 1999-2005 Id Software, Inc.
+Copyright (C) 2000-2009 Darklegion Development
+
+This file is part of Tremulous.
+
+Tremulous 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.
+
+Tremulous 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 Tremulous; if not, write to the Free Software
+Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
+===========================================================================
+*/
+
+// cg_players.c -- handle the media and animation for player entities
+
+
+#include "cg_local.h"
+
+char *cg_customSoundNames[ MAX_CUSTOM_SOUNDS ] =
+{
+  "*death1.wav",
+  "*death2.wav",
+  "*death3.wav",
+  "*jump1.wav",
+  "*pain25_1.wav",
+  "*pain50_1.wav",
+  "*pain75_1.wav",
+  "*pain100_1.wav",
+  "*falling1.wav",
+  "*gasp.wav",
+  "*drown.wav",
+  "*fall1.wav",
+  "*taunt.wav"
+};
+
+
+/*
+================
+CG_CustomSound
+
+================
+*/
+sfxHandle_t CG_CustomSound( int clientNum, const char *soundName )
+{
+  clientInfo_t  *ci;
+  int           i;
+
+  if( soundName[ 0 ] != '*' )
+    return trap_S_RegisterSound( soundName, qfalse );
+
+  if( clientNum < 0 || clientNum >= MAX_CLIENTS )
+    clientNum = 0;
+
+  ci = &cgs.clientinfo[ clientNum ];
+
+  for( i = 0; i < MAX_CUSTOM_SOUNDS && cg_customSoundNames[ i ]; i++ )
+  {
+    if( !strcmp( soundName, cg_customSoundNames[ i ] ) )
+      return ci->sounds[ i ];
+  }
+
+  CG_Error( "Unknown custom sound: %s", soundName );
+  return 0;
+}
+
+
+
+/*
+=============================================================================
+
+CLIENT INFO
+
+=============================================================================
+*/
+
+/*
+======================
+CG_ParseAnimationFile
+
+Read a configuration file containing animation coutns and rates
+models/players/visor/animation.cfg, etc
+======================
+*/
+static qboolean CG_ParseAnimationFile( const char *filename, clientInfo_t *ci )
+{
+  char          *text_p, *prev;
+  int           len;
+  int           i;
+  char          *token;
+  float         fps;
+  int           skip;
+  char          text[ 20000 ];
+  fileHandle_t  f;
+  animation_t   *animations;
+
+  animations = ci->animations;
+
+  // load the file
+  len = trap_FS_FOpenFile( filename, &f, FS_READ );
+  if( len < 0 )
+    return qfalse;
+
+  if( len == 0 || len >= sizeof( text ) - 1 )
+  {
+    CG_Printf( "File %s is %s\n", filename, len == 0 ? "empty" : "too long" );
+    trap_FS_FCloseFile( f );
+    return qfalse;
+  }
+
+  trap_FS_Read( text, len, f );
+  text[ len ] = 0;
+  trap_FS_FCloseFile( f );
+
+  // parse the text
+  text_p = text;
+  skip = 0; // quite the compiler warning
+
+  ci->footsteps = FOOTSTEP_NORMAL;
+  VectorClear( ci->headOffset );
+  ci->gender = GENDER_MALE;
+  ci->fixedlegs = qfalse;
+  ci->fixedtorso = qfalse;
+  ci->nonsegmented = qfalse;
+
+  // read optional parameters
+  while( 1 )
+  {
+    prev = text_p;  // so we can unget
+    token = COM_Parse( &text_p );
+
+    if( !token )
+      break;
+
+    if( !Q_stricmp( token, "footsteps" ) )
+    {
+      token = COM_Parse( &text_p );
+      if( !token )
+        break;
+
+      if( !Q_stricmp( token, "default" ) || !Q_stricmp( token, "normal" ) )
+        ci->footsteps = FOOTSTEP_NORMAL;
+      else if( !Q_stricmp( token, "flesh" ) )
+        ci->footsteps = FOOTSTEP_FLESH;
+      else if( !Q_stricmp( token, "none" ) )
+        ci->footsteps = FOOTSTEP_NONE;
+      else if( !Q_stricmp( token, "custom" ) )
+        ci->footsteps = FOOTSTEP_CUSTOM;
+      else
+        CG_Printf( "Bad footsteps parm in %s: %s\n", filename, token );
+
+      continue;
+    }
+    else if( !Q_stricmp( token, "headoffset" ) )
+    {
+      for( i = 0 ; i < 3 ; i++ )
+      {
+        token = COM_Parse( &text_p );
+        if( !token )
+          break;
+
+        ci->headOffset[ i ] = atof( token );
+      }
+
+      continue;
+    }
+    else if( !Q_stricmp( token, "sex" ) )
+    {
+      token = COM_Parse( &text_p );
+
+      if( !token )
+        break;
+
+      if( token[ 0 ] == 'f' || token[ 0 ] == 'F' )
+        ci->gender = GENDER_FEMALE;
+      else if( token[ 0 ] == 'n' || token[ 0 ] == 'N' )
+        ci->gender = GENDER_NEUTER;
+      else
+        ci->gender = GENDER_MALE;
+
+      continue;
+    }
+    else if( !Q_stricmp( token, "fixedlegs" ) )
+    {
+      ci->fixedlegs = qtrue;
+      continue;
+    }
+    else if( !Q_stricmp( token, "fixedtorso" ) )
+    {
+      ci->fixedtorso = qtrue;
+      continue;
+    }
+    else if( !Q_stricmp( token, "nonsegmented" ) )
+    {
+      ci->nonsegmented = qtrue;
+      continue;
+    }
+
+    // if it is a number, start parsing animations
+    if( token[ 0 ] >= '0' && token[ 0 ] <= '9' )
+    {
+      text_p = prev;  // unget the token
+      break;
+    }
+
+    Com_Printf( "unknown token '%s' is %s\n", token, filename );
+  }
+
+  if( !ci->nonsegmented )
+  {
+    // read information for each frame
+    for( i = 0; i < MAX_PLAYER_ANIMATIONS; i++ )
+    {
+      token = COM_Parse( &text_p );
+
+      if( !*token )
+      {
+        if( i >= TORSO_GETFLAG && i <= TORSO_NEGATIVE )
+        {
+          animations[ i ].firstFrame = animations[ TORSO_GESTURE ].firstFrame;
+          animations[ i ].frameLerp = animations[ TORSO_GESTURE ].frameLerp;
+          animations[ i ].initialLerp = animations[ TORSO_GESTURE ].initialLerp;
+          animations[ i ].loopFrames = animations[ TORSO_GESTURE ].loopFrames;
+          animations[ i ].numFrames = animations[ TORSO_GESTURE ].numFrames;
+          animations[ i ].reversed = qfalse;
+          animations[ i ].flipflop = qfalse;
+          continue;
+        }
+
+        break;
+      }
+
+      animations[ i ].firstFrame = atoi( token );
+
+      // leg only frames are adjusted to not count the upper body only frames
+      if( i == LEGS_WALKCR )
+        skip = animations[ LEGS_WALKCR ].firstFrame - animations[ TORSO_GESTURE ].firstFrame;
+
+      if( i >= LEGS_WALKCR && i<TORSO_GETFLAG )
+        animations[ i ].firstFrame -= skip;
+
+      token = COM_Parse( &text_p );
+      if( !*token )
+        break;
+
+      animations[ i ].numFrames = atoi( token );
+      animations[ i ].reversed = qfalse;
+      animations[ i ].flipflop = qfalse;
+
+      // if numFrames is negative the animation is reversed
+      if( animations[ i ].numFrames < 0 )
+      {
+        animations[ i ].numFrames = -animations[ i ].numFrames;
+        animations[ i ].reversed = qtrue;
+      }
+
+      token = COM_Parse( &text_p );
+
+      if( !*token )
+        break;
+
+      animations[ i ].loopFrames = atoi( token );
+
+      token = COM_Parse( &text_p );
+
+      if( !*token )
+        break;
+
+      fps = atof( token );
+      if( fps == 0 )
+        fps = 1;
+
+      animations[ i ].frameLerp = 1000 / fps;
+      animations[ i ].initialLerp = 1000 / fps;
+    }
+
+    if( i != MAX_PLAYER_ANIMATIONS )
+    {
+      CG_Printf( "Error parsing animation file: %s", filename );
+      return qfalse;
+    }
+    // crouch backward animation
+    memcpy( &animations[ LEGS_BACKCR ], &animations[ LEGS_WALKCR ], sizeof( animation_t ) );
+    animations[ LEGS_BACKCR ].reversed = qtrue;
+    // walk backward animation
+    memcpy( &animations[ LEGS_BACKWALK ], &animations[ LEGS_WALK ], sizeof( animation_t ) );
+    animations[ LEGS_BACKWALK ].reversed = qtrue;
+    // flag moving fast
+    animations[ FLAG_RUN ].firstFrame = 0;
+    animations[ FLAG_RUN ].numFrames = 16;
+    animations[ FLAG_RUN ].loopFrames = 16;
+    animations[ FLAG_RUN ].frameLerp = 1000 / 15;
+    animations[ FLAG_RUN ].initialLerp = 1000 / 15;
+    animations[ FLAG_RUN ].reversed = qfalse;
+    // flag not moving or moving slowly
+    animations[ FLAG_STAND ].firstFrame = 16;
+    animations[ FLAG_STAND ].numFrames = 5;
+    animations[ FLAG_STAND ].loopFrames = 0;
+    animations[ FLAG_STAND ].frameLerp = 1000 / 20;
+    animations[ FLAG_STAND ].initialLerp = 1000 / 20;
+    animations[ FLAG_STAND ].reversed = qfalse;
+    // flag speeding up
+    animations[ FLAG_STAND2RUN ].firstFrame = 16;
+    animations[ FLAG_STAND2RUN ].numFrames = 5;
+    animations[ FLAG_STAND2RUN ].loopFrames = 1;
+    animations[ FLAG_STAND2RUN ].frameLerp = 1000 / 15;
+    animations[ FLAG_STAND2RUN ].initialLerp = 1000 / 15;
+    animations[ FLAG_STAND2RUN ].reversed = qtrue;
+  }
+  else
+  {
+    // read information for each frame
+    for( i = 0; i < MAX_NONSEG_PLAYER_ANIMATIONS; i++ )
+    {
+      token = COM_Parse( &text_p );
+
+      if( !*token )
+        break;
+
+      animations[ i ].firstFrame = atoi( token );
+
+      token = COM_Parse( &text_p );
+      if( !*token )
+        break;
+
+      animations[ i ].numFrames = atoi( token );
+      animations[ i ].reversed = qfalse;
+      animations[ i ].flipflop = qfalse;
+
+      // if numFrames is negative the animation is reversed
+      if( animations[ i ].numFrames < 0 )
+      {
+        animations[ i ].numFrames = -animations[ i ].numFrames;
+        animations[ i ].reversed = qtrue;
+      }
+
+      token = COM_Parse( &text_p );
+
+      if( !*token )
+        break;
+
+      animations[ i ].loopFrames = atoi( token );
+
+      token = COM_Parse( &text_p );
+
+      if( !*token )
+        break;
+
+      fps = atof( token );
+      if( fps == 0 )
+        fps = 1;
+
+      animations[ i ].frameLerp = 1000 / fps;
+      animations[ i ].initialLerp = 1000 / fps;
+    }
+
+    if( i != MAX_NONSEG_PLAYER_ANIMATIONS )
+    {
+      CG_Printf( "Error parsing animation file: %s", filename );
+      return qfalse;
+    }
+
+    // walk backward animation
+    memcpy( &animations[ NSPA_WALKBACK ], &animations[ NSPA_WALK ], sizeof( animation_t ) );
+    animations[ NSPA_WALKBACK ].reversed = qtrue;
+  }
+
+  return qtrue;
+}
+
+/*
+==========================
+CG_RegisterClientSkin
+==========================
+*/
+static qboolean CG_RegisterClientSkin( clientInfo_t *ci, const char *modelName, const char *skinName )
+{
+  char filename[ MAX_QPATH ];
+
+  if( !ci->nonsegmented )
+  {
+    Com_sprintf( filename, sizeof( filename ), "models/players/%s/lower_%s.skin", modelName, skinName );
+    ci->legsSkin = trap_R_RegisterSkin( filename );
+    if( !ci->legsSkin )
+      Com_Printf( "Leg skin load failure: %s\n", filename );
+
+    Com_sprintf( filename, sizeof( filename ), "models/players/%s/upper_%s.skin", modelName, skinName );
+    ci->torsoSkin = trap_R_RegisterSkin( filename );
+    if( !ci->torsoSkin )
+      Com_Printf( "Torso skin load failure: %s\n", filename );
+
+    Com_sprintf( filename, sizeof( filename ), "models/players/%s/head_%s.skin", modelName, skinName );
+    ci->headSkin = trap_R_RegisterSkin( filename );
+    if( !ci->headSkin )
+      Com_Printf( "Head skin load failure: %s\n", filename );
+
+    if( !ci->legsSkin || !ci->torsoSkin || !ci->headSkin )
+      return qfalse;
+  }
+  else
+  {
+    Com_sprintf( filename, sizeof( filename ), "models/players/%s/nonseg_%s.skin", modelName, skinName );
+    ci->nonSegSkin = trap_R_RegisterSkin( filename );
+    if( !ci->nonSegSkin )
+      Com_Printf( "Non-segmented skin load failure: %s\n", filename );
+
+    if( !ci->nonSegSkin )
+      return qfalse;
+  }
+
+  return qtrue;
+}
+
+/*
+==========================
+CG_RegisterClientModelname
+==========================
+*/
+static qboolean CG_RegisterClientModelname( clientInfo_t *ci, const char *modelName, const char *skinName )
+{
+  char filename[ MAX_QPATH * 2 ];
+
+  // do this first so the nonsegmented property is set
+  // load the animations
+  Com_sprintf( filename, sizeof( filename ), "models/players/%s/animation.cfg", modelName );
+  if( !CG_ParseAnimationFile( filename, ci ) )
+  {
+    Com_Printf( "Failed to load animation file %s\n", filename );
+    return qfalse;
+  }
+
+  // load cmodels before models so filecache works
+
+  if( !ci->nonsegmented )
+  {
+    Com_sprintf( filename, sizeof( filename ), "models/players/%s/lower.md3", modelName );
+    ci->legsModel = trap_R_RegisterModel( filename );
+    if( !ci->legsModel )
+    {
+      Com_Printf( "Failed to load model file %s\n", filename );
+      return qfalse;
+    }
+
+    Com_sprintf( filename, sizeof( filename ), "models/players/%s/upper.md3", modelName );
+    ci->torsoModel = trap_R_RegisterModel( filename );
+    if( !ci->torsoModel )
+    {
+      Com_Printf( "Failed to load model file %s\n", filename );
+      return qfalse;
+    }
+
+    Com_sprintf( filename, sizeof( filename ), "models/players/%s/head.md3", modelName );
+    ci->headModel = trap_R_RegisterModel( filename );
+    if( !ci->headModel )
+    {
+      Com_Printf( "Failed to load model file %s\n", filename );
+      return qfalse;
+    }
+  }
+  else
+  {
+    Com_sprintf( filename, sizeof( filename ), "models/players/%s/nonseg.md3", modelName );
+    ci->nonSegModel = trap_R_RegisterModel( filename );
+    if( !ci->nonSegModel )
+    {
+      Com_Printf( "Failed to load model file %s\n", filename );
+      return qfalse;
+    }
+  }
+
+  // if any skins failed to load, return failure
+  if( !CG_RegisterClientSkin( ci, modelName, skinName ) )
+  {
+    Com_Printf( "Failed to load skin file: %s : %s\n", modelName, skinName );
+    return qfalse;
+  }
+
+  return qtrue;
+}
+
+
+/*
+===================
+CG_LoadClientInfo
+
+Load it now, taking the disk hits
+===================
+*/
+static void CG_LoadClientInfo( clientInfo_t *ci )
+{
+  const char  *dir;
+  int         i;
+  const char  *s;
+  int         clientNum;
+
+  if( !CG_RegisterClientModelname( ci, ci->modelName, ci->skinName ) )
+    CG_Error( "CG_RegisterClientModelname( %s, %s ) failed", ci->modelName, ci->skinName );
+
+  // sounds
+  dir = ci->modelName;
+
+  for( i = 0; i < MAX_CUSTOM_SOUNDS; i++ )
+  {
+    s = cg_customSoundNames[ i ];
+
+    if( !s )
+      break;
+
+    // fanny about a bit with sounds that are missing
+    if( !CG_FileExists( va( "sound/player/%s/%s", dir, s + 1 ) ) )
+    {
+      //file doesn't exist
+
+      if( i == 11 || i == 8 ) //fall or falling
+      {
+        ci->sounds[ i ] = trap_S_RegisterSound( "sound/null.wav", qfalse );
+      }
+      else
+      {
+        if( i == 9 ) //gasp
+          s = cg_customSoundNames[ 7 ]; //pain100_1
+        else if( i == 10 ) //drown
+          s = cg_customSoundNames[ 0 ]; //death1
+
+        ci->sounds[ i ] = trap_S_RegisterSound( va( "sound/player/%s/%s", dir, s + 1 ), qfalse );
+      }
+    }
+    else
+    {
+      ci->sounds[ i ] = trap_S_RegisterSound( va( "sound/player/%s/%s", dir, s + 1 ), qfalse );
+    }
+  }
+
+  if( ci->footsteps == FOOTSTEP_CUSTOM )
+  {
+    for( i = 0; i < 4; i++ )
+    {
+      ci->customFootsteps[ i ] = trap_S_RegisterSound( va( "sound/player/%s/step%d.wav", dir, i + 1 ), qfalse );
+      if( !ci->customFootsteps[ i ] )
+        ci->customFootsteps[ i ] = trap_S_RegisterSound( va( "sound/player/footsteps/step%d.wav", i + 1 ), qfalse );
+
+      ci->customMetalFootsteps[ i ] = trap_S_RegisterSound( va( "sound/player/%s/clank%d.wav", dir, i + 1 ), qfalse );
+      if( !ci->customMetalFootsteps[ i ] )
+        ci->customMetalFootsteps[ i ] = trap_S_RegisterSound( va( "sound/player/footsteps/clank%d.wav", i + 1 ), qfalse );
+    }
+  }
+
+  // reset any existing players and bodies, because they might be in bad
+  // frames for this new model
+  clientNum = ci - cgs.clientinfo;
+  for( i = 0; i < MAX_GENTITIES; i++ )
+  {
+    if( cg_entities[ i ].currentState.clientNum == clientNum &&
+        cg_entities[ i ].currentState.eType == ET_PLAYER )
+      CG_ResetPlayerEntity( &cg_entities[ i ] );
+  }
+}
+
+/*
+======================
+CG_CopyClientInfoModel
+======================
+*/
+static void CG_CopyClientInfoModel( clientInfo_t *from, clientInfo_t *to )
+{
+  VectorCopy( from->headOffset, to->headOffset );
+  to->footsteps = from->footsteps;
+  to->gender = from->gender;
+
+  to->legsModel = from->legsModel;
+  to->legsSkin = from->legsSkin;
+  to->torsoModel = from->torsoModel;
+  to->torsoSkin = from->torsoSkin;
+  to->headModel = from->headModel;
+  to->headSkin = from->headSkin;
+  to->nonSegModel = from->nonSegModel;
+  to->nonSegSkin = from->nonSegSkin;
+  to->nonsegmented = from->nonsegmented;
+  to->modelIcon = from->modelIcon;
+
+  memcpy( to->animations, from->animations, sizeof( to->animations ) );
+  memcpy( to->sounds, from->sounds, sizeof( to->sounds ) );
+  memcpy( to->customFootsteps, from->customFootsteps, sizeof( to->customFootsteps ) );
+  memcpy( to->customMetalFootsteps, from->customMetalFootsteps, sizeof( to->customMetalFootsteps ) );
+}
+
+
+/*
+======================
+CG_GetCorpseNum
+======================
+*/
+static int CG_GetCorpseNum( class_t class )
+{
+  int           i;
+  clientInfo_t  *match;
+  char          *modelName;
+  char          *skinName;
+
+  modelName = BG_ClassConfig( class )->modelName;
+  skinName = BG_ClassConfig( class )->skinName;
+
+  for( i = PCL_NONE + 1; i < PCL_NUM_CLASSES; i++ )
+  {
+    match = &cgs.corpseinfo[ i ];
+
+    if( !match->infoValid )
+      continue;
+
+    if( !Q_stricmp( modelName, match->modelName ) &&
+        !Q_stricmp( skinName, match->skinName ) )
+    {
+      // this clientinfo is identical, so use it's handles
+      return i;
+    }
+  }
+
+  //something has gone horribly wrong
+  return -1;
+}
+
+
+/*
+======================
+CG_ScanForExistingClientInfo
+======================
+*/
+static qboolean CG_ScanForExistingClientInfo( clientInfo_t *ci )
+{
+  int   i;
+  clientInfo_t  *match;
+
+  for( i = PCL_NONE + 1; i < PCL_NUM_CLASSES; i++ )
+  {
+    match = &cgs.corpseinfo[ i ];
+
+    if( !match->infoValid )
+      continue;
+
+    if( !Q_stricmp( ci->modelName, match->modelName ) &&
+        !Q_stricmp( ci->skinName, match->skinName ) )
+    {
+      // this clientinfo is identical, so use it's handles
+      CG_CopyClientInfoModel( match, ci );
+
+      return qtrue;
+    }
+  }
+
+  // shouldn't happen
+  return qfalse;
+}
+
+
+/*
+======================
+CG_PrecacheClientInfo
+======================
+*/
+void CG_PrecacheClientInfo( class_t class, char *model, char *skin )
+{
+  clientInfo_t  *ci;
+  clientInfo_t  newInfo;
+
+  ci = &cgs.corpseinfo[ class ];
+
+  // the old value
+  memset( &newInfo, 0, sizeof( newInfo ) );
+
+  // model
+  Q_strncpyz( newInfo.modelName, model, sizeof( newInfo.modelName ) );
+
+  // modelName did not include a skin name
+  if( !skin )
+    Q_strncpyz( newInfo.skinName, "default", sizeof( newInfo.skinName ) );
+  else
+    Q_strncpyz( newInfo.skinName, skin, sizeof( newInfo.skinName ) );
+
+  newInfo.infoValid = qtrue;
+
+  // actually register the models
+  *ci = newInfo;
+  CG_LoadClientInfo( ci );
+}
+
+/*
+=============
+CG_StatusMessages
+
+Print messages for player status changes
+=============
+*/
+static void CG_StatusMessages( clientInfo_t *new, clientInfo_t *old )
+{
+  if( !old->infoValid )
+    return;
+
+  if( strcmp( new->name, old->name ) )
+    CG_Printf( "%s" S_COLOR_WHITE " ^5renamed to %s\n", old->name, new->name );
+
+  if( old->team != new->team )
+  {
+    if( new->team == TEAM_NONE )
+    {
+      if ( old->team == TEAM_ALIENS )
+        CG_Printf( "%s" S_COLOR_WHITE " ^1left the ^1%ss^1\n", new->name, BG_TeamName( old->team ) );
+      else
+        CG_Printf( "%s" S_COLOR_WHITE " ^4left the ^4%ss^1\n", new->name, BG_TeamName( old->team ) );
+    }
+    else if( old->team == TEAM_NONE )
+    {
+      if ( new->team == TEAM_ALIENS )
+        CG_Printf( "%s" S_COLOR_WHITE " ^1joined the ^1%ss^1\n", new->name, BG_TeamName( new->team ) );
+      else
+        CG_Printf( "%s" S_COLOR_WHITE " ^4joined the ^4%ss^1\n", new->name, BG_TeamName( new->team ) );
+    }
+    else
+    {
+      if ( old->team == TEAM_ALIENS )
+        CG_Printf( "%s" S_COLOR_WHITE " ^1left the ^1%ss^7 ^5and ^4joined the ^4%ss^7\n", new->name, BG_TeamName( old->team ), BG_TeamName( new->team ) );
+      else
+        CG_Printf( "%s" S_COLOR_WHITE " ^4left the ^4%ss^7 ^5and ^1joined the ^1%ss^7\n", new->name, BG_TeamName( old->team ), BG_TeamName( new->team ) );
+    }
+  }
+}
+
+/*
+======================
+CG_NewClientInfo
+======================
+*/
+void CG_NewClientInfo( int clientNum )
+{
+  clientInfo_t  *ci;
+  clientInfo_t  newInfo;
+  const char    *configstring;
+  const char    *v;
+  char          *slash;
+
+  ci = &cgs.clientinfo[ clientNum ];
+
+  configstring = CG_ConfigString( clientNum + CS_PLAYERS );
+  if( !configstring[ 0 ] )
+  {
+    memset( ci, 0, sizeof( *ci ) );
+    return;   // player just left
+  }
+
+  // the old value
+  memset( &newInfo, 0, sizeof( newInfo ) );
+ 
+  // grab our own ignoreList 
+  if( clientNum == cg.predictedPlayerState.clientNum )
+  {
+    v = Info_ValueForKey( configstring, "ig" );
+    Com_ClientListParse( &cgs.ignoreList, v );
+  }
+
+  // isolate the player's name
+  v = Info_ValueForKey( configstring, "n" );
+  Q_strncpyz( newInfo.name, v, sizeof( newInfo.name ) );
+
+  // team
+  v = Info_ValueForKey( configstring, "t" );
+  newInfo.team = atoi( v );
+
+  // if this is us, execute team-specific config files
+  // the spectator config is a little unreliable because it's easy to get on
+  // to the spectator team without joining it - e.g. when a new game starts.
+  // It's not a big deal because the spec config is the least important
+  // slash used anyway.
+  // I guess it's possible for someone to change teams during a restart and
+  // for that to then be missed here. But that's rare enough that people can
+  // just exec the configs manually, I think.
+  if( clientNum == cg.clientNum && ci->infoValid &&
+      ci->team != newInfo.team )
+  {
+    char config[ MAX_CVAR_VALUE_STRING ];
+
+    trap_Cvar_VariableStringBuffer(
+      va( "cg_%sConfig", BG_TeamName( newInfo.team ) ),
+      config, sizeof( config ) );
+
+    if( config[ 0 ] )
+      trap_SendConsoleCommand( va( "exec \"%s\"\n", config ) );
+  }
+
+  // model
+  v = Info_ValueForKey( configstring, "model" );
+  Q_strncpyz( newInfo.modelName, v, sizeof( newInfo.modelName ) );
+
+  slash = strchr( newInfo.modelName, '/' );
+
+  if( !slash )
+  {
+    // modelName didn not include a skin name
+    Q_strncpyz( newInfo.skinName, "default", sizeof( newInfo.skinName ) );
+  }
+  else
+  {
+    Q_strncpyz( newInfo.skinName, slash + 1, sizeof( newInfo.skinName ) );
+    // truncate modelName
+    *slash = 0;
+  }
+
+  // voice
+  v = Info_ValueForKey( configstring, "v" );
+  Q_strncpyz( newInfo.voice, v, sizeof( newInfo.voice ) );
+
+  CG_StatusMessages( &newInfo, ci );
+
+  // replace whatever was there with the new one
+  newInfo.infoValid = qtrue;
+  *ci = newInfo;
+
+  // scan for an existing clientinfo that matches this modelname
+  // so we can avoid loading checks if possible
+  if( !CG_ScanForExistingClientInfo( ci ) )
+    CG_LoadClientInfo( ci );
+}
+
+
+
+/*
+=============================================================================
+
+PLAYER ANIMATION
+
+=============================================================================
+*/
+
+
+/*
+===============
+CG_SetLerpFrameAnimation
+
+may include ANIM_TOGGLEBIT
+===============
+*/
+static void CG_SetLerpFrameAnimation( clientInfo_t *ci, lerpFrame_t *lf, int newAnimation )
+{
+  animation_t *anim;
+
+  lf->animationNumber = newAnimation;
+  newAnimation &= ~ANIM_TOGGLEBIT;
+
+  if( newAnimation < 0 || newAnimation >= MAX_PLAYER_TOTALANIMATIONS )
+    CG_Error( "Bad animation number: %i", newAnimation );
+
+  anim = &ci->animations[ newAnimation ];
+
+  lf->animation = anim;
+  lf->animationTime = lf->frameTime + anim->initialLerp;
+
+  if( cg_debugAnim.integer )
+    CG_Printf( "Anim: %i\n", newAnimation );
+}
+
+/*
+===============
+CG_RunPlayerLerpFrame
+
+Sets cg.snap, cg.oldFrame, and cg.backlerp
+cg.time should be between oldFrameTime and frameTime after exit
+===============
+*/
+static void CG_RunPlayerLerpFrame( clientInfo_t *ci, lerpFrame_t *lf, int newAnimation, float speedScale )
+{
+  // see if the animation sequence is switching
+  if( newAnimation != lf->animationNumber || !lf->animation )
+    CG_SetLerpFrameAnimation( ci, lf, newAnimation );
+
+  CG_RunLerpFrame( lf, speedScale );
+}
+
+
+/*
+===============
+CG_ClearLerpFrame
+===============
+*/
+static void CG_ClearLerpFrame( clientInfo_t *ci, lerpFrame_t *lf, int animationNumber )
+{
+  lf->frameTime = lf->oldFrameTime = cg.time;
+  CG_SetLerpFrameAnimation( ci, lf, animationNumber );
+  lf->oldFrame = lf->frame = lf->animation->firstFrame;
+}
+
+
+/*
+===============
+CG_PlayerAnimation
+===============
+*/
+static void CG_PlayerAnimation( centity_t *cent, int *legsOld, int *legs, float *legsBackLerp,
+                                int *torsoOld, int *torso, float *torsoBackLerp )
+{
+  clientInfo_t  *ci;
+  int           clientNum;
+  float         speedScale = 1.0f;
+
+  clientNum = cent->currentState.clientNum;
+
+  if( cg_noPlayerAnims.integer )
+  {
+    *legsOld = *legs = *torsoOld = *torso = 0;
+    return;
+  }
+
+  ci = &cgs.clientinfo[ clientNum ];
+
+  // do the shuffle turn frames locally
+  if( cent->pe.legs.yawing && ( cent->currentState.legsAnim & ~ANIM_TOGGLEBIT ) == LEGS_IDLE )
+    CG_RunPlayerLerpFrame( ci, &cent->pe.legs, LEGS_TURN, speedScale );
+  else
+    CG_RunPlayerLerpFrame( ci, &cent->pe.legs, cent->currentState.legsAnim, speedScale );
+
+  *legsOld = cent->pe.legs.oldFrame;
+  *legs = cent->pe.legs.frame;
+  *legsBackLerp = cent->pe.legs.backlerp;
+
+  CG_RunPlayerLerpFrame( ci, &cent->pe.torso, cent->currentState.torsoAnim, speedScale );
+
+  *torsoOld = cent->pe.torso.oldFrame;
+  *torso = cent->pe.torso.frame;
+  *torsoBackLerp = cent->pe.torso.backlerp;
+}
+
+
+/*
+===============
+CG_PlayerNonSegAnimation
+===============
+*/
+static void CG_PlayerNonSegAnimation( centity_t *cent, int *nonSegOld,
+                                      int *nonSeg, float *nonSegBackLerp )
+{
+  clientInfo_t  *ci;
+  int           clientNum;
+  float         speedScale = 1.0f;
+
+  clientNum = cent->currentState.clientNum;
+
+  if( cg_noPlayerAnims.integer )
+  {
+    *nonSegOld = *nonSeg = 0;
+    return;
+  }
+
+  ci = &cgs.clientinfo[ clientNum ];
+
+  // do the shuffle turn frames locally
+  if( cent->pe.nonseg.yawing && ( cent->currentState.legsAnim & ~ANIM_TOGGLEBIT ) == NSPA_STAND )
+    CG_RunPlayerLerpFrame( ci, &cent->pe.nonseg, NSPA_TURN, speedScale );
+  else
+    CG_RunPlayerLerpFrame( ci, &cent->pe.nonseg, cent->currentState.legsAnim, speedScale );
+
+  *nonSegOld = cent->pe.nonseg.oldFrame;
+  *nonSeg = cent->pe.nonseg.frame;
+  *nonSegBackLerp = cent->pe.nonseg.backlerp;
+}
+
+/*
+=============================================================================
+
+PLAYER ANGLES
+
+=============================================================================
+*/
+
+/*
+==================
+CG_SwingAngles
+==================
+*/
+static void CG_SwingAngles( float destination, float swingTolerance, float clampTolerance,
+                            float speed, float *angle, qboolean *swinging )
+{
+  float swing;
+  float move;
+  float scale;
+
+  if( !*swinging )
+  {
+    // see if a swing should be started
+    swing = AngleSubtract( *angle, destination );
+
+    if( swing > swingTolerance || swing < -swingTolerance )
+      *swinging = qtrue;
+  }
+
+  if( !*swinging )
+    return;
+
+  // modify the speed depending on the delta
+  // so it doesn't seem so linear
+  swing = AngleSubtract( destination, *angle );
+  scale = fabs( swing );
+
+  if( scale < swingTolerance * 0.5 )
+    scale = 0.5;
+  else if( scale < swingTolerance )
+    scale = 1.0;
+  else
+    scale = 2.0;
+
+  // swing towards the destination angle
+  if( swing >= 0 )
+  {
+    move = cg.frametime * scale * speed;
+
+    if( move >= swing )
+    {
+      move = swing;
+      *swinging = qfalse;
+    }
+    *angle = AngleMod( *angle + move );
+  }
+  else if( swing < 0 )
+  {
+    move = cg.frametime * scale * -speed;
+
+    if( move <= swing )
+    {
+      move = swing;
+      *swinging = qfalse;
+    }
+    *angle = AngleMod( *angle + move );
+  }
+
+  // clamp to no more than tolerance
+  swing = AngleSubtract( destination, *angle );
+  if( swing > clampTolerance )
+    *angle = AngleMod( destination - ( clampTolerance - 1 ) );
+  else if( swing < -clampTolerance )
+    *angle = AngleMod( destination + ( clampTolerance - 1 ) );
+}
+
+/*
+=================
+CG_AddPainTwitch
+=================
+*/
+static void CG_AddPainTwitch( centity_t *cent, vec3_t torsoAngles )
+{
+  int   t;
+  float f;
+
+  t = cg.time - cent->pe.painTime;
+
+  if( t >= PAIN_TWITCH_TIME )
+    return;
+
+  f = 1.0 - (float)t / PAIN_TWITCH_TIME;
+
+  if( cent->pe.painDirection )
+    torsoAngles[ ROLL ] += 20 * f;
+  else
+    torsoAngles[ ROLL ] -= 20 * f;
+}
+
+
+/*
+===============
+CG_PlayerAngles
+
+Handles seperate torso motion
+
+  legs pivot based on direction of movement
+
+  head always looks exactly at cent->lerpAngles
+
+  if motion < 20 degrees, show in head only
+  if < 45 degrees, also show in torso
+===============
+*/
+static void CG_PlayerAngles( centity_t *cent, vec3_t srcAngles,
+                             vec3_t legs[ 3 ], vec3_t torso[ 3 ], vec3_t head[ 3 ] )
+{
+  vec3_t        legsAngles, torsoAngles, headAngles;
+  float         dest;
+  static int    movementOffsets[ 8 ] = { 0, 22, 45, -22, 0, 22, -45, -22 };
+  vec3_t        velocity;
+  float         speed;
+  int           dir, clientNum;
+  clientInfo_t  *ci;
+
+  VectorCopy( srcAngles, headAngles );
+  headAngles[ YAW ] = AngleMod( headAngles[ YAW ] );
+  VectorClear( legsAngles );
+  VectorClear( torsoAngles );
+
+  // --------- yaw -------------
+
+  // allow yaw to drift a bit
+  if( ( cent->currentState.legsAnim & ~ANIM_TOGGLEBIT ) != LEGS_IDLE ||
+      ( cent->currentState.torsoAnim & ~ANIM_TOGGLEBIT ) != TORSO_STAND  )
+  {
+    // if not standing still, always point all in the same direction
+    cent->pe.torso.yawing = qtrue;  // always center
+    cent->pe.torso.pitching = qtrue;  // always center
+    cent->pe.legs.yawing = qtrue; // always center
+  }
+
+  // adjust legs for movement dir
+  if( cent->currentState.eFlags & EF_DEAD )
+  {
+    // don't let dead bodies twitch
+    dir = 0;
+  }
+  else
+  {
+    // did use angles2.. now uses time2.. looks a bit funny but time2 isn't used othwise
+    dir = cent->currentState.time2;
+    if( dir < 0 || dir > 7 )
+      CG_Error( "Bad player movement angle" );
+  }
+
+  legsAngles[ YAW ] = headAngles[ YAW ] + movementOffsets[ dir ];
+  torsoAngles[ YAW ] = headAngles[ YAW ] + 0.25 * movementOffsets[ dir ];
+
+  // torso
+  if( cent->currentState.eFlags & EF_DEAD )
+  {
+    CG_SwingAngles( torsoAngles[ YAW ], 0, 0, cg_swingSpeed.value,
+      &cent->pe.torso.yawAngle, &cent->pe.torso.yawing );
+    CG_SwingAngles( legsAngles[ YAW ], 0, 0, cg_swingSpeed.value,
+      &cent->pe.legs.yawAngle, &cent->pe.legs.yawing );
+  }
+  else
+  {
+    CG_SwingAngles( torsoAngles[ YAW ], 25, 90, cg_swingSpeed.value,
+      &cent->pe.torso.yawAngle, &cent->pe.torso.yawing );
+    CG_SwingAngles( legsAngles[ YAW ], 40, 90, cg_swingSpeed.value,
+      &cent->pe.legs.yawAngle, &cent->pe.legs.yawing );
+  }
+
+  torsoAngles[ YAW ] = cent->pe.torso.yawAngle;
+  legsAngles[ YAW ] = cent->pe.legs.yawAngle;
+
+  // --------- pitch -------------
+
+  // only show a fraction of the pitch angle in the torso
+  if( headAngles[ PITCH ] > 180 )
+    dest = ( -360 + headAngles[ PITCH ] ) * 0.75f;
+  else
+    dest = headAngles[ PITCH ] * 0.75f;
+
+  CG_SwingAngles( dest, 15, 30, 0.1f, &cent->pe.torso.pitchAngle, &cent->pe.torso.pitching );
+  torsoAngles[ PITCH ] = cent->pe.torso.pitchAngle;
+
+  //
+  clientNum = cent->currentState.clientNum;
+
+  if( clientNum >= 0 && clientNum < MAX_CLIENTS )
+  {
+    ci = &cgs.clientinfo[ clientNum ];
+    if( ci->fixedtorso )
+      torsoAngles[ PITCH ] = 0.0f;
+  }
+
+  // --------- roll -------------
+
+
+  // lean towards the direction of travel
+  VectorCopy( cent->currentState.pos.trDelta, velocity );
+  speed = VectorNormalize( velocity );
+
+  if( speed )
+  {
+    vec3_t  axis[ 3 ];
+    float   side;
+
+    speed *= 0.05f;
+
+    AnglesToAxis( legsAngles, axis );
+    side = speed * DotProduct( velocity, axis[ 1 ] );
+    legsAngles[ ROLL ] -= side;
+
+    side = speed * DotProduct( velocity, axis[ 0 ] );
+    legsAngles[ PITCH ] += side;
+  }
+
+  //
+  clientNum = cent->currentState.clientNum;
+
+  if( clientNum >= 0 && clientNum < MAX_CLIENTS )
+  {
+    ci = &cgs.clientinfo[ clientNum ];
+
+    if( ci->fixedlegs )
+    {
+      legsAngles[ YAW ] = torsoAngles[ YAW ];
+      legsAngles[ PITCH ] = 0.0f;
+      legsAngles[ ROLL ] = 0.0f;
+    }
+  }
+
+  // pain twitch
+  CG_AddPainTwitch( cent, torsoAngles );
+
+  // pull the angles back out of the hierarchial chain
+  AnglesSubtract( headAngles, torsoAngles, headAngles );
+  AnglesSubtract( torsoAngles, legsAngles, torsoAngles );
+  AnglesToAxis( legsAngles, legs );
+  AnglesToAxis( torsoAngles, torso );
+  AnglesToAxis( headAngles, head );
+}
+
+#define MODEL_WWSMOOTHTIME 200
+
+/*
+===============
+CG_PlayerWWSmoothing
+
+Smooth the angles of transitioning wall walkers
+===============
+*/
+static void CG_PlayerWWSmoothing( centity_t *cent, vec3_t in[ 3 ], vec3_t out[ 3 ] )
+{
+  entityState_t *es = &cent->currentState;
+  int           i;
+  vec3_t        surfNormal, rotAxis, temp;
+  vec3_t        refNormal     = { 0.0f, 0.0f,  1.0f };
+  vec3_t        ceilingNormal = { 0.0f, 0.0f, -1.0f };
+  float         stLocal, sFraction, rotAngle;
+  vec3_t        inAxis[ 3 ], lastAxis[ 3 ], outAxis[ 3 ];
+
+  //set surfNormal
+  if( !(es->eFlags & EF_WALLCLIMB ) )
+    VectorCopy( refNormal, surfNormal );
+  else if( !( es->eFlags & EF_WALLCLIMBCEILING ) )
+    VectorCopy( es->angles2, surfNormal );
+  else
+    VectorCopy( ceilingNormal, surfNormal );
+
+  AxisCopy( in, inAxis );
+
+  if( !VectorCompare( surfNormal, cent->pe.lastNormal ) )
+  {
+    //if we moving from the ceiling to the floor special case
+    //( x product of colinear vectors is undefined)
+    if( VectorCompare( ceilingNormal, cent->pe.lastNormal ) &&
+        VectorCompare( refNormal,     surfNormal ) )
+    {
+      VectorCopy( in[ 1 ], rotAxis );
+      rotAngle = 180.0f;
+    }
+    else
+    {
+      AxisCopy( cent->pe.lastAxis, lastAxis );
+      rotAngle = DotProduct( inAxis[ 0 ], lastAxis[ 0 ] ) +
+                 DotProduct( inAxis[ 1 ], lastAxis[ 1 ] ) +
+                 DotProduct( inAxis[ 2 ], lastAxis[ 2 ] );
+
+      rotAngle = RAD2DEG( acos( ( rotAngle - 1.0f ) / 2.0f ) );
+
+      CrossProduct( lastAxis[ 0 ], inAxis[ 0 ], temp );
+      VectorCopy( temp, rotAxis );
+      CrossProduct( lastAxis[ 1 ], inAxis[ 1 ], temp );
+      VectorAdd( rotAxis, temp, rotAxis );
+      CrossProduct( lastAxis[ 2 ], inAxis[ 2 ], temp );
+      VectorAdd( rotAxis, temp, rotAxis );
+
+      VectorNormalize( rotAxis );
+    }
+
+    //iterate through smooth array
+    for( i = 0; i < MAXSMOOTHS; i++ )
+    {
+      //found an unused index in the smooth array
+      if( cent->pe.sList[ i ].time + MODEL_WWSMOOTHTIME < cg.time )
+      {
+        //copy to array and stop
+        VectorCopy( rotAxis, cent->pe.sList[ i ].rotAxis );
+        cent->pe.sList[ i ].rotAngle = rotAngle;
+        cent->pe.sList[ i ].time = cg.time;
+        break;
+      }
+    }
+  }
+
+  //iterate through ops
+  for( i = MAXSMOOTHS - 1; i >= 0; i-- )
+  {
+    //if this op has time remaining, perform it
+    if( cg.time < cent->pe.sList[ i ].time + MODEL_WWSMOOTHTIME )
+    {
+      stLocal = 1.0f - ( ( ( cent->pe.sList[ i ].time + MODEL_WWSMOOTHTIME ) - cg.time ) / MODEL_WWSMOOTHTIME );
+      sFraction = -( cos( stLocal * M_PI ) + 1.0f ) / 2.0f;
+
+      RotatePointAroundVector( outAxis[ 0 ], cent->pe.sList[ i ].rotAxis,
+        inAxis[ 0 ], sFraction * cent->pe.sList[ i ].rotAngle );
+      RotatePointAroundVector( outAxis[ 1 ], cent->pe.sList[ i ].rotAxis,
+        inAxis[ 1 ], sFraction * cent->pe.sList[ i ].rotAngle );
+      RotatePointAroundVector( outAxis[ 2 ], cent->pe.sList[ i ].rotAxis,
+        inAxis[ 2 ], sFraction * cent->pe.sList[ i ].rotAngle );
+
+      AxisCopy( outAxis, inAxis );
+    }
+  }
+
+  //outAxis has been copied to inAxis
+  AxisCopy( inAxis, out );
+}
+
+/*
+===============
+CG_PlayerNonSegAngles
+
+Resolve angles for non-segmented models
+===============
+*/
+static void CG_PlayerNonSegAngles( centity_t *cent, vec3_t srcAngles, vec3_t nonSegAxis[ 3 ] )
+{
+  vec3_t        localAngles;
+  vec3_t        velocity;
+  float         speed;
+  int           dir;
+  entityState_t *es = &cent->currentState;
+  vec3_t        surfNormal;
+  vec3_t        ceilingNormal = { 0.0f, 0.0f, -1.0f };
+
+  VectorCopy( srcAngles, localAngles );
+  localAngles[ YAW ] = AngleMod( localAngles[ YAW ] );
+  localAngles[ PITCH ] = 0.0f;
+  localAngles[ ROLL ] = 0.0f;
+
+  //set surfNormal
+  if( !( es->eFlags & EF_WALLCLIMBCEILING ) )
+    VectorCopy( es->angles2, surfNormal );
+  else
+    VectorCopy( ceilingNormal, surfNormal );
+
+  //make sure that WW transitions don't cause the swing stuff to go nuts
+  if( !VectorCompare( surfNormal, cent->pe.lastNormal ) )
+  {
+    //stop CG_SwingAngles having an eppy
+    cent->pe.nonseg.yawAngle = localAngles[ YAW ];
+    cent->pe.nonseg.yawing = qfalse;
+  }
+
+  // --------- yaw -------------
+
+  // allow yaw to drift a bit
+  if( ( cent->currentState.legsAnim & ~ANIM_TOGGLEBIT ) != NSPA_STAND )
+  {
+    // if not standing still, always point all in the same direction
+    cent->pe.nonseg.yawing = qtrue; // always center
+  }
+
+  // adjust legs for movement dir
+  if( cent->currentState.eFlags & EF_DEAD )
+  {
+    // don't let dead bodies twitch
+    dir = 0;
+  }
+  else
+  {
+    // did use angles2.. now uses time2.. looks a bit funny but time2 isn't used othwise
+    dir = cent->currentState.time2;
+    if( dir < 0 || dir > 7 )
+      CG_Error( "Bad player movement angle" );
+  }
+
+  // torso
+  if( cent->currentState.eFlags & EF_DEAD )
+  {
+    CG_SwingAngles( localAngles[ YAW ], 0, 0, cg_swingSpeed.value,
+      &cent->pe.nonseg.yawAngle, &cent->pe.nonseg.yawing );
+  }
+  else
+  {
+    CG_SwingAngles( localAngles[ YAW ], 40, 90, cg_swingSpeed.value,
+      &cent->pe.nonseg.yawAngle, &cent->pe.nonseg.yawing );
+  }
+
+  localAngles[ YAW ] = cent->pe.nonseg.yawAngle;
+
+  // --------- pitch -------------
+
+  //NO PITCH!
+
+
+  // --------- roll -------------
+
+
+  // lean towards the direction of travel
+  VectorCopy( cent->currentState.pos.trDelta, velocity );
+  speed = VectorNormalize( velocity );
+
+  if( speed )
+  {
+    vec3_t  axis[ 3 ];
+    float   side;
+
+    //much less than with the regular model system
+    speed *= 0.01f;
+
+    AnglesToAxis( localAngles, axis );
+    side = speed * DotProduct( velocity, axis[ 1 ] );
+    localAngles[ ROLL ] -= side;
+
+    side = speed * DotProduct( velocity, axis[ 0 ] );
+    localAngles[ PITCH ] += side;
+  }
+
+  //FIXME: PAIN[123] animations?
+  // pain twitch
+  //CG_AddPainTwitch( cent, torsoAngles );
+
+  AnglesToAxis( localAngles, nonSegAxis );
+}
+
+
+//==========================================================================
+
+/*
+===============
+CG_PlayerUpgrade
+===============
+*/
+static void CG_PlayerUpgrades( centity_t *cent, refEntity_t *torso )
+{
+  int           held, active;
+  refEntity_t   jetpack;
+  refEntity_t   battpack;
+  refEntity_t   flash;
+  entityState_t *es = &cent->currentState;
+
+  held = es->modelindex;
+  active = es->modelindex2;
+
+  if( held & ( 1 << UP_JETPACK ) )
+  {
+    memset( &jetpack, 0, sizeof( jetpack ) );
+    VectorCopy( torso->lightingOrigin, jetpack.lightingOrigin );
+    jetpack.shadowPlane = torso->shadowPlane;
+    jetpack.renderfx = torso->renderfx;
+
+    jetpack.hModel = cgs.media.jetpackModel;
+
+    //identity matrix
+    AxisCopy( axisDefault, jetpack.axis );
+
+    //FIXME: change to tag_back when it exists
+    CG_PositionRotatedEntityOnTag( &jetpack, torso, torso->hModel, "tag_head" );
+
+    trap_R_AddRefEntityToScene( &jetpack );
+
+    if( active & ( 1 << UP_JETPACK ) )
+    {
+      if( es->pos.trDelta[ 2 ] > 10.0f )
+      {
+        if( cent->jetPackState != JPS_ASCENDING )
+        {
+          if( CG_IsParticleSystemValid( &cent->jetPackPS ) )
+            CG_DestroyParticleSystem( &cent->jetPackPS );
+
+          cent->jetPackPS = CG_SpawnNewParticleSystem( cgs.media.jetPackAscendPS );
+          cent->jetPackState = JPS_ASCENDING;
+        }
+
+        trap_S_AddLoopingSound( cent->currentState.number, cent->lerpOrigin,
+                                vec3_origin, cgs.media.jetpackAscendSound );
+      }
+      else if( es->pos.trDelta[ 2 ] < -10.0f )
+      {
+        if( cent->jetPackState != JPS_DESCENDING )
+        {
+          if( CG_IsParticleSystemValid( &cent->jetPackPS ) )
+            CG_DestroyParticleSystem( &cent->jetPackPS );
+
+          cent->jetPackPS = CG_SpawnNewParticleSystem( cgs.media.jetPackDescendPS );
+          cent->jetPackState = JPS_DESCENDING;
+        }
+
+        trap_S_AddLoopingSound( cent->currentState.number, cent->lerpOrigin,
+                                vec3_origin, cgs.media.jetpackDescendSound );
+      }
+      else
+      {
+        if( cent->jetPackState != JPS_HOVERING )
+        {
+          if( CG_IsParticleSystemValid( &cent->jetPackPS ) )
+            CG_DestroyParticleSystem( &cent->jetPackPS );
+
+          cent->jetPackPS = CG_SpawnNewParticleSystem( cgs.media.jetPackHoverPS );
+          cent->jetPackState = JPS_HOVERING;
+        }
+
+        trap_S_AddLoopingSound( cent->currentState.number, cent->lerpOrigin,
+                                vec3_origin, cgs.media.jetpackIdleSound );
+      }
+
+      memset( &flash, 0, sizeof( flash ) );
+      VectorCopy( torso->lightingOrigin, flash.lightingOrigin );
+      flash.shadowPlane = torso->shadowPlane;
+      flash.renderfx = torso->renderfx;
+
+      flash.hModel = cgs.media.jetpackFlashModel;
+      if( !flash.hModel )
+        return;
+
+      AxisCopy( axisDefault, flash.axis );
+
+      CG_PositionRotatedEntityOnTag( &flash, &jetpack, jetpack.hModel, "tag_flash" );
+      trap_R_AddRefEntityToScene( &flash );
+
+      if( CG_IsParticleSystemValid( &cent->jetPackPS ) )
+      {
+        CG_SetAttachmentTag( &cent->jetPackPS->attachment,
+            jetpack, jetpack.hModel, "tag_flash" );
+        CG_SetAttachmentCent( &cent->jetPackPS->attachment, cent );
+        CG_AttachToTag( &cent->jetPackPS->attachment );
+      }
+    }
+    else if( CG_IsParticleSystemValid( &cent->jetPackPS ) )
+    {
+      CG_DestroyParticleSystem( &cent->jetPackPS );
+      cent->jetPackState = JPS_OFF;
+    }
+  }
+  else if( CG_IsParticleSystemValid( &cent->jetPackPS ) )
+  {
+    CG_DestroyParticleSystem( &cent->jetPackPS );
+    cent->jetPackState = JPS_OFF;
+  }
+
+  if( held & ( 1 << UP_BATTPACK ) )
+  {
+    memset( &battpack, 0, sizeof( battpack ) );
+    VectorCopy( torso->lightingOrigin, battpack.lightingOrigin );
+    battpack.shadowPlane = torso->shadowPlane;
+    battpack.renderfx = torso->renderfx;
+
+    battpack.hModel = cgs.media.battpackModel;
+
+    //identity matrix
+    AxisCopy( axisDefault, battpack.axis );
+
+    //FIXME: change to tag_back when it exists
+    CG_PositionRotatedEntityOnTag( &battpack, torso, torso->hModel, "tag_head" );
+
+    trap_R_AddRefEntityToScene( &battpack );
+  }
+
+  if( es->eFlags & EF_BLOBLOCKED )
+  {
+    vec3_t  temp, origin, up = { 0.0f, 0.0f, 1.0f };
+    trace_t tr;
+    float   size;
+
+    VectorCopy( es->pos.trBase, temp );
+    temp[ 2 ] -= 4096.0f;
+
+    CG_Trace( &tr, es->pos.trBase, NULL, NULL, temp, es->number, MASK_SOLID );
+    VectorCopy( tr.endpos, origin );
+
+    size = 32.0f;
+
+    if( size > 0.0f )
+      CG_ImpactMark( cgs.media.creepShader, origin, up,
+                     0.0f, 1.0f, 1.0f, 1.0f, 1.0f, qfalse, size, qtrue );
+  }
+}
+
+
+/*
+===============
+CG_PlayerFloatSprite
+
+Float a sprite over the player's head
+===============
+*/
+static void CG_PlayerFloatSprite( centity_t *cent, qhandle_t shader )
+{
+  int           rf;
+  refEntity_t   ent;
+
+  if( cent->currentState.number == cg.snap->ps.clientNum && !cg.renderingThirdPerson )
+    rf = RF_THIRD_PERSON;   // only show in mirrors
+  else
+    rf = 0;
+
+  memset( &ent, 0, sizeof( ent ) );
+  VectorCopy( cent->lerpOrigin, ent.origin );
+  ent.origin[ 2 ] += 48;
+  ent.reType = RT_SPRITE;
+  ent.customShader = shader;
+  ent.radius = 10;
+  ent.renderfx = rf;
+  ent.shaderRGBA[ 0 ] = 255;
+  ent.shaderRGBA[ 1 ] = 255;
+  ent.shaderRGBA[ 2 ] = 255;
+  ent.shaderRGBA[ 3 ] = 255;
+  trap_R_AddRefEntityToScene( &ent );
+}
+
+
+
+/*
+===============
+CG_PlayerSprites
+
+Float sprites over the player's head
+===============
+*/
+static void CG_PlayerSprites( centity_t *cent )
+{
+  if( cent->currentState.eFlags & EF_CONNECTION )
+  {
+    CG_PlayerFloatSprite( cent, cgs.media.connectionShader );
+    return;
+  }
+  
+
+}
+
+/*
+===============
+CG_PlayerShadow
+
+Returns the Z component of the surface being shadowed
+
+  should it return a full plane instead of a Z?
+===============
+*/
+#define SHADOW_DISTANCE   128
+static qboolean CG_PlayerShadow( centity_t *cent, float *shadowPlane, class_t class )
+{
+  vec3_t        end, mins, maxs;
+  trace_t       trace;
+  float         alpha;
+  entityState_t *es = &cent->currentState;
+  vec3_t        surfNormal = { 0.0f, 0.0f, 1.0f };
+
+  BG_ClassBoundingBox( class, mins, maxs, NULL, NULL, NULL );
+  mins[ 2 ] = 0.0f;
+  maxs[ 2 ] = 2.0f;
+
+  // cloak
+  if( es->eFlags & EF_MOVER_STOP )
+    return qfalse;
+
+  if( es->eFlags & EF_WALLCLIMB )
+  {
+    if( es->eFlags & EF_WALLCLIMBCEILING )
+      VectorSet( surfNormal, 0.0f, 0.0f, -1.0f );
+    else
+      VectorCopy( es->angles2, surfNormal );
+  }
+
+  *shadowPlane = 0;
+
+  if( cg_shadows.integer == 0 )
+    return qfalse;
+
+  // send a trace down from the player to the ground
+  VectorCopy( cent->lerpOrigin, end );
+  VectorMA( cent->lerpOrigin, -SHADOW_DISTANCE, surfNormal, end );
+
+  trap_CM_BoxTrace( &trace, cent->lerpOrigin, end, mins, maxs, 0, MASK_PLAYERSOLID );
+
+  // no shadow if too high
+  if( trace.fraction == 1.0 || trace.startsolid || trace.allsolid )
+    return qfalse;
+
+  // FIXME: stencil shadows will be broken for walls.
+  //           Unfortunately there isn't much that can be
+  //           done since Q3 references only the Z coord
+  //           of the shadowPlane
+  if( surfNormal[ 2 ] < 0.0f )
+    *shadowPlane = trace.endpos[ 2 ] - 1.0f;
+  else
+    *shadowPlane = trace.endpos[ 2 ] + 1.0f;
+
+  if( cg_shadows.integer != 1 )  // no mark for stencil or projection shadows
+    return qtrue;
+
+  // fade the shadow out with height
+  alpha = 1.0 - trace.fraction;
+
+  // add the mark as a temporary, so it goes directly to the renderer
+  // without taking a spot in the cg_marks array
+  CG_ImpactMark( cgs.media.shadowMarkShader, trace.endpos, trace.plane.normal,
+                 cent->pe.legs.yawAngle, 0.0f, 0.0f, 0.0f, alpha, qfalse,
+                 24.0f * BG_ClassConfig( class )->shadowScale, qtrue );
+
+  return qtrue;
+}
+
+
+/*
+===============
+CG_PlayerSplash
+
+Draw a mark at the water surface
+===============
+*/
+static void CG_PlayerSplash( centity_t *cent, class_t class )
+{
+  vec3_t      start, end;
+  vec3_t      mins, maxs;
+  trace_t     trace;
+  int         contents;
+
+  if( !cg_shadows.integer )
+    return;
+
+  BG_ClassBoundingBox( class, mins, maxs, NULL, NULL, NULL );
+
+  VectorCopy( cent->lerpOrigin, end );
+  end[ 2 ] += mins[ 2 ];
+
+  // if the feet aren't in liquid, don't make a mark
+  // this won't handle moving water brushes, but they wouldn't draw right anyway...
+  contents = trap_CM_PointContents( end, 0 );
+
+  if( !( contents & ( CONTENTS_WATER | CONTENTS_SLIME | CONTENTS_LAVA ) ) )
+    return;
+
+  VectorCopy( cent->lerpOrigin, start );
+  start[ 2 ] += 32;
+
+  // if the head isn't out of liquid, don't make a mark
+  contents = trap_CM_PointContents( start, 0 );
+
+  if( contents & ( CONTENTS_SOLID | CONTENTS_WATER | CONTENTS_SLIME | CONTENTS_LAVA ) )
+    return;
+
+  // trace down to find the surface
+  trap_CM_BoxTrace( &trace, start, end, NULL, NULL, 0,
+      ( CONTENTS_WATER | CONTENTS_SLIME | CONTENTS_LAVA ) );
+
+  if( trace.fraction == 1.0f )
+    return;
+
+  CG_ImpactMark( cgs.media.wakeMarkShader, trace.endpos, trace.plane.normal,
+                 cent->pe.legs.yawAngle, 1.0f, 1.0f, 1.0f, 1.0f, qfalse,
+                 32.0f * BG_ClassConfig( class )->shadowScale, qtrue );
+}
+
+
+/*
+=================
+CG_LightVerts
+=================
+*/
+int CG_LightVerts( vec3_t normal, int numVerts, polyVert_t *verts )
+{
+  int       i, j;
+  float     incoming;
+  vec3_t      ambientLight;
+  vec3_t      lightDir;
+  vec3_t      directedLight;
+
+  trap_R_LightForPoint( verts[ 0 ].xyz, ambientLight, directedLight, lightDir );
+
+  for( i = 0; i < numVerts; i++ )
+  {
+    incoming = DotProduct( normal, lightDir );
+
+    if( incoming <= 0 )
+    {
+      verts[ i ].modulate[ 0 ] = ambientLight[ 0 ];
+      verts[ i ].modulate[ 1 ] = ambientLight[ 1 ];
+      verts[ i ].modulate[ 2 ] = ambientLight[ 2 ];
+      verts[ i ].modulate[ 3 ] = 255;
+      continue;
+    }
+
+    j = ( ambientLight[ 0 ] + incoming * directedLight[ 0 ] );
+
+    if( j > 255 )
+      j = 255;
+
+    verts[ i ].modulate[ 0 ] = j;
+
+    j = ( ambientLight[ 1 ] + incoming * directedLight[ 1 ] );
+
+    if( j > 255 )
+      j = 255;
+
+    verts[ i ].modulate[ 1 ] = j;
+
+    j = ( ambientLight[ 2 ] + incoming * directedLight[ 2 ] );
+
+    if( j > 255 )
+      j = 255;
+
+    verts[ i ].modulate[ 2 ] = j;
+
+    verts[ i ].modulate[ 3 ] = 255;
+  }
+  return qtrue;
+}
+
+
+/*
+=================
+CG_LightFromDirection
+=================
+*/
+int CG_LightFromDirection( vec3_t point, vec3_t direction )
+{
+  int       j;
+  float     incoming;
+  vec3_t    ambientLight;
+  vec3_t    lightDir;
+  vec3_t    directedLight;
+  vec3_t    result;
+
+  trap_R_LightForPoint( point, ambientLight, directedLight, lightDir );
+
+  incoming = DotProduct( direction, lightDir );
+
+  if( incoming <= 0 )
+  {
+    result[ 0 ] = ambientLight[ 0 ];
+    result[ 1 ] = ambientLight[ 1 ];
+    result[ 2 ] = ambientLight[ 2 ];
+    return (int)( (float)( result[ 0 ] + result[ 1 ] + result[ 2 ] ) / 3.0f );
+  }
+
+  j = ( ambientLight[ 0 ] + incoming * directedLight[ 0 ] );
+
+  if( j > 255 )
+    j = 255;
+
+  result[ 0 ] = j;
+
+  j = ( ambientLight[ 1 ] + incoming * directedLight[ 1 ] );
+
+  if( j > 255 )
+    j = 255;
+
+  result[ 1 ] = j;
+
+  j = ( ambientLight[ 2 ] + incoming * directedLight[ 2 ] );
+
+  if( j > 255 )
+    j = 255;
+
+  result[ 2 ] = j;
+
+  return (int)((float)( result[ 0 ] + result[ 1 ] + result[ 2 ] ) / 3.0f );
+}
+
+
+/*
+=================
+CG_AmbientLight
+=================
+*/
+int CG_AmbientLight( vec3_t point )
+{
+  vec3_t      ambientLight;
+  vec3_t      lightDir;
+  vec3_t      directedLight;
+  vec3_t      result;
+
+  trap_R_LightForPoint( point, ambientLight, directedLight, lightDir );
+
+  result[ 0 ] = ambientLight[ 0 ];
+  result[ 1 ] = ambientLight[ 1 ];
+  result[ 2 ] = ambientLight[ 2 ];
+  return (int)((float)( result[ 0 ] + result[ 1 ] + result[ 2 ] ) / 3.0f );
+}
+
+#define TRACE_DEPTH 32.0f
+
+/*
+===============
+CG_Player
+===============
+*/
+void CG_Player( centity_t *cent )
+{
+  clientInfo_t  *ci;
+
+  // NOTE: legs is used for nonsegmented models
+  //       this helps reduce code to be changed
+  refEntity_t   legs;
+  refEntity_t   torso;
+  refEntity_t   head;
+  int           clientNum;
+  int           renderfx;
+  qboolean      shadow = qfalse;
+  float         shadowPlane = 0.0f;
+  entityState_t *es = &cent->currentState;
+  class_t       class = ( es->misc >> 8 ) & 0xFF;
+  float         scale;
+  vec3_t        tempAxis[ 3 ], tempAxis2[ 3 ];
+  vec3_t        angles;
+  int           held = es->modelindex;
+  vec3_t        surfNormal = { 0.0f, 0.0f, 1.0f };
+
+  // the client number is stored in clientNum.  It can't be derived
+  // from the entity number, because a single client may have
+  // multiple corpses on the level using the same clientinfo
+  clientNum = es->clientNum;
+  if( clientNum < 0 || clientNum >= MAX_CLIENTS )
+    CG_Error( "Bad clientNum on player entity" );
+
+  ci = &cgs.clientinfo[ clientNum ];
+
+  // it is possible to see corpses from disconnected players that may
+  // not have valid clientinfo
+  if( !ci->infoValid )
+    return;
+
+  //don't draw
+  if( es->eFlags & EF_NODRAW )
+    return;
+
+  // get the player model information
+  renderfx = 0;
+  if( es->number == cg.snap->ps.clientNum )
+  {
+    if( !cg.renderingThirdPerson )
+      renderfx = RF_THIRD_PERSON;     // only draw in mirrors
+    else if( cg_cameraMode.integer )
+      return;
+  }
+
+  if( cg_drawBBOX.integer )
+  {
+    vec3_t  mins, maxs;
+
+    BG_ClassBoundingBox( class, mins, maxs, NULL, NULL, NULL );
+    CG_DrawBoundingBox( cent->lerpOrigin, mins, maxs );
+  }
+  
+  memset( &legs,    0, sizeof( legs ) );
+  memset( &torso,   0, sizeof( torso ) );
+  memset( &head,    0, sizeof( head ) );
+
+  VectorCopy( cent->lerpAngles, angles );
+  AnglesToAxis( cent->lerpAngles, tempAxis );
+
+  //rotate lerpAngles to floor
+  if( es->eFlags & EF_WALLCLIMB &&
+      BG_RotateAxis( es->angles2, tempAxis, tempAxis2, qtrue, es->eFlags & EF_WALLCLIMBCEILING ) )
+    AxisToAngles( tempAxis2, angles );
+  else
+    VectorCopy( cent->lerpAngles, angles );
+
+  //normalise the pitch
+  if( angles[ PITCH ] < -180.0f )
+    angles[ PITCH ] += 360.0f;
+
+  // get the rotation information
+  if( !ci->nonsegmented )
+    CG_PlayerAngles( cent, angles, legs.axis, torso.axis, head.axis );
+  else
+    CG_PlayerNonSegAngles( cent, angles, legs.axis );
+
+  AxisCopy( legs.axis, tempAxis );
+
+  //rotate the legs axis to back to the wall
+  if( es->eFlags & EF_WALLCLIMB &&
+      BG_RotateAxis( es->angles2, legs.axis, tempAxis, qfalse, es->eFlags & EF_WALLCLIMBCEILING ) )
+    AxisCopy( tempAxis, legs.axis );
+
+  //smooth out WW transitions so the model doesn't hop around
+  CG_PlayerWWSmoothing( cent, legs.axis, legs.axis );
+
+  AxisCopy( tempAxis, cent->pe.lastAxis );
+
+  // get the animation state (after rotation, to allow feet shuffle)
+  if( !ci->nonsegmented )
+    CG_PlayerAnimation( cent, &legs.oldframe, &legs.frame, &legs.backlerp,
+                        &torso.oldframe, &torso.frame, &torso.backlerp );
+  else
+    CG_PlayerNonSegAnimation( cent, &legs.oldframe, &legs.frame, &legs.backlerp );
+
+  // add the talk baloon or disconnect icon
+  CG_PlayerSprites( cent );
+
+  // add the shadow
+  if( ( es->number == cg.snap->ps.clientNum && cg.renderingThirdPerson ) ||
+      es->number != cg.snap->ps.clientNum )
+  shadow = CG_PlayerShadow( cent, &shadowPlane, class );
+
+  // add a water splash if partially in and out of water
+  CG_PlayerSplash( cent, class );
+
+  if( cg_shadows.integer == 3 && shadow )
+    renderfx |= RF_SHADOW_PLANE;
+
+  renderfx |= RF_LIGHTING_ORIGIN;     // use the same origin for all
+
+  //
+  // add the legs
+  //
+  if( !ci->nonsegmented )
+  {
+    legs.hModel = ci->legsModel;
+
+    if( held & ( 1 << UP_LIGHTARMOUR ) )
+      legs.customSkin = cgs.media.larmourLegsSkin;
+    else
+      legs.customSkin = ci->legsSkin;
+    // we can't hit what we can't see :P
+    if( es->eFlags & EF_MOVER_STOP )
+    {
+      if( ci->team != cg.snap->ps.stats[ STAT_TEAM ] )
+        legs.customShader = cgs.media.invisibleShader;
+      else
+        legs.customShader = cgs.media.invisibleShaderTeam;
+    }
+
+  }
+  else
+  {
+    legs.hModel = ci->nonSegModel;
+    legs.customSkin = ci->nonSegSkin;
+
+    // we can't hit what we can't see :P
+    if( es->weapon == WP_ALEVEL1_UPG )
+    {
+      if( es->eFlags & EF_MOVER_STOP )
+      {
+        if( !cent->invisible )
+        {
+          cent->invisibleTime = cg.time;
+          cent->invisible = qtrue;
+        }
+      }
+      else
+      {
+        if( cent->invisible )
+        {
+          cent->invisibleTime = cg.time;
+          cent->invisible = qfalse;
+        }
+      }
+
+      if( cent->invisible )
+      {
+        legs.shaderTime = cent->invisibleTime/1000.0f;
+
+        if( cg.time - cent->invisibleTime < 1000.0f )
+          legs.customShader = cgs.media.invisibleFadeShader;
+		  
+        else{
+			if( ci->team != cg.snap->ps.stats[ STAT_TEAM ] )
+                legs.customShader = cgs.media.invisibleShader;
+				else
+                 legs.customShader = cgs.media.invisibleShaderTeam;
+		    }
+      }
+      else
+      {
+        if( cg.time - cent->invisibleTime < 500.0f )
+        {
+          legs.shaderTime = (cent->invisibleTime+500.0f)/1000.0f;
+          legs.customShader = cgs.media.invisibleFadeShader;
+        }
+	  }
+    }
+
+  if( ( cent->currentState.legsAnim & ~ANIM_TOGGLEBIT ) == NSPA_SWIM  && es->weapon == WP_ALEVEL5)
+    {
+        trap_S_AddLoopingSound( cent->currentState.number, cent->lerpOrigin, vec3_origin, cgs.media.hummelSound );
+	}
+
+	
+  }
+
+  VectorCopy( cent->lerpOrigin, legs.origin );
+
+  VectorCopy( cent->lerpOrigin, legs.lightingOrigin );
+  legs.shadowPlane = shadowPlane;
+  legs.renderfx = renderfx;
+  VectorCopy( legs.origin, legs.oldorigin ); // don't positionally lerp at all
+
+  //move the origin closer into the wall with a CapTrace
+  if( es->eFlags & EF_WALLCLIMB && !( es->eFlags & EF_DEAD ) && !( cg.intermissionStarted ) )
+  {
+    vec3_t  start, end, mins, maxs;
+    trace_t tr;
+
+    if( es->eFlags & EF_WALLCLIMBCEILING )
+      VectorSet( surfNormal, 0.0f, 0.0f, -1.0f );
+    else
+      VectorCopy( es->angles2, surfNormal );
+
+    BG_ClassBoundingBox( class, mins, maxs, NULL, NULL, NULL );
+
+    VectorMA( legs.origin, -TRACE_DEPTH, surfNormal, end );
+    VectorMA( legs.origin, 1.0f, surfNormal, start );
+    CG_CapTrace( &tr, start, mins, maxs, end, es->number, MASK_PLAYERSOLID );
+
+    //if the trace misses completely then just use legs.origin
+    //apparently capsule traces are "smaller" than box traces
+    if( tr.fraction != 1.0f )
+      VectorMA( legs.origin, tr.fraction * -TRACE_DEPTH, surfNormal, legs.origin );
+
+    VectorCopy( legs.origin, legs.lightingOrigin );
+    VectorCopy( legs.origin, legs.oldorigin ); // don't positionally lerp at all
+  }
+
+  //rescale the model
+  scale = BG_ClassConfig( class )->modelScale;
+
+  if( scale != 1.0f )
+  {
+    VectorScale( legs.axis[ 0 ], scale, legs.axis[ 0 ] );
+    VectorScale( legs.axis[ 1 ], scale, legs.axis[ 1 ] );
+    VectorScale( legs.axis[ 2 ], scale, legs.axis[ 2 ] );
+
+    legs.nonNormalizedAxes = qtrue;
+  }
+
+  //offset on the Z axis if required
+  VectorMA( legs.origin, BG_ClassConfig( class )->zOffset, surfNormal, legs.origin );
+  VectorCopy( legs.origin, legs.lightingOrigin );
+  VectorCopy( legs.origin, legs.oldorigin ); // don't positionally lerp at all
+
+  trap_R_AddRefEntityToScene( &legs );
+
+  // if the model failed, allow the default nullmodel to be displayed
+  if( !legs.hModel )
+    return;
+
+  if( !ci->nonsegmented )
+  {
+    //
+    // add the torso
+    //
+    torso.hModel = ci->torsoModel;
+
+    if( held & ( 1 << UP_LIGHTARMOUR ) )
+      torso.customSkin = cgs.media.larmourTorsoSkin;
+    else
+      torso.customSkin = ci->torsoSkin;
+
+    // we can't hit what we can't see :P
+    if( es->eFlags & EF_MOVER_STOP )
+    {
+      if( ci->team != cg.snap->ps.stats[ STAT_TEAM ] )
+        torso.customShader = cgs.media.invisibleShader;
+      else
+        torso.customShader = cgs.media.invisibleShaderTeam;
+    }
+
+    if( !torso.hModel )
+      return;
+
+    VectorCopy( cent->lerpOrigin, torso.lightingOrigin );
+
+    CG_PositionRotatedEntityOnTag( &torso, &legs, ci->legsModel, "tag_torso" );
+
+    torso.shadowPlane = shadowPlane;
+    torso.renderfx = renderfx;
+
+    trap_R_AddRefEntityToScene( &torso );
+
+    //
+    // add the head
+    //
+    head.hModel = ci->headModel;
+
+    if( held & ( 1 << UP_HELMET ) )
+      head.customSkin = cgs.media.larmourHeadSkin;
+    else
+      head.customSkin = ci->headSkin;
+
+    // we can't hit what we can't see :P
+    if( es->eFlags & EF_MOVER_STOP )
+    {
+      if( ci->team != cg.snap->ps.stats[ STAT_TEAM ] )
+        head.customShader = cgs.media.invisibleShader;
+      else
+        head.customShader = cgs.media.invisibleShaderTeam;
+    }
+
+    if( !head.hModel )
+      return;
+
+    VectorCopy( cent->lerpOrigin, head.lightingOrigin );
+
+    CG_PositionRotatedEntityOnTag( &head, &torso, ci->torsoModel, "tag_head" );
+
+    head.shadowPlane = shadowPlane;
+    head.renderfx = renderfx;
+
+    trap_R_AddRefEntityToScene( &head );
+	
+	
+    // if this player has been hit with poison cloud, add an effect PS
+    if( ( es->eFlags & EF_POISONCLOUDED ) &&
+        ( es->number != cg.snap->ps.clientNum || cg.renderingThirdPerson ) )
+    {
+      if( !CG_IsParticleSystemValid( &cent->poisonCloudedPS ) )
+        cent->poisonCloudedPS = CG_SpawnNewParticleSystem( cgs.media.poisonCloudedPS );
+
+      CG_SetAttachmentTag( &cent->poisonCloudedPS->attachment,
+                           head, head.hModel, "tag_head" );
+      CG_SetAttachmentCent( &cent->poisonCloudedPS->attachment, cent );
+      CG_AttachToTag( &cent->poisonCloudedPS->attachment );
+    }
+    else if( CG_IsParticleSystemValid( &cent->poisonCloudedPS ) )
+      CG_DestroyParticleSystem( &cent->poisonCloudedPS );
+  }
+
+  //
+  // add the gun / barrel / flash
+  //
+  if( es->weapon != WP_NONE )
+  {
+    if( !ci->nonsegmented )
+      CG_AddPlayerWeapon( &torso, NULL, cent );
+    else
+      CG_AddPlayerWeapon( &legs, NULL, cent );
+  }
+
+  CG_PlayerUpgrades( cent, &torso );
+  
+  //sanity check that particle systems are stopped when dead
+  if( es->eFlags & EF_DEAD )
+  {
+    if( CG_IsParticleSystemValid( &cent->muzzlePS ) )
+      CG_DestroyParticleSystem( &cent->muzzlePS );
+
+    if( CG_IsParticleSystemValid( &cent->jetPackPS ) )
+      CG_DestroyParticleSystem( &cent->jetPackPS );
+  }
+
+  VectorCopy( surfNormal, cent->pe.lastNormal );
+}
+
+/*
+===============
+CG_Corpse
+===============
+*/
+void CG_Corpse( centity_t *cent )
+{
+  clientInfo_t  *ci;
+  refEntity_t   legs;
+  refEntity_t   torso;
+  refEntity_t   head;
+  entityState_t *es = &cent->currentState;
+  int           corpseNum;
+  int           renderfx;
+  qboolean      shadow = qfalse;
+  float         shadowPlane;
+  vec3_t        origin, liveZ, deadZ;
+  float         scale;
+
+  corpseNum = CG_GetCorpseNum( es->clientNum );
+
+  if( corpseNum < 0 || corpseNum >= MAX_CLIENTS )
+    CG_Error( "Bad corpseNum on corpse entity: %d", corpseNum );
+
+  ci = &cgs.corpseinfo[ corpseNum ];
+
+  // it is possible to see corpses from disconnected players that may
+  // not have valid clientinfo
+  if( !ci->infoValid )
+    return;
+
+  memset( &legs, 0, sizeof( legs ) );
+  memset( &torso, 0, sizeof( torso ) );
+  memset( &head, 0, sizeof( head ) );
+
+  VectorCopy( cent->lerpOrigin, origin );
+  BG_ClassBoundingBox( es->clientNum, liveZ, NULL, NULL, deadZ, NULL );
+  origin[ 2 ] -= ( liveZ[ 2 ] - deadZ[ 2 ] );
+
+  VectorCopy( es->angles, cent->lerpAngles );
+
+  // get the rotation information
+  if( !ci->nonsegmented )
+    CG_PlayerAngles( cent, cent->lerpAngles, legs.axis, torso.axis, head.axis );
+  else
+    CG_PlayerNonSegAngles( cent, cent->lerpAngles, legs.axis );
+
+  //set the correct frame (should always be dead)
+  if( cg_noPlayerAnims.integer )
+    legs.oldframe = legs.frame = torso.oldframe = torso.frame = 0;
+  else if( !ci->nonsegmented )
+  {
+    memset( &cent->pe.legs, 0, sizeof( lerpFrame_t ) );
+    CG_RunPlayerLerpFrame( ci, &cent->pe.legs, es->legsAnim, 1 );
+    legs.oldframe = cent->pe.legs.oldFrame;
+    legs.frame = cent->pe.legs.frame;
+    legs.backlerp = cent->pe.legs.backlerp;
+
+    memset( &cent->pe.torso, 0, sizeof( lerpFrame_t ) );
+    CG_RunPlayerLerpFrame( ci, &cent->pe.torso, es->torsoAnim, 1 );
+    torso.oldframe = cent->pe.torso.oldFrame;
+    torso.frame = cent->pe.torso.frame;
+    torso.backlerp = cent->pe.torso.backlerp;
+  }
+  else
+  {
+    memset( &cent->pe.nonseg, 0, sizeof( lerpFrame_t ) );
+    CG_RunPlayerLerpFrame( ci, &cent->pe.nonseg, es->legsAnim, 1 );
+    legs.oldframe = cent->pe.nonseg.oldFrame;
+    legs.frame = cent->pe.nonseg.frame;
+    legs.backlerp = cent->pe.nonseg.backlerp;
+  }
+
+  // add the shadow
+  shadow = CG_PlayerShadow( cent, &shadowPlane, es->clientNum );
+
+  // get the player model information
+  renderfx = 0;
+
+  if( cg_shadows.integer == 3 && shadow )
+    renderfx |= RF_SHADOW_PLANE;
+
+  renderfx |= RF_LIGHTING_ORIGIN;     // use the same origin for all
+
+  //
+  // add the legs
+  //
+  if( !ci->nonsegmented )
+  {
+    legs.hModel = ci->legsModel;
+    legs.customSkin = ci->legsSkin;
+  }
+  else
+  {
+    legs.hModel = ci->nonSegModel;
+    legs.customSkin = ci->nonSegSkin;
+  }
+
+  VectorCopy( origin, legs.origin );
+
+  VectorCopy( origin, legs.lightingOrigin );
+  legs.shadowPlane = shadowPlane;
+  legs.renderfx = renderfx;
+  legs.origin[ 2 ] += BG_ClassConfig( es->clientNum )->zOffset;
+  VectorCopy( legs.origin, legs.oldorigin ); // don't positionally lerp at all
+
+  //rescale the model
+  scale = BG_ClassConfig( es->clientNum )->modelScale;
+
+  if( scale != 1.0f )
+  {
+    VectorScale( legs.axis[ 0 ], scale, legs.axis[ 0 ] );
+    VectorScale( legs.axis[ 1 ], scale, legs.axis[ 1 ] );
+    VectorScale( legs.axis[ 2 ], scale, legs.axis[ 2 ] );
+
+    legs.nonNormalizedAxes = qtrue;
+  }
+
+  trap_R_AddRefEntityToScene( &legs );
+
+  // if the model failed, allow the default nullmodel to be displayed
+  if( !legs.hModel )
+    return;
+
+  if( !ci->nonsegmented )
+  {
+    //
+    // add the torso
+    //
+    torso.hModel = ci->torsoModel;
+    if( !torso.hModel )
+      return;
+
+    torso.customSkin = ci->torsoSkin;
+
+    VectorCopy( origin, torso.lightingOrigin );
+
+    CG_PositionRotatedEntityOnTag( &torso, &legs, ci->legsModel, "tag_torso" );
+
+    torso.shadowPlane = shadowPlane;
+    torso.renderfx = renderfx;
+
+    trap_R_AddRefEntityToScene( &torso );
+
+    //
+    // add the head
+    //
+    head.hModel = ci->headModel;
+    if( !head.hModel )
+      return;
+
+    head.customSkin = ci->headSkin;
+
+    VectorCopy( origin, head.lightingOrigin );
+
+    CG_PositionRotatedEntityOnTag( &head, &torso, ci->torsoModel, "tag_head");
+
+    head.shadowPlane = shadowPlane;
+    head.renderfx = renderfx;
+
+    trap_R_AddRefEntityToScene( &head );
+  }
+}
+
+
+//=====================================================================
+
+/*
+===============
+CG_ResetPlayerEntity
+
+A player just came into view or teleported, so reset all animation info
+===============
+*/
+void CG_ResetPlayerEntity( centity_t *cent )
+{
+  cent->errorTime = -99999;   // guarantee no error decay added
+  cent->extrapolated = qfalse;
+
+  CG_ClearLerpFrame( &cgs.clientinfo[ cent->currentState.clientNum ],
+                     &cent->pe.legs, cent->currentState.legsAnim );
+  CG_ClearLerpFrame( &cgs.clientinfo[ cent->currentState.clientNum ],
+                     &cent->pe.torso, cent->currentState.torsoAnim );
+  CG_ClearLerpFrame( &cgs.clientinfo[ cent->currentState.clientNum ],
+                     &cent->pe.nonseg, cent->currentState.legsAnim );
+
+  BG_EvaluateTrajectory( &cent->currentState.pos, cg.time, cent->lerpOrigin );
+  BG_EvaluateTrajectory( &cent->currentState.apos, cg.time, cent->lerpAngles );
+
+  VectorCopy( cent->lerpOrigin, cent->rawOrigin );
+  VectorCopy( cent->lerpAngles, cent->rawAngles );
+
+  memset( &cent->pe.legs, 0, sizeof( cent->pe.legs ) );
+  cent->pe.legs.yawAngle = cent->rawAngles[ YAW ];
+  cent->pe.legs.yawing = qfalse;
+  cent->pe.legs.pitchAngle = 0;
+  cent->pe.legs.pitching = qfalse;
+
+  memset( &cent->pe.torso, 0, sizeof( cent->pe.legs ) );
+  cent->pe.torso.yawAngle = cent->rawAngles[ YAW ];
+  cent->pe.torso.yawing = qfalse;
+  cent->pe.torso.pitchAngle = cent->rawAngles[ PITCH ];
+  cent->pe.torso.pitching = qfalse;
+
+  memset( &cent->pe.nonseg, 0, sizeof( cent->pe.nonseg ) );
+  cent->pe.nonseg.yawAngle = cent->rawAngles[ YAW ];
+  cent->pe.nonseg.yawing = qfalse;
+  cent->pe.nonseg.pitchAngle = cent->rawAngles[ PITCH ];
+  cent->pe.nonseg.pitching = qfalse;
+
+  if( cg_debugPosition.integer )
+    CG_Printf( "%i ResetPlayerEntity yaw=%i\n", cent->currentState.number, cent->pe.torso.yawAngle );
+}
+
+/*
+==================
+CG_PlayerDisconnect
+
+Player disconnecting
+==================
+*/
+void CG_PlayerDisconnect( vec3_t org )
+{
+  particleSystem_t  *ps;
+
+  trap_S_StartSound( org, ENTITYNUM_WORLD, CHAN_AUTO, cgs.media.disconnectSound );
+
+  ps = CG_SpawnNewParticleSystem( cgs.media.disconnectPS );
+
+  if( CG_IsParticleSystemValid( &ps ) )
+  {
+    CG_SetAttachmentPoint( &ps->attachment, org );
+    CG_AttachToPoint( &ps->attachment );
+  }
+}
+
+centity_t *CG_GetPlayerLocation( void )
+{
+  int i;
+  centity_t   *eloc, *best;
+  float       bestlen, len;
+  vec3_t      origin;
+
+  best = NULL;
+  bestlen = 3.0f * 8192.0f * 8192.0f;
+
+  VectorCopy( cg.predictedPlayerState.origin, origin );
+
+  for( i = MAX_CLIENTS; i < MAX_GENTITIES; i++ )
+  {
+    eloc = &cg_entities[ i ];
+    if( !eloc->valid || eloc->currentState.eType != ET_LOCATION )
+      continue;
+
+    len = DistanceSquared(origin, eloc->lerpOrigin);
+
+    if( len > bestlen )
+      continue;
+
+    if( !trap_R_inPVS( origin, eloc->lerpOrigin ) )
+      continue;
+
+    bestlen = len;
+    best = eloc;
+  }
+
+  return best;
+}
+
diff --git a/src/cgame/cg_playerstate.c b/src/cgame/cg_playerstate.c
new file mode 100644
index 0000000..8a8e739
--- /dev/null
+++ b/src/cgame/cg_playerstate.c
@@ -0,0 +1,324 @@
+/*
+===========================================================================
+Copyright (C) 1999-2005 Id Software, Inc.
+Copyright (C) 2000-2009 Darklegion Development
+
+This file is part of Tremulous.
+
+Tremulous 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.
+
+Tremulous 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 Tremulous; if not, write to the Free Software
+Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
+===========================================================================
+*/
+
+// cg_playerstate.c -- this file acts on changes in a new playerState_t
+// With normal play, this will be done after local prediction, but when
+// following another player or playing back a demo, it will be checked
+// when the snapshot transitions like all the other entities
+
+
+#include "cg_local.h"
+
+/*
+==============
+CG_DamageFeedback
+==============
+*/
+void CG_DamageFeedback( int yawByte, int pitchByte, int damage )
+{
+  float   left, front, up;
+  float   kick;
+  int     health;
+  float   scale;
+  vec3_t  dir;
+  vec3_t  angles;
+  float   dist;
+  float   yaw, pitch;
+
+  // show the attacking player's head and name in corner
+  cg.attackerTime = cg.time;
+
+  // the lower on health you are, the greater the view kick will be
+  health = cg.snap->ps.stats[STAT_HEALTH];
+
+  if( health < 40 )
+    scale = 1;
+  else
+    scale = 40.0 / health;
+
+  kick = damage * scale;
+
+  if( kick < 5 )
+    kick = 5;
+
+  if( kick > 10 )
+    kick = 10;
+
+  // if yaw and pitch are both 255, make the damage always centered (falling, etc)
+  if( yawByte == 255 && pitchByte == 255 )
+  {
+    cg.damageX = 0;
+    cg.damageY = 0;
+    cg.v_dmg_roll = 0;
+    cg.v_dmg_pitch = -kick;
+  }
+  else
+  {
+    // positional
+    pitch = pitchByte / 255.0 * 360;
+    yaw = yawByte / 255.0 * 360;
+
+    angles[ PITCH ] = pitch;
+    angles[ YAW ] = yaw;
+    angles[ ROLL ] = 0;
+
+    AngleVectors( angles, dir, NULL, NULL );
+    VectorSubtract( vec3_origin, dir, dir );
+
+    front = DotProduct( dir, cg.refdef.viewaxis[ 0 ] );
+    left = DotProduct( dir, cg.refdef.viewaxis[ 1 ] );
+    up = DotProduct( dir, cg.refdef.viewaxis[ 2 ] );
+
+    dir[ 0 ] = front;
+    dir[ 1 ] = left;
+    dir[ 2 ] = 0;
+    dist = VectorLength( dir );
+
+    if( dist < 0.1f )
+      dist = 0.1f;
+
+    cg.v_dmg_roll = kick * left;
+
+    cg.v_dmg_pitch = -kick * front;
+
+    if( front <= 0.1 )
+      front = 0.1f;
+
+    cg.damageX = -left / front;
+    cg.damageY = up / dist;
+  }
+
+  // clamp the position
+  if( cg.damageX > 1.0 )
+    cg.damageX = 1.0;
+
+  if( cg.damageX < - 1.0 )
+    cg.damageX = -1.0;
+
+  if( cg.damageY > 1.0 )
+    cg.damageY = 1.0;
+
+  if( cg.damageY < - 1.0 )
+    cg.damageY = -1.0;
+
+  // don't let the screen flashes vary as much
+  if( kick > 10 )
+    kick = 10;
+
+  cg.damageValue = kick;
+  cg.v_dmg_time = cg.time + DAMAGE_TIME;
+  cg.damageTime = cg.snap->serverTime;
+}
+
+
+
+
+/*
+================
+CG_Respawn
+
+A respawn happened this snapshot
+================
+*/
+void CG_Respawn( void )
+{
+  // no error decay on player movement
+  cg.thisFrameTeleport = qtrue;
+
+  // display weapons available
+  cg.weaponSelectTime = cg.time;
+
+  // select the weapon the server says we are using
+  cg.weaponSelect = cg.snap->ps.weapon;
+
+  CG_ResetPainBlend( );
+}
+
+/*
+==============
+CG_CheckPlayerstateEvents
+
+==============
+*/
+void CG_CheckPlayerstateEvents( playerState_t *ps, playerState_t *ops )
+{
+  int       i;
+  int       event;
+  centity_t *cent;
+
+  if( ps->externalEvent && ps->externalEvent != ops->externalEvent )
+  {
+    cent = &cg_entities[ ps->clientNum ];
+    cent->currentState.event = ps->externalEvent;
+    cent->currentState.eventParm = ps->externalEventParm;
+    CG_EntityEvent( cent, cent->lerpOrigin );
+  }
+
+  cent = &cg.predictedPlayerEntity; // cg_entities[ ps->clientNum ];
+
+  // go through the predictable events buffer
+  for( i = ps->eventSequence - MAX_PS_EVENTS; i < ps->eventSequence; i++ )
+  {
+    // if we have a new predictable event
+    if( i >= ops->eventSequence ||
+      // or the server told us to play another event instead of a predicted event we already issued
+      // or something the server told us changed our prediction causing a different event
+      ( i > ops->eventSequence - MAX_PS_EVENTS && ps->events[ i & ( MAX_PS_EVENTS - 1 ) ] !=
+        ops->events[ i & ( MAX_PS_EVENTS - 1 ) ] ) )
+    {
+      event = ps->events[ i & ( MAX_PS_EVENTS - 1 ) ];
+
+      cent->currentState.event = event;
+      cent->currentState.eventParm = ps->eventParms[ i & ( MAX_PS_EVENTS - 1 ) ];
+      CG_EntityEvent( cent, cent->lerpOrigin );
+      cg.predictableEvents[ i & ( MAX_PREDICTED_EVENTS - 1 ) ] = event;
+
+      cg.eventSequence++;
+    }
+  }
+}
+
+
+/*
+==================
+CG_CheckChangedPredictableEvents
+==================
+*/
+void CG_CheckChangedPredictableEvents( playerState_t *ps )
+{
+  int       i;
+  int       event;
+  centity_t *cent;
+
+  cent = &cg.predictedPlayerEntity;
+
+  for( i = ps->eventSequence - MAX_PS_EVENTS; i < ps->eventSequence; i++ )
+  {
+    //
+    if( i >= cg.eventSequence )
+      continue;
+
+    // if this event is not further back in than the maximum predictable events we remember
+    if( i > cg.eventSequence - MAX_PREDICTED_EVENTS )
+    {
+      // if the new playerstate event is different from a previously predicted one
+      if( ps->events[ i & ( MAX_PS_EVENTS - 1 ) ] != cg.predictableEvents[ i & ( MAX_PREDICTED_EVENTS - 1 ) ] )
+      {
+        event = ps->events[ i & ( MAX_PS_EVENTS - 1 ) ];
+        cent->currentState.event = event;
+        cent->currentState.eventParm = ps->eventParms[ i & ( MAX_PS_EVENTS - 1 ) ];
+        CG_EntityEvent( cent, cent->lerpOrigin );
+
+        cg.predictableEvents[ i & ( MAX_PREDICTED_EVENTS - 1 ) ] = event;
+
+        if( cg_showmiss.integer )
+          CG_Printf( "WARNING: changed predicted event\n" );
+      }
+    }
+  }
+}
+
+/*
+==================
+CG_CheckLocalSounds
+==================
+*/
+void CG_CheckLocalSounds( playerState_t *ps, playerState_t *ops )
+{
+  int reward;
+
+  // don't play the sounds if the player just spawned
+  if( ps->persistant[ PERS_SPECSTATE ] != ops->persistant[ PERS_SPECSTATE ] )
+    return;
+
+  // health changes of more than -1 should make pain sounds
+  if( ps->stats[ STAT_HEALTH ] < ops->stats[ STAT_HEALTH ] - 1 )
+  {
+    if( ps->stats[ STAT_HEALTH ] > 0 )
+      CG_PainEvent( &cg.predictedPlayerEntity, ps->stats[ STAT_HEALTH ] );
+  }
+
+
+  // if we are going into the intermission, don't start any voices
+  if( cg.intermissionStarted )
+    return;
+
+  // reward sounds
+  reward = qfalse;
+}
+
+
+/*
+===============
+CG_TransitionPlayerState
+
+===============
+*/
+void CG_TransitionPlayerState( playerState_t *ps, playerState_t *ops )
+{
+  // check for changing follow mode
+  if( ps->clientNum != ops->clientNum )
+  {
+    cg.thisFrameTeleport = qtrue;
+    // make sure we don't get any unwanted transition effects
+    *ops = *ps;
+
+    CG_ResetPainBlend( );
+  }
+
+  // damage events (player is getting wounded)
+  if( ps->damageEvent != ops->damageEvent && ps->damageCount )
+    CG_DamageFeedback( ps->damageYaw, ps->damagePitch, ps->damageCount );
+
+  // respawning
+  if( ps->persistant[ PERS_SPAWN_COUNT ] != ops->persistant[ PERS_SPAWN_COUNT ] )
+    CG_Respawn( );
+
+  if( cg.mapRestart )
+  {
+    CG_Respawn( );
+    cg.mapRestart = qfalse;
+  }
+
+  if( cg.snap->ps.pm_type != PM_INTERMISSION &&
+      ps->persistant[ PERS_SPECSTATE ] == SPECTATOR_NOT )
+    CG_CheckLocalSounds( ps, ops );
+
+  // run events
+  CG_CheckPlayerstateEvents( ps, ops );
+
+  // smooth the ducking viewheight change
+  if( ps->viewheight != ops->viewheight )
+  {
+    cg.duckChange = ps->viewheight - ops->viewheight;
+    cg.duckTime = cg.time;
+  }
+  
+  // changed team
+  if( ps->stats[ STAT_TEAM ] != ops->stats[ STAT_TEAM ] )
+  {
+    cg.lastHealthCross = 0;
+    cg.chargeMeterAlpha = 0.0f;
+  }
+}
+
diff --git a/src/cgame/cg_predict.c b/src/cgame/cg_predict.c
new file mode 100644
index 0000000..5b29250
--- /dev/null
+++ b/src/cgame/cg_predict.c
@@ -0,0 +1,900 @@
+/*
+===========================================================================
+Copyright (C) 1999-2005 Id Software, Inc.
+Copyright (C) 2000-2009 Darklegion Development
+
+This file is part of Tremulous.
+
+Tremulous 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.
+
+Tremulous 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 Tremulous; if not, write to the Free Software
+Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
+===========================================================================
+*/
+
+// cg_predict.c -- this file generates cg.predictedPlayerState by either
+// interpolating between snapshots from the server or locally predicting
+// ahead the client's movement.
+// It also handles local physics interaction, like fragments bouncing off walls
+
+
+#include "cg_local.h"
+
+static  pmove_t   cg_pmove;
+
+static  int     cg_numSolidEntities;
+static  centity_t *cg_solidEntities[MAX_ENTITIES_IN_SNAPSHOT];
+static  int     cg_numTriggerEntities;
+static  centity_t *cg_triggerEntities[MAX_ENTITIES_IN_SNAPSHOT];
+extern vmCvar_t cg_EDGEFPSFIX;
+/*
+====================
+CG_BuildSolidList
+
+When a new cg.snap has been set, this function builds a sublist
+of the entities that are actually solid, to make for more
+efficient collision detection
+====================
+*/
+void CG_BuildSolidList( void )
+{
+  int           i;
+  centity_t     *cent;
+  snapshot_t    *snap;
+  entityState_t *ent;
+
+  cg_numSolidEntities = 0;
+  cg_numTriggerEntities = 0;
+
+  if( cg.nextSnap && !cg.nextFrameTeleport && !cg.thisFrameTeleport )
+    snap = cg.nextSnap;
+  else
+    snap = cg.snap;
+
+  for( i = 0; i < snap->numEntities; i++ )
+  {
+    cent = &cg_entities[ snap->entities[ i ].number ];
+    ent = &cent->currentState;
+
+    if( ent->eType == ET_ITEM || ent->eType == ET_PUSH_TRIGGER || ent->eType == ET_TELEPORT_TRIGGER )
+    {
+      cg_triggerEntities[ cg_numTriggerEntities ] = cent;
+      cg_numTriggerEntities++;
+      continue;
+    }
+
+    if( cent->nextState.solid && ent->eType != ET_MISSILE )
+    {
+      cg_solidEntities[ cg_numSolidEntities ] = cent;
+      cg_numSolidEntities++;
+      continue;
+    }
+  }
+}
+
+/*
+====================
+CG_ClipMoveToEntities
+
+====================
+*/
+static void CG_ClipMoveToEntities ( const vec3_t start, const vec3_t mins,
+    const vec3_t maxs, const vec3_t end, int skipNumber,
+    int mask, trace_t *tr, traceType_t collisionType )
+{
+  int           i, j, x, zd, zu;
+  trace_t       trace;
+  entityState_t *ent;
+  clipHandle_t  cmodel;
+  vec3_t        tmins, tmaxs;
+  vec3_t        bmins, bmaxs;
+  vec3_t        origin, angles;
+  centity_t     *cent;
+
+  //SUPAR HACK
+  //this causes a trace to collide with the local player
+  if( skipNumber == MAGIC_TRACE_HACK )
+    j = cg_numSolidEntities + 1;
+  else
+    j = cg_numSolidEntities;
+
+  if (cg_EDGEFPSFIX.integer)
+  {
+    // calculate bounding box of the trace
+    ClearBounds( tmins, tmaxs );
+    AddPointToBounds( start, tmins, tmaxs );
+    AddPointToBounds( end, tmins, tmaxs );
+    if( mins )
+      VectorAdd( mins, tmins, tmins );
+    if( maxs )
+      VectorAdd( maxs, tmaxs, tmaxs );
+  } 
+	
+  for( i = 0; i < j; i++ )
+  {
+    if( i < cg_numSolidEntities )
+      cent = cg_solidEntities[ i ];
+    else
+      cent = &cg.predictedPlayerEntity;
+
+    ent = &cent->currentState;
+
+    if( ent->number == skipNumber )
+      continue;
+
+    if( ent->solid == SOLID_BMODEL )
+    {
+      // special value for bmodel
+      cmodel = trap_CM_InlineModel( ent->modelindex );
+      VectorCopy( cent->lerpAngles, angles );
+      BG_EvaluateTrajectory( &cent->currentState.pos, cg.physicsTime, origin );
+    }
+    else
+    {
+      // encoded bbox
+      x = ( ent->solid & 255 );
+      zd = ( ( ent->solid >> 8 ) & 255 );
+      zu = ( ( ent->solid >> 16 ) & 255 ) - 32;
+
+      bmins[ 0 ] = bmins[ 1 ] = -x;
+      bmaxs[ 0 ] = bmaxs[ 1 ] = x;
+      bmins[ 2 ] = -zd;
+      bmaxs[ 2 ] = zu;
+
+      if( i == cg_numSolidEntities )
+        BG_ClassBoundingBox( ( ent->misc >> 8 ) & 0xFF, bmins, bmaxs, NULL, NULL, NULL );
+
+      if (cg_EDGEFPSFIX.integer)
+      {
+        VectorAdd( cent->lerpOrigin, bmins, bmins );
+        VectorAdd( cent->lerpOrigin, bmaxs, bmaxs );
+        if( !BoundsIntersect( bmins, bmaxs, tmins, tmaxs ) )
+          continue;
+      } 
+		
+      cmodel = trap_CM_TempBoxModel( bmins, bmaxs );
+      VectorCopy( vec3_origin, angles );
+//      VectorCopy( cent->lerpOrigin, origin );
+      if (cg_EDGEFPSFIX.integer)
+        VectorCopy( vec3_origin, origin );
+      else
+        VectorCopy( cent->lerpOrigin, origin );
+    }
+    
+
+    if( collisionType == TT_CAPSULE )
+    {
+      trap_CM_TransformedCapsuleTrace ( &trace, start, end,
+        mins, maxs, cmodel,  mask, origin, angles );
+    }
+    else if( collisionType == TT_AABB )
+    {
+      trap_CM_TransformedBoxTrace ( &trace, start, end,
+        mins, maxs, cmodel,  mask, origin, angles );
+    }
+    else if( collisionType == TT_BISPHERE )
+    {
+      trap_CM_TransformedBiSphereTrace( &trace, start, end,
+        mins[ 0 ], maxs[ 0 ], cmodel, mask, origin );
+    }
+
+    if( trace.allsolid || trace.fraction < tr->fraction )
+    {
+      trace.entityNum = ent->number;
+
+      if( tr->lateralFraction < trace.lateralFraction )
+      {
+        float oldLateralFraction = tr->lateralFraction;
+        *tr = trace;
+        tr->lateralFraction = oldLateralFraction;
+      }
+      else
+        *tr = trace;
+    }
+    else if( trace.startsolid )
+    {
+      tr->startsolid = qtrue;
+      tr->entityNum = ent->number;
+    }
+
+    if( tr->allsolid )
+      return;
+  }
+}
+
+/*
+================
+CG_Trace
+================
+*/
+void  CG_Trace( trace_t *result, const vec3_t start, const vec3_t mins, const vec3_t maxs, const vec3_t end,
+                int skipNumber, int mask )
+{
+  trace_t t;
+
+  trap_CM_BoxTrace( &t, start, end, mins, maxs, 0, mask );
+  t.entityNum = t.fraction != 1.0 ? ENTITYNUM_WORLD : ENTITYNUM_NONE;
+  // check all other solid models
+  CG_ClipMoveToEntities( start, mins, maxs, end, skipNumber, mask, &t, TT_AABB );
+
+  *result = t;
+}
+
+/*
+================
+CG_CapTrace
+================
+*/
+void  CG_CapTrace( trace_t *result, const vec3_t start, const vec3_t mins, const vec3_t maxs, const vec3_t end,
+                   int skipNumber, int mask )
+{
+  trace_t t;
+
+  trap_CM_CapsuleTrace( &t, start, end, mins, maxs, 0, mask );
+  t.entityNum = t.fraction != 1.0 ? ENTITYNUM_WORLD : ENTITYNUM_NONE;
+  // check all other solid models
+  CG_ClipMoveToEntities( start, mins, maxs, end, skipNumber, mask, &t, TT_CAPSULE );
+
+  *result = t;
+}
+
+/*
+================
+CG_BiSphereTrace
+================
+*/
+void CG_BiSphereTrace( trace_t *result, const vec3_t start, const vec3_t end,
+    const float startRadius, const float endRadius, int skipNumber, int mask )
+{
+  trace_t t;
+  vec3_t  mins, maxs;
+
+  mins[ 0 ] = startRadius;
+  maxs[ 0 ] = endRadius;
+
+  trap_CM_BiSphereTrace( &t, start, end, startRadius, endRadius, 0, mask );
+  t.entityNum = t.fraction != 1.0 ? ENTITYNUM_WORLD : ENTITYNUM_NONE;
+  // check all other solid models
+  CG_ClipMoveToEntities( start, mins, maxs, end, skipNumber, mask, &t, TT_BISPHERE );
+
+  *result = t;
+}
+
+/*
+================
+CG_PointContents
+================
+*/
+int   CG_PointContents( const vec3_t point, int passEntityNum )
+{
+  int           i;
+  entityState_t *ent;
+  centity_t     *cent;
+  clipHandle_t  cmodel;
+  int           contents;
+
+  contents = trap_CM_PointContents (point, 0);
+
+  for( i = 0; i < cg_numSolidEntities; i++ )
+  {
+    cent = cg_solidEntities[ i ];
+
+    ent = &cent->currentState;
+
+    if( ent->number == passEntityNum )
+      continue;
+
+    if( ent->solid != SOLID_BMODEL ) // special value for bmodel
+      continue;
+
+    cmodel = trap_CM_InlineModel( ent->modelindex );
+
+    if( !cmodel )
+      continue;
+
+    contents |= trap_CM_TransformedPointContents( point, cmodel, ent->origin, ent->angles );
+  }
+
+  return contents;
+}
+
+
+/*
+========================
+CG_InterpolatePlayerState
+
+Generates cg.predictedPlayerState by interpolating between
+cg.snap->player_state and cg.nextFrame->player_state
+========================
+*/
+static void CG_InterpolatePlayerState( qboolean grabAngles )
+{
+  float         f;
+  int           i;
+  playerState_t *out;
+  snapshot_t    *prev, *next;
+
+  out = &cg.predictedPlayerState;
+  prev = cg.snap;
+  next = cg.nextSnap;
+
+  *out = cg.snap->ps;
+
+  // if we are still allowing local input, short circuit the view angles
+  if( grabAngles )
+  {
+    usercmd_t cmd;
+    int     cmdNum;
+
+    cmdNum = trap_GetCurrentCmdNumber( );
+    trap_GetUserCmd( cmdNum, &cmd );
+
+    PM_UpdateViewAngles( out, &cmd );
+  }
+
+  // if the next frame is a teleport, we can't lerp to it
+  if( cg.nextFrameTeleport )
+    return;
+
+  if( !next || next->serverTime <= prev->serverTime )
+    return;
+
+  f = (float)( cg.time - prev->serverTime ) / ( next->serverTime - prev->serverTime );
+
+  i = next->ps.bobCycle;
+  if( i < prev->ps.bobCycle )
+    i += 256;   // handle wraparound
+
+  out->bobCycle = prev->ps.bobCycle + f * ( i - prev->ps.bobCycle );
+
+  for( i = 0; i < 3; i++ )
+  {
+    out->origin[ i ] = prev->ps.origin[ i ] + f * ( next->ps.origin[ i ] - prev->ps.origin[ i ] );
+
+    if( !grabAngles )
+      out->viewangles[ i ] = LerpAngle( prev->ps.viewangles[ i ], next->ps.viewangles[ i ], f );
+
+    out->velocity[ i ] = prev->ps.velocity[ i ] +
+      f * (next->ps.velocity[ i ] - prev->ps.velocity[ i ] );
+  }
+}
+
+
+/*
+=========================
+CG_TouchTriggerPrediction
+
+Predict push triggers and items
+=========================
+*/
+static void CG_TouchTriggerPrediction( void )
+{
+  int           i;
+  trace_t       trace;
+  entityState_t *ent;
+  clipHandle_t  cmodel;
+  centity_t     *cent;
+  qboolean      spectator;
+
+  // dead clients don't activate triggers
+  if( cg.predictedPlayerState.stats[ STAT_HEALTH ] <= 0 )
+    return;
+
+  spectator = ( cg.predictedPlayerState.pm_type == PM_SPECTATOR );
+
+  if( cg.predictedPlayerState.pm_type != PM_NORMAL && !spectator )
+    return;
+
+  for( i = 0; i < cg_numTriggerEntities; i++ )
+  {
+    cent = cg_triggerEntities[ i ];
+    ent = &cent->currentState;
+
+    if( ent->solid != SOLID_BMODEL )
+      continue;
+
+    cmodel = trap_CM_InlineModel( ent->modelindex );
+    if( !cmodel )
+      continue;
+
+    trap_CM_BoxTrace( &trace, cg.predictedPlayerState.origin, cg.predictedPlayerState.origin,
+                      cg_pmove.mins, cg_pmove.maxs, cmodel, -1 );
+
+    if( !trace.startsolid )
+      continue;
+
+    if( ent->eType == ET_TELEPORT_TRIGGER )
+      cg.hyperspace = qtrue;
+  }
+}
+
+static int CG_IsUnacceptableError( playerState_t *ps, playerState_t *pps )
+{
+  vec3_t delta;
+  int i;
+
+  if( pps->pm_type != ps->pm_type ||
+    pps->pm_flags != ps->pm_flags ||
+    pps->pm_time != ps->pm_time )
+  {
+    return 1;
+  }
+
+  VectorSubtract( pps->origin, ps->origin, delta );
+  if( VectorLengthSquared( delta ) > 0.1f * 0.1f )
+  {
+    if( cg_showmiss.integer )
+    {
+      CG_Printf( "origin delta: %.2f  ", VectorLength( delta ) );
+    }
+    return 2;
+  }
+
+  VectorSubtract( pps->velocity, ps->velocity, delta );
+  if( VectorLengthSquared( delta ) > 0.1f * 0.1f )
+  {
+    if( cg_showmiss.integer )
+    {
+      CG_Printf( "velocity delta: %.2f  ", VectorLength( delta ) );
+    }
+    return 3;
+  }
+
+  if( pps->weaponTime != ps->weaponTime ||
+    pps->gravity != ps->gravity ||
+    pps->speed != ps->speed ||
+    pps->delta_angles[ 0 ] != ps->delta_angles[ 0 ] ||
+    pps->delta_angles[ 1 ] != ps->delta_angles[ 1 ] ||
+    pps->delta_angles[ 2 ] != ps->delta_angles[ 2 ] || 
+    pps->groundEntityNum != ps->groundEntityNum )
+  {
+    return 4;
+  }
+
+  if( pps->legsTimer != ps->legsTimer ||
+    pps->legsAnim != ps->legsAnim ||
+    pps->torsoTimer != ps->torsoTimer ||
+    pps->torsoAnim != ps->torsoAnim ||
+    pps->movementDir != ps->movementDir )
+  {
+    return 5;
+  }
+
+  VectorSubtract( pps->grapplePoint, ps->grapplePoint, delta );
+  if( VectorLengthSquared( delta ) > 0.1f * 0.1f )
+    return 6;
+
+  if( pps->eFlags != ps->eFlags )
+    return 7;
+
+  if( pps->eventSequence != ps->eventSequence )
+    return 8;
+
+  for( i = 0; i < MAX_PS_EVENTS; i++ )
+  {
+    if ( pps->events[ i ] != ps->events[ i ] ||
+      pps->eventParms[ i ] != ps->eventParms[ i ] )
+    {
+      return 9;
+    }
+  }
+
+  if( pps->externalEvent != ps->externalEvent ||
+    pps->externalEventParm != ps->externalEventParm ||
+    pps->externalEventTime != ps->externalEventTime )
+  {
+    return 10;
+  }
+
+  if( pps->clientNum != ps->clientNum ||
+    pps->weapon != ps->weapon ||
+    pps->weaponstate != ps->weaponstate )
+  {
+    return 11;
+  }
+
+  if( fabs( AngleDelta( ps->viewangles[ 0 ], pps->viewangles[ 0 ] ) ) > 1.0f ||
+    fabs( AngleDelta( ps->viewangles[ 1 ], pps->viewangles[ 1 ] ) ) > 1.0f ||
+    fabs( AngleDelta( ps->viewangles[ 2 ], pps->viewangles[ 2 ] ) ) > 1.0f )
+  {
+    return 12;
+  }
+
+  if( pps->viewheight != ps->viewheight )
+    return 13;
+
+  if( pps->damageEvent != ps->damageEvent ||
+    pps->damageYaw != ps->damageYaw ||
+    pps->damagePitch != ps->damagePitch ||
+    pps->damageCount != ps->damageCount )
+  {
+    return 14;
+  }
+
+  for( i = 0; i < MAX_STATS; i++ )
+  {
+    if( pps->stats[ i ] != ps->stats[ i ] )
+      return 15;
+  }
+
+  for( i = 0; i < MAX_PERSISTANT; i++ )
+  {
+    if( pps->persistant[ i ] != ps->persistant[ i ] )
+      return 16;
+  }
+
+  if( pps->generic1 != ps->generic1 ||
+    pps->loopSound != ps->loopSound )
+  {
+    return 19;
+  }
+
+  return 0;
+}
+
+
+/*
+=================
+CG_PredictPlayerState
+
+Generates cg.predictedPlayerState for the current cg.time
+cg.predictedPlayerState is guaranteed to be valid after exiting.
+
+For demo playback, this will be an interpolation between two valid
+playerState_t.
+
+For normal gameplay, it will be the result of predicted usercmd_t on
+top of the most recent playerState_t received from the server.
+
+Each new snapshot will usually have one or more new usercmd over the last,
+but we simulate all unacknowledged commands each time, not just the new ones.
+This means that on an internet connection, quite a few pmoves may be issued
+each frame.
+
+OPTIMIZE: don't re-simulate unless the newly arrived snapshot playerState_t
+differs from the predicted one.  Would require saving all intermediate
+playerState_t during prediction.
+
+We detect prediction errors and allow them to be decayed off over several frames
+to ease the jerk.
+=================
+*/
+void CG_PredictPlayerState( void )
+{
+  int     cmdNum, current, i;
+  playerState_t oldPlayerState;
+  qboolean  moved;
+  usercmd_t oldestCmd;
+  usercmd_t latestCmd;
+  int stateIndex = 0, predictCmd = 0;
+
+  cg.hyperspace = qfalse; // will be set if touching a trigger_teleport
+
+  // if this is the first frame we must guarantee
+  // predictedPlayerState is valid even if there is some
+  // other error condition
+  if( !cg.validPPS )
+  {
+    cg.validPPS = qtrue;
+    cg.predictedPlayerState = cg.snap->ps;
+  }
+
+
+  // demo playback just copies the moves
+  if( cg.demoPlayback || (cg.snap->ps.pm_flags & PMF_FOLLOW) )
+  {
+    CG_InterpolatePlayerState( qfalse );
+    return;
+  }
+
+  // non-predicting local movement will grab the latest angles
+  if( cg_nopredict.integer || cg_synchronousClients.integer )
+  {
+    CG_InterpolatePlayerState( qtrue );
+    return;
+  }
+
+  // prepare for pmove
+  cg_pmove.ps = &cg.predictedPlayerState;
+  cg_pmove.pmext = &cg.pmext;
+  cg_pmove.trace = CG_Trace;
+  cg_pmove.pointcontents = CG_PointContents;
+  cg_pmove.debugLevel = cg_debugMove.integer;
+
+  if( cg_pmove.ps->pm_type == PM_DEAD )
+    cg_pmove.tracemask = MASK_DEADSOLID;
+  else
+    cg_pmove.tracemask = MASK_PLAYERSOLID;
+
+  if( cg.snap->ps.persistant[ PERS_SPECSTATE ] != SPECTATOR_NOT )
+    cg_pmove.tracemask = MASK_DEADSOLID; // spectators can fly through bodies
+
+  cg_pmove.noFootsteps = 0;
+
+  // save the state before the pmove so we can detect transitions
+  oldPlayerState = cg.predictedPlayerState;
+
+  current = trap_GetCurrentCmdNumber( );
+
+  // if we don't have the commands right after the snapshot, we
+  // can't accurately predict a current position, so just freeze at
+  // the last good position we had
+  cmdNum = current - CMD_BACKUP + 1;
+  trap_GetUserCmd( cmdNum, &oldestCmd );
+
+  if( oldestCmd.serverTime > cg.snap->ps.commandTime &&
+      oldestCmd.serverTime < cg.time )
+  { // special check for map_restart
+    if( cg_showmiss.integer )
+      CG_Printf( "exceeded PACKET_BACKUP on commands\n" );
+
+    return;
+  }
+
+  // get the latest command so we can know which commands are from previous map_restarts
+  trap_GetUserCmd( current, &latestCmd );
+
+  // get the most recent information we have, even if
+  // the server time is beyond our current cg.time,
+  // because predicted player positions are going to
+  // be ahead of everything else anyway
+  if( cg.nextSnap && !cg.nextFrameTeleport && !cg.thisFrameTeleport )
+  {
+    cg.predictedPlayerState = cg.nextSnap->ps;
+    cg.physicsTime = cg.nextSnap->serverTime;
+  }
+  else
+  {
+    cg.predictedPlayerState = cg.snap->ps;
+    cg.physicsTime = cg.snap->serverTime;
+  }
+
+  if( pmove_msec.integer < 8 )
+    trap_Cvar_Set( "pmove_msec", "8" );
+  else if( pmove_msec.integer > 33 )
+    trap_Cvar_Set( "pmove_msec", "33" );
+
+  cg_pmove.pmove_fixed = pmove_fixed.integer;// | cg_pmove_fixed.integer;
+  cg_pmove.pmove_msec = pmove_msec.integer;
+
+  // Like the comments described above, a player's state is entirely
+  // re-predicted from the last valid snapshot every client frame, which
+  // can be really, really, really slow.  Every old command has to be
+  // run again.  For every client frame that is *not* directly after a
+  // snapshot, this is unnecessary, since we have no new information.
+  // For those, we'll play back the predictions from the last frame and
+  // predict only the newest commands.  Essentially, we'll be doing
+  // an incremental predict instead of a full predict.
+  //
+  // If we have a new snapshot, we can compare its player state's command
+  // time to the command times in the queue to find a match.  If we find
+  // a matching state, and the predicted version has not deviated, we can
+  // use the predicted state as a base - and also do an incremental predict.
+  //
+  // With this method, we get incremental predicts on every client frame
+  // except a frame following a new snapshot in which there was a prediction
+  // error.  This yeilds anywhere from a 15% to 40% performance increase,
+  // depending on how much of a bottleneck the CPU is.
+  if( cg_optimizePrediction.integer )
+  {
+    if( cg.nextFrameTeleport || cg.thisFrameTeleport )
+    {
+      // do a full predict
+      cg.lastPredictedCommand = 0;
+      cg.stateTail = cg.stateHead;
+      predictCmd = current - CMD_BACKUP + 1;
+    }
+    // cg.physicsTime is the current snapshot's serverTime if it's the same
+    // as the last one
+    else if( cg.physicsTime == cg.lastServerTime )
+    {
+      // we have no new information, so do an incremental predict
+      predictCmd = cg.lastPredictedCommand + 1;
+    }
+    else
+    {
+      // we have a new snapshot
+      int i;
+      int errorcode;
+      qboolean error = qtrue;
+
+      // loop through the saved states queue
+      for( i = cg.stateHead; i != cg.stateTail;
+        i = ( i + 1 ) % NUM_SAVED_STATES )
+      {
+        // if we find a predicted state whose commandTime matches the snapshot
+        // player state's commandTime
+        if( cg.savedPmoveStates[ i ].commandTime !=
+          cg.predictedPlayerState.commandTime )
+        {
+          continue;
+        }
+        // make sure the state differences are acceptable
+        errorcode = CG_IsUnacceptableError( &cg.predictedPlayerState,
+          &cg.savedPmoveStates[ i ] );
+
+        if( errorcode )
+        {
+          if( cg_showmiss.integer )
+            CG_Printf("errorcode %d at %d\n", errorcode, cg.time);
+          break;
+        }
+
+        // this one is almost exact, so we'll copy it in as the starting point
+        *cg_pmove.ps = cg.savedPmoveStates[ i ];
+        // advance the head
+        cg.stateHead = ( i + 1 ) % NUM_SAVED_STATES;
+
+        // set the next command to predict
+        predictCmd = cg.lastPredictedCommand + 1;
+
+        // a saved state matched, so flag it
+        error = qfalse;
+        break;
+      }
+
+      // if no saved states matched
+      if( error )
+      {
+        // do a full predict
+        cg.lastPredictedCommand = 0;
+        cg.stateTail = cg.stateHead;
+        predictCmd = current - CMD_BACKUP + 1;
+      }
+    }
+
+    // keep track of the server time of the last snapshot so we
+    // know when we're starting from a new one in future calls
+    cg.lastServerTime = cg.physicsTime;
+    stateIndex = cg.stateHead;
+  }
+
+  // run cmds
+  moved = qfalse;
+
+  for( cmdNum = current - CMD_BACKUP + 1; cmdNum <= current; cmdNum++ )
+  {
+    // get the command
+    trap_GetUserCmd( cmdNum, &cg_pmove.cmd );
+
+    if( cg_pmove.pmove_fixed )
+      PM_UpdateViewAngles( cg_pmove.ps, &cg_pmove.cmd );
+
+    // don't do anything if the time is before the snapshot player time
+    if( cg_pmove.cmd.serverTime <= cg.predictedPlayerState.commandTime )
+      continue;
+
+    // don't do anything if the command was from a previous map_restart
+    if( cg_pmove.cmd.serverTime > latestCmd.serverTime )
+      continue;
+
+    // check for a prediction error from last frame
+    // on a lan, this will often be the exact value
+    // from the snapshot, but on a wan we will have
+    // to predict several commands to get to the point
+    // we want to compare
+    if( cg.predictedPlayerState.commandTime == oldPlayerState.commandTime )
+    {
+      vec3_t  delta;
+      float   len;
+
+      if( cg.thisFrameTeleport )
+      {
+        // a teleport will not cause an error decay
+        VectorClear( cg.predictedError );
+
+        if( cg_showmiss.integer )
+          CG_Printf( "PredictionTeleport\n" );
+
+        cg.thisFrameTeleport = qfalse;
+      }
+      else
+      {
+        vec3_t  adjusted;
+        CG_AdjustPositionForMover( cg.predictedPlayerState.origin,
+          cg.predictedPlayerState.groundEntityNum, cg.physicsTime, cg.oldTime, adjusted );
+
+        if( cg_showmiss.integer )
+        {
+          if( !VectorCompare( oldPlayerState.origin, adjusted ) )
+            CG_Printf("prediction error\n");
+        }
+
+        VectorSubtract( oldPlayerState.origin, adjusted, delta );
+        len = VectorLength( delta );
+
+        if( len > 0.1 )
+        {
+          if( cg_showmiss.integer )
+            CG_Printf( "Prediction miss: %f\n", len );
+
+          if( cg_errorDecay.integer )
+          {
+            int   t;
+            float f;
+
+            t = cg.time - cg.predictedErrorTime;
+            f = ( cg_errorDecay.value - t ) / cg_errorDecay.value;
+
+            if( f < 0 )
+              f = 0;
+
+            if( f > 0 && cg_showmiss.integer )
+              CG_Printf( "Double prediction decay: %f\n", f );
+
+            VectorScale( cg.predictedError, f, cg.predictedError );
+          }
+          else
+            VectorClear( cg.predictedError );
+
+          VectorAdd( delta, cg.predictedError, cg.predictedError );
+          cg.predictedErrorTime = cg.oldTime;
+        }
+      }
+    }
+
+    // don't predict gauntlet firing, which is only supposed to happen
+    // when it actually inflicts damage
+    for( i = WP_NONE + 1; i < WP_NUM_WEAPONS; i++ )
+      cg_pmove.autoWeaponHit[ i ] = qfalse;
+
+    if( cg_pmove.pmove_fixed )
+      cg_pmove.cmd.serverTime = ( ( cg_pmove.cmd.serverTime + pmove_msec.integer - 1 ) /
+                                  pmove_msec.integer ) * pmove_msec.integer;
+
+    if( !cg_optimizePrediction.integer )
+    {
+      Pmove( &cg_pmove );
+    }
+    else if( cg_optimizePrediction.integer && ( cmdNum >= predictCmd ||
+      ( stateIndex + 1 ) % NUM_SAVED_STATES == cg.stateHead ) )
+    {
+      Pmove( &cg_pmove );
+      // record the last predicted command
+      cg.lastPredictedCommand = cmdNum;
+
+      // if we haven't run out of space in the saved states queue
+      if( ( stateIndex + 1 ) % NUM_SAVED_STATES != cg.stateHead )
+      {
+        // save the state for the false case ( of cmdNum >= predictCmd )
+        // in later calls to this function
+        cg.savedPmoveStates[ stateIndex ] = *cg_pmove.ps;
+        stateIndex = ( stateIndex + 1 ) % NUM_SAVED_STATES;
+        cg.stateTail = stateIndex;
+      }
+    }
+    else
+    {
+      *cg_pmove.ps = cg.savedPmoveStates[ stateIndex ];
+      stateIndex = ( stateIndex + 1 ) % NUM_SAVED_STATES;
+    }
+
+    moved = qtrue;
+
+    // add push trigger movement effects
+    CG_TouchTriggerPrediction( );
+
+    // check for predictable events that changed from previous predictions
+    //CG_CheckChangedPredictableEvents(&cg.predictedPlayerState);
+  }
+
+  // adjust for the movement of the groundentity
+  CG_AdjustPositionForMover( cg.predictedPlayerState.origin,
+    cg.predictedPlayerState.groundEntityNum,
+    cg.physicsTime, cg.time, cg.predictedPlayerState.origin );
+
+
+  // fire events and other transition triggered things
+  CG_TransitionPlayerState( &cg.predictedPlayerState, &oldPlayerState );
+
+
+}
diff --git a/src/cgame/cg_public.h b/src/cgame/cg_public.h
new file mode 100644
index 0000000..9d197ff
--- /dev/null
+++ b/src/cgame/cg_public.h
@@ -0,0 +1,262 @@
+/*
+===========================================================================
+Copyright (C) 1999-2005 Id Software, Inc.
+Copyright (C) 2000-2009 Darklegion Development
+
+This file is part of Tremulous.
+
+Tremulous 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.
+
+Tremulous 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 Tremulous; if not, write to the Free Software
+Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
+===========================================================================
+*/
+
+
+#define CMD_BACKUP      64
+#define CMD_MASK      (CMD_BACKUP - 1)
+// allow a lot of command backups for very fast systems
+// multiple commands may be combined into a single packet, so this
+// needs to be larger than PACKET_BACKUP
+
+
+#define MAX_ENTITIES_IN_SNAPSHOT  256
+
+// snapshots are a view of the server at a given time
+
+// Snapshots are generated at regular time intervals by the server,
+// but they may not be sent if a client's rate level is exceeded, or
+// they may be dropped by the network.
+typedef struct
+{
+  int           snapFlags;                            // SNAPFLAG_RATE_DELAYED, etc
+  int           ping;
+
+  int           serverTime;                           // server time the message is valid for (in msec)
+
+  byte          areamask[ MAX_MAP_AREA_BYTES ];       // portalarea visibility bits
+
+  playerState_t ps;                                   // complete information about the current player at this time
+
+  int           numEntities;                          // all of the entities that need to be presented
+  entityState_t entities[ MAX_ENTITIES_IN_SNAPSHOT ]; // at the time of this snapshot
+
+  int           numServerCommands;                    // text based server commands to execute when this
+  int           serverCommandSequence;                // snapshot becomes current
+} snapshot_t;
+
+enum
+{
+  CGAME_EVENT_NONE,
+  CGAME_EVENT_TEAMMENU,
+  CGAME_EVENT_SCOREBOARD,
+  CGAME_EVENT_EDITHUD
+};
+
+/*
+==================================================================
+
+functions imported from the main executable
+
+==================================================================
+*/
+
+#define CGAME_IMPORT_API_VERSION  4
+
+typedef enum
+{
+  CG_PRINT,
+  CG_ERROR,
+  CG_MILLISECONDS,
+  CG_CVAR_REGISTER,
+  CG_CVAR_UPDATE,
+  CG_CVAR_SET,
+  CG_CVAR_VARIABLESTRINGBUFFER,
+  CG_ARGC,
+  CG_ARGV,
+  CG_ARGS,
+  CG_FS_FOPENFILE,
+  CG_FS_READ,
+  CG_FS_WRITE,
+  CG_FS_FCLOSEFILE,
+  CG_SENDCONSOLECOMMAND,
+  CG_ADDCOMMAND,
+  CG_SENDCLIENTCOMMAND,
+  CG_UPDATESCREEN,
+  CG_CM_LOADMAP,
+  CG_CM_NUMINLINEMODELS,
+  CG_CM_INLINEMODEL,
+  CG_CM_LOADMODEL,
+  CG_CM_TEMPBOXMODEL,
+  CG_CM_POINTCONTENTS,
+  CG_CM_TRANSFORMEDPOINTCONTENTS,
+  CG_CM_BOXTRACE,
+  CG_CM_TRANSFORMEDBOXTRACE,
+  CG_CM_MARKFRAGMENTS,
+  CG_S_STARTSOUND,
+  CG_S_STARTLOCALSOUND,
+  CG_S_CLEARLOOPINGSOUNDS,
+  CG_S_ADDLOOPINGSOUND,
+  CG_S_UPDATEENTITYPOSITION,
+  CG_S_RESPATIALIZE,
+  CG_S_REGISTERSOUND,
+  CG_S_STARTBACKGROUNDTRACK,
+  CG_R_LOADWORLDMAP,
+  CG_R_REGISTERMODEL,
+  CG_R_REGISTERSKIN,
+  CG_R_REGISTERSHADER,
+  CG_R_CLEARSCENE,
+  CG_R_ADDREFENTITYTOSCENE,
+  CG_R_ADDPOLYTOSCENE,
+  CG_R_ADDLIGHTTOSCENE,
+  CG_R_RENDERSCENE,
+  CG_R_SETCOLOR,
+  CG_R_SETCLIPREGION,
+  CG_R_DRAWSTRETCHPIC,
+  CG_R_MODELBOUNDS,
+  CG_R_LERPTAG,
+  CG_GETGLCONFIG,
+  CG_GETGAMESTATE,
+  CG_GETCURRENTSNAPSHOTNUMBER,
+  CG_GETSNAPSHOT,
+  CG_GETSERVERCOMMAND,
+  CG_GETCURRENTCMDNUMBER,
+  CG_GETUSERCMD,
+  CG_SETUSERCMDVALUE,
+  CG_R_REGISTERSHADERNOMIP,
+  CG_MEMORY_REMAINING,
+  CG_R_REGISTERFONT,
+  CG_KEY_ISDOWN,
+  CG_KEY_GETCATCHER,
+  CG_KEY_SETCATCHER,
+  CG_KEY_GETKEY,
+  CG_S_STOPBACKGROUNDTRACK,
+  CG_REAL_TIME,
+  CG_SNAPVECTOR,
+  CG_REMOVECOMMAND,
+  CG_R_LIGHTFORPOINT,
+  CG_CIN_PLAYCINEMATIC,
+  CG_CIN_STOPCINEMATIC,
+  CG_CIN_RUNCINEMATIC,
+  CG_CIN_DRAWCINEMATIC,
+  CG_CIN_SETEXTENTS,
+  CG_R_REMAP_SHADER,
+  CG_S_ADDREALLOOPINGSOUND,
+  CG_S_STOPLOOPINGSOUND,
+
+  CG_CM_TEMPCAPSULEMODEL,
+  CG_CM_CAPSULETRACE,
+  CG_CM_TRANSFORMEDCAPSULETRACE,
+  CG_R_ADDADDITIVELIGHTTOSCENE,
+  CG_GET_ENTITY_TOKEN,
+  CG_R_ADDPOLYSTOSCENE,
+  CG_R_INPVS,
+  CG_FS_SEEK,
+  CG_FS_GETFILELIST,
+  CG_LITERAL_ARGS,
+  CG_CM_BISPHERETRACE,
+  CG_CM_TRANSFORMEDBISPHERETRACE,
+  CG_GETDEMOSTATE,
+  CG_GETDEMOPOS,
+  CG_GETDEMONAME,
+
+  CG_KEY_KEYNUMTOSTRINGBUF,
+  CG_KEY_GETBINDINGBUF,
+  CG_KEY_SETBINDING,
+
+  CG_PARSE_ADD_GLOBAL_DEFINE,
+  CG_PARSE_LOAD_SOURCE,
+  CG_PARSE_FREE_SOURCE,
+  CG_PARSE_READ_TOKEN,
+  CG_PARSE_SOURCE_FILE_AND_LINE,
+
+  CG_KEY_SETOVERSTRIKEMODE,
+  CG_KEY_GETOVERSTRIKEMODE,
+
+  CG_S_SOUNDDURATION,
+
+  CG_MEMSET = 200,
+  CG_MEMCPY,
+  CG_STRNCPY,
+  CG_SIN,
+  CG_COS,
+  CG_ATAN2,
+  CG_SQRT,
+  CG_FLOOR,
+  CG_CEIL,
+
+  CG_TESTPRINTINT,
+  CG_TESTPRINTFLOAT,
+  CG_ACOS
+} cgameImport_t;
+
+
+/*
+==================================================================
+
+functions exported to the main executable
+
+==================================================================
+*/
+
+typedef enum
+{
+  CG_INIT,
+  // void CG_Init( int serverMessageNum, int serverCommandSequence, int clientNum )
+  // called when the level loads or when the renderer is restarted
+  // all media should be registered at this time
+  // cgame will display loading status by calling SCR_Update, which
+  // will call CG_DrawInformation during the loading process
+  // reliableCommandSequence will be 0 on fresh loads, but higher for
+  // demos, tourney restarts, or vid_restarts
+
+  CG_SHUTDOWN,
+  // void (*CG_Shutdown)( void );
+  // oportunity to flush and close any open files
+
+  CG_CONSOLE_COMMAND,
+  // qboolean (*CG_ConsoleCommand)( void );
+  // a console command has been issued locally that is not recognized by the
+  // main game system.
+  // use Cmd_Argc() / Cmd_Argv() to read the command, return qfalse if the
+  // command is not known to the game
+
+  CG_DRAW_ACTIVE_FRAME,
+  // void (*CG_DrawActiveFrame)( int serverTime, stereoFrame_t stereoView, qboolean demoPlayback );
+  // Generates and draws a game scene and status information at the given time.
+  // If demoPlayback is set, local movement prediction will not be enabled
+
+  CG_CROSSHAIR_PLAYER,
+  // int (*CG_CrosshairPlayer)( void );
+
+  CG_LAST_ATTACKER,
+  // int (*CG_LastAttacker)( void );
+
+  CG_KEY_EVENT,
+  // void  (*CG_KeyEvent)( int key, qboolean down );
+
+  CG_MOUSE_EVENT,
+  // void  (*CG_MouseEvent)( int dx, int dy );
+  CG_EVENT_HANDLING,
+  // void (*CG_EventHandling)(int type);
+
+  CG_CONSOLE_TEXT,
+  // void (*CG_ConsoleText)( void );
+  // pass text that has been printed to the console to cgame
+  // use Cmd_Argc() / Cmd_Argv() to read it
+
+  CG_VOIP_STRING
+  // char *(*CG_VoIPString)( void );
+  // returns a string of comma-delimited clientnums based on cl_voipSendTarget
+} cgameExport_t;
+
+//----------------------------------------------
diff --git a/src/cgame/cg_scanner.c b/src/cgame/cg_scanner.c
new file mode 100644
index 0000000..f4c86de
--- /dev/null
+++ b/src/cgame/cg_scanner.c
@@ -0,0 +1,447 @@
+/*
+===========================================================================
+Copyright (C) 2000-2009 Darklegion Development
+
+This file is part of Tremulous.
+
+Tremulous 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.
+
+Tremulous 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 Tremulous; if not, write to the Free Software
+Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
+===========================================================================
+*/
+
+
+#include "cg_local.h"
+
+static entityPos_t entityPositions;
+
+#define HUMAN_SCANNER_UPDATE_PERIOD 25
+
+/*
+=============
+CG_UpdateEntityPositions
+
+Update this client's perception of entity positions
+=============
+*/
+void CG_UpdateEntityPositions( void )
+{
+  centity_t *cent = NULL;
+  int       i;
+
+  if( cg.predictedPlayerState.stats[ STAT_TEAM ] == TEAM_HUMANS )
+  {
+    if( entityPositions.lastUpdateTime + HUMAN_SCANNER_UPDATE_PERIOD > cg.time )
+      return;
+  }
+
+  VectorCopy( cg.refdef.vieworg, entityPositions.origin );
+  VectorCopy( cg.refdefViewAngles, entityPositions.vangles );
+  entityPositions.lastUpdateTime = cg.time;
+
+  entityPositions.numAlienBuildables = 0;
+  entityPositions.numHumanBuildables = 0;
+  entityPositions.numAlienClients = 0;
+  entityPositions.numHumanClients = 0;
+
+  for( i = 0; i < cg.snap->numEntities; i++ )
+  {
+    cent = &cg_entities[ cg.snap->entities[ i ].number ];
+     //make adv basilisk invisble to human helmet if invisble
+    if( cent->currentState.eType == ET_BUILDABLE &&
+     !( cent->currentState.eFlags & EF_DEAD ))
+    {
+      // add to list of item positions (for creep)
+      if( cent->currentState.modelindex2 == TEAM_ALIENS )
+      {
+        VectorCopy( cent->lerpOrigin, entityPositions.alienBuildablePos[
+            entityPositions.numAlienBuildables ] );
+        entityPositions.alienBuildableTimes[
+            entityPositions.numAlienBuildables ] = cent->miscTime;
+
+        if( entityPositions.numAlienBuildables < MAX_GENTITIES )
+          entityPositions.numAlienBuildables++;
+      }
+      else if( cent->currentState.modelindex2 == TEAM_HUMANS )
+      {
+        VectorCopy( cent->lerpOrigin, entityPositions.humanBuildablePos[
+            entityPositions.numHumanBuildables ] );
+
+        if( entityPositions.numHumanBuildables < MAX_GENTITIES )
+          entityPositions.numHumanBuildables++;
+      }
+    }
+    else if( cent->currentState.eType == ET_PLAYER  )
+    {
+      int team = cent->currentState.misc & 0x00FF;
+    //make adv basilisk invisble to radar if invisble
+      if( team == TEAM_ALIENS && !(cent->invisible ) )
+      {
+        VectorCopy( cent->lerpOrigin, entityPositions.alienClientPos[
+            entityPositions.numAlienClients ] );
+
+        if( entityPositions.numAlienClients < MAX_CLIENTS )
+          entityPositions.numAlienClients++;
+      }
+      else if( team == TEAM_HUMANS )
+      {
+        VectorCopy( cent->lerpOrigin, entityPositions.humanClientPos[
+            entityPositions.numHumanClients ] );
+
+        if( entityPositions.numHumanClients < MAX_CLIENTS )
+          entityPositions.numHumanClients++;
+      }
+    }
+  }
+}
+
+#define STALKWIDTH  (2.0f * cgDC.aspectScale)
+#define BLIPX       (14.0f * cgDC.aspectScale)
+#define BLIPY       6.0f
+#define FAR_ALPHA   0.8f
+#define NEAR_ALPHA  1.2f
+
+/*
+=============
+CG_DrawBlips
+
+Draw blips and stalks for the human scanner
+=============
+*/
+static void CG_DrawBlips( rectDef_t *rect, vec3_t origin, vec4_t colour )
+{
+  vec3_t  drawOrigin;
+  vec3_t  up = { 0, 0, 1 };
+  float   alphaMod = 1.0f;
+  float   timeFractionSinceRefresh = 1.0f -( (float)( cg.time - entityPositions.lastUpdateTime ) / (float)HUMAN_SCANNER_UPDATE_PERIOD );
+  vec4_t  localColour;
+
+  Vector4Copy( colour, localColour );
+
+  RotatePointAroundVector( drawOrigin, up, origin, -entityPositions.vangles[ 1 ] - 90 );
+  drawOrigin[ 0 ] /= ( 2 * HELMET_RANGE / rect->w );
+  drawOrigin[ 1 ] /= ( 2 * HELMET_RANGE / rect->h );
+  drawOrigin[ 2 ] /= ( 2 * HELMET_RANGE / rect->w );
+
+  alphaMod = FAR_ALPHA +
+    ( ( drawOrigin[ 1 ] + ( rect->h / 2.0f ) ) / rect->h ) * ( NEAR_ALPHA - FAR_ALPHA );
+
+  localColour[ 3 ] *= alphaMod;
+  localColour[ 3 ] *= ( 0.5f + ( timeFractionSinceRefresh * 0.5f ) );
+
+  if( localColour[ 3 ] > 1.0f )
+    localColour[ 3 ] = 1.0f;
+  else if( localColour[ 3 ] < 0.0f )
+    localColour[ 3 ] = 0.0f;
+
+  trap_R_SetColor( localColour );
+
+  if( drawOrigin[ 2 ] > 0 )
+    CG_DrawPic( rect->x + ( rect->w / 2 ) - ( STALKWIDTH / 2 ) - drawOrigin[ 0 ],
+                rect->y + ( rect->h / 2 ) + drawOrigin[ 1 ] - drawOrigin[ 2 ],
+                STALKWIDTH, drawOrigin[ 2 ], cgs.media.scannerLineShader );
+  else
+    CG_DrawPic( rect->x + ( rect->w / 2 ) - ( STALKWIDTH / 2 ) - drawOrigin[ 0 ],
+                rect->y + ( rect->h / 2 ) + drawOrigin[ 1 ],
+                STALKWIDTH, -drawOrigin[ 2 ], cgs.media.scannerLineShader );
+
+  CG_DrawPic( rect->x + ( rect->w / 2 ) - ( BLIPX / 2 ) - drawOrigin[ 0 ],
+              rect->y + ( rect->h / 2 ) - ( BLIPY / 2 ) + drawOrigin[ 1 ] - drawOrigin[ 2 ],
+              BLIPX, BLIPY, cgs.media.scannerBlipShader );
+  trap_R_SetColor( NULL );
+}
+
+/*
+=============
+CG_DrawBlipsPlayer
+
+Draw blips and stalks for the human scanner
+=============
+*/
+static void CG_DrawBlipsPlayer( rectDef_t *rect, vec3_t origin, vec4_t colour )
+{
+  vec3_t  drawOrigin;
+  vec3_t  up = { 0, 0, 1 };
+  float   alphaMod = 1.0f;
+  float   timeFractionSinceRefresh = 1.0f -( (float)( cg.time - entityPositions.lastUpdateTime ) / (float)HUMAN_SCANNER_UPDATE_PERIOD );
+  vec4_t  localColour;
+
+  Vector4Copy( colour, localColour );
+
+  RotatePointAroundVector( drawOrigin, up, origin, -entityPositions.vangles[ 1 ] - 90 );
+  drawOrigin[ 0 ] /= ( 2 * HELMET_RANGE / rect->w );
+  drawOrigin[ 1 ] /= ( 2 * HELMET_RANGE / rect->h );
+  drawOrigin[ 2 ] /= ( 2 * HELMET_RANGE / rect->w );
+
+  alphaMod = FAR_ALPHA +
+    ( ( drawOrigin[ 1 ] + ( rect->h / 2.0f ) ) / rect->h ) * ( NEAR_ALPHA - FAR_ALPHA );
+
+  localColour[ 3 ] *= alphaMod;
+  localColour[ 3 ] *= ( 0.5f + ( timeFractionSinceRefresh * 0.5f ) );
+
+  if( localColour[ 3 ] > 1.0f )
+    localColour[ 3 ] = 1.0f;
+  else if( localColour[ 3 ] < 0.0f )
+    localColour[ 3 ] = 0.0f;
+
+  trap_R_SetColor( localColour );
+
+  if( drawOrigin[ 2 ] > 0 )
+    CG_DrawPic( rect->x + ( rect->w / 2 ) - ( STALKWIDTH / 2 ) - drawOrigin[ 0 ],
+                rect->y + ( rect->h / 2 ) + drawOrigin[ 1 ] - drawOrigin[ 2 ],
+                STALKWIDTH, drawOrigin[ 2 ], cgs.media.scannerLineShader );
+  else
+    CG_DrawPic( rect->x + ( rect->w / 2 ) - ( STALKWIDTH / 2 ) - drawOrigin[ 0 ],
+                rect->y + ( rect->h / 2 ) + drawOrigin[ 1 ],
+                STALKWIDTH, -drawOrigin[ 2 ], cgs.media.scannerLineShader );
+
+  CG_DrawPic( rect->x + ( rect->w / 2 ) - ( BLIPX / 2 ) - drawOrigin[ 0 ],
+              rect->y + ( rect->h / 2 ) - ( BLIPY / 2 ) + drawOrigin[ 1 ] - drawOrigin[ 2 ],
+              BLIPX, BLIPY, cgs.media.scannerBlipShaderPlayer );
+  trap_R_SetColor( NULL );
+}
+
+#define BLIPX2  (24.0f * cgDC.aspectScale)
+#define BLIPY2  24.0f
+
+/*
+=============
+CG_DrawDir
+
+Draw dot marking the direction to an enemy
+=============
+*/
+static void CG_DrawDir( rectDef_t *rect, vec3_t origin, vec4_t colour )
+{
+  vec3_t  drawOrigin;
+  vec3_t  noZOrigin;
+  vec3_t  normal, antinormal, normalDiff;
+  vec3_t  view, noZview;
+  vec3_t  up  = { 0.0f, 0.0f,   1.0f };
+  vec3_t  top = { 0.0f, -1.0f,  0.0f };
+  float   angle;
+  playerState_t *ps = &cg.snap->ps;
+
+  BG_GetClientNormal( ps, normal );
+
+  AngleVectors( entityPositions.vangles, view, NULL, NULL );
+
+  ProjectPointOnPlane( noZOrigin, origin, normal );
+  ProjectPointOnPlane( noZview, view, normal );
+  VectorNormalize( noZOrigin );
+  VectorNormalize( noZview );
+
+  //calculate the angle between the images of the blip and the view
+  angle = RAD2DEG( acos( DotProduct( noZOrigin, noZview ) ) );
+  CrossProduct( noZOrigin, noZview, antinormal );
+  VectorNormalize( antinormal );
+
+  //decide which way to rotate
+  VectorSubtract( normal, antinormal, normalDiff );
+  if( VectorLength( normalDiff ) < 1.0f )
+    angle = 360.0f - angle;
+
+  RotatePointAroundVector( drawOrigin, up, top, angle );
+
+  trap_R_SetColor( colour );
+  CG_DrawPic( rect->x + ( rect->w / 2 ) - ( BLIPX2 / 2 ) - drawOrigin[ 0 ] * ( rect->w / 2 ),
+              rect->y + ( rect->h / 2 ) - ( BLIPY2 / 2 ) + drawOrigin[ 1 ] * ( rect->h / 2 ),
+              BLIPX2, BLIPY2, cgs.media.scannerBlipShader );
+  trap_R_SetColor( NULL );
+}
+
+/*
+=============
+CG_DrawDirPlayer
+
+Draw dot marking the direction to an enemy
+=============
+*/
+static void CG_DrawDirPlayer( rectDef_t *rect, vec3_t origin, vec4_t colour )
+{
+  vec3_t  drawOrigin;
+  vec3_t  noZOrigin;
+  vec3_t  normal, antinormal, normalDiff;
+  vec3_t  view, noZview;
+  vec3_t  up  = { 0.0f, 0.0f,   1.0f };
+  vec3_t  top = { 0.0f, -1.0f,  0.0f };
+  float   angle;
+  playerState_t *ps = &cg.snap->ps;
+
+  BG_GetClientNormal( ps, normal );
+
+  AngleVectors( entityPositions.vangles, view, NULL, NULL );
+
+  ProjectPointOnPlane( noZOrigin, origin, normal );
+  ProjectPointOnPlane( noZview, view, normal );
+  VectorNormalize( noZOrigin );
+  VectorNormalize( noZview );
+
+  //calculate the angle between the images of the blip and the view
+  angle = RAD2DEG( acos( DotProduct( noZOrigin, noZview ) ) );
+  CrossProduct( noZOrigin, noZview, antinormal );
+  VectorNormalize( antinormal );
+
+  //decide which way to rotate
+  VectorSubtract( normal, antinormal, normalDiff );
+  if( VectorLength( normalDiff ) < 1.0f )
+    angle = 360.0f - angle;
+
+  RotatePointAroundVector( drawOrigin, up, top, angle );
+
+  trap_R_SetColor( colour );
+  CG_DrawPic( rect->x + ( rect->w / 2 ) - ( BLIPX2 / 2 ) - drawOrigin[ 0 ] * ( rect->w / 2 ),
+              rect->y + ( rect->h / 2 ) - ( BLIPY2 / 2 ) + drawOrigin[ 1 ] * ( rect->h / 2 ),
+              BLIPX2, BLIPY2, cgs.media.scannerBlipShaderPlayer );
+  trap_R_SetColor( NULL );
+}
+
+/*
+=============
+CG_AlienSense
+=============
+*/
+void CG_AlienSense( rectDef_t *rect )
+{
+  int     i;
+  vec3_t  origin;
+  vec3_t  relOrigin;
+  vec4_t  buildableb = { 0.0f, 0.5f, 1.0f, 0.75f };
+  vec4_t  client     = { 0.0f, 0.9f, 1.0f, 0.75f };
+  VectorCopy( entityPositions.origin, origin );
+  
+  //draw human buildables
+  for( i = 0; i < entityPositions.numHumanBuildables; i++ )
+  {
+    VectorClear( relOrigin );
+    VectorSubtract( entityPositions.humanBuildablePos[ i ], origin, relOrigin );
+
+    if( VectorLength( relOrigin ) < ALIENSENSE_RANGE )
+      CG_DrawDir( rect, relOrigin, buildableb );
+  }
+
+  //draw human clients
+  for( i = 0; i < entityPositions.numHumanClients; i++ )
+  {
+    VectorClear( relOrigin );
+    VectorSubtract( entityPositions.humanClientPos[ i ], origin, relOrigin );
+
+    if( VectorLength( relOrigin ) < ALIENSENSE_RANGE )
+      CG_DrawDirPlayer( rect, relOrigin, client );
+  }
+}
+
+/*
+=============
+CG_Scanner
+=============
+*/
+void CG_Scanner( rectDef_t *rect, qhandle_t shader, vec4_t color )
+{
+  int     i;
+  vec3_t  origin;
+  vec3_t  relOrigin;
+  vec4_t  hIabove = { 0.0f, 1.0f, 1.0f, 0.75f };
+  vec4_t  hIbelow = { 0.0f, 0.8f, 0.8f, 0.60f };
+  vec4_t  aIabove = { 1.0f, 0.0f, 0.0f, 0.75f };
+  vec4_t  aIbelow = { 0.8f, 0.0f, 0.0f, 0.60f };
+
+  VectorCopy( entityPositions.origin, origin );
+
+  //draw human buildables below scanner plane
+  for( i = 0; i < entityPositions.numHumanBuildables; i++ )
+  {
+    VectorClear( relOrigin );
+    VectorSubtract( entityPositions.humanBuildablePos[ i ], origin, relOrigin );
+
+    if( VectorLength( relOrigin ) < HELMET_RANGE && ( relOrigin[ 2 ] < 0 ) )
+      CG_DrawBlips( rect, relOrigin, hIbelow );
+  }
+
+  //draw alien buildables below scanner plane
+  for( i = 0; i < entityPositions.numAlienBuildables; i++ )
+  {
+    VectorClear( relOrigin );
+    VectorSubtract( entityPositions.alienBuildablePos[ i ], origin, relOrigin );
+
+    if( VectorLength( relOrigin ) < HELMET_RANGE && ( relOrigin[ 2 ] < 0 ) )
+      CG_DrawBlips( rect, relOrigin, aIbelow );
+  }
+
+  //draw human clients below scanner plane
+  for( i = 0; i < entityPositions.numHumanClients; i++ )
+  {
+    VectorClear( relOrigin );
+    VectorSubtract( entityPositions.humanClientPos[ i ], origin, relOrigin );
+
+    if( VectorLength( relOrigin ) < HELMET_RANGE && ( relOrigin[ 2 ] < 0 ) )
+      CG_DrawBlipsPlayer( rect, relOrigin, hIbelow );
+  }
+
+  //draw alien clients below scanner plane
+  for( i = 0; i < entityPositions.numAlienClients; i++ )
+  {
+    VectorClear( relOrigin );
+    VectorSubtract( entityPositions.alienClientPos[ i ], origin, relOrigin );
+
+    if( VectorLength( relOrigin ) < HELMET_RANGE && ( relOrigin[ 2 ] < 0 ) )
+      CG_DrawBlipsPlayer( rect, relOrigin, aIbelow );
+  }
+
+  if( !cg_disableScannerPlane.integer )
+  {
+    trap_R_SetColor( color );
+    CG_DrawPic( rect->x, rect->y, rect->w, rect->h, shader );
+    trap_R_SetColor( NULL );
+  }
+
+  //draw human buildables above scanner plane
+  for( i = 0; i < entityPositions.numHumanBuildables; i++ )
+  {
+    VectorClear( relOrigin );
+    VectorSubtract( entityPositions.humanBuildablePos[ i ], origin, relOrigin );
+
+    if( VectorLength( relOrigin ) < HELMET_RANGE && ( relOrigin[ 2 ] > 0 ) )
+      CG_DrawBlips( rect, relOrigin, hIabove );
+  }
+
+  //draw alien buildables above scanner plane
+  for( i = 0; i < entityPositions.numAlienBuildables; i++ )
+  {
+    VectorClear( relOrigin );
+    VectorSubtract( entityPositions.alienBuildablePos[ i ], origin, relOrigin );
+
+    if( VectorLength( relOrigin ) < HELMET_RANGE && ( relOrigin[ 2 ] > 0 ) )
+      CG_DrawBlips( rect, relOrigin, aIabove );
+  }
+
+  //draw human clients above scanner plane
+  for( i = 0; i < entityPositions.numHumanClients; i++ )
+  {
+    VectorClear( relOrigin );
+    VectorSubtract( entityPositions.humanClientPos[ i ], origin, relOrigin );
+
+    if( VectorLength( relOrigin ) < HELMET_RANGE && ( relOrigin[ 2 ] > 0 ) )
+      CG_DrawBlipsPlayer( rect, relOrigin, hIabove );
+  }
+
+  //draw alien clients above scanner plane
+  for( i = 0; i < entityPositions.numAlienClients; i++ )
+  {
+    VectorClear( relOrigin );
+    VectorSubtract( entityPositions.alienClientPos[ i ], origin, relOrigin );
+
+    if( VectorLength( relOrigin ) < HELMET_RANGE && ( relOrigin[ 2 ] > 0 ) )
+      CG_DrawBlipsPlayer( rect, relOrigin, aIabove );
+  }
+}
diff --git a/src/cgame/cg_servercmds.c b/src/cgame/cg_servercmds.c
new file mode 100644
index 0000000..a5de53d
--- /dev/null
+++ b/src/cgame/cg_servercmds.c
@@ -0,0 +1,1381 @@
+/*
+===========================================================================
+Copyright (C) 1999-2005 Id Software, Inc.
+Copyright (C) 2000-2009 Darklegion Development
+
+This file is part of Tremulous.
+
+Tremulous 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.
+
+Tremulous 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 Tremulous; if not, write to the Free Software
+Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
+===========================================================================
+*/
+
+// cg_servercmds.c -- reliably sequenced text commands sent by the server
+// these are processed at snapshot transition time, so there will definately
+// be a valid snapshot this frame
+
+
+#include "cg_local.h"
+
+/*
+=================
+CG_ParseScores
+
+=================
+*/
+static void CG_ParseScores( void )
+{
+  int   i;
+
+  cg.numScores = ( trap_Argc( ) - 3 ) / 6;
+
+  if( cg.numScores > MAX_CLIENTS )
+    cg.numScores = MAX_CLIENTS;
+
+  cg.teamScores[ 0 ] = atoi( CG_Argv( 1 ) );
+  cg.teamScores[ 1 ] = atoi( CG_Argv( 2 ) );
+
+  memset( cg.scores, 0, sizeof( cg.scores ) );
+
+  if( cg_debugRandom.integer )
+    CG_Printf( "cg.numScores: %d\n", cg.numScores );
+
+  for( i = 0; i < cg.numScores; i++ )
+  {
+    //
+    cg.scores[ i ].client = atoi( CG_Argv( i * 6 + 3 ) );
+    cg.scores[ i ].score = atoi( CG_Argv( i * 6 + 4 ) );
+    cg.scores[ i ].ping = atoi( CG_Argv( i * 6 + 5 ) );
+    cg.scores[ i ].time = atoi( CG_Argv( i * 6 + 6 ) );
+    cg.scores[ i ].weapon = atoi( CG_Argv( i * 6 + 7 ) );
+    cg.scores[ i ].upgrade = atoi( CG_Argv( i * 6 + 8 ) );
+
+    if( cg.scores[ i ].client < 0 || cg.scores[ i ].client >= MAX_CLIENTS )
+      cg.scores[ i ].client = 0;
+
+    cgs.clientinfo[ cg.scores[ i ].client ].score = cg.scores[ i ].score;
+
+    cg.scores[ i ].team = cgs.clientinfo[ cg.scores[ i ].client ].team;
+  }
+}
+
+/*
+=================
+CG_ParseTeamInfo
+
+=================
+*/
+static void CG_ParseTeamInfo( void )
+{
+  int   i;
+  int   count;
+  int   client;
+
+  count = ( trap_Argc( ) - 1 ) / 5;
+
+  cgs.teaminfoReceievedTime = cg.time;
+
+  for( i = 0; i < count; i++ )
+  {
+    client = atoi( CG_Argv( i * 5 + 1 ) );
+    if( client < 0 || client >= MAX_CLIENTS )
+    {
+      CG_Printf( "[skipnotify]CG_ParseTeamInfo: bad client number: %d\n", client );
+      return;
+    }
+
+    cgs.clientinfo[ client ].location = atoi( CG_Argv( i * 5 + 2 ) );
+    cgs.clientinfo[ client ].health = atoi( CG_Argv( i * 5 + 3 ) );
+    cgs.clientinfo[ client ].curWeaponClass = atoi( CG_Argv( i * 5 + 4 ) );
+    cgs.clientinfo[ client ].upgrade = atoi( CG_Argv( i * 5 + 5 ) );
+  }
+}
+
+
+/*
+================
+CG_ParseServerinfo
+
+This is called explicitly when the gamestate is first received,
+and whenever the server updates any serverinfo flagged cvars
+================
+*/
+void CG_ParseServerinfo( void )
+{
+  const char  *info;
+  char  *mapname;
+
+  info = CG_ConfigString( CS_SERVERINFO );
+  cgs.timelimit = atoi( Info_ValueForKey( info, "timelimit" ) );
+  cgs.maxclients = atoi( Info_ValueForKey( info, "sv_maxclients" ) );
+  cgs.markDeconstruct = atoi( Info_ValueForKey( info, "g_markDeconstruct" ) );
+  mapname = Info_ValueForKey( info, "mapname" );
+  Com_sprintf( cgs.mapname, sizeof( cgs.mapname ), "maps/%s.bsp", mapname );
+}
+
+/*
+==================
+CG_ParseWarmup
+==================
+*/
+static void CG_ParseWarmup( void )
+{
+  const char  *info;
+  int         warmup;
+
+  info = CG_ConfigString( CS_WARMUP );
+
+  warmup = atoi( info );
+  cg.warmupTime = warmup;
+}
+
+/*
+================
+CG_SetConfigValues
+
+Called on load to set the initial values from configure strings
+================
+*/
+void CG_SetConfigValues( void )
+{
+  const char *alienStages = CG_ConfigString( CS_ALIEN_STAGES );
+  const char *humanStages = CG_ConfigString( CS_HUMAN_STAGES );
+
+  if( alienStages[0] )
+  {
+    sscanf( alienStages, "%d %d %d", &cgs.alienStage, &cgs.alienCredits,
+        &cgs.alienNextStageThreshold );
+  }
+  else
+    cgs.alienStage = cgs.alienCredits = cgs.alienNextStageThreshold = 0;
+
+
+  if( humanStages[0] )
+  {
+    sscanf( humanStages, "%d %d %d", &cgs.humanStage, &cgs.humanCredits,
+        &cgs.humanNextStageThreshold );
+  }
+  else
+    cgs.humanStage = cgs.humanCredits = cgs.humanNextStageThreshold = 0;
+
+  cgs.levelStartTime = atoi( CG_ConfigString( CS_LEVEL_START_TIME ) );
+  cg.warmupTime = atoi( CG_ConfigString( CS_WARMUP ) );
+}
+
+
+/*
+=====================
+CG_ShaderStateChanged
+=====================
+*/
+void CG_ShaderStateChanged( void )
+{
+  char        originalShader[ MAX_QPATH ];
+  char        newShader[ MAX_QPATH ];
+  char        timeOffset[ 16 ];
+  const char  *o;
+  char        *n, *t;
+
+  o = CG_ConfigString( CS_SHADERSTATE );
+
+  while( o && *o )
+  {
+    n = strstr( o, "=" );
+
+    if( n && *n )
+    {
+      strncpy( originalShader, o, n - o );
+      originalShader[ n - o ] = 0;
+      n++;
+      t = strstr( n, ":" );
+
+      if( t && *t )
+      {
+        strncpy( newShader, n, t - n );
+        newShader[ t - n ] = 0;
+      }
+      else
+        break;
+
+      t++;
+      o = strstr( t, "@" );
+
+      if( o )
+      {
+        strncpy( timeOffset, t, o - t );
+        timeOffset[ o - t ] = 0;
+        o++;
+        trap_R_RemapShader( originalShader, newShader, timeOffset );
+      }
+    }
+    else
+      break;
+  }
+}
+
+/*
+================
+CG_AnnounceAlienStageTransistion
+================
+*/
+static void CG_AnnounceAlienStageTransistion( stage_t from, stage_t to )
+{
+  if( cg.predictedPlayerState.stats[ STAT_TEAM ] != TEAM_ALIENS )
+    return;
+
+  trap_S_StartLocalSound( cgs.media.alienStageTransition, CHAN_ANNOUNCER );
+  CG_CenterPrint( "^5We have evolved!", 200, GIANTCHAR_WIDTH * 4 );
+}
+
+
+/*
+================
+CG_AnnounceHumanStageTransistion
+================
+*/
+static void CG_AnnounceHumanStageTransistion( stage_t from, stage_t to )
+{
+  if( cg.predictedPlayerState.stats[ STAT_TEAM ] != TEAM_HUMANS )
+    return;
+
+  trap_S_StartLocalSound( cgs.media.humanStageTransition, CHAN_ANNOUNCER );
+  CG_CenterPrint( "^5Reinforcements have arrived!", 200, GIANTCHAR_WIDTH * 4 );
+}
+
+
+/*
+================
+CG_ConfigStringModified
+
+================
+*/
+static void CG_ConfigStringModified( void )
+{
+  const char  *str;
+  int         num;
+
+  num = atoi( CG_Argv( 1 ) );
+
+  // get the gamestate from the client system, which will have the
+  // new configstring already integrated
+  trap_GetGameState( &cgs.gameState );
+
+  // look up the individual string that was modified
+  str = CG_ConfigString( num );
+
+  // do something with it if necessary
+  if( num == CS_MUSIC )
+    CG_StartMusic( );
+  else if( num == CS_SERVERINFO )
+    CG_ParseServerinfo( );
+  else if( num == CS_WARMUP )
+    CG_ParseWarmup( );
+  else if( num == CS_ALIEN_STAGES )
+  {
+    stage_t oldAlienStage = cgs.alienStage;
+
+    if( str[0] )
+    {
+      sscanf( str, "%d %d %d", &cgs.alienStage, &cgs.alienCredits,
+          &cgs.alienNextStageThreshold );
+
+      if( cgs.alienStage != oldAlienStage )
+        CG_AnnounceAlienStageTransistion( oldAlienStage, cgs.alienStage );
+    }
+    else
+    {
+      cgs.alienStage = cgs.alienCredits = cgs.alienNextStageThreshold = 0;
+    }
+  }
+  else if( num == CS_HUMAN_STAGES )
+  {
+    stage_t oldHumanStage = cgs.humanStage;
+
+    if( str[0] )
+    {
+      sscanf( str, "%d %d %d", &cgs.humanStage, &cgs.humanCredits,
+          &cgs.humanNextStageThreshold );
+
+      if( cgs.humanStage != oldHumanStage )
+        CG_AnnounceHumanStageTransistion( oldHumanStage, cgs.humanStage );
+    }
+    else
+    {
+      cgs.humanStage = cgs.humanCredits = cgs.humanNextStageThreshold = 0;
+    }
+  }
+  else if( num == CS_LEVEL_START_TIME )
+    cgs.levelStartTime = atoi( str );
+  else if( num >= CS_VOTE_TIME && num < CS_VOTE_TIME + NUM_TEAMS )
+  {
+    cgs.voteTime[ num - CS_VOTE_TIME ] = atoi( str );
+    cgs.voteModified[ num - CS_VOTE_TIME ] = qtrue;
+
+    if( num - CS_VOTE_TIME == TEAM_NONE )
+      trap_Cvar_Set( "ui_voteActive", cgs.voteTime[ TEAM_NONE ] ? "1" : "0" );
+    else if( num - CS_VOTE_TIME == TEAM_ALIENS )
+      trap_Cvar_Set( "ui_alienTeamVoteActive",
+        cgs.voteTime[ TEAM_ALIENS ] ? "1" : "0" );
+    else if( num - CS_VOTE_TIME == TEAM_HUMANS )
+      trap_Cvar_Set( "ui_humanTeamVoteActive",
+        cgs.voteTime[ TEAM_HUMANS ] ? "1" : "0" );
+  }
+  else if( num >= CS_VOTE_YES && num < CS_VOTE_YES + NUM_TEAMS )
+  {
+    cgs.voteYes[ num - CS_VOTE_YES ] = atoi( str );
+    cgs.voteModified[ num - CS_VOTE_YES ] = qtrue;
+  }
+  else if( num >= CS_VOTE_NO && num < CS_VOTE_NO + NUM_TEAMS )
+  {
+    cgs.voteNo[ num - CS_VOTE_NO ] = atoi( str );
+    cgs.voteModified[ num - CS_VOTE_NO ] = qtrue;
+  }
+  else if( num >= CS_VOTE_STRING && num < CS_VOTE_STRING + NUM_TEAMS )
+    Q_strncpyz( cgs.voteString[ num - CS_VOTE_STRING ], str,
+      sizeof( cgs.voteString[ num - CS_VOTE_STRING ] ) );
+  else if( num >= CS_VOTE_CALLER && num < CS_VOTE_CALLER + NUM_TEAMS )
+    Q_strncpyz( cgs.voteCaller[ num - CS_VOTE_CALLER ], str,
+      sizeof( cgs.voteCaller[ num - CS_VOTE_CALLER ] ) );
+  else if( num == CS_INTERMISSION )
+    cg.intermissionStarted = atoi( str );
+  else if( num >= CS_MODELS && num < CS_MODELS+MAX_MODELS )
+    cgs.gameModels[ num - CS_MODELS ] = trap_R_RegisterModel( str );
+  else if( num >= CS_SHADERS && num < CS_SHADERS+MAX_GAME_SHADERS )
+    cgs.gameShaders[ num - CS_SHADERS ] = trap_R_RegisterShader( str );
+  else if( num >= CS_PARTICLE_SYSTEMS && num < CS_PARTICLE_SYSTEMS+MAX_GAME_PARTICLE_SYSTEMS )
+    cgs.gameParticleSystems[ num - CS_PARTICLE_SYSTEMS ] = CG_RegisterParticleSystem( (char *)str );
+  else if( num >= CS_SOUNDS && num < CS_SOUNDS+MAX_SOUNDS )
+  {
+    if( str[ 0 ] != '*' )
+    {  // player specific sounds don't register here
+      cgs.gameSounds[ num - CS_SOUNDS ] = trap_S_RegisterSound( str, qfalse );
+    }
+  }
+  else if( num >= CS_PLAYERS && num < CS_PLAYERS+MAX_CLIENTS )
+  {
+    CG_NewClientInfo( num - CS_PLAYERS );
+    CG_BuildSpectatorString( );
+  }
+  else if( num == CS_WINNER )
+  {
+    trap_Cvar_Set( "ui_winner", str );
+  }
+  else if( num == CS_SHADERSTATE )
+  {
+    CG_ShaderStateChanged( );
+  }
+}
+
+
+/*
+===============
+CG_MapRestart
+
+The server has issued a map_restart, so the next snapshot
+is completely new and should not be interpolated to.
+
+A tournement restart will clear everything, but doesn't
+require a reload of all the media
+===============
+*/
+static void CG_MapRestart( void )
+{
+  if( cg_showmiss.integer )
+    CG_Printf( "CG_MapRestart\n" );
+
+  CG_InitMarkPolys( );
+
+  // make sure the "3 frags left" warnings play again
+  cg.fraglimitWarnings = 0;
+
+  cg.timelimitWarnings = 0;
+
+  cg.intermissionStarted = qfalse;
+
+  cgs.voteTime[ TEAM_NONE ] = 0;
+
+  cg.mapRestart = qtrue;
+
+  CG_StartMusic( );
+
+  trap_S_ClearLoopingSounds( qtrue );
+
+  // we really should clear more parts of cg here and stop sounds
+
+  trap_Cvar_Set( "cg_thirdPerson", "0" );
+}
+
+/*
+==============
+CG_Menu
+==============
+*/
+void CG_Menu( int menu, int arg )
+{
+  const char *cmd;              // command to send
+  const char *longMsg  = NULL; // command parameter
+  const char *shortMsg = NULL; // non-modal version of message
+  const char *dialog;
+  dialogType_t type = 0; // controls which cg_disable var will switch it off
+  
+  switch( cg.snap->ps.stats[ STAT_TEAM ] )
+  {
+    case TEAM_ALIENS:
+      dialog = "menu tremulous_alien_dialog\n";
+      break;
+    case TEAM_HUMANS:
+      dialog = "menu tremulous_human_dialog\n";
+      break;
+    default:
+      dialog = "menu tremulous_default_dialog\n";
+  }
+  cmd = dialog;
+
+  switch( menu )
+  {
+     case MN_WELCOME:
+      cmd       = "menu ingame\n";
+      type      = DT_INTERACTIVE;
+      break;
+ 
+    case MN_TEAM:
+      cmd       = "menu tremulous_teamselect\n";
+      type      = DT_INTERACTIVE;
+      break;
+
+    case MN_A_CLASS:
+      cmd       = "menu tremulous_alienclass\n";
+      type      = DT_INTERACTIVE;
+      break;
+
+    case MN_H_SPAWN:
+      cmd       = "menu tremulous_humanitem\n";
+      type      = DT_INTERACTIVE;
+      break;
+
+    case MN_A_BUILD:
+      cmd       = "menu tremulous_alienbuild\n";
+      type      = DT_INTERACTIVE;
+      break;
+
+    case MN_H_BUILD:
+      cmd       = "menu tremulous_humanbuild\n";
+      type      = DT_INTERACTIVE;
+      break;
+
+    case MN_H_ARMOURY:
+      cmd       = "menu tremulous_humanarmoury\n";
+      type      = DT_INTERACTIVE;
+      break;
+
+    case MN_H_UNKNOWNITEM:
+      shortMsg  = "Unknown item";
+      type      = DT_ARMOURYEVOLVE;
+      break;
+
+    case MN_A_TEAMFULL:
+      longMsg   = "The alien team has too many players. Please wait until slots "
+                  "become available or join the human team.";
+      shortMsg  = "^5The alien team has too many players";
+      type      = DT_COMMAND;
+      break;
+
+    case MN_H_TEAMFULL:
+      longMsg   = "The human team has too many players. Please wait until slots "
+                  "become available or join the alien team.";
+      shortMsg  = "^5The human team has too many players";
+      type      = DT_COMMAND;
+      break;
+
+    case MN_A_TEAMLOCKED:
+      longMsg   = "The alien team is locked. You cannot join the aliens "
+                  "at this time.";
+      shortMsg  = "^5The alien team is locked";
+      type      = DT_COMMAND;
+      break;
+
+    case MN_H_TEAMLOCKED:
+      longMsg   = "The human team is locked. You cannot join the humans "
+                  "at this time.";
+      shortMsg  = "^5The human team is locked";
+      type      = DT_COMMAND;
+      break;
+
+    case MN_PLAYERLIMIT:
+      longMsg   = "The maximum number of playing clients has been reached. "
+                  "Please wait until slots become available.";
+      shortMsg  = "^5No free player slots";
+      type      = DT_COMMAND;
+      break;
+
+    case MN_WARMUP:
+      longMsg   = "You must wait until the warmup time is finished "
+                  "before joining a team. ";
+      shortMsg  = "^5You cannot join a team during warmup.";
+      type      = DT_COMMAND;
+      break;
+
+    //===============================
+
+    // Since cheating commands have no default binds, they will often be done
+    // via console. In light of this, perhaps opening a menu is 
+    // counterintuitive
+    case MN_CMD_CHEAT:
+      //longMsg   = "This action is considered cheating. It can only be used "
+      //            "in cheat mode, which is not enabled on this server.";
+      shortMsg  = "^5Cheats are not enabled on this server";
+      type      = DT_COMMAND;
+      break;
+
+    case MN_CMD_CHEAT_TEAM:
+      shortMsg  = "^5Cheats are not enabled on this server, so "
+                  "^5you may not use this command while on a team";
+      type      = DT_COMMAND;
+      break;
+
+    case MN_CMD_TEAM:
+      //longMsg   = "You must be on a team to perform this action. Join the alien"
+      //            "or human team and try again.";
+      shortMsg  = "^5Join a team first";
+      type      = DT_COMMAND;
+      break;
+
+    case MN_CMD_SPEC:
+      //longMsg   = "You may not perform this action while on a team. Become a "
+      //            "spectator before trying again.";
+      shortMsg  = "^5You can only use this command when spectating";
+      type      = DT_COMMAND;
+      break;
+
+    case MN_CMD_ALIEN:
+      //longMsg   = "You must be on the alien team to perform this action.";
+      shortMsg  = "^5Must be alien to use this command";
+      type      = DT_COMMAND;
+      break;
+
+    case MN_CMD_HUMAN:
+      //longMsg   = "You must be on the human team to perform this action.";
+      shortMsg  = "^5Must be human to use this command";
+      type      = DT_COMMAND;
+      break;
+
+    case MN_CMD_LIVING:
+      //longMsg   = "You must be living to perform this action.";
+      shortMsg  = "^5Must be living to use this command";
+      type      = DT_COMMAND;
+      break;
+
+
+    //===============================
+    
+    case MN_B_NOROOM:
+      longMsg   = "There is no room to build here. Move until the structure turns "
+                  "translucent green, indicating a valid build location.";
+      shortMsg  = "^5There is no room to build here";
+      type      = DT_BUILD;
+      break;
+
+    case MN_B_NORMAL:
+      longMsg   = "Cannot build on this surface. The surface is too steep or "
+                  "unsuitable for building. Please choose another site for this "
+                  "structure.";
+      shortMsg  = "^5Cannot build on this surface";
+      type      = DT_BUILD;
+      break;
+
+    case MN_B_CANNOT:
+      longMsg   = NULL;
+      shortMsg  = "^5You cannot build that structure";
+      type      = DT_BUILD;
+      break;
+
+    // FIXME: MN_H_ and MN_A_?
+    case MN_B_LASTSPAWN:
+      longMsg   = "This action would remove your team's last spawn point, "
+                  "which often quickly results in a loss. Try building more "
+                  "spawns.";
+      shortMsg  = "^5You may not deconstruct the last spawn";
+      break;
+
+	  case MN_B_BLOCKEDBYENEMY:
+      switch( cg.snap->ps.stats[ STAT_TEAM ] )
+      {
+        case TEAM_ALIENS:
+          longMsg   = "You cannot build within the range of a human reactor "
+                      "or a repeater. In order to build here your team has "
+                      "to destroy the nearby human structure first.";
+          shortMsg  = "^5A nearby human reactor or a repeater is disrupting creep here.";
+          break;
+        case TEAM_HUMANS:
+          longMsg   = "You cannot build on alien creep, within the range of "
+                      "an egg. In order to build here your team has "
+                      "to destroy the nearby alien egg(s) first.";
+          shortMsg  = "^5Alien creep is disrupting power here.";
+          break;
+      }
+      break;
+
+    case MN_B_GTHRBLOCKED:
+      switch( cg.snap->ps.stats[ STAT_TEAM ] )
+      {
+        case TEAM_ALIENS:
+          longMsg   = "Creep colonies cannot be near each other, " // or near the Overmind, "
+                      "there must be enough distance between them. You need to build "
+                      "a creep colony at a place that is far enough from others.";
+                      // " and form the Overmind.";
+          shortMsg  = "^5A creep colony has already been built in this area.";
+          break;
+        case TEAM_HUMANS:
+          longMsg   = "Refineries cannot be near each other, " // or near the Reactor, "
+                      "there must be enough distance between them. You need to build "
+                      "a refinery at a place that is far enough from others.";
+                      // " and from the Reactor.";
+          shortMsg  = "^5A refinery has already been built in this area.";
+          break;
+      }
+      break; 
+	  
+	  
+	  
+    case MN_B_SUDDENDEATH:
+      longMsg   = "Neither team has prevailed after a certain time and the "
+                  "game has entered Sudden Death. During Sudden Death "
+                  "building is not allowed.";
+      shortMsg  = "^5Cannot build during Sudden Death";
+      type      = DT_BUILD;
+      break;
+
+    case MN_B_REVOKED:
+      longMsg   = "Your teammates have lost faith in your ability to build "
+                  "for the team. You will not be allowed to build until your "
+                  "team votes to reinstate your building rights.";
+      shortMsg  = "^5Your building rights have been revoked";
+      type      = DT_BUILD;
+      break;
+
+    case MN_B_SURRENDER:
+      longMsg   = "Your team has decided to admit defeat and concede the game:"
+                  "traitors and cowards are not allowed to build.";
+                  // too harsh?
+      shortMsg  = "^5Building is denied to traitorous cowards";
+      break;
+
+    //===============================
+
+    case MN_H_NOBP:
+      if( cgs.markDeconstruct )
+        longMsg   = "^5There is no power remaining. Free up power by marking "
+                    "existing buildable objects.";
+      else
+        longMsg   = "There is no power remaining. Free up power by deconstructing "
+                    "existing buildable objects.";
+      shortMsg  = "^5There is no power remaining";
+      type      = DT_BUILD;
+      break;
+
+    case MN_H_NOTPOWERED:
+      longMsg   = "This buildable is not powered. Build a Reactor and/or Repeater "
+                  "in order to power it.";
+      shortMsg  = "^5This buildable is not powered";
+      type      = DT_BUILD;
+      break;
+
+    case MN_H_ONEREACTOR:
+      longMsg   = "There can only be one Reactor. Deconstruct the existing one if you "
+                  "wish to move it.";
+      shortMsg  = "^5There can only be one Reactor";
+      type      = DT_BUILD;
+      break;
+
+    case MN_H_NOPOWERHERE:
+      longMsg   = "There is no power here. If available, a Repeater may be used to "
+                  "transmit power to this location.";
+      shortMsg  = "^5There is no power here";
+      type      = DT_BUILD;
+      break;
+
+    case MN_H_NODCC:
+      longMsg   = "There is no Defense Computer. A Defense Computer is needed to "
+                  "build this.";
+      shortMsg  = "^5There is no Defense Computer";
+      type      = DT_BUILD;
+      break;
+
+    case MN_H_RPTPOWERHERE:
+      longMsg   = "This area already has power. A Repeater is not required here.";
+      shortMsg  = "^5This area already has power";
+      type      = DT_BUILD;
+      break;
+
+    case MN_H_NOSLOTS:
+      longMsg   = "You have no room to carry this. Please sell any conflicting "
+                  "upgrades before purchasing this item.";
+      shortMsg  = "^5You have no room to carry this";
+      type      = DT_ARMOURYEVOLVE;
+      break;
+
+    case MN_H_NOFUNDS:
+      longMsg   = "Insufficient funds. You do not have enough credits to perform "
+                  "this action.";
+      shortMsg  = "^5Insufficient funds";
+      type      = DT_ARMOURYEVOLVE;
+      break;
+
+    case MN_H_ITEMHELD:
+      longMsg   = "You already hold this item. It is not possible to carry multiple "
+                  "items of the same type.";
+      shortMsg  = "^5You already hold this item";
+      type      = DT_ARMOURYEVOLVE;
+      break;
+
+    case MN_H_NOARMOURYHERE:
+      longMsg   = "You must be near a powered Armoury in order to purchase "
+                  "weapons, upgrades or ammunition.";
+      shortMsg  = "^5You must be near a powered Armoury or Ammo Supply";
+      type      = DT_ARMOURYEVOLVE;
+      break;
+
+    case MN_H_NOENERGYAMMOHERE:
+      longMsg   = "You must be near a Reactor or a powered Armoury or Repeater "
+                  "in order to purchase energy ammunition.";
+      shortMsg  = "^5You must be near a Reactor or a powered Armoury or Repeater";
+      type      = DT_ARMOURYEVOLVE;
+      break;
+
+    case MN_H_NOROOMBSUITON:
+      longMsg   = "There is not enough room here to put on a Battle Suit. "
+                  "Make sure you have enough head room to climb in.";
+      shortMsg  = "^5Not enough room here to put on a Battle Suit";
+      type      = DT_ARMOURYEVOLVE;
+      break;
+
+    case MN_H_NOROOMBSUITOFF:
+      longMsg   = "There is not enough room here to take off your Battle Suit. "
+                  "Make sure you have enough head room to climb out.";
+      shortMsg  = "^5Not enough room here to take off your Battle Suit";
+      type      = DT_ARMOURYEVOLVE;
+      break;
+
+    case MN_H_ARMOURYBUILDTIMER:
+      longMsg   = "You are not allowed to buy or sell weapons until your "
+                  "build timer has expired.";
+      shortMsg  = "^5You can not buy or sell weapons until your build timer "
+                  "^5expires";
+      type      = DT_ARMOURYEVOLVE;
+      break;
+
+    case MN_H_DEADTOCLASS:
+      shortMsg  = "^5You must be dead to use the class command";
+      type      = DT_COMMAND;
+      break;
+
+    case MN_H_UNKNOWNSPAWNITEM:
+      shortMsg  = "^5Builder limit hit, wait until a free builder slot is free / unknown item";
+      type      = DT_COMMAND;
+      break;
+
+    //===============================
+
+    case MN_A_NOCREEP:
+      longMsg   = "There is no creep here. You must build near existing Eggs or "
+                  "the Overmind. Alien structures will not support themselves.";
+      shortMsg  = "^5There is no creep here";
+      type      = DT_BUILD;
+      break;
+
+    case MN_A_NOOVMND:
+      longMsg   = "There is no Overmind. An Overmind must be built to control "
+                  "the structure you tried to place.";
+      shortMsg  = "^5There is no Overmind";
+      type      = DT_BUILD;
+      break;
+
+    case MN_A_ONEOVERMIND:
+      longMsg   = "There can only be one Overmind. Deconstruct the existing one if you "
+                  "wish to move it.";
+      shortMsg  = "^5There can only be one Overmind";
+      type      = DT_BUILD;
+      break;
+
+	case MN_A_COCOON:
+      longMsg   = "There can only be one Cocoon. Deconstruct the existing one if you "
+                  "wish to move it.";
+      shortMsg  = "^5There can only be one Cocoon";
+      type      = DT_BUILD;
+      break;
+	  
+    case MN_A_NOBP:
+      longMsg   = "The Overmind cannot control any more structures. Deconstruct existing "
+                  "structures to build more.";
+      shortMsg  = "^5The Overmind cannot control any more structures";
+      type      = DT_BUILD;
+      break;
+
+    case MN_A_NOEROOM:
+      longMsg   = "There is no room to evolve here. Move away from walls or other "
+                  "nearby objects and try again.";
+      shortMsg  = "^5There is no room to evolve here";
+      type      = DT_ARMOURYEVOLVE;
+      break;
+
+    case MN_A_TOOCLOSE:
+      longMsg   = "This location is too close to the enemy to evolve. Move away "
+                  "from the enemy's presence and try again.";
+      shortMsg  = "^5This location is too close to the enemy to evolve";
+      type      = DT_ARMOURYEVOLVE;
+      break;
+
+    case MN_A_NOOVMND_EVOLVE:
+      longMsg   = "There is no Overmind. An Overmind must be built to allow "
+                  "you to upgrade.";
+      shortMsg  = "^5There is no Overmind";
+      type      = DT_ARMOURYEVOLVE;
+      break;
+
+    case MN_A_EVOLVEBUILDTIMER:
+      longMsg   = "You cannot evolve until your build timer has expired.";
+      shortMsg  = "^5You cannot evolve until your build timer expires";
+      type      = DT_ARMOURYEVOLVE;
+      break;
+
+    case MN_A_INFEST:
+      trap_Cvar_Set( "ui_currentClass",
+         va( "%d %d", cg.snap->ps.stats[ STAT_CLASS ],
+                      cg.snap->ps.persistant[ PERS_CREDIT ] ) );
+
+      cmd       = "menu tremulous_alienupgrade\n";
+      type      = DT_INTERACTIVE;
+      break;
+
+    case MN_A_CANTEVOLVE:
+      shortMsg  = va( "^5You cannot evolve into a %s", 
+                      BG_ClassConfig( arg )->humanName );
+      type      = DT_ARMOURYEVOLVE;
+      break;
+
+    case MN_A_EVOLVEWALLWALK:
+      shortMsg  = "^5You cannot evolve while wallwalking";
+      type      = DT_ARMOURYEVOLVE;
+      break;
+
+    case MN_A_UNKNOWNCLASS:
+      shortMsg  = "^5Unknown class";
+      type      = DT_ARMOURYEVOLVE;
+      break;
+      
+    case MN_A_CLASSNOTSPAWN:
+      shortMsg  = va( "^5You cannot spawn as a %s after SD", 
+                      BG_ClassConfig( arg )->humanName );
+      type      = DT_ARMOURYEVOLVE;
+      break;
+    
+    case MN_A_CLASSNOTALLOWED:
+      shortMsg  = va( "^5The %s is not allowed due builder limit. Wait until a builder slots gets free / unknown class",
+                      BG_ClassConfig( arg )->humanName );
+      type      = DT_ARMOURYEVOLVE;
+      break;
+
+    case MN_A_CLASSNOTATSTAGE:
+      shortMsg  = va( "^5The %s is not allowed at Stage %d",
+                      BG_ClassConfig( arg )->humanName,
+                      cgs.alienStage + 1 );
+      type      = DT_ARMOURYEVOLVE;
+      break;
+
+    default:
+      Com_Printf( "cgame: debug: no such menu %d\n", menu );
+  }
+  
+  if( type == DT_ARMOURYEVOLVE && cg_disableUpgradeDialogs.integer )
+    return;
+
+  if( type == DT_BUILD && cg_disableBuildDialogs.integer )
+    return;
+
+  if( type == DT_COMMAND && cg_disableCommandDialogs.integer )
+    return;
+
+  if( cmd != dialog )
+  {
+    trap_SendConsoleCommand( cmd );
+  }
+  else if( longMsg && cg_disableWarningDialogs.integer == 0 )
+  {
+    trap_Cvar_Set( "ui_dialog", longMsg );
+    trap_SendConsoleCommand( cmd );
+  }
+  else if( shortMsg && cg_disableWarningDialogs.integer < 2 )
+  {
+    CG_Printf( "%s\n", shortMsg );
+  }
+}
+
+/*
+=================
+CG_Say
+=================
+*/
+static void CG_Say( int clientNum, saymode_t mode, const char *text )
+{
+  char *name;
+  char prefix[ 11 ] = "";
+  char *ignore = "";
+  char *location = "";
+  char *color;
+  char *maybeColon;
+
+  if( clientNum >= 0 && clientNum < MAX_CLIENTS )
+  {
+    clientInfo_t *ci = &cgs.clientinfo[ clientNum ];
+    char         *tcolor = S_COLOR_WHITE;
+
+    name = ci->name;
+
+    if( ci->team == TEAM_ALIENS )
+      tcolor = S_COLOR_RED;
+    else if( ci->team == TEAM_HUMANS )
+      tcolor = S_COLOR_CYAN;
+
+    if( cg_chatTeamPrefix.integer )
+      Com_sprintf( prefix, sizeof( prefix ), "[%s%c" S_COLOR_WHITE "] ",
+                   tcolor, toupper( *( BG_TeamName( ci->team ) ) ) );
+
+    if( Com_ClientListContains( &cgs.ignoreList, clientNum ) )
+      ignore = "[skipnotify]";
+
+    if( ( mode == SAY_TEAM || mode == SAY_AREA ) &&
+        cg.snap->ps.pm_type != PM_INTERMISSION )
+    {
+      int locationNum;
+
+      if( clientNum == cg.snap->ps.clientNum )
+      {
+        centity_t     *locent;
+
+        locent = CG_GetPlayerLocation( );
+        if( locent )
+          locationNum = locent->currentState.generic1;
+        else
+          locationNum = 0;
+      }
+      else
+        locationNum = ci->location;
+
+      if( locationNum > 0 && locationNum < MAX_LOCATIONS )
+      {
+        const char *s = CG_ConfigString( CS_LOCATIONS + locationNum );
+
+        if( *s )
+          location = va( " (%s" S_COLOR_WHITE ")", s );
+      }
+    }
+  }
+  else
+    name = "console";
+
+  // IRC-like /me parsing
+  if( mode != SAY_RAW && Q_stricmpn( text, "/me ", 4 ) == 0 )
+  {
+    text += 4;
+    Q_strcat( prefix, sizeof( prefix ), "* " );
+    maybeColon = "";
+  }
+  else
+    maybeColon = ":";
+	
+
+  switch( mode )
+  {
+    case SAY_ALL:
+      // might already be ignored but in that case no harm is done
+      if( cg_teamChatsOnly.integer )
+        ignore = "[skipnotify]";
+
+      CG_Printf( "%s%s%s" S_COLOR_WHITE "%s %c" S_COLOR_GREEN "%s\n",
+                 ignore, prefix, name, maybeColon, INDENT_MARKER, text );
+      break;
+    case SAY_TEAM:
+      CG_Printf( "%s%s(%s" S_COLOR_WHITE ")%s%s %c" S_COLOR_CYAN "%s\n",
+                 ignore, prefix, name, location, maybeColon, INDENT_MARKER, text );
+      break;
+    case SAY_ADMINS:
+    case SAY_ADMINS_PUBLIC:
+      CG_Printf( "%s%s%s%s" S_COLOR_WHITE "%s %c" S_COLOR_MAGENTA "%s\n",
+                 ignore, prefix,
+                 ( mode == SAY_ADMINS ) ? "[ADMIN]" : "[PLAYER]",
+                 name, maybeColon, INDENT_MARKER, text );
+      break;
+    case SAY_AREA:
+      CG_Printf( "%s%s<%s" S_COLOR_WHITE ">%s%s %c" S_COLOR_BLUE "%s\n",
+                 ignore, prefix, name, location, maybeColon, INDENT_MARKER, text );
+      break;
+    case SAY_PRIVMSG:
+    case SAY_TPRIVMSG:
+      color = ( mode == SAY_TPRIVMSG ) ? S_COLOR_CYAN : S_COLOR_GREEN;
+      CG_Printf( "%s%s[%s" S_COLOR_WHITE " -> %s" S_COLOR_WHITE "]%s %c%s%s\n",
+                 ignore, prefix, name, cgs.clientinfo[ cg.clientNum ].name,
+                 maybeColon, INDENT_MARKER, color, text );
+      if( !ignore[0] )
+        CG_CenterPrint( va( "%sPrivate message from: " S_COLOR_WHITE "%s", 
+                            color, name ), 200, GIANTCHAR_WIDTH * 4 );
+      break;
+    case SAY_RAW:
+      CG_Printf( "%s\n", text );
+      break;
+	  
+  }
+
+  switch( mode )
+  {
+    case SAY_TEAM:
+    case SAY_AREA:
+    case SAY_TPRIVMSG:
+      if( cg.snap->ps.stats[ STAT_TEAM ] == TEAM_ALIENS )
+      {
+        trap_S_StartLocalSound( cgs.media.alienTalkSound, CHAN_LOCAL_SOUND );
+        break;
+      }
+      else if( cg.snap->ps.stats[ STAT_TEAM ] == TEAM_HUMANS )
+      {
+        trap_S_StartLocalSound( cgs.media.humanTalkSound, CHAN_LOCAL_SOUND );
+        break;
+      }
+    default: 
+      trap_S_StartLocalSound( cgs.media.talkSound, CHAN_LOCAL_SOUND );
+  }
+}
+
+
+/*
+=================
+CG_VoiceTrack
+
+return the voice indexed voice track or print errors quietly to console
+in case someone is on an unpure server and wants to know which voice pak
+is missing or incomplete
+=================
+*/
+static voiceTrack_t *CG_VoiceTrack( char *voice, int cmd, int track )
+{
+  voice_t *v;
+  voiceCmd_t *c;
+  voiceTrack_t *t;
+
+  v = BG_VoiceByName( cgs.voices, voice );
+  if( !v )
+  {
+    CG_Printf( "[skipnotify]WARNING: could not find voice \"%s\"\n", voice );
+    return NULL;
+  }
+  c = BG_VoiceCmdByNum( v->cmds, cmd );
+  if( !c )
+  {
+    CG_Printf( "[skipnotify]WARNING: could not find command %d "
+      "in voice \"%s\"\n", cmd, voice );
+    return NULL;
+  }
+  t = BG_VoiceTrackByNum( c->tracks, track );
+  if( !t )
+  {
+    CG_Printf( "[skipnotify]WARNING: could not find track %d for command %d in "
+      "voice \"%s\"\n", track, cmd, voice );
+    return NULL;
+  }
+  return t;
+}
+
+/*
+=================
+CG_ParseVoice
+
+voice clientNum vChan cmdNum trackNum [sayText]
+=================
+*/
+static void CG_ParseVoice( void )
+{
+  int clientNum;
+  voiceChannel_t vChan;
+  char sayText[ MAX_SAY_TEXT] = {""};
+  voiceTrack_t *track;
+  clientInfo_t *ci;
+
+  if( trap_Argc() < 5 || trap_Argc() > 6 )
+    return;
+
+  if( trap_Argc() == 6 )
+    Q_strncpyz( sayText, CG_Argv( 5 ), sizeof( sayText ) );
+
+  clientNum = atoi( CG_Argv( 1 ) );
+  if( clientNum < 0 || clientNum >= MAX_CLIENTS )
+    return;
+
+  vChan = atoi( CG_Argv( 2 ) );
+  if( vChan < 0 || vChan >= VOICE_CHAN_NUM_CHANS )
+    return;
+
+  if( cg_teamChatsOnly.integer && vChan != VOICE_CHAN_TEAM )
+    return;
+
+  ci = &cgs.clientinfo[ clientNum ];
+
+  // this joker is still talking
+  if( ci->voiceTime > cg.time )
+    return;
+
+  track = CG_VoiceTrack( ci->voice, atoi( CG_Argv( 3 ) ), atoi( CG_Argv( 4 ) ) );
+
+  // keep track of how long the player will be speaking
+  // assume it takes 3s to say "*unintelligible gibberish*" 
+  if( track )
+    ci->voiceTime = cg.time + track->duration;
+  else
+    ci->voiceTime = cg.time + 3000;
+
+  if( !sayText[ 0 ] )
+  {
+    if( track )
+      Q_strncpyz( sayText, track->text, sizeof( sayText ) );
+    else
+      Q_strncpyz( sayText, "*unintelligible gibberish*", sizeof( sayText ) );
+  }
+ 
+  if( !cg_noVoiceText.integer ) 
+  {
+    switch( vChan )
+    {
+      case VOICE_CHAN_ALL:
+        CG_Say( clientNum, SAY_ALL, sayText );
+        break;
+      case VOICE_CHAN_TEAM:
+        CG_Say( clientNum, SAY_TEAM, sayText );
+        break;
+      default:
+        break;
+    }
+  }
+
+  // playing voice audio tracks disabled
+  if( cg_noVoiceChats.integer )
+    return;
+
+  // no audio track to play
+  if( !track )
+    return;
+
+  // don't play audio track for lamers
+  if( Com_ClientListContains( &cgs.ignoreList, clientNum ) )
+    return;
+
+  switch( vChan )
+  {
+    case VOICE_CHAN_ALL:
+      trap_S_StartLocalSound( track->track, CHAN_VOICE );
+      break;
+    case VOICE_CHAN_TEAM:
+      trap_S_StartLocalSound( track->track, CHAN_VOICE );
+      break;
+    case VOICE_CHAN_LOCAL:
+      trap_S_StartSound( NULL, clientNum, CHAN_VOICE, track->track );
+      break;
+    default:
+        break;
+  } 
+}
+
+/*
+=================
+CG_CenterPrint_f
+=================
+*/
+static void CG_CenterPrint_f( void )
+{
+  CG_CenterPrint( CG_Argv( 1 ), SCREEN_HEIGHT * 0.30, BIGCHAR_WIDTH );
+}
+
+/*
+=================
+CG_Print_f
+=================
+*/
+static void CG_Print_f( void )
+{
+  CG_Printf( "%s", CG_Argv( 1 ) );
+}
+
+/*
+=================
+CG_Chat_f
+=================
+*/
+static void CG_Chat_f( void )
+{
+  char     id[ 3 ];
+  char     mode[ 3 ];
+
+  /*
+
+  char     chatclientnum[ 3 ];
+  char     teamSelection[ 3 ];
+
+
+  trap_Argv( 1, chatclientnum, sizeof( id ) );
+  trap_Argv( 2, teamSelection, sizeof( mode ) );
+  
+  trap_Argv( 3, id, sizeof( id ) );
+  trap_Argv( 4, mode, sizeof( mode ) );
+
+  if (atoi( chatclientnum ) == -1)//pro kompatibilitu se starymi funkcemi, jen se prida parametr -1
+  {
+    trap_Argv( 1, id, sizeof( id ) );
+    trap_Argv( 2, mode, sizeof( mode ) );
+  }
+
+  CG_Say( atoi( chatclientnum ), atoi( teamSelection ), atoi( id ), atoi( mode ), CG_Argv( 5 ) );
+  */
+
+  trap_Argv( 1, id, sizeof( id ) );
+  trap_Argv( 2, mode, sizeof( mode ) );
+
+  CG_Say( atoi( id ), atoi( mode ), CG_Argv( 3 ) );
+}
+
+/*
+=================
+CG_ClientLevelShot_f
+=================
+*/
+static void CG_ClientLevelShot_f( void )
+{
+  cg.levelShot = qtrue;
+}
+
+/*
+=================
+CG_ServerMenu_f
+=================
+*/
+static void CG_ServerMenu_f( void )
+{
+  if( !cg.demoPlayback )
+  {
+    if( trap_Argc( ) == 2 )
+      CG_Menu( atoi( CG_Argv( 1 ) ), 0 );
+    else if( trap_Argc( ) == 3 )
+      CG_Menu( atoi( CG_Argv( 1 ) ), atoi( CG_Argv( 2 ) ) );
+  }
+}
+
+/*
+=================
+CG_ServerCloseMenus_f
+=================
+*/
+static void CG_ServerCloseMenus_f( void )
+{
+  trap_SendConsoleCommand( "closemenus\n" );
+}
+
+/*
+=================
+CG_PoisonCloud_f
+=================
+*/
+static void CG_PoisonCloud_f( void )
+{
+  cg.poisonedTime = cg.time;
+
+  if( CG_IsParticleSystemValid( &cg.poisonCloudPS ) )
+  {
+    cg.poisonCloudPS = CG_SpawnNewParticleSystem( cgs.media.poisonCloudPS );
+    CG_SetAttachmentCent( &cg.poisonCloudPS->attachment, &cg.predictedPlayerEntity );
+    CG_AttachToCent( &cg.poisonCloudPS->attachment );
+  }
+}
+
+static void CG_GameCmds_f( void )
+{
+  int i;
+  int c = trap_Argc( );
+
+  /*
+  There is no corresponding trap_RemoveCommand because a server could send
+  something like
+    cmds quit
+  which would result in trap_RemoveCommand( "quit" ), which would be really bad
+  */
+  for( i = 1; i < c; i++ )
+    trap_AddCommand( CG_Argv( i ) );
+}
+
+static consoleCommand_t svcommands[ ] =
+{
+  { "chat", CG_Chat_f },
+  { "clientLevelShot", CG_ClientLevelShot_f },
+  { "cmds", CG_GameCmds_f },
+  { "cp", CG_CenterPrint_f },
+  { "cs", CG_ConfigStringModified },
+  { "map_restart", CG_MapRestart },
+  { "poisoncloud", CG_PoisonCloud_f },
+  { "print", CG_Print_f },
+  { "scores", CG_ParseScores },
+  { "serverclosemenus", CG_ServerCloseMenus_f },
+  { "servermenu", CG_ServerMenu_f },
+  { "tinfo", CG_ParseTeamInfo },
+  { "voice", CG_ParseVoice }
+};
+
+/*
+=================
+CG_ServerCommand
+
+The string has been tokenized and can be retrieved with
+Cmd_Argc() / Cmd_Argv()
+=================
+*/
+static void CG_ServerCommand( void )
+{
+  const char       *cmd;
+  consoleCommand_t *command;
+
+  cmd = CG_Argv( 0 );
+  command = bsearch( cmd, svcommands, sizeof( svcommands ) /
+                     sizeof( svcommands[ 0 ]), sizeof( svcommands[ 0 ] ),
+                     cmdcmp );
+
+  if( command )
+  {
+    command->function( );
+    return;
+  }
+
+  CG_Printf( "Unknown client game command: %s\n", cmd );
+}
+
+
+/*
+====================
+CG_ExecuteNewServerCommands
+
+Execute all of the server commands that were received along
+with this this snapshot.
+====================
+*/
+void CG_ExecuteNewServerCommands( int latestSequence )
+{
+  while( cgs.serverCommandSequence < latestSequence )
+  {
+    if( trap_GetServerCommand( ++cgs.serverCommandSequence ) )
+      CG_ServerCommand( );
+  }
+}
diff --git a/src/cgame/cg_snapshot.c b/src/cgame/cg_snapshot.c
new file mode 100644
index 0000000..673558d
--- /dev/null
+++ b/src/cgame/cg_snapshot.c
@@ -0,0 +1,408 @@
+/*
+===========================================================================
+Copyright (C) 1999-2005 Id Software, Inc.
+Copyright (C) 2000-2009 Darklegion Development
+
+This file is part of Tremulous.
+
+Tremulous 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.
+
+Tremulous 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 Tremulous; if not, write to the Free Software
+Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
+===========================================================================
+*/
+
+// cg_snapshot.c -- things that happen on snapshot transition,
+// not necessarily every single rendered frame
+
+
+#include "cg_local.h"
+
+/*
+==================
+CG_ResetEntity
+==================
+*/
+static void CG_ResetEntity( centity_t *cent )
+{
+  // if the previous snapshot this entity was updated in is at least
+  // an event window back in time then we can reset the previous event
+  if( cent->snapShotTime < cg.time - EVENT_VALID_MSEC )
+    cent->previousEvent = 0;
+
+  cent->trailTime = cg.snap->serverTime;
+
+  VectorCopy( cent->currentState.origin, cent->lerpOrigin );
+  VectorCopy( cent->currentState.angles, cent->lerpAngles );
+
+  if( cent->currentState.eType == ET_PLAYER )
+    CG_ResetPlayerEntity( cent );
+}
+
+/*
+===============
+CG_TransitionEntity
+
+cent->nextState is moved to cent->currentState and events are fired
+===============
+*/
+static void CG_TransitionEntity( centity_t *cent )
+{
+  cent->currentState = cent->nextState;
+  cent->currentValid = qtrue;
+
+  // reset if the entity wasn't in the last frame or was teleported
+  if( !cent->interpolate )
+    CG_ResetEntity( cent );
+
+  // clear the next state.  if will be set by the next CG_SetNextSnap
+  cent->interpolate = qfalse;
+
+  // check for events
+  CG_CheckEvents( cent );
+}
+
+
+/*
+==================
+CG_SetInitialSnapshot
+
+This will only happen on the very first snapshot, or
+on tourney restarts.  All other times will use
+CG_TransitionSnapshot instead.
+
+FIXME: Also called by map_restart?
+==================
+*/
+void CG_SetInitialSnapshot( snapshot_t *snap )
+{
+  int           i;
+  centity_t     *cent;
+  entityState_t *state;
+
+  cg.snap = snap;
+
+  BG_PlayerStateToEntityState( &snap->ps, &cg_entities[ snap->ps.clientNum ].currentState, qfalse );
+
+  // sort out solid entities
+  CG_BuildSolidList( );
+
+  CG_ExecuteNewServerCommands( snap->serverCommandSequence );
+
+  // set our local weapon selection pointer to
+  // what the server has indicated the current weapon is
+  CG_Respawn( );
+
+  for( i = 0; i < cg.snap->numEntities; i++ )
+  {
+    state = &cg.snap->entities[ i ];
+    cent = &cg_entities[ state->number ];
+
+    memcpy( &cent->currentState, state, sizeof( entityState_t ) );
+    //cent->currentState = *state;
+    cent->interpolate = qfalse;
+    cent->currentValid = qtrue;
+
+    CG_ResetEntity( cent );
+
+    // check for events
+    CG_CheckEvents( cent );
+  }
+}
+
+
+/*
+===================
+CG_TransitionSnapshot
+
+The transition point from snap to nextSnap has passed
+===================
+*/
+static void CG_TransitionSnapshot( void )
+{
+  centity_t   *cent;
+  snapshot_t  *oldFrame;
+  int         i;
+
+  if( !cg.snap )
+    CG_Error( "CG_TransitionSnapshot: NULL cg.snap" );
+
+  if( !cg.nextSnap )
+    CG_Error( "CG_TransitionSnapshot: NULL cg.nextSnap" );
+
+  // execute any server string commands before transitioning entities
+  CG_ExecuteNewServerCommands( cg.nextSnap->serverCommandSequence );
+
+  // clear the currentValid flag for all entities in the existing snapshot
+  for( i = 0; i < cg.snap->numEntities; i++ )
+  {
+    cent = &cg_entities[ cg.snap->entities[ i ].number ];
+    cent->currentValid = qfalse;
+  }
+
+  // move nextSnap to snap and do the transitions
+  oldFrame = cg.snap;
+  cg.snap = cg.nextSnap;
+
+  BG_PlayerStateToEntityState( &cg.snap->ps, &cg_entities[ cg.snap->ps.clientNum ].currentState, qfalse );
+  cg_entities[ cg.snap->ps.clientNum ].interpolate = qfalse;
+
+  for( i = 0; i < cg.snap->numEntities; i++ )
+  {
+    cent = &cg_entities[ cg.snap->entities[ i ].number ];
+    CG_TransitionEntity( cent );
+
+    // remember time of snapshot this entity was last updated in
+    cent->snapShotTime = cg.snap->serverTime;
+  }
+
+  cg.nextSnap = NULL;
+
+  // check for playerstate transition events
+  if( oldFrame )
+  {
+    playerState_t *ops, *ps;
+
+    ops = &oldFrame->ps;
+    ps = &cg.snap->ps;
+    // teleporting checks are irrespective of prediction
+    if( ( ps->eFlags ^ ops->eFlags ) & EF_TELEPORT_BIT )
+      cg.thisFrameTeleport = qtrue; // will be cleared by prediction code
+
+    // if we are not doing client side movement prediction for any
+    // reason, then the client events and view changes will be issued now
+    if( cg.demoPlayback || ( cg.snap->ps.pm_flags & PMF_FOLLOW ) ||
+        cg_nopredict.integer || cg_synchronousClients.integer )
+      CG_TransitionPlayerState( ps, ops );
+  }
+}
+
+
+/*
+===================
+CG_SetNextSnap
+
+A new snapshot has just been read in from the client system.
+===================
+*/
+static void CG_SetNextSnap( snapshot_t *snap )
+{
+  int           num;
+  entityState_t *es;
+  centity_t     *cent;
+
+  cg.nextSnap = snap;
+
+  BG_PlayerStateToEntityState( &snap->ps, &cg_entities[ snap->ps.clientNum ].nextState, qfalse );
+  cg_entities[ cg.snap->ps.clientNum ].interpolate = qtrue;
+
+  // check for extrapolation errors
+  for( num = 0 ; num < snap->numEntities ; num++ )
+  {
+    es = &snap->entities[ num ];
+    cent = &cg_entities[ es->number ];
+
+    memcpy( &cent->nextState, es, sizeof( entityState_t ) );
+    //cent->nextState = *es;
+
+    // if this frame is a teleport, or the entity wasn't in the
+    // previous frame, don't interpolate
+    if( !cent->currentValid || ( ( cent->currentState.eFlags ^ es->eFlags ) & EF_TELEPORT_BIT ) )
+      cent->interpolate = qfalse;
+    else
+      cent->interpolate = qtrue;
+  }
+
+  // if the next frame is a teleport for the playerstate, we
+  // can't interpolate during demos
+  if( cg.snap && ( ( snap->ps.eFlags ^ cg.snap->ps.eFlags ) & EF_TELEPORT_BIT ) )
+    cg.nextFrameTeleport = qtrue;
+  else
+    cg.nextFrameTeleport = qfalse;
+
+  // if changing follow mode, don't interpolate
+  if( cg.nextSnap->ps.clientNum != cg.snap->ps.clientNum )
+    cg.nextFrameTeleport = qtrue;
+
+  // if changing server restarts, don't interpolate
+  if( ( cg.nextSnap->snapFlags ^ cg.snap->snapFlags ) & SNAPFLAG_SERVERCOUNT )
+    cg.nextFrameTeleport = qtrue;
+
+  // sort out solid entities
+  CG_BuildSolidList( );
+}
+
+
+/*
+========================
+CG_ReadNextSnapshot
+
+This is the only place new snapshots are requested
+This may increment cgs.processedSnapshotNum multiple
+times if the client system fails to return a
+valid snapshot.
+========================
+*/
+static snapshot_t *CG_ReadNextSnapshot( void )
+{
+  qboolean    r;
+  snapshot_t  *dest;
+
+  if( cg.latestSnapshotNum > cgs.processedSnapshotNum + 1000 )
+  {
+    CG_Printf( "WARNING: CG_ReadNextSnapshot: way out of range, %i > %i\n",
+      cg.latestSnapshotNum, cgs.processedSnapshotNum );
+  }
+
+  while( cgs.processedSnapshotNum < cg.latestSnapshotNum )
+  {
+    // decide which of the two slots to load it into
+    if( cg.snap == &cg.activeSnapshots[ 0 ] )
+      dest = &cg.activeSnapshots[ 1 ];
+    else
+      dest = &cg.activeSnapshots[ 0 ];
+
+    // try to read the snapshot from the client system
+    cgs.processedSnapshotNum++;
+    r = trap_GetSnapshot( cgs.processedSnapshotNum, dest );
+
+    // FIXME: why would trap_GetSnapshot return a snapshot with the same server time
+    if( cg.snap && r && dest->serverTime == cg.snap->serverTime )
+    {
+      //continue;
+    }
+
+    // if it succeeded, return
+    if( r )
+    {
+      CG_AddLagometerSnapshotInfo( dest );
+      return dest;
+    }
+
+    // a GetSnapshot will return failure if the snapshot
+    // never arrived, or  is so old that its entities
+    // have been shoved off the end of the circular
+    // buffer in the client system.
+
+    // record as a dropped packet
+    CG_AddLagometerSnapshotInfo( NULL );
+
+    // If there are additional snapshots, continue trying to
+    // read them.
+  }
+
+  // nothing left to read
+  return NULL;
+}
+
+
+/*
+============
+CG_ProcessSnapshots
+
+We are trying to set up a renderable view, so determine
+what the simulated time is, and try to get snapshots
+both before and after that time if available.
+
+If we don't have a valid cg.snap after exiting this function,
+then a 3D game view cannot be rendered.  This should only happen
+right after the initial connection.  After cg.snap has been valid
+once, it will never turn invalid.
+
+Even if cg.snap is valid, cg.nextSnap may not be, if the snapshot
+hasn't arrived yet (it becomes an extrapolating situation instead
+of an interpolating one)
+
+============
+*/
+void CG_ProcessSnapshots( void )
+{
+  snapshot_t  *snap;
+  int         n;
+
+  // see what the latest snapshot the client system has is
+  trap_GetCurrentSnapshotNumber( &n, &cg.latestSnapshotTime );
+
+  if( n != cg.latestSnapshotNum )
+  {
+    if( n < cg.latestSnapshotNum )
+    {
+      // this should never happen
+      CG_Error( "CG_ProcessSnapshots: n < cg.latestSnapshotNum" );
+    }
+
+    cg.latestSnapshotNum = n;
+  }
+
+  // If we have yet to receive a snapshot, check for it.
+  // Once we have gotten the first snapshot, cg.snap will
+  // always have valid data for the rest of the game
+  while( !cg.snap )
+  {
+    snap = CG_ReadNextSnapshot( );
+
+    if( !snap )
+    {
+      // we can't continue until we get a snapshot
+      return;
+    }
+
+    // set our weapon selection to what
+    // the playerstate is currently using
+    if( !( snap->snapFlags & SNAPFLAG_NOT_ACTIVE ) )
+      CG_SetInitialSnapshot( snap );
+  }
+
+  // loop until we either have a valid nextSnap with a serverTime
+  // greater than cg.time to interpolate towards, or we run
+  // out of available snapshots
+  do
+  {
+    // if we don't have a nextframe, try and read a new one in
+    if( !cg.nextSnap )
+    {
+      snap = CG_ReadNextSnapshot( );
+
+      // if we still don't have a nextframe, we will just have to
+      // extrapolate
+      if( !snap )
+        break;
+
+      CG_SetNextSnap( snap );
+
+      // if time went backwards, we have a level restart
+      if( cg.nextSnap->serverTime < cg.snap->serverTime )
+        CG_Error( "CG_ProcessSnapshots: Server time went backwards" );
+    }
+
+    // if our time is < nextFrame's, we have a nice interpolating state
+    if( cg.time >= cg.snap->serverTime && cg.time < cg.nextSnap->serverTime )
+      break;
+
+    // we have passed the transition from nextFrame to frame
+    CG_TransitionSnapshot( );
+  } while( 1 );
+
+  // assert our valid conditions upon exiting
+  if( cg.snap == NULL )
+    CG_Error( "CG_ProcessSnapshots: cg.snap == NULL" );
+
+  if( cg.time < cg.snap->serverTime )
+  {
+    // this can happen right after a vid_restart
+    cg.time = cg.snap->serverTime;
+  }
+
+  if( cg.nextSnap != NULL && cg.nextSnap->serverTime <= cg.time )
+    CG_Error( "CG_ProcessSnapshots: cg.nextSnap->serverTime <= cg.time" );
+}
+
diff --git a/src/cgame/cg_syscalls.asm b/src/cgame/cg_syscalls.asm
new file mode 100644
index 0000000..2537c91
--- /dev/null
+++ b/src/cgame/cg_syscalls.asm
@@ -0,0 +1,121 @@
+code
+
+equ trap_Print                        -1
+equ trap_Error                        -2
+equ trap_Milliseconds                 -3
+equ trap_Cvar_Register                -4
+equ trap_Cvar_Update                  -5
+equ trap_Cvar_Set                     -6
+equ trap_Cvar_VariableStringBuffer    -7
+equ trap_Argc                         -8
+equ trap_Argv                         -9
+equ trap_Args                         -10
+equ trap_FS_FOpenFile                 -11
+equ trap_FS_Read                      -12
+equ trap_FS_Write                     -13 
+equ trap_FS_FCloseFile                -14
+equ trap_SendConsoleCommand           -15
+equ trap_AddCommand                   -16
+equ trap_SendClientCommand            -17
+equ trap_UpdateScreen                 -18
+equ trap_CM_LoadMap                   -19
+equ trap_CM_NumInlineModels           -20
+equ trap_CM_InlineModel               -21
+equ trap_CM_LoadModel                 -22
+equ trap_CM_TempBoxModel              -23
+equ trap_CM_PointContents             -24
+equ trap_CM_TransformedPointContents  -25
+equ trap_CM_BoxTrace                  -26
+equ trap_CM_TransformedBoxTrace       -27
+equ trap_CM_MarkFragments             -28
+equ trap_S_StartSound                 -29
+equ trap_S_StartLocalSound            -30
+equ trap_S_ClearLoopingSounds         -31
+equ trap_S_AddLoopingSound            -32
+equ trap_S_UpdateEntityPosition       -33
+equ trap_S_Respatialize               -34
+equ trap_S_RegisterSound              -35
+equ trap_S_StartBackgroundTrack       -36
+equ trap_R_LoadWorldMap               -37
+equ trap_R_RegisterModel              -38
+equ trap_R_RegisterSkin               -39
+equ trap_R_RegisterShader             -40
+equ trap_R_ClearScene                 -41
+equ trap_R_AddRefEntityToScene        -42
+equ trap_R_AddPolyToScene             -43
+equ trap_R_AddLightToScene            -44
+equ trap_R_RenderScene                -45
+equ trap_R_SetColor                   -46
+equ trap_R_SetClipRegion              -47
+equ trap_R_DrawStretchPic             -48
+equ trap_R_ModelBounds                -49
+equ trap_R_LerpTag                    -50
+equ trap_GetGlconfig                  -51
+equ trap_GetGameState                 -52
+equ trap_GetCurrentSnapshotNumber     -53
+equ trap_GetSnapshot                  -54
+equ trap_GetServerCommand             -55
+equ trap_GetCurrentCmdNumber          -56
+equ trap_GetUserCmd                   -57
+equ trap_SetUserCmdValue              -58
+equ trap_R_RegisterShaderNoMip        -59
+equ trap_MemoryRemaining              -60
+equ trap_R_RegisterFont               -61
+equ trap_Key_IsDown                   -62
+equ trap_Key_GetCatcher               -63
+equ trap_Key_SetCatcher               -64
+equ trap_Key_GetKey                   -65
+equ trap_S_StopBackgroundTrack        -66
+equ trap_RealTime                     -67
+equ trap_SnapVector                   -68
+equ trap_RemoveCommand                -69
+equ trap_R_LightForPoint              -70
+equ trap_CIN_PlayCinematic            -71
+equ trap_CIN_StopCinematic            -72
+equ trap_CIN_RunCinematic             -73
+equ trap_CIN_DrawCinematic            -74
+equ trap_CIN_SetExtents               -75
+equ trap_R_RemapShader                -76
+equ trap_S_AddRealLoopingSound        -77
+equ trap_S_StopLoopingSound           -78
+equ trap_CM_TempCapsuleModel          -79
+equ trap_CM_CapsuleTrace              -80
+equ trap_CM_TransformedCapsuleTrace   -81
+equ trap_R_AddAdditiveLightToScene    -82
+equ trap_GetEntityToken               -83
+equ trap_R_AddPolysToScene            -84
+equ trap_R_inPVS                      -85
+equ trap_FS_Seek                      -86
+equ trap_FS_GetFileList               -87
+equ trap_LiteralArgs                  -88
+equ trap_CM_BiSphereTrace             -89
+equ trap_CM_TransformedBiSphereTrace  -90
+equ trap_GetDemoState                 -91
+equ trap_GetDemoPos                   -92
+equ trap_GetDemoName                  -93
+equ trap_Key_KeynumToStringBuf        -94
+equ trap_Key_GetBindingBuf            -95
+equ trap_Key_SetBinding               -96
+
+equ trap_Parse_AddGlobalDefine        -97
+equ trap_Parse_LoadSource             -98
+equ trap_Parse_FreeSource             -99
+equ trap_Parse_ReadToken              -100
+equ trap_Parse_SourceFileAndLine      -101
+equ trap_Key_SetOverstrikeMode        -102
+equ trap_Key_GetOverstrikeMode        -103
+
+equ trap_S_SoundDuration              -104
+
+equ memset                            -201
+equ memcpy                            -202
+equ strncpy                           -203
+equ sin                               -204
+equ cos                               -205
+equ atan2                             -206
+equ sqrt                              -207
+equ floor                             -208
+equ ceil                              -209
+equ testPrintInt                      -210
+equ testPrintFloat                    -211
+
diff --git a/src/cgame/cg_syscalls.c b/src/cgame/cg_syscalls.c
new file mode 100644
index 0000000..0c0b722
--- /dev/null
+++ b/src/cgame/cg_syscalls.c
@@ -0,0 +1,592 @@
+/*
+===========================================================================
+Copyright (C) 1999-2005 Id Software, Inc.
+Copyright (C) 2000-2009 Darklegion Development
+
+This file is part of Tremulous.
+
+Tremulous 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.
+
+Tremulous 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 Tremulous; if not, write to the Free Software
+Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
+===========================================================================
+*/
+
+// cg_syscalls.c -- this file is only included when building a dll
+// cg_syscalls.asm is included instead when building a qvm
+
+
+#include "cg_local.h"
+
+static intptr_t (QDECL *syscall)( intptr_t arg, ... ) = (intptr_t (QDECL *)( intptr_t, ...))-1;
+
+
+Q_EXPORT void dllEntry( intptr_t (QDECL  *syscallptr)( intptr_t arg,... ) )
+{
+  syscall = syscallptr;
+}
+
+
+int PASSFLOAT( float x )
+{
+  float floatTemp;
+  floatTemp = x;
+  return *(int *)&floatTemp;
+}
+
+void trap_Print( const char *fmt )
+{
+  syscall( CG_PRINT, fmt );
+}
+
+void trap_Error( const char *fmt )
+{
+  syscall( CG_ERROR, fmt );
+}
+
+int trap_Milliseconds( void )
+{
+  return syscall( CG_MILLISECONDS );
+}
+
+void trap_Cvar_Register( vmCvar_t *vmCvar, const char *varName, const char *defaultValue, int flags )
+{
+  syscall( CG_CVAR_REGISTER, vmCvar, varName, defaultValue, flags );
+}
+
+void trap_Cvar_Update( vmCvar_t *vmCvar )
+{
+  syscall( CG_CVAR_UPDATE, vmCvar );
+}
+
+void trap_Cvar_Set( const char *var_name, const char *value )
+{
+  syscall( CG_CVAR_SET, var_name, value );
+}
+
+void trap_Cvar_VariableStringBuffer( const char *var_name, char *buffer, int bufsize )
+{
+  syscall( CG_CVAR_VARIABLESTRINGBUFFER, var_name, buffer, bufsize );
+}
+
+int trap_Argc( void )
+{
+  return syscall( CG_ARGC );
+}
+
+void  trap_Argv( int n, char *buffer, int bufferLength )
+{
+  syscall( CG_ARGV, n, buffer, bufferLength );
+}
+
+void  trap_Args( char *buffer, int bufferLength )
+{
+  syscall( CG_ARGS, buffer, bufferLength );
+}
+
+void  trap_LiteralArgs( char *buffer, int bufferLength )
+{
+  syscall( CG_LITERAL_ARGS, buffer, bufferLength );
+}
+
+int trap_FS_FOpenFile( const char *qpath, fileHandle_t *f, fsMode_t mode )
+{
+  return syscall( CG_FS_FOPENFILE, qpath, f, mode );
+}
+
+void  trap_FS_Read( void *buffer, int len, fileHandle_t f )
+{
+  syscall( CG_FS_READ, buffer, len, f );
+}
+
+void trap_FS_Write( const void *buffer, int len, fileHandle_t f )
+{
+  syscall( CG_FS_WRITE, buffer, len, f );
+}
+
+void  trap_FS_FCloseFile( fileHandle_t f )
+{
+  syscall( CG_FS_FCLOSEFILE, f );
+}
+
+void trap_FS_Seek( fileHandle_t f, long offset, fsOrigin_t origin )
+{
+  syscall( CG_FS_SEEK, f, offset, origin );
+}
+
+int trap_FS_GetFileList( const char *path, const char *extension, char *listbuf, int bufsize )
+{
+  return syscall( CG_FS_GETFILELIST, path, extension, listbuf, bufsize );
+}
+
+void  trap_SendConsoleCommand( const char *text )
+{
+  syscall( CG_SENDCONSOLECOMMAND, text );
+}
+
+void  trap_AddCommand( const char *cmdName )
+{
+  syscall( CG_ADDCOMMAND, cmdName );
+}
+
+void  trap_RemoveCommand( const char *cmdName )
+{
+    syscall( CG_REMOVECOMMAND, cmdName );
+}
+
+void  trap_SendClientCommand( const char *s )
+{
+  syscall( CG_SENDCLIENTCOMMAND, s );
+}
+
+void  trap_UpdateScreen( void )
+{
+  syscall( CG_UPDATESCREEN );
+}
+
+void  trap_CM_LoadMap( const char *mapname )
+{
+  syscall( CG_CM_LOADMAP, mapname );
+}
+
+int   trap_CM_NumInlineModels( void )
+{
+  return syscall( CG_CM_NUMINLINEMODELS );
+}
+
+clipHandle_t trap_CM_InlineModel( int index )
+{
+  return syscall( CG_CM_INLINEMODEL, index );
+}
+
+clipHandle_t trap_CM_TempBoxModel( const vec3_t mins, const vec3_t maxs )
+{
+  return syscall( CG_CM_TEMPBOXMODEL, mins, maxs );
+}
+
+clipHandle_t trap_CM_TempCapsuleModel( const vec3_t mins, const vec3_t maxs )
+{
+  return syscall( CG_CM_TEMPCAPSULEMODEL, mins, maxs );
+}
+
+int   trap_CM_PointContents( const vec3_t p, clipHandle_t model )
+{
+  return syscall( CG_CM_POINTCONTENTS, p, model );
+}
+
+int   trap_CM_TransformedPointContents( const vec3_t p, clipHandle_t model, const vec3_t origin,
+                                        const vec3_t angles )
+{
+  return syscall( CG_CM_TRANSFORMEDPOINTCONTENTS, p, model, origin, angles );
+}
+
+void  trap_CM_BoxTrace( trace_t *results, const vec3_t start, const vec3_t end,
+              const vec3_t mins, const vec3_t maxs,
+              clipHandle_t model, int brushmask )
+{
+  syscall( CG_CM_BOXTRACE, results, start, end, mins, maxs, model, brushmask );
+}
+
+void  trap_CM_CapsuleTrace( trace_t *results, const vec3_t start, const vec3_t end,
+              const vec3_t mins, const vec3_t maxs,
+              clipHandle_t model, int brushmask )
+{
+  syscall( CG_CM_CAPSULETRACE, results, start, end, mins, maxs, model, brushmask );
+}
+
+void  trap_CM_TransformedBoxTrace( trace_t *results, const vec3_t start, const vec3_t end,
+              const vec3_t mins, const vec3_t maxs,
+              clipHandle_t model, int brushmask,
+              const vec3_t origin, const vec3_t angles )
+{
+  syscall( CG_CM_TRANSFORMEDBOXTRACE, results, start, end, mins, maxs, model, brushmask, origin, angles );
+}
+
+void  trap_CM_TransformedCapsuleTrace( trace_t *results, const vec3_t start, const vec3_t end,
+              const vec3_t mins, const vec3_t maxs,
+              clipHandle_t model, int brushmask,
+              const vec3_t origin, const vec3_t angles )
+{
+  syscall( CG_CM_TRANSFORMEDCAPSULETRACE, results, start, end, mins, maxs, model, brushmask, origin, angles );
+}
+
+void trap_CM_BiSphereTrace( trace_t *results, const vec3_t start,
+             const vec3_t end, float startRad, float endRad,
+             clipHandle_t model, int mask )
+{
+  syscall( CG_CM_BISPHERETRACE, results, start, end,
+      PASSFLOAT( startRad ), PASSFLOAT( endRad ), model, mask );
+}
+
+void trap_CM_TransformedBiSphereTrace( trace_t *results, const vec3_t start,
+             const vec3_t end, float startRad, float endRad,
+             clipHandle_t model, int mask,
+             const vec3_t origin )
+{
+  syscall( CG_CM_TRANSFORMEDBISPHERETRACE, results, start, end, PASSFLOAT( startRad ),
+      PASSFLOAT( endRad ), model, mask, origin );
+}
+
+int   trap_CM_MarkFragments( int numPoints, const vec3_t *points,
+        const vec3_t projection,
+        int maxPoints, vec3_t pointBuffer,
+        int maxFragments, markFragment_t *fragmentBuffer )
+{
+  return syscall( CG_CM_MARKFRAGMENTS, numPoints, points, projection, maxPoints,
+                  pointBuffer, maxFragments, fragmentBuffer );
+}
+
+void  trap_S_StartSound( vec3_t origin, int entityNum, int entchannel, sfxHandle_t sfx )
+{
+  syscall( CG_S_STARTSOUND, origin, entityNum, entchannel, sfx );
+}
+
+void  trap_S_StartLocalSound( sfxHandle_t sfx, int channelNum )
+{
+  syscall( CG_S_STARTLOCALSOUND, sfx, channelNum );
+}
+
+void  trap_S_ClearLoopingSounds( qboolean killall )
+{
+  syscall( CG_S_CLEARLOOPINGSOUNDS, killall );
+}
+
+void  trap_S_AddLoopingSound( int entityNum, const vec3_t origin, const vec3_t velocity, sfxHandle_t sfx )
+{
+  syscall( CG_S_ADDLOOPINGSOUND, entityNum, origin, velocity, sfx );
+}
+
+void  trap_S_AddRealLoopingSound( int entityNum, const vec3_t origin, const vec3_t velocity, sfxHandle_t sfx )
+{
+  syscall( CG_S_ADDREALLOOPINGSOUND, entityNum, origin, velocity, sfx );
+}
+
+void  trap_S_StopLoopingSound( int entityNum )
+{
+  syscall( CG_S_STOPLOOPINGSOUND, entityNum );
+}
+
+void  trap_S_UpdateEntityPosition( int entityNum, const vec3_t origin )
+{
+  syscall( CG_S_UPDATEENTITYPOSITION, entityNum, origin );
+}
+
+void  trap_S_Respatialize( int entityNum, const vec3_t origin, vec3_t axis[3], int inwater )
+{
+  syscall( CG_S_RESPATIALIZE, entityNum, origin, axis, inwater );
+}
+
+sfxHandle_t trap_S_RegisterSound( const char *sample, qboolean compressed )
+{
+  return syscall( CG_S_REGISTERSOUND, sample, compressed );
+}
+
+int trap_S_SoundDuration( sfxHandle_t handle )
+{
+  return syscall( CG_S_SOUNDDURATION, handle );
+}
+
+void  trap_S_StartBackgroundTrack( const char *intro, const char *loop )
+{
+  syscall( CG_S_STARTBACKGROUNDTRACK, intro, loop );
+}
+
+void  trap_R_LoadWorldMap( const char *mapname )
+{
+  syscall( CG_R_LOADWORLDMAP, mapname );
+}
+
+qhandle_t trap_R_RegisterModel( const char *name )
+{
+  return syscall( CG_R_REGISTERMODEL, name );
+}
+
+qhandle_t trap_R_RegisterSkin( const char *name )
+{
+  return syscall( CG_R_REGISTERSKIN, name );
+}
+
+qhandle_t trap_R_RegisterShader( const char *name )
+{
+  return syscall( CG_R_REGISTERSHADER, name );
+}
+
+qhandle_t trap_R_RegisterShaderNoMip( const char *name )
+{
+  return syscall( CG_R_REGISTERSHADERNOMIP, name );
+}
+
+void trap_R_RegisterFont( const char *fontName, int pointSize, fontInfo_t *font )
+{
+  syscall(CG_R_REGISTERFONT, fontName, pointSize, font );
+}
+
+void  trap_R_ClearScene( void )
+{
+  syscall( CG_R_CLEARSCENE );
+}
+
+void  trap_R_AddRefEntityToScene( const refEntity_t *re )
+{
+  syscall( CG_R_ADDREFENTITYTOSCENE, re );
+}
+
+void trap_R_AddPolyToScene( qhandle_t hShader , int numVerts, const polyVert_t *verts )
+{
+  syscall( CG_R_ADDPOLYTOSCENE, hShader, numVerts, verts );
+}
+
+qboolean trap_R_inPVS( const vec3_t p1, const vec3_t p2 )
+{
+  return syscall( CG_R_INPVS, p1, p2 );
+}
+
+void trap_R_AddPolysToScene( qhandle_t hShader , int numVerts, const polyVert_t *verts, int num )
+{
+  syscall( CG_R_ADDPOLYSTOSCENE, hShader, numVerts, verts, num );
+}
+
+int trap_R_LightForPoint( vec3_t point, vec3_t ambientLight, vec3_t directedLight, vec3_t lightDir )
+{
+  return syscall( CG_R_LIGHTFORPOINT, point, ambientLight, directedLight, lightDir );
+}
+
+void  trap_R_AddLightToScene( const vec3_t org, float intensity, float r, float g, float b )
+{
+  syscall( CG_R_ADDLIGHTTOSCENE, org, PASSFLOAT(intensity), PASSFLOAT(r), PASSFLOAT(g), PASSFLOAT(b) );
+}
+
+void  trap_R_AddAdditiveLightToScene( const vec3_t org, float intensity, float r, float g, float b )
+{
+  syscall( CG_R_ADDADDITIVELIGHTTOSCENE, org, PASSFLOAT(intensity), PASSFLOAT(r), PASSFLOAT(g), PASSFLOAT(b) );
+}
+
+void  trap_R_RenderScene( const refdef_t *fd )
+{
+  syscall( CG_R_RENDERSCENE, fd );
+}
+
+void  trap_R_SetColor( const float *rgba )
+{
+  syscall( CG_R_SETCOLOR, rgba );
+}
+
+void  trap_R_SetClipRegion( const float *region )
+{
+  syscall( CG_R_SETCLIPREGION, region );
+}
+
+void  trap_R_DrawStretchPic( float x, float y, float w, float h,
+                 float s1, float t1, float s2, float t2, qhandle_t hShader )
+{
+  syscall( CG_R_DRAWSTRETCHPIC, PASSFLOAT(x), PASSFLOAT(y), PASSFLOAT(w), PASSFLOAT(h),
+           PASSFLOAT(s1), PASSFLOAT(t1), PASSFLOAT(s2), PASSFLOAT(t2), hShader );
+}
+
+void  trap_R_ModelBounds( clipHandle_t model, vec3_t mins, vec3_t maxs ) {
+  syscall( CG_R_MODELBOUNDS, model, mins, maxs );
+}
+
+int   trap_R_LerpTag( orientation_t *tag, clipHandle_t mod, int startFrame, int endFrame,
+             float frac, const char *tagName )
+{
+  return syscall( CG_R_LERPTAG, tag, mod, startFrame, endFrame, PASSFLOAT(frac), tagName );
+}
+
+void  trap_R_RemapShader( const char *oldShader, const char *newShader, const char *timeOffset )
+{
+  syscall( CG_R_REMAP_SHADER, oldShader, newShader, timeOffset );
+}
+
+void trap_GetGlconfig( glconfig_t *glconfig )
+{
+  syscall( CG_GETGLCONFIG, glconfig );
+}
+
+void trap_GetGameState( gameState_t *gamestate )
+{
+  syscall( CG_GETGAMESTATE, gamestate );
+}
+
+void trap_GetCurrentSnapshotNumber( int *snapshotNumber, int *serverTime )
+{
+  syscall( CG_GETCURRENTSNAPSHOTNUMBER, snapshotNumber, serverTime );
+}
+
+qboolean  trap_GetSnapshot( int snapshotNumber, snapshot_t *snapshot )
+{
+  return syscall( CG_GETSNAPSHOT, snapshotNumber, snapshot );
+}
+
+qboolean  trap_GetServerCommand( int serverCommandNumber )
+{
+  return syscall( CG_GETSERVERCOMMAND, serverCommandNumber );
+}
+
+int trap_GetCurrentCmdNumber( void )
+{
+  return syscall( CG_GETCURRENTCMDNUMBER );
+}
+
+qboolean  trap_GetUserCmd( int cmdNumber, usercmd_t *ucmd )
+{
+  return syscall( CG_GETUSERCMD, cmdNumber, ucmd );
+}
+
+void    trap_SetUserCmdValue( int stateValue, float sensitivityScale )
+{
+  syscall( CG_SETUSERCMDVALUE, stateValue, PASSFLOAT( sensitivityScale ) );
+}
+
+void    testPrintInt( char *string, int i )
+{
+  syscall( CG_TESTPRINTINT, string, i );
+}
+
+void    testPrintFloat( char *string, float f )
+{
+  syscall( CG_TESTPRINTFLOAT, string, PASSFLOAT(f) );
+}
+
+int trap_MemoryRemaining( void )
+{
+  return syscall( CG_MEMORY_REMAINING );
+}
+
+qboolean trap_Key_IsDown( int keynum )
+{
+  return syscall( CG_KEY_ISDOWN, keynum );
+}
+
+int trap_Key_GetCatcher( void )
+{
+  return syscall( CG_KEY_GETCATCHER );
+}
+
+void trap_Key_SetCatcher( int catcher )
+{
+  syscall( CG_KEY_SETCATCHER, catcher );
+}
+
+int trap_Key_GetKey( const char *binding )
+{
+  return syscall( CG_KEY_GETKEY, binding );
+}
+
+int trap_Parse_AddGlobalDefine( char *define )
+{
+  return syscall( CG_PARSE_ADD_GLOBAL_DEFINE, define );
+}
+
+int trap_Parse_LoadSource( const char *filename )
+{
+  return syscall( CG_PARSE_LOAD_SOURCE, filename );
+}
+
+int trap_Parse_FreeSource( int handle )
+{
+  return syscall( CG_PARSE_FREE_SOURCE, handle );
+}
+
+int trap_Parse_ReadToken( int handle, pc_token_t *pc_token )
+{
+  return syscall( CG_PARSE_READ_TOKEN, handle, pc_token );
+}
+
+int trap_Parse_SourceFileAndLine( int handle, char *filename, int *line )
+{
+  return syscall( CG_PARSE_SOURCE_FILE_AND_LINE, handle, filename, line );
+}
+
+void  trap_S_StopBackgroundTrack( void )
+{
+  syscall( CG_S_STOPBACKGROUNDTRACK );
+}
+
+int trap_RealTime(qtime_t *qtime)
+{
+  return syscall( CG_REAL_TIME, qtime );
+}
+
+void trap_SnapVector( float *v )
+{
+  syscall( CG_SNAPVECTOR, v );
+}
+
+// this returns a handle.  arg0 is the name in the format "idlogo.roq", set arg1 to NULL, alteredstates to qfalse (do not alter gamestate)
+int trap_CIN_PlayCinematic( const char *arg0, int xpos, int ypos, int width, int height, int bits )
+{
+  return syscall(CG_CIN_PLAYCINEMATIC, arg0, xpos, ypos, width, height, bits);
+}
+
+// stops playing the cinematic and ends it.  should always return FMV_EOF
+// cinematics must be stopped in reverse order of when they are started
+e_status trap_CIN_StopCinematic( int handle )
+{
+  return syscall(CG_CIN_STOPCINEMATIC, handle);
+}
+
+
+// will run a frame of the cinematic but will not draw it.  Will return FMV_EOF if the end of the cinematic has been reached.
+e_status trap_CIN_RunCinematic( int handle )
+{
+  return syscall(CG_CIN_RUNCINEMATIC, handle);
+}
+
+
+// draws the current frame
+void trap_CIN_DrawCinematic( int handle )
+{
+  syscall(CG_CIN_DRAWCINEMATIC, handle);
+}
+
+
+// allows you to resize the animation dynamically
+void trap_CIN_SetExtents( int handle, int x, int y, int w, int h )
+{
+  syscall(CG_CIN_SETEXTENTS, handle, x, y, w, h);
+}
+
+int trap_GetDemoState( void )
+{
+  return syscall( CG_GETDEMOSTATE );
+}
+
+int trap_GetDemoPos( void )
+{
+  return syscall( CG_GETDEMOPOS );
+}
+
+void trap_GetDemoName( char *buffer, int size )
+{
+  syscall( CG_GETDEMONAME, buffer, size );
+}
+
+void trap_Key_KeynumToStringBuf( int keynum, char *buf, int buflen ) {
+  syscall( CG_KEY_KEYNUMTOSTRINGBUF, keynum, buf, buflen );
+}
+
+void trap_Key_GetBindingBuf( int keynum, char *buf, int buflen ) {
+  syscall( CG_KEY_GETBINDINGBUF, keynum, buf, buflen );
+}
+
+void trap_Key_SetBinding( int keynum, const char *binding ) {
+  syscall( CG_KEY_SETBINDING, keynum, binding );
+}
+
+void trap_Key_SetOverstrikeMode( qboolean state ) {
+  syscall( CG_KEY_SETOVERSTRIKEMODE, state );
+}
+
+qboolean trap_Key_GetOverstrikeMode( void ) {
+  return syscall( CG_KEY_GETOVERSTRIKEMODE );
+}
diff --git a/src/cgame/cg_trails.c b/src/cgame/cg_trails.c
new file mode 100644
index 0000000..4d16e2a
--- /dev/null
+++ b/src/cgame/cg_trails.c
@@ -0,0 +1,1531 @@
+/*
+===========================================================================
+Copyright (C) 2000-2009 Darklegion Development
+
+This file is part of Tremulous.
+
+Tremulous 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.
+
+Tremulous 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 Tremulous; if not, write to the Free Software
+Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
+===========================================================================
+*/
+
+// cg_trails.c -- the trail system
+
+
+#include "cg_local.h"
+
+static baseTrailSystem_t  baseTrailSystems[ MAX_BASETRAIL_SYSTEMS ];
+static baseTrailBeam_t    baseTrailBeams[ MAX_BASETRAIL_BEAMS ];
+static int                numBaseTrailSystems = 0;
+static int                numBaseTrailBeams = 0;
+
+static trailSystem_t      trailSystems[ MAX_TRAIL_SYSTEMS ];
+static trailBeam_t        trailBeams[ MAX_TRAIL_BEAMS ];
+
+/*
+===============
+CG_CalculateBeamNodeProperties
+
+Fills in trailBeamNode_t.textureCoord
+===============
+*/
+static void CG_CalculateBeamNodeProperties( trailBeam_t *tb )
+{
+  trailBeamNode_t *i = NULL;
+  trailSystem_t   *ts;
+  baseTrailBeam_t *btb;
+  float           nodeDistances[ MAX_TRAIL_BEAM_NODES ];
+  float           totalDistance = 0.0f, position = 0.0f;
+  int             j, numNodes = 0;
+  float           TCRange, widthRange, alphaRange;
+  vec3_t          colorRange;
+  float           fadeAlpha = 1.0f;
+
+  if( !tb || !tb->nodes )
+    return;
+
+  ts = tb->parent;
+  btb = tb->class;
+
+  if( ts->destroyTime > 0 && btb->fadeOutTime )
+  {
+    fadeAlpha -= (float)( cg.time - ts->destroyTime ) / btb->fadeOutTime;
+
+    if( fadeAlpha < 0.0f )
+      fadeAlpha = 0.0f;
+  }
+
+  TCRange = tb->class->backTextureCoord -
+    tb->class->frontTextureCoord;
+  widthRange = tb->class->backWidth -
+    tb->class->frontWidth;
+  alphaRange = tb->class->backAlpha -
+    tb->class->frontAlpha;
+  VectorSubtract( tb->class->backColor,
+      tb->class->frontColor, colorRange );
+
+  for( i = tb->nodes; i && i->next; i = i->next )
+  {
+    nodeDistances[ numNodes++ ] =
+      Distance( i->position, i->next->position );
+  }
+
+  for( j = 0; j < numNodes; j++ )
+    totalDistance += nodeDistances[ j ];
+
+  for( j = 0, i = tb->nodes; i; i = i->next, j++ )
+  {
+    if( tb->class->textureType == TBTT_STRETCH )
+    {
+      i->textureCoord = tb->class->frontTextureCoord +
+        ( ( position / totalDistance ) * TCRange );
+    }
+    else if( tb->class->textureType == TBTT_REPEAT )
+    {
+      if( tb->class->clampToBack )
+        i->textureCoord = ( totalDistance - position ) /
+          tb->class->repeatLength;
+      else
+        i->textureCoord = position / tb->class->repeatLength;
+    }
+
+    i->halfWidth = ( tb->class->frontWidth +
+      ( ( position / totalDistance ) * widthRange ) ) / 2.0f;
+    i->alpha = (byte)( (float)0xFF * ( tb->class->frontAlpha +
+      ( ( position / totalDistance ) * alphaRange ) ) * fadeAlpha );
+    VectorMA( tb->class->frontColor, ( position / totalDistance ),
+        colorRange, i->color );
+
+    position += nodeDistances[ j ];
+  }
+}
+
+/*
+===============
+CG_LightVertex
+
+Lights a particular vertex
+===============
+*/
+static void CG_LightVertex( vec3_t point, byte alpha, byte *rgba )
+{
+  int     i;
+  vec3_t  alight, dlight, lightdir;
+
+  trap_R_LightForPoint( point, alight, dlight, lightdir );
+  for( i = 0; i <= 2; i++ )
+    rgba[ i ] = (int)alight[ i ];
+
+  rgba[ 3 ] = alpha;
+}
+
+/*
+===============
+CG_RenderBeam
+
+Renders a beam
+===============
+*/
+static void CG_RenderBeam( trailBeam_t *tb )
+{
+  trailBeamNode_t   *i = NULL;
+  trailBeamNode_t   *prev = NULL;
+  trailBeamNode_t   *next = NULL;
+  vec3_t            up;
+  polyVert_t        verts[ ( MAX_TRAIL_BEAM_NODES - 1 ) * 4 ];
+  int               numVerts = 0;
+  baseTrailBeam_t   *btb;
+  trailSystem_t     *ts;
+  baseTrailSystem_t *bts;
+
+  if( !tb || !tb->nodes )
+    return;
+
+  btb = tb->class;
+  ts = tb->parent;
+  bts = ts->class;
+
+  if( bts->thirdPersonOnly &&
+      ( CG_AttachmentCentNum( &ts->frontAttachment ) == cg.snap->ps.clientNum ||
+        CG_AttachmentCentNum( &ts->backAttachment ) == cg.snap->ps.clientNum ) &&
+      !cg.renderingThirdPerson )
+    return;
+
+  CG_CalculateBeamNodeProperties( tb );
+
+  i = tb->nodes;
+
+  do
+  {
+    prev = i->prev;
+    next = i->next;
+
+    if( prev && next )
+    {
+      //this node has two neighbours
+      GetPerpendicularViewVector( cg.refdef.vieworg, next->position, prev->position, up );
+    }
+    else if( !prev && next )
+    {
+      //this is the front
+      GetPerpendicularViewVector( cg.refdef.vieworg, next->position, i->position, up );
+    }
+    else if( prev && !next )
+    {
+      //this is the back
+      GetPerpendicularViewVector( cg.refdef.vieworg, i->position, prev->position, up );
+    }
+    else
+      break;
+
+    if( prev )
+    {
+      VectorMA( i->position, i->halfWidth, up, verts[ numVerts ].xyz );
+      verts[ numVerts ].st[ 0 ] = i->textureCoord;
+      verts[ numVerts ].st[ 1 ] = 1.0f;
+
+      if( btb->realLight )
+        CG_LightVertex( verts[ numVerts ].xyz, i->alpha, verts[ numVerts ].modulate );
+      else
+      {
+        VectorCopy( i->color, verts[ numVerts ].modulate );
+        verts[ numVerts ].modulate[ 3 ] = i->alpha;
+      }
+
+      numVerts++;
+
+      VectorMA( i->position, -i->halfWidth, up, verts[ numVerts ].xyz );
+      verts[ numVerts ].st[ 0 ] = i->textureCoord;
+      verts[ numVerts ].st[ 1 ] = 0.0f;
+
+      if( btb->realLight )
+        CG_LightVertex( verts[ numVerts ].xyz, i->alpha, verts[ numVerts ].modulate );
+      else
+      {
+        VectorCopy( i->color, verts[ numVerts ].modulate );
+        verts[ numVerts ].modulate[ 3 ] = i->alpha;
+      }
+
+      numVerts++;
+    }
+
+    if( next )
+    {
+      VectorMA( i->position, -i->halfWidth, up, verts[ numVerts ].xyz );
+      verts[ numVerts ].st[ 0 ] = i->textureCoord;
+      verts[ numVerts ].st[ 1 ] = 0.0f;
+
+      if( btb->realLight )
+        CG_LightVertex( verts[ numVerts ].xyz, i->alpha, verts[ numVerts ].modulate );
+      else
+      {
+        VectorCopy( i->color, verts[ numVerts ].modulate );
+        verts[ numVerts ].modulate[ 3 ] = i->alpha;
+      }
+
+      numVerts++;
+
+      VectorMA( i->position, i->halfWidth, up, verts[ numVerts ].xyz );
+      verts[ numVerts ].st[ 0 ] = i->textureCoord;
+      verts[ numVerts ].st[ 1 ] = 1.0f;
+
+      if( btb->realLight )
+        CG_LightVertex( verts[ numVerts ].xyz, i->alpha, verts[ numVerts ].modulate );
+      else
+      {
+        VectorCopy( i->color, verts[ numVerts ].modulate );
+        verts[ numVerts ].modulate[ 3 ] = i->alpha;
+      }
+
+      numVerts++;
+    }
+
+    i = i->next;
+  } while( i );
+
+  trap_R_AddPolysToScene( tb->class->shader, 4, &verts[ 0 ], numVerts / 4 );
+}
+
+/*
+===============
+CG_AllocateBeamNode
+
+Allocates a trailBeamNode_t from a trailBeam_t's nodePool
+===============
+*/
+static trailBeamNode_t *CG_AllocateBeamNode( trailBeam_t *tb )
+{
+  baseTrailBeam_t *btb = tb->class;
+  int             i;
+  trailBeamNode_t *tbn;
+
+  for( i = 0; i < MAX_TRAIL_BEAM_NODES; i++ )
+  {
+    tbn = &tb->nodePool[ i ];
+    if( !tbn->used )
+    {
+      tbn->timeLeft = btb->segmentTime;
+      tbn->prev = NULL;
+      tbn->next = NULL;
+      tbn->used = qtrue;
+      return tbn;
+    }
+  }
+
+  // no space left
+  return NULL;
+}
+
+/*
+===============
+CG_DestroyBeamNode
+
+Removes a node from a beam
+Returns the new head
+===============
+*/
+static trailBeamNode_t *CG_DestroyBeamNode( trailBeamNode_t *tbn )
+{
+  trailBeamNode_t *newHead = NULL;
+
+  if( tbn->prev )
+  {
+    if( tbn->next )
+    {
+      // node is in the middle
+      tbn->prev->next = tbn->next;
+      tbn->next->prev = tbn->prev;
+    }
+    else // node is at the back
+      tbn->prev->next = NULL;
+
+    // find the new head (shouldn't have changed)
+    newHead = tbn->prev;
+
+    while( newHead->prev )
+      newHead = newHead->prev;
+  }
+  else if( tbn->next )
+  {
+    //node is at the front
+    tbn->next->prev = NULL;
+    newHead = tbn->next;
+  }
+
+  tbn->prev = NULL;
+  tbn->next = NULL;
+  tbn->used = qfalse;
+
+  return newHead;
+}
+
+/*
+===============
+CG_FindLastBeamNode
+
+Returns the last beam node in a beam
+===============
+*/
+static trailBeamNode_t *CG_FindLastBeamNode( trailBeam_t *tb )
+{
+  trailBeamNode_t *i = tb->nodes;
+
+  while( i && i->next )
+    i = i->next;
+
+  return i;
+}
+
+/*
+===============
+CG_CountBeamNodes
+
+Returns the number of nodes in a beam
+===============
+*/
+static int CG_CountBeamNodes( trailBeam_t *tb )
+{
+  trailBeamNode_t *i = tb->nodes;
+  int             numNodes = 0;
+
+  while( i )
+  {
+    numNodes++;
+    i = i->next;
+  }
+
+  return numNodes;
+}
+
+/*
+===============
+CG_PrependBeamNode
+
+Prepend a new beam node to the front of a beam
+Returns the new node
+===============
+*/
+static trailBeamNode_t *CG_PrependBeamNode( trailBeam_t *tb )
+{
+  trailBeamNode_t *i;
+
+  if( tb->nodes )
+  {
+    // prepend another node
+    i = CG_AllocateBeamNode( tb );
+
+    if( i )
+    {
+      i->next = tb->nodes;
+      tb->nodes->prev = i;
+      tb->nodes = i;
+    }
+  }
+  else //add first node
+  {
+    i = CG_AllocateBeamNode( tb );
+
+    if( i )
+      tb->nodes = i;
+  }
+
+  return i;
+}
+
+/*
+===============
+CG_AppendBeamNode
+
+Append a new beam node to the back of a beam
+Returns the new node
+===============
+*/
+static trailBeamNode_t *CG_AppendBeamNode( trailBeam_t *tb )
+{
+  trailBeamNode_t *last, *i;
+
+  if( tb->nodes )
+  {
+    // append another node
+    last = CG_FindLastBeamNode( tb );
+    i = CG_AllocateBeamNode( tb );
+
+    if( i )
+    {
+      last->next = i;
+      i->prev = last;
+      i->next = NULL;
+    }
+  }
+  else //add first node
+  {
+    i = CG_AllocateBeamNode( tb );
+
+    if( i )
+      tb->nodes = i;
+  }
+
+  return i;
+}
+
+/*
+===============
+CG_ApplyJitters
+===============
+*/
+static void CG_ApplyJitters( trailBeam_t *tb )
+{
+  trailBeamNode_t *i = NULL;
+  int             j;
+  baseTrailBeam_t *btb;
+  trailSystem_t   *ts;
+  trailBeamNode_t *start;
+  trailBeamNode_t *end;
+
+  if( !tb || !tb->nodes )
+    return;
+
+  btb = tb->class;
+  ts = tb->parent;
+
+  for( j = 0; j < btb->numJitters; j++ )
+  {
+    if( tb->nextJitterTimes[ j ] <= cg.time )
+    {
+      for( i = tb->nodes; i; i = i->next )
+      {
+        i->jitters[ j ][ 0 ] = ( crandom( ) * btb->jitters[ j ].magnitude );
+        i->jitters[ j ][ 1 ] = ( crandom( ) * btb->jitters[ j ].magnitude );
+      }
+
+      tb->nextJitterTimes[ j ] = cg.time + btb->jitters[ j ].period;
+    }
+  }
+
+  start = tb->nodes;
+  end = CG_FindLastBeamNode( tb );
+
+  if( !btb->jitterAttachments )
+  {
+    if( CG_Attached( &ts->frontAttachment ) && start->next )
+      start = start->next;
+
+    if( CG_Attached( &ts->backAttachment ) && end->prev )
+      end = end->prev;
+  }
+
+  for( i = start; i; i = i->next )
+  {
+    vec3_t          forward, right, up;
+    trailBeamNode_t *prev;
+    trailBeamNode_t *next;
+    float           upJitter = 0.0f, rightJitter = 0.0f;
+
+    prev = i->prev;
+    next = i->next;
+
+    if( prev && next )
+    {
+      //this node has two neighbours
+      GetPerpendicularViewVector( cg.refdef.vieworg, next->position, prev->position, up );
+      VectorSubtract( next->position, prev->position, forward );
+    }
+    else if( !prev && next )
+    {
+      //this is the front
+      GetPerpendicularViewVector( cg.refdef.vieworg, next->position, i->position, up );
+      VectorSubtract( next->position, i->position, forward );
+    }
+    else if( prev && !next )
+    {
+      //this is the back
+      GetPerpendicularViewVector( cg.refdef.vieworg, i->position, prev->position, up );
+      VectorSubtract( i->position, prev->position, forward );
+    }
+
+    VectorNormalize( forward );
+    CrossProduct( forward, up, right );
+    VectorNormalize( right );
+
+    for( j = 0; j < btb->numJitters; j++ )
+    {
+      upJitter += i->jitters[ j ][ 0 ];
+      rightJitter += i->jitters[ j ][ 1 ];
+    }
+
+    VectorMA( i->position, upJitter, up, i->position );
+    VectorMA( i->position, rightJitter, right, i->position );
+
+    if( i == end )
+      break;
+  }
+}
+
+/*
+===============
+CG_UpdateBeam
+
+Updates a beam
+===============
+*/
+static void CG_UpdateBeam( trailBeam_t *tb )
+{
+  baseTrailBeam_t *btb;
+  trailSystem_t   *ts;
+  trailBeamNode_t *i;
+  int             deltaTime;
+  int             nodesToAdd;
+  int             j;
+  int             numNodes;
+
+  if( !tb )
+    return;
+
+  btb = tb->class;
+  ts = tb->parent;
+
+  deltaTime = cg.time - tb->lastEvalTime;
+  tb->lastEvalTime = cg.time;
+
+  // first make sure this beam has enough nodes
+  if( ts->destroyTime <= 0 || btb->fadeOutTime > 0 )
+  {
+    nodesToAdd = btb->numSegments - CG_CountBeamNodes( tb ) + 1;
+
+    while( nodesToAdd-- )
+    {
+      i = CG_AppendBeamNode( tb );
+
+      if( !tb->nodes->next && CG_Attached( &ts->frontAttachment ) )
+      {
+        // this is the first node to be added
+        if( !CG_AttachmentPoint( &ts->frontAttachment, i->refPosition ) )
+          CG_DestroyTrailSystem( &ts );
+      }
+      else
+        VectorCopy( i->prev->refPosition, i->refPosition );
+    }
+  }
+
+  numNodes = CG_CountBeamNodes( tb );
+
+  for( i = tb->nodes; i; i = i->next )
+    VectorCopy( i->refPosition, i->position );
+
+  if( CG_Attached( &ts->frontAttachment ) && CG_Attached( &ts->backAttachment ) )
+  {
+    // beam between two attachments
+    vec3_t dir, front, back;
+
+    if( ts->destroyTime > 0 && ( cg.time - ts->destroyTime ) >= btb->fadeOutTime )
+    {
+      tb->valid = qfalse;
+      return;
+    }
+
+    if( !CG_AttachmentPoint( &ts->frontAttachment, front ) )
+      CG_DestroyTrailSystem( &ts );
+
+    if( !CG_AttachmentPoint( &ts->backAttachment, back ) )
+      CG_DestroyTrailSystem( &ts );
+
+    VectorSubtract( back, front, dir );
+
+    for( j = 0, i = tb->nodes; i; i = i->next, j++ )
+    {
+      float scale = (float)j / (float)( numNodes - 1 );
+
+      VectorMA( front, scale, dir, i->position );
+    }
+  }
+  else if( CG_Attached( &ts->frontAttachment ) )
+  {
+    // beam from one attachment
+
+    // cull the trail tail
+    i = CG_FindLastBeamNode( tb );
+
+    if( i && i->timeLeft >= 0 )
+    {
+      i->timeLeft -= deltaTime;
+
+      if( i->timeLeft < 0 )
+      {
+        tb->nodes = CG_DestroyBeamNode( i );
+
+        if( !tb->nodes )
+        {
+          tb->valid = qfalse;
+          return;
+        }
+
+        // if the ts has been destroyed, stop creating new nodes
+        if( ts->destroyTime <= 0 )
+          CG_PrependBeamNode( tb );
+      }
+      else if( i->timeLeft >= 0 && i->prev )
+      {
+        vec3_t  dir;
+        float   length;
+
+        VectorSubtract( i->refPosition, i->prev->refPosition, dir );
+        length = VectorNormalize( dir ) *
+          ( (float)i->timeLeft / (float)tb->class->segmentTime );
+
+        VectorMA( i->prev->refPosition, length, dir, i->position );
+      }
+    }
+
+    if( tb->nodes )
+    {
+      if( !CG_AttachmentPoint( &ts->frontAttachment, tb->nodes->refPosition ) )
+        CG_DestroyTrailSystem( &ts );
+
+      VectorCopy( tb->nodes->refPosition, tb->nodes->position );
+    }
+  }
+
+  CG_ApplyJitters( tb );
+}
+
+/*
+===============
+CG_ParseTrailBeamColor
+===============
+*/
+static qboolean CG_ParseTrailBeamColor( byte *c, char **text_p )
+{
+  char  *token;
+  int   i;
+
+  for( i = 0; i <= 2; i++ )
+  {
+    token = COM_Parse( text_p );
+
+    if( !Q_stricmp( token, "" ) )
+      return qfalse;
+
+    c[ i ] = (int)( (float)0xFF * atof_neg( token, qfalse ) );
+  }
+
+  return qtrue;
+}
+
+/*
+===============
+CG_ParseTrailBeam
+
+Parse a trail beam
+===============
+*/
+static qboolean CG_ParseTrailBeam( baseTrailBeam_t *btb, char **text_p )
+{
+  char  *token;
+
+  // read optional parameters
+  while( 1 )
+  {
+    token = COM_Parse( text_p );
+
+    if( !Q_stricmp( token, "" ) )
+      return qfalse;
+
+    if( !Q_stricmp( token, "segments" ) )
+    {
+      token = COM_Parse( text_p );
+      if( !Q_stricmp( token, "" ) )
+        break;
+
+      btb->numSegments = atoi_neg( token, qfalse );
+
+      if( btb->numSegments >= MAX_TRAIL_BEAM_NODES )
+      {
+        btb->numSegments = MAX_TRAIL_BEAM_NODES - 1;
+        CG_Printf( S_COLOR_YELLOW "WARNING: too many segments in trail beam\n" );
+      }
+      continue;
+    }
+    else if( !Q_stricmp( token, "width" ) )
+    {
+      token = COM_Parse( text_p );
+      if( !Q_stricmp( token, "" ) )
+        break;
+
+      btb->frontWidth = atof_neg( token, qfalse );
+
+      token = COM_Parse( text_p );
+      if( !Q_stricmp( token, "" ) )
+        break;
+
+      if( !Q_stricmp( token, "-" ) )
+        btb->backWidth = btb->frontWidth;
+      else
+        btb->backWidth = atof_neg( token, qfalse );
+      continue;
+    }
+    else if( !Q_stricmp( token, "alpha" ) )
+    {
+      token = COM_Parse( text_p );
+      if( !Q_stricmp( token, "" ) )
+        break;
+
+      btb->frontAlpha = atof_neg( token, qfalse );
+
+      token = COM_Parse( text_p );
+      if( !Q_stricmp( token, "" ) )
+        break;
+
+      if( !Q_stricmp( token, "-" ) )
+        btb->backAlpha = btb->frontAlpha;
+      else
+        btb->backAlpha = atof_neg( token, qfalse );
+      continue;
+    }
+    else if( !Q_stricmp( token, "color" ) )
+    {
+      token = COM_Parse( text_p );
+      if( !Q_stricmp( token, "" ) )
+        break;
+
+      if( !Q_stricmp( token, "{" ) )
+      {
+        if( !CG_ParseTrailBeamColor( btb->frontColor, text_p ) )
+          break;
+
+        token = COM_Parse( text_p );
+        if( Q_stricmp( token, "}" ) )
+        {
+          CG_Printf( S_COLOR_RED "ERROR: missing '}'\n" );
+          break;
+        }
+
+        token = COM_Parse( text_p );
+        if( !Q_stricmp( token, "" ) )
+          break;
+
+        if( !Q_stricmp( token, "-" ) )
+        {
+          btb->backColor[ 0 ] = btb->frontColor[ 0 ];
+          btb->backColor[ 1 ] = btb->frontColor[ 1 ];
+          btb->backColor[ 2 ] = btb->frontColor[ 2 ];
+        }
+        else if( !Q_stricmp( token, "{" ) )
+        {
+          if( !CG_ParseTrailBeamColor( btb->backColor, text_p ) )
+            break;
+
+          token = COM_Parse( text_p );
+          if( Q_stricmp( token, "}" ) )
+          {
+            CG_Printf( S_COLOR_RED "ERROR: missing '}'\n" );
+            break;
+          }
+        }
+        else
+        {
+          CG_Printf( S_COLOR_RED "ERROR: missing '{'\n" );
+          break;
+        }
+      }
+      else
+      {
+        CG_Printf( S_COLOR_RED "ERROR: missing '{'\n" );
+        break;
+      }
+
+      continue;
+    }
+    else if( !Q_stricmp( token, "segmentTime" ) )
+    {
+      token = COM_Parse( text_p );
+      if( !Q_stricmp( token, "" ) )
+        break;
+
+      btb->segmentTime = atoi_neg( token, qfalse );
+      continue;
+    }
+    else if( !Q_stricmp( token, "fadeOutTime" ) )
+    {
+      token = COM_Parse( text_p );
+      if( !Q_stricmp( token, "" ) )
+        break;
+
+      btb->fadeOutTime = atoi_neg( token, qfalse );
+      continue;
+    }
+    else if( !Q_stricmp( token, "shader" ) )
+    {
+      token = COM_Parse( text_p );
+      if( !Q_stricmp( token, "" ) )
+        break;
+
+      Q_strncpyz( btb->shaderName, token, MAX_QPATH );
+
+      continue;
+    }
+    else if( !Q_stricmp( token, "textureType" ) )
+    {
+      token = COM_Parse( text_p );
+      if( !Q_stricmp( token, "" ) )
+        break;
+
+      if( !Q_stricmp( token, "stretch" ) )
+      {
+        btb->textureType = TBTT_STRETCH;
+
+        token = COM_Parse( text_p );
+        if( !Q_stricmp( token, "" ) )
+          break;
+
+        btb->frontTextureCoord = atof_neg( token, qfalse );
+
+        token = COM_Parse( text_p );
+        if( !Q_stricmp( token, "" ) )
+          break;
+
+        btb->backTextureCoord = atof_neg( token, qfalse );
+      }
+      else if( !Q_stricmp( token, "repeat" ) )
+      {
+        btb->textureType = TBTT_REPEAT;
+
+        token = COM_Parse( text_p );
+        if( !Q_stricmp( token, "" ) )
+          break;
+
+        if( !Q_stricmp( token, "front" ) )
+          btb->clampToBack = qfalse;
+        else if( !Q_stricmp( token, "back" ) )
+          btb->clampToBack = qtrue;
+        else
+        {
+          CG_Printf( S_COLOR_RED "ERROR: unknown textureType clamp \"%s\"\n", token );
+          break;
+        }
+
+        token = COM_Parse( text_p );
+        if( !Q_stricmp( token, "" ) )
+          break;
+
+        btb->repeatLength = atof_neg( token, qfalse );
+      }
+      else
+      {
+        CG_Printf( S_COLOR_RED "ERROR: unknown textureType \"%s\"\n", token );
+        break;
+      }
+
+      continue;
+    }
+    else if( !Q_stricmp( token, "realLight" ) )
+    {
+      btb->realLight = qtrue;
+
+      continue;
+    }
+    else if( !Q_stricmp( token, "jitter" ) )
+    {
+      if( btb->numJitters == MAX_TRAIL_BEAM_JITTERS )
+      {
+        CG_Printf( S_COLOR_RED "ERROR: too many jitters\n", token );
+        break;
+      }
+
+      token = COM_Parse( text_p );
+      if( !Q_stricmp( token, "" ) )
+        break;
+
+      btb->jitters[ btb->numJitters ].magnitude = atof_neg( token, qfalse );
+
+      token = COM_Parse( text_p );
+      if( !Q_stricmp( token, "" ) )
+        break;
+
+      btb->jitters[ btb->numJitters ].period = atoi_neg( token, qfalse );
+
+      btb->numJitters++;
+
+      continue;
+    }
+    else if( !Q_stricmp( token, "jitterAttachments" ) )
+    {
+      btb->jitterAttachments = qtrue;
+
+      continue;
+    }
+    else if( !Q_stricmp( token, "}" ) )
+      return qtrue; //reached the end of this trail beam
+    else
+    {
+      CG_Printf( S_COLOR_RED "ERROR: unknown token '%s' in trail beam\n", token );
+      return qfalse;
+    }
+  }
+
+  return qfalse;
+}
+
+/*
+===============
+CG_InitialiseBaseTrailBeam
+===============
+*/
+static void CG_InitialiseBaseTrailBeam( baseTrailBeam_t *btb )
+{
+  memset( btb, 0, sizeof( baseTrailBeam_t ) );
+
+  btb->numSegments = 1;
+  btb->frontWidth = btb->backWidth = 1.0f;
+  btb->frontAlpha = btb->backAlpha = 1.0f;
+  memset( btb->frontColor, 0xFF, sizeof( btb->frontColor ) );
+  memset( btb->backColor, 0xFF, sizeof( btb->backColor ) );
+
+  btb->segmentTime = 100;
+
+  btb->textureType = TBTT_STRETCH;
+  btb->frontTextureCoord = 0.0f;
+  btb->backTextureCoord = 1.0f;
+}
+
+/*
+===============
+CG_ParseTrailSystem
+
+Parse a trail system section
+===============
+*/
+static qboolean CG_ParseTrailSystem( baseTrailSystem_t *bts, char **text_p, const char *name )
+{
+  char *token;
+
+  // read optional parameters
+  while( 1 )
+  {
+    token = COM_Parse( text_p );
+
+    if( !Q_stricmp( token, "" ) )
+      return qfalse;
+
+    if( !Q_stricmp( token, "{" ) )
+    {
+      CG_InitialiseBaseTrailBeam( &baseTrailBeams[ numBaseTrailBeams ] );
+
+      if( !CG_ParseTrailBeam( &baseTrailBeams[ numBaseTrailBeams ], text_p ) )
+      {
+        CG_Printf( S_COLOR_RED "ERROR: failed to parse trail beam\n" );
+        return qfalse;
+      }
+
+      if( bts->numBeams == MAX_BEAMS_PER_SYSTEM )
+      {
+        CG_Printf( S_COLOR_RED "ERROR: trail system has > %d beams\n", MAX_BEAMS_PER_SYSTEM );
+        return qfalse;
+      }
+      else if( numBaseTrailBeams == MAX_BASETRAIL_BEAMS )
+      {
+        CG_Printf( S_COLOR_RED "ERROR: maximum number of trail beams (%d) reached\n",
+            MAX_BASETRAIL_BEAMS );
+        return qfalse;
+      }
+      else
+      {
+        //start parsing beams again
+        bts->beams[ bts->numBeams ] = &baseTrailBeams[ numBaseTrailBeams ];
+        bts->numBeams++;
+        numBaseTrailBeams++;
+      }
+      continue;
+    }
+    else if( !Q_stricmp( token, "thirdPersonOnly" ) )
+      bts->thirdPersonOnly = qtrue;
+    else if( !Q_stricmp( token, "lifeTime" ) )
+    {
+      token = COM_Parse( text_p );
+      if( !Q_stricmp( token, "" ) )
+        break;
+
+      bts->lifeTime = atoi_neg( token, qfalse );
+      continue;
+    }
+    else if( !Q_stricmp( token, "beam" ) ) //acceptable text
+      continue;
+    else if( !Q_stricmp( token, "}" ) )
+    {
+      if( cg_debugTrails.integer >= 1 )
+        CG_Printf( "Parsed trail system %s\n", name );
+
+      return qtrue; //reached the end of this trail system
+    }
+    else
+    {
+      CG_Printf( S_COLOR_RED "ERROR: unknown token '%s' in trail system %s\n", token, bts->name );
+      return qfalse;
+    }
+  }
+
+  return qfalse;
+}
+
+/*
+===============
+CG_ParseTrailFile
+
+Load the trail systems from a trail file
+===============
+*/
+static qboolean CG_ParseTrailFile( const char *fileName )
+{
+  char          *text_p;
+  int           i;
+  int           len;
+  char          *token;
+  char          text[ 32000 ];
+  char          tsName[ MAX_QPATH ];
+  qboolean      tsNameSet = qfalse;
+  fileHandle_t  f;
+
+  // load the file
+  len = trap_FS_FOpenFile( fileName, &f, FS_READ );
+  if( len <= 0 )
+    return qfalse;
+
+  if( len == 0 || len >= sizeof( text ) - 1 )
+  {
+    trap_FS_FCloseFile( f );
+    CG_Printf( S_COLOR_RED "ERROR: trail file %s is %s\n", fileName,
+      len == 0 ? "empty" : "too long" );
+    return qfalse;
+  }
+
+  trap_FS_Read( text, len, f );
+  text[ len ] = 0;
+  trap_FS_FCloseFile( f );
+
+  // parse the text
+  text_p = text;
+
+  // read optional parameters
+  while( 1 )
+  {
+    token = COM_Parse( &text_p );
+
+    if( !Q_stricmp( token, "" ) )
+      break;
+
+    if( !Q_stricmp( token, "{" ) )
+    {
+      if( tsNameSet )
+      {
+        //check for name space clashes
+        for( i = 0; i < numBaseTrailSystems; i++ )
+        {
+          if( !Q_stricmp( baseTrailSystems[ i ].name, tsName ) )
+          {
+            CG_Printf( S_COLOR_RED "ERROR: a trail system is already named %s\n", tsName );
+            return qfalse;
+          }
+        }
+
+        Q_strncpyz( baseTrailSystems[ numBaseTrailSystems ].name, tsName, MAX_QPATH );
+
+        if( !CG_ParseTrailSystem( &baseTrailSystems[ numBaseTrailSystems ], &text_p, tsName ) )
+        {
+          CG_Printf( S_COLOR_RED "ERROR: %s: failed to parse trail system %s\n", fileName, tsName );
+          return qfalse;
+        }
+
+        //start parsing trail systems again
+        tsNameSet = qfalse;
+
+        if( numBaseTrailSystems == MAX_BASETRAIL_SYSTEMS )
+        {
+          CG_Printf( S_COLOR_RED "ERROR: maximum number of trail systems (%d) reached\n",
+              MAX_BASETRAIL_SYSTEMS );
+          return qfalse;
+        }
+        else
+          numBaseTrailSystems++;
+
+        continue;
+      }
+      else
+      {
+        CG_Printf( S_COLOR_RED "ERROR: unamed trail system\n" );
+        return qfalse;
+      }
+    }
+
+    if( !tsNameSet )
+    {
+      Q_strncpyz( tsName, token, sizeof( tsName ) );
+      tsNameSet = qtrue;
+    }
+    else
+    {
+      CG_Printf( S_COLOR_RED "ERROR: trail system already named\n" );
+      return qfalse;
+    }
+  }
+
+  return qtrue;
+}
+
+/*
+===============
+CG_LoadTrailSystems
+
+Load trail system templates
+===============
+*/
+void CG_LoadTrailSystems( void )
+{
+  int   i, numFiles, fileLen;
+  char  fileList[ MAX_TRAIL_FILES * MAX_QPATH ];
+  char  fileName[ MAX_QPATH ];
+  char  *filePtr;
+
+  //clear out the old
+  numBaseTrailSystems = 0;
+  numBaseTrailBeams = 0;
+
+  for( i = 0; i < MAX_BASETRAIL_SYSTEMS; i++ )
+  {
+    baseTrailSystem_t  *bts = &baseTrailSystems[ i ];
+    memset( bts, 0, sizeof( baseTrailSystem_t ) );
+  }
+
+  for( i = 0; i < MAX_BASETRAIL_BEAMS; i++ )
+  {
+    baseTrailBeam_t  *btb = &baseTrailBeams[ i ];
+    memset( btb, 0, sizeof( baseTrailBeam_t ) );
+  }
+
+  //and bring in the new
+  numFiles = trap_FS_GetFileList( "scripts", ".trail",
+      fileList, MAX_TRAIL_FILES * MAX_QPATH );
+  filePtr = fileList;
+
+  for( i = 0; i < numFiles; i++, filePtr += fileLen + 1 )
+  {
+    fileLen = strlen( filePtr );
+    strcpy( fileName, "scripts/" );
+    strcat( fileName, filePtr );
+    //CG_Printf( "...loading '%s'\n", fileName );
+    CG_ParseTrailFile( fileName );
+  }
+}
+
+/*
+===============
+CG_RegisterTrailSystem
+
+Load the media that a trail system needs
+===============
+*/
+qhandle_t CG_RegisterTrailSystem( char *name )
+{
+  int               i, j;
+  baseTrailSystem_t *bts;
+  baseTrailBeam_t   *btb;
+
+  for( i = 0; i < MAX_BASETRAIL_SYSTEMS; i++ )
+  {
+    bts = &baseTrailSystems[ i ];
+
+    if( !Q_stricmp( bts->name, name ) )
+    {
+      //already registered
+      if( bts->registered )
+        return i + 1;
+
+      for( j = 0; j < bts->numBeams; j++ )
+      {
+        btb = bts->beams[ j ];
+
+        btb->shader = trap_R_RegisterShader( btb->shaderName );
+      }
+
+      if( cg_debugTrails.integer >= 1 )
+        CG_Printf( "Registered trail system %s\n", name );
+
+      bts->registered = qtrue;
+
+      //avoid returning 0
+      return i + 1;
+    }
+  }
+
+  CG_Printf( S_COLOR_RED "ERROR: failed to register trail system %s\n", name );
+  return 0;
+}
+
+
+/*
+===============
+CG_SpawnNewTrailBeam
+
+Allocate a new trail beam
+===============
+*/
+static trailBeam_t *CG_SpawnNewTrailBeam( baseTrailBeam_t *btb,
+    trailSystem_t *parent )
+{
+  int           i;
+  trailBeam_t   *tb = NULL;
+  trailSystem_t *ts = parent;
+
+  for( i = 0; i < MAX_TRAIL_BEAMS; i++ )
+  {
+    tb = &trailBeams[ i ];
+
+    if( !tb->valid )
+    {
+      memset( tb, 0, sizeof( trailBeam_t ) );
+
+      //found a free slot
+      tb->class = btb;
+      tb->parent = ts;
+
+      tb->valid = qtrue;
+
+      if( cg_debugTrails.integer >= 1 )
+        CG_Printf( "TB %s created\n", ts->class->name );
+
+      return tb;
+    }
+  }
+
+  if( cg_debugTrails.integer >= 1 )
+    CG_Printf( "MAX_TRAIL_BEAMS\n" );
+
+  return NULL;
+}
+
+
+/*
+===============
+CG_SpawnNewTrailSystem
+
+Spawns a new trail system
+===============
+*/
+trailSystem_t *CG_SpawnNewTrailSystem( qhandle_t psHandle )
+{
+  int               i, j;
+  trailSystem_t     *ts = NULL;
+  baseTrailSystem_t *bts = &baseTrailSystems[ psHandle - 1 ];
+
+  if( !bts->registered )
+  {
+    CG_Printf( S_COLOR_RED "ERROR: a trail system has not been registered yet\n" );
+    return NULL;
+  }
+
+  for( i = 0; i < MAX_TRAIL_SYSTEMS; i++ )
+  {
+    ts = &trailSystems[ i ];
+
+    if( !ts->valid )
+    {
+      memset( ts, 0, sizeof( trailSystem_t ) );
+
+      //found a free slot
+      ts->class = bts;
+
+      ts->valid = qtrue;
+      ts->destroyTime = -1;
+      ts->birthTime = cg.time;
+      
+      for( j = 0; j < bts->numBeams; j++ )
+        CG_SpawnNewTrailBeam( bts->beams[ j ], ts );
+
+      if( cg_debugTrails.integer >= 1 )
+        CG_Printf( "TS %s created\n", bts->name );
+
+      return ts;
+    }
+  }
+
+  if( cg_debugTrails.integer >= 1 )
+    CG_Printf( "MAX_TRAIL_SYSTEMS\n" );
+
+  return NULL;
+}
+
+/*
+===============
+CG_DestroyTrailSystem
+
+Destroy a trail system
+===============
+*/
+void CG_DestroyTrailSystem( trailSystem_t **ts )
+{
+  (*ts)->destroyTime = cg.time;
+
+  if( CG_Attached( &(*ts)->frontAttachment ) &&
+      !CG_Attached( &(*ts)->backAttachment ) )
+  {
+    vec3_t v;
+
+    // attach the trail head to a static point
+    CG_AttachmentPoint( &(*ts)->frontAttachment, v );
+    CG_SetAttachmentPoint( &(*ts)->frontAttachment, v );
+    CG_AttachToPoint( &(*ts)->frontAttachment );
+
+    (*ts)->frontAttachment.centValid = qfalse; // a bit naughty
+  }
+
+  ts = NULL;
+}
+
+/*
+===============
+CG_IsTrailSystemValid
+
+Test a trail system for validity
+===============
+*/
+qboolean CG_IsTrailSystemValid( trailSystem_t **ts )
+{
+  if( *ts == NULL || ( *ts && !(*ts)->valid ) )
+  {
+    if( *ts && !(*ts)->valid )
+      *ts = NULL;
+
+    return qfalse;
+  }
+
+  return qtrue;
+}
+
+/*
+===============
+CG_GarbageCollectTrailSystems
+
+Destroy inactive trail systems
+===============
+*/
+static void CG_GarbageCollectTrailSystems( void )
+{
+  int           i, j, count;
+  trailSystem_t *ts;
+  trailBeam_t   *tb;
+  int           centNum;
+
+  for( i = 0; i < MAX_TRAIL_SYSTEMS; i++ )
+  {
+    ts = &trailSystems[ i ];
+    count = 0;
+
+    //don't bother checking already invalid systems
+    if( !ts->valid )
+      continue;
+
+    for( j = 0; j < MAX_TRAIL_BEAMS; j++ )
+    {
+      tb = &trailBeams[ j ];
+
+      if( tb->valid && tb->parent == ts )
+        count++;
+    }
+
+    if( !count )
+      ts->valid = qfalse;
+
+    //check systems where the parent cent has left the PVS
+    //( local player entity is always valid )
+    if( ( centNum = CG_AttachmentCentNum( &ts->frontAttachment ) ) >= 0 &&
+        centNum != cg.snap->ps.clientNum )
+    {
+      trailSystem_t *tempTS = ts;
+
+      if( !cg_entities[ centNum ].valid )
+        CG_DestroyTrailSystem( &tempTS );
+    }
+
+    if( ( centNum = CG_AttachmentCentNum( &ts->backAttachment ) ) >= 0 &&
+        centNum != cg.snap->ps.clientNum )
+    {
+      trailSystem_t *tempTS = ts;
+
+      if( !cg_entities[ centNum ].valid )
+        CG_DestroyTrailSystem( &tempTS );
+    }
+
+    // lifetime expired
+    if( ts->destroyTime <= 0 && ts->class->lifeTime &&
+        ts->birthTime + ts->class->lifeTime < cg.time )
+    {
+      trailSystem_t *tempTS = ts;
+
+      CG_DestroyTrailSystem( &tempTS );
+      if( cg_debugTrails.integer >= 1 )
+        CG_Printf( "TS %s expired (born %d, lives %d, now %d)\n",
+                   ts->class->name, ts->birthTime, ts->class->lifeTime,
+                   cg.time );
+    }
+
+    if( cg_debugTrails.integer >= 1 && !ts->valid )
+      CG_Printf( "TS %s garbage collected\n", ts->class->name );
+  }
+}
+
+/*
+===============
+CG_AddTrails
+
+Add trails to the scene
+===============
+*/
+void CG_AddTrails( void )
+{
+  int           i;
+  trailBeam_t   *tb;
+  int           numTS = 0, numTB = 0;
+
+  //remove expired trail systems
+  CG_GarbageCollectTrailSystems( );
+
+  for( i = 0; i < MAX_TRAIL_BEAMS; i++ )
+  {
+    tb = &trailBeams[ i ];
+
+    if( tb->valid )
+    {
+      CG_UpdateBeam( tb );
+      CG_RenderBeam( tb );
+    }
+  }
+
+  if( cg_debugTrails.integer >= 2 )
+  {
+    for( i = 0; i < MAX_TRAIL_SYSTEMS; i++ )
+      if( trailSystems[ i ].valid )
+        numTS++;
+
+    for( i = 0; i < MAX_TRAIL_BEAMS; i++ )
+      if( trailBeams[ i ].valid )
+        numTB++;
+
+    CG_Printf( "TS: %d  TB: %d\n", numTS, numTB );
+  }
+}
+
+static trailSystem_t  *testTS;
+static qhandle_t      testTSHandle;
+
+/*
+===============
+CG_DestroyTestTS_f
+
+Destroy the test a trail system
+===============
+*/
+void CG_DestroyTestTS_f( void )
+{
+  if( CG_IsTrailSystemValid( &testTS ) )
+    CG_DestroyTrailSystem( &testTS );
+}
+
+/*
+===============
+CG_TestTS_f
+
+Test a trail system
+===============
+*/
+void CG_TestTS_f( void )
+{
+  char tsName[ MAX_QPATH ];
+
+  if( trap_Argc( ) < 2 )
+    return;
+
+  Q_strncpyz( tsName, CG_Argv( 1 ), MAX_QPATH );
+  testTSHandle = CG_RegisterTrailSystem( tsName );
+
+  if( testTSHandle )
+  {
+    CG_DestroyTestTS_f( );
+
+    testTS = CG_SpawnNewTrailSystem( testTSHandle );
+
+    if( CG_IsTrailSystemValid( &testTS ) )
+    {
+      CG_SetAttachmentCent( &testTS->frontAttachment, &cg_entities[ 0 ] );
+      CG_AttachToCent( &testTS->frontAttachment );
+    }
+  }
+}
diff --git a/src/cgame/cg_tutorial.c b/src/cgame/cg_tutorial.c
new file mode 100644
index 0000000..6839c3a
--- /dev/null
+++ b/src/cgame/cg_tutorial.c
@@ -0,0 +1,731 @@
+/*
+===========================================================================
+Copyright (C) 2000-2009 Darklegion Development
+
+This file is part of Tremulous.
+
+Tremulous 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.
+
+Tremulous 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 Tremulous; if not, write to the Free Software
+Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
+===========================================================================
+*/
+
+// cg_tutorial.c -- the tutorial system
+
+#include "cg_local.h"
+
+typedef struct
+{
+  char      *command;
+  char      *humanName;
+  keyNum_t  keys[ 2 ];
+} bind_t;
+
+static bind_t bindings[ ] =
+{
+  { "+button2",       "Activate Upgrade",       { -1, -1 } },
+  { "+speed",         "Run/Walk",               { -1, -1 } },
+  { "+button6",       "Dodge",                  { -1, -1 } },
+  { "+button8",       "Sprint",                 { -1, -1 } },
+  { "+moveup",        "Jump",                   { -1, -1 } },
+  { "+movedown",      "Crouch",                 { -1, -1 } },
+  { "+attack",        "Primary Attack",         { -1, -1 } },
+  { "+button5",       "Secondary Attack",       { -1, -1 } },
+  { "reload",         "Reload",                 { -1, -1 } },
+  { "buy ammo",       "Buy Ammo",               { -1, -1 } },
+  { "itemact medkit", "Use Medkit",             { -1, -1 } },
+  { "+button7",       "Use Structure/Evolve",   { -1, -1 } },
+  { "deconstruct",    "Deconstruct Structure",  { -1, -1 } },
+  { "weapprev",       "Previous Upgrade",       { -1, -1 } },
+  { "weapnext",       "Next Upgrade",           { -1, -1 } }
+};
+
+static const int numBindings = sizeof( bindings ) / sizeof( bind_t );
+
+/*
+=================
+CG_GetBindings
+=================
+*/
+static void CG_GetBindings( void )
+{
+  int   i, j, numKeys;
+  char  buffer[ MAX_STRING_CHARS ];
+
+  for( i = 0; i < numBindings; i++ )
+  {
+    bindings[ i ].keys[ 0 ] = bindings[ i ].keys[ 1 ] = K_NONE;
+    numKeys = 0;
+
+    for( j = 0; j < K_LAST_KEY; j++ )
+    {
+      trap_Key_GetBindingBuf( j, buffer, MAX_STRING_CHARS );
+
+      if( buffer[ 0 ] == 0 )
+        continue;
+
+      if( !Q_stricmp( buffer, bindings[ i ].command ) )
+      {
+        bindings[ i ].keys[ numKeys++ ] = j;
+
+        if( numKeys > 1 )
+          break;
+      }
+    }
+  }
+}
+
+/*
+===============
+CG_KeyNameForCommand
+===============
+*/
+static const char *CG_KeyNameForCommand( const char *command )
+{
+  int         i, j;
+  static char buffer[ MAX_STRING_CHARS ];
+  int         firstKeyLength;
+
+  buffer[ 0 ] = '\0';
+
+  for( i = 0; i < numBindings; i++ )
+  {
+    if( !Q_stricmp( command, bindings[ i ].command ) )
+    {
+      if( bindings[ i ].keys[ 0 ] != K_NONE )
+      {
+        trap_Key_KeynumToStringBuf( bindings[ i ].keys[ 0 ],
+            buffer, MAX_STRING_CHARS );
+        firstKeyLength = strlen( buffer );
+
+        for( j = 0; j < firstKeyLength; j++ )
+          buffer[ j ] = toupper( buffer[ j ] );
+
+        if( bindings[ i ].keys[ 1 ] != K_NONE )
+        {
+          Q_strcat( buffer, MAX_STRING_CHARS, " or " );
+          trap_Key_KeynumToStringBuf( bindings[ i ].keys[ 1 ],
+              buffer + strlen( buffer ), MAX_STRING_CHARS - strlen( buffer ) );
+
+          for( j = firstKeyLength + 4; j < strlen( buffer ); j++ )
+            buffer[ j ] = toupper( buffer[ j ] );
+        }
+      }
+      else
+      {
+        Com_sprintf( buffer, MAX_STRING_CHARS, "\"%s\" (unbound)",
+          bindings[ i ].humanName );
+      }
+
+      return buffer;
+    }
+  }
+
+  return "";
+}
+
+#define MAX_TUTORIAL_TEXT 4096
+
+/*
+===============
+CG_BuildableInRange
+===============
+*/
+static entityState_t *CG_BuildableInRange( playerState_t *ps, float *healthFraction )
+{
+  vec3_t        view, point;
+  trace_t       trace;
+  entityState_t *es;
+  int           health;
+
+  AngleVectors( cg.refdefViewAngles, view, NULL, NULL );
+  VectorMA( cg.refdef.vieworg, 64, view, point );
+  CG_Trace( &trace, cg.refdef.vieworg, NULL, NULL,
+            point, ps->clientNum, MASK_SHOT );
+
+  es = &cg_entities[ trace.entityNum ].currentState;
+
+  if( healthFraction )
+  {
+    health = es->generic1;
+    *healthFraction = (float)health / BG_Buildable( es->modelindex )->health;
+  }
+
+  if( es->eType == ET_BUILDABLE &&
+      ps->stats[ STAT_TEAM ] == BG_Buildable( es->modelindex )->team )
+    return es;
+  else
+    return NULL;
+}
+
+/*
+===============
+CG_AlienBuilderText
+===============
+*/
+static void CG_AlienBuilderText( char *text, playerState_t *ps )
+{
+  buildable_t   buildable = ps->stats[ STAT_BUILDABLE ] & ~SB_VALID_TOGGLEBIT;
+  entityState_t *es;
+
+  if( buildable > BA_NONE )
+  {
+    Q_strcat( text, MAX_TUTORIAL_TEXT,
+        va( "Press %s to place the %s\n",
+          CG_KeyNameForCommand( "+attack" ),
+          BG_Buildable( buildable )->humanName ) );
+
+    Q_strcat( text, MAX_TUTORIAL_TEXT,
+        va( "Press %s to cancel placing the %s\n",
+          CG_KeyNameForCommand( "+button5" ),
+          BG_Buildable( buildable )->humanName ) );
+  }
+  else
+  {
+    Q_strcat( text, MAX_TUTORIAL_TEXT,
+        va( "Press %s to build a structure\n",
+          CG_KeyNameForCommand( "+attack" ) ) );
+  }
+
+  if( ( es = CG_BuildableInRange( ps, NULL ) ) )
+  {
+    if( cgs.markDeconstruct )
+    {
+      if( es->eFlags & EF_B_MARKED )
+      {
+        Q_strcat( text, MAX_TUTORIAL_TEXT,
+            va( "Press %s to unmark this structure for replacement\n",
+              CG_KeyNameForCommand( "deconstruct" ) ) );
+      }
+      else
+      {
+        Q_strcat( text, MAX_TUTORIAL_TEXT,
+            va( "Press %s to mark this structure for replacement\n",
+              CG_KeyNameForCommand( "deconstruct" ) ) );
+      }
+    }
+    else
+    {
+      Q_strcat( text, MAX_TUTORIAL_TEXT,
+          va( "Press %s to destroy this structure\n",
+            CG_KeyNameForCommand( "deconstruct" ) ) );
+    }
+  }
+
+  if( ( ps->stats[ STAT_BUILDABLE ] & ~SB_VALID_TOGGLEBIT ) == BA_NONE )
+  {
+    Q_strcat( text, MAX_TUTORIAL_TEXT,
+        va( "Press %s to swipe\n",
+          CG_KeyNameForCommand( "+button5" ) ) );
+  }
+
+  if( ps->stats[ STAT_CLASS ] == PCL_ALIEN_BUILDER0_UPG )
+  {
+    Q_strcat( text, MAX_TUTORIAL_TEXT,
+        va( "Press %s to launch a projectile\n",
+        CG_KeyNameForCommand( "+button2" ) ) );
+
+    Q_strcat( text, MAX_TUTORIAL_TEXT,
+        va( "Press %s to walk on walls\n",
+        CG_KeyNameForCommand( "+movedown" ) ) );
+  }
+}
+
+/*
+===============
+CG_AlienLevel0Text
+===============
+*/
+static void CG_AlienLevel0Text( char *text, playerState_t *ps )
+{
+  Q_strcat( text, MAX_TUTORIAL_TEXT,
+      "Touch humans to damage them\n" );
+
+  Q_strcat( text, MAX_TUTORIAL_TEXT,
+      va( "Press %s to walk on walls\n",
+        CG_KeyNameForCommand( "+movedown" ) ) );
+}
+
+/*
+===============
+CG_AlienLevel1Text
+===============
+*/
+static void CG_AlienLevel1Text( char *text, playerState_t *ps )
+{
+  Q_strcat( text, MAX_TUTORIAL_TEXT,
+      "Touch humans to grab them\n" );
+
+  Q_strcat( text, MAX_TUTORIAL_TEXT,
+      va( "Press %s to swipe\n",
+        CG_KeyNameForCommand( "+attack" ) ) );
+
+  if( ps->stats[ STAT_CLASS ] == PCL_ALIEN_LEVEL1_UPG )
+  {
+    Q_strcat( text, MAX_TUTORIAL_TEXT,
+        va( "Press %s to spray poisonous gas\n",
+          CG_KeyNameForCommand( "+button5" ) ) );
+  }
+
+  Q_strcat( text, MAX_TUTORIAL_TEXT,
+      va( "Press %s to walk on walls\n",
+        CG_KeyNameForCommand( "+movedown" ) ) );
+}
+
+/*
+===============
+CG_AlienLevel2Text
+===============
+*/
+static void CG_AlienLevel2Text( char *text, playerState_t *ps )
+{
+  Q_strcat( text, MAX_TUTORIAL_TEXT,
+      va( "Press %s to bite\n",
+        CG_KeyNameForCommand( "+attack" ) ) );
+
+  if( ps->stats[ STAT_CLASS ] == PCL_ALIEN_LEVEL2_UPG )
+  {
+    Q_strcat( text, MAX_TUTORIAL_TEXT,
+        va( "Press %s to invoke an electrical attack\n",
+          CG_KeyNameForCommand( "+button5" ) ) );
+  }
+
+  Q_strcat( text, MAX_TUTORIAL_TEXT,
+      va( "Hold down %s then touch a wall to wall jump\n",
+        CG_KeyNameForCommand( "+moveup" ) ) );
+}
+
+/*
+===============
+CG_AlienLevel3Text
+===============
+*/
+static void CG_AlienLevel3Text( char *text, playerState_t *ps )
+{
+  Q_strcat( text, MAX_TUTORIAL_TEXT,
+      va( "Press %s to bite\n",
+        CG_KeyNameForCommand( "+attack" ) ) );
+
+  if( ps->stats[ STAT_CLASS ] == PCL_ALIEN_LEVEL3_UPG )
+  {
+    Q_strcat( text, MAX_TUTORIAL_TEXT,
+        va( "Press %s to launch a projectile\n",
+          CG_KeyNameForCommand( "+button2" ) ) );
+  }
+
+  Q_strcat( text, MAX_TUTORIAL_TEXT,
+      va( "Hold down and release %s to pounce\n",
+        CG_KeyNameForCommand( "+button5" ) ) );
+}
+
+/*
+===============
+CG_AlienLevel4Text
+===============
+*/
+static void CG_AlienLevel4Text( char *text, playerState_t *ps )
+{
+  Q_strcat( text, MAX_TUTORIAL_TEXT,
+      va( "Press %s to swipe\n",
+        CG_KeyNameForCommand( "+attack" ) ) );
+
+  Q_strcat( text, MAX_TUTORIAL_TEXT,
+      va( "Hold down and release %s to trample\n",
+        CG_KeyNameForCommand( "+button5" ) ) );
+}
+
+/*
+===============
+CG_HumanCkitText
+===============
+*/
+static void CG_HumanCkitText( char *text, playerState_t *ps )
+{
+  buildable_t   buildable = ps->stats[ STAT_BUILDABLE ] & ~SB_VALID_TOGGLEBIT;
+  entityState_t *es;
+
+  if( buildable > BA_NONE )
+  {
+    Q_strcat( text, MAX_TUTORIAL_TEXT,
+        va( "Press %s to place the %s\n",
+          CG_KeyNameForCommand( "+attack" ),
+          BG_Buildable( buildable )->humanName ) );
+
+    Q_strcat( text, MAX_TUTORIAL_TEXT,
+        va( "Press %s to cancel placing the %s\n",
+          CG_KeyNameForCommand( "+button5" ),
+          BG_Buildable( buildable )->humanName ) );
+  }
+  else
+  {
+    Q_strcat( text, MAX_TUTORIAL_TEXT,
+        va( "Press %s to build a structure\n",
+          CG_KeyNameForCommand( "+attack" ) ) );
+  }
+
+  if( ( es = CG_BuildableInRange( ps, NULL ) ) )
+  {
+    if( cgs.markDeconstruct )
+    {
+      if( es->eFlags & EF_B_MARKED )
+      {
+        Q_strcat( text, MAX_TUTORIAL_TEXT,
+            va( "Press %s to unmark this structure\n",
+              CG_KeyNameForCommand( "deconstruct" ) ) );
+      }
+      else
+      {
+        Q_strcat( text, MAX_TUTORIAL_TEXT,
+            va( "Press %s to mark this structure\n",
+              CG_KeyNameForCommand( "deconstruct" ) ) );
+      }
+    }
+    else
+    {
+      Q_strcat( text, MAX_TUTORIAL_TEXT,
+          va( "Press %s to destroy this structure\n",
+            CG_KeyNameForCommand( "deconstruct" ) ) );
+    }
+  }
+}
+
+/*
+===============
+CG_HumanText
+===============
+*/
+static void CG_HumanText( char *text, playerState_t *ps )
+{
+  char      *name;
+  upgrade_t upgrade = UP_NONE;
+
+  if( cg.weaponSelect < 32 )
+    name = cg_weapons[ cg.weaponSelect ].humanName;
+  else
+  {
+    name = cg_upgrades[ cg.weaponSelect - 32 ].humanName;
+    upgrade = cg.weaponSelect - 32;
+  }
+
+  if( !ps->ammo && !ps->clips && !BG_Weapon( ps->weapon )->infiniteAmmo )
+  {
+    //no ammo
+    switch( ps->weapon )
+    {
+      case WP_MACHINEGUN:
+      case WP_CHAINGUN:
+      case WP_SHOTGUN:
+      case WP_FLAMER:
+        Q_strcat( text, MAX_TUTORIAL_TEXT,
+            va( "Find an Armoury and press %s for more ammo\n",
+              CG_KeyNameForCommand( "buy ammo" ) ) );
+        break;
+
+      case WP_LAS_GUN:
+      case WP_PULSE_RIFLE:
+      case WP_MASS_DRIVER:
+      case WP_LUCIFER_CANNON:
+        Q_strcat( text, MAX_TUTORIAL_TEXT,
+            va( "Find an Armoury, Reactor, or Repeater and press %s for more ammo\n",
+              CG_KeyNameForCommand( "buy ammo" ) ) );
+        break;
+
+      default:
+        break;
+    }
+  }
+  else
+  {
+    switch( ps->weapon )
+    {
+      case WP_BLASTER:
+      case WP_MACHINEGUN:
+      case WP_SHOTGUN:
+      case WP_LAS_GUN:
+      case WP_CHAINGUN:
+      case WP_PULSE_RIFLE:
+      case WP_FLAMER:
+        Q_strcat( text, MAX_TUTORIAL_TEXT,
+            va( "Press %s to fire the %s\n",
+              CG_KeyNameForCommand( "+attack" ),
+              BG_Weapon( ps->weapon )->humanName ) );
+        break;
+
+      case WP_MASS_DRIVER:
+        Q_strcat( text, MAX_TUTORIAL_TEXT,
+            va( "Press %s to fire the %s\n",
+              CG_KeyNameForCommand( "+attack" ),
+              BG_Weapon( ps->weapon )->humanName ) );
+
+        Q_strcat( text, MAX_TUTORIAL_TEXT,
+            va( "Hold %s to zoom\n",
+              CG_KeyNameForCommand( "+button5" ) ) );
+        break;
+
+      case WP_PAIN_SAW:
+        Q_strcat( text, MAX_TUTORIAL_TEXT,
+            va( "Hold %s to activate the %s\n",
+              CG_KeyNameForCommand( "+attack" ),
+              BG_Weapon( ps->weapon )->humanName ) );
+        break;
+
+      case WP_LUCIFER_CANNON:
+        Q_strcat( text, MAX_TUTORIAL_TEXT,
+            va( "Hold and release %s to fire a charged shot\n",
+              CG_KeyNameForCommand( "+attack" ) ) );
+
+        Q_strcat( text, MAX_TUTORIAL_TEXT,
+            va( "Press %s to fire the %s\n",
+              CG_KeyNameForCommand( "+button5" ),
+              BG_Weapon( ps->weapon )->humanName ) );
+        break;
+
+      case WP_HBUILD:
+        CG_HumanCkitText( text, ps );
+        break;
+
+      default:
+        break;
+    }
+  }
+
+  Q_strcat( text, MAX_TUTORIAL_TEXT,
+      va( "Press %s and ",
+          CG_KeyNameForCommand( "weapprev" ) ) );
+  Q_strcat( text, MAX_TUTORIAL_TEXT,
+      va( "%s to select an upgrade\n",
+          CG_KeyNameForCommand( "weapnext" ) ) );
+
+  if( upgrade == UP_NONE ||
+      ( upgrade > UP_NONE && BG_Upgrade( upgrade )->usable ) )
+  {
+    Q_strcat( text, MAX_TUTORIAL_TEXT,
+        va( "Press %s to use the %s\n",
+            CG_KeyNameForCommand( "+button2" ),
+            name ) );
+  }
+
+  if( ps->stats[ STAT_HEALTH ] <= 35 &&
+      BG_InventoryContainsUpgrade( UP_MEDKIT, ps->stats ) )
+  {
+    Q_strcat( text, MAX_TUTORIAL_TEXT,
+        va( "Press %s to use your %s\n",
+          CG_KeyNameForCommand( "itemact medkit" ),
+          BG_Upgrade( UP_MEDKIT )->humanName ) );
+  }
+
+  if( ps->stats[ STAT_STAMINA ] <= STAMINA_BLACKOUT_LEVEL )
+  {
+    Q_strcat( text, MAX_TUTORIAL_TEXT,
+        "You are blacking out. Stop sprinting to recover stamina\n" );
+  }
+  else if( ps->stats[ STAT_STAMINA ] <= STAMINA_SLOW_LEVEL )
+  {
+    Q_strcat( text, MAX_TUTORIAL_TEXT,
+        "Your stamina is low. Stop sprinting to recover\n" );
+  }
+
+  switch( cg.nearUsableBuildable )
+  {
+    case BA_H_ARMOURY:
+      Q_strcat( text, MAX_TUTORIAL_TEXT,
+          va( "Press %s to buy equipment upgrades at the %s. Sell your old weapon first!\n",
+            CG_KeyNameForCommand( "+button7" ),
+            BG_Buildable( cg.nearUsableBuildable )->humanName ) );
+      break;
+    case BA_H_REPEATER:
+    case BA_H_REACTOR:
+      Q_strcat( text, MAX_TUTORIAL_TEXT,
+          va( "Press %s to refill your energy weapon's ammo at the %s\n",
+            CG_KeyNameForCommand( "+button7" ),
+            BG_Buildable( cg.nearUsableBuildable )->humanName ) );
+      break;
+    case BA_NONE:
+      break;
+    default:
+      Q_strcat( text, MAX_TUTORIAL_TEXT,
+          va( "Press %s to use the %s\n",
+            CG_KeyNameForCommand( "+button7" ),
+            BG_Buildable( cg.nearUsableBuildable )->humanName ) );
+      break;
+  }
+
+  Q_strcat( text, MAX_TUTORIAL_TEXT,
+      va( "Press %s and any direction to sprint\n",
+        CG_KeyNameForCommand( "+button8" ) ) );
+
+  Q_strcat( text, MAX_TUTORIAL_TEXT,
+      va( "Press %s and back or strafe to dodge\n",
+        CG_KeyNameForCommand( "+button6" ) ) );
+}
+
+/*
+===============
+CG_SpectatorText
+===============
+*/
+static void CG_SpectatorText( char *text, playerState_t *ps )
+{
+  if( cgs.clientinfo[ cg.clientNum ].team != TEAM_NONE )
+  {
+    if( ps->pm_flags & PMF_QUEUED )
+      Q_strcat( text, MAX_TUTORIAL_TEXT,
+                va( "Press %s to leave spawn queue\n",
+                    CG_KeyNameForCommand( "+attack" ) ) );
+    else
+      Q_strcat( text, MAX_TUTORIAL_TEXT,
+                va( "Press %s to spawn\n",
+                    CG_KeyNameForCommand( "+attack" ) ) );
+  }
+  else 
+  {
+    Q_strcat( text, MAX_TUTORIAL_TEXT,
+        va( "Press %s to join a team\n",
+          CG_KeyNameForCommand( "+attack" ) ) );
+  }
+
+  if( ps->pm_flags & PMF_FOLLOW )
+  {
+    if( !cg.chaseFollow )
+      Q_strcat( text, MAX_TUTORIAL_TEXT,
+                va( "Press %s to switch to chase-cam spectator mode\n",
+                    CG_KeyNameForCommand( "+button2" ) ) );
+    else if( cgs.clientinfo[ cg.clientNum ].team == TEAM_NONE )
+      Q_strcat( text, MAX_TUTORIAL_TEXT,
+                va( "Press %s to return to free spectator mode\n",
+                    CG_KeyNameForCommand( "+button2" ) ) );
+    else
+      Q_strcat( text, MAX_TUTORIAL_TEXT,
+                va( "Press %s to stop following\n",
+                    CG_KeyNameForCommand( "+button2" ) ) );
+
+    Q_strcat( text, MAX_TUTORIAL_TEXT,
+        va( "Press %s or ",
+          CG_KeyNameForCommand( "weapprev" ) ) );
+    Q_strcat( text, MAX_TUTORIAL_TEXT,
+        va( "%s to change player\n",
+          CG_KeyNameForCommand( "weapnext" ) ) );
+  }
+  else
+  {
+    Q_strcat( text, MAX_TUTORIAL_TEXT,
+        va( "Press %s to follow a player\n",
+            CG_KeyNameForCommand( "+button2" ) ) );
+  }
+}
+
+#define BINDING_REFRESH_INTERVAL 30
+
+/*
+===============
+CG_TutorialText
+
+Returns context help for the current class/weapon
+===============
+*/
+const char *CG_TutorialText( void )
+{
+  playerState_t *ps;
+  static char   text[ MAX_TUTORIAL_TEXT ];
+  static int    refreshBindings = 0;
+
+  if( refreshBindings == 0 )
+    CG_GetBindings( );
+
+  refreshBindings = ( refreshBindings + 1 ) % BINDING_REFRESH_INTERVAL;
+
+  text[ 0 ] = '\0';
+  ps = &cg.snap->ps;
+
+  if( !cg.intermissionStarted && !cg.demoPlayback )
+  {
+    if( ps->persistant[ PERS_SPECSTATE ] != SPECTATOR_NOT ||
+        ps->pm_flags & PMF_FOLLOW )
+    {
+      CG_SpectatorText( text, ps );
+    }
+    else if( ps->stats[ STAT_HEALTH ] > 0 )
+    {
+      switch( ps->stats[ STAT_CLASS ] )
+      {
+        case PCL_ALIEN_BUILDER0:
+        case PCL_ALIEN_BUILDER0_UPG:
+          CG_AlienBuilderText( text, ps );
+          break;
+
+        case PCL_ALIEN_LEVEL0:
+          CG_AlienLevel0Text( text, ps );
+          break;
+
+        case PCL_ALIEN_LEVEL1:
+        case PCL_ALIEN_LEVEL1_UPG:
+          CG_AlienLevel1Text( text, ps );
+          break;
+
+        case PCL_ALIEN_LEVEL2:
+        case PCL_ALIEN_LEVEL2_UPG:
+          CG_AlienLevel2Text( text, ps );
+          break;
+
+        case PCL_ALIEN_LEVEL3:
+        case PCL_ALIEN_LEVEL3_UPG:
+          CG_AlienLevel3Text( text, ps );
+          break;
+
+        case PCL_ALIEN_LEVEL4:
+          CG_AlienLevel4Text( text, ps );
+          break;
+
+        case PCL_HUMAN:
+        case PCL_HUMAN_BSUIT:
+          CG_HumanText( text, ps );
+          break;
+
+        default:
+          break;
+      }
+
+      if( ps->stats[ STAT_TEAM ] == TEAM_ALIENS )
+      {
+        if( BG_AlienCanEvolve( ps->stats[ STAT_CLASS ],
+                                    ps->persistant[ PERS_CREDIT ],
+                                    cgs.alienStage ) )
+        {
+          Q_strcat( text, MAX_TUTORIAL_TEXT,
+              va( "Press %s to evolve\n",
+                CG_KeyNameForCommand( "+button7" ) ) );
+        }
+      }
+    }
+  }
+  else if( !cg.demoPlayback )
+  {
+    if( !CG_ClientIsReady( ps->clientNum ) )
+    {
+      Q_strcat( text, MAX_TUTORIAL_TEXT,
+          va( "Press %s when ready to continue\n",
+            CG_KeyNameForCommand( "+attack" ) ) );
+    }
+    else
+    {
+      Q_strcat( text, MAX_TUTORIAL_TEXT, "Waiting for other players to be ready\n" );
+    }
+  }
+
+  if( !cg.demoPlayback )
+  {
+    Q_strcat( text, MAX_TUTORIAL_TEXT, "Press ESC for the menu" );
+  }
+
+  return text;
+}
diff --git a/src/cgame/cg_view.c b/src/cgame/cg_view.c
new file mode 100644
index 0000000..a0142d0
--- /dev/null
+++ b/src/cgame/cg_view.c
@@ -0,0 +1,1517 @@
+/*
+===========================================================================
+Copyright (C) 1999-2005 Id Software, Inc.
+Copyright (C) 2000-2009 Darklegion Development
+
+This file is part of Tremulous.
+
+Tremulous 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.
+
+Tremulous 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 Tremulous; if not, write to the Free Software
+Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
+===========================================================================
+*/
+
+// cg_view.c -- setup all the parameters (position, angle, etc)
+// for a 3D rendering
+
+
+#include "cg_local.h"
+
+
+/*
+=============================================================================
+
+  MODEL TESTING
+
+The viewthing and gun positioning tools from Q2 have been integrated and
+enhanced into a single model testing facility.
+
+Model viewing can begin with either "testmodel <modelname>" or "testgun <modelname>".
+
+The names must be the full pathname after the basedir, like
+"models/weapons/v_launch/tris.md3" or "players/male/tris.md3"
+
+Testmodel will create a fake entity 100 units in front of the current view
+position, directly facing the viewer.  It will remain immobile, so you can
+move around it to view it from different angles.
+
+Testgun will cause the model to follow the player around and supress the real
+view weapon model.  The default frame 0 of most guns is completely off screen,
+so you will probably have to cycle a couple frames to see it.
+
+"nextframe", "prevframe", "nextskin", and "prevskin" commands will change the
+frame or skin of the testmodel.  These are bound to F5, F6, F7, and F8 in
+q3default.cfg.
+
+If a gun is being tested, the "gun_x", "gun_y", and "gun_z" variables will let
+you adjust the positioning.
+
+Note that none of the model testing features update while the game is paused, so
+it may be convenient to test with deathmatch set to 1 so that bringing down the
+console doesn't pause the game.
+
+=============================================================================
+*/
+
+/*
+=================
+CG_TestModel_f
+
+Creates an entity in front of the current position, which
+can then be moved around
+=================
+*/
+void CG_TestModel_f( void )
+{
+  vec3_t    angles;
+
+  memset( &cg.testModelEntity, 0, sizeof( cg.testModelEntity ) );
+  memset( &cg.testModelBarrelEntity, 0, sizeof( cg.testModelBarrelEntity ) );
+
+  if( trap_Argc( ) < 2 )
+    return;
+
+  Q_strncpyz( cg.testModelName, CG_Argv( 1 ), MAX_QPATH );
+  cg.testModelEntity.hModel = trap_R_RegisterModel( cg.testModelName );
+
+  Q_strncpyz( cg.testModelBarrelName, CG_Argv( 1 ), MAX_QPATH );
+  cg.testModelBarrelName[ strlen( cg.testModelBarrelName ) - 4 ] = '\0';
+  Q_strcat( cg.testModelBarrelName, MAX_QPATH, "_barrel.md3" );
+  cg.testModelBarrelEntity.hModel = trap_R_RegisterModel( cg.testModelBarrelName );
+
+  if( trap_Argc( ) == 3 )
+  {
+    cg.testModelEntity.backlerp = atof( CG_Argv( 2 ) );
+    cg.testModelEntity.frame = 1;
+    cg.testModelEntity.oldframe = 0;
+  }
+
+  if( !cg.testModelEntity.hModel )
+  {
+    CG_Printf( "Can't register model\n" );
+    return;
+  }
+
+  VectorMA( cg.refdef.vieworg, 100, cg.refdef.viewaxis[ 0 ], cg.testModelEntity.origin );
+
+  angles[ PITCH ] = 0;
+  angles[ YAW ] = 180 + cg.refdefViewAngles[ 1 ];
+  angles[ ROLL ] = 0;
+
+  AnglesToAxis( angles, cg.testModelEntity.axis );
+  cg.testGun = qfalse;
+
+  if( cg.testModelBarrelEntity.hModel )
+  {
+    angles[ YAW ] = 0;
+    angles[ PITCH ] = 0;
+    angles[ ROLL ] = 0;
+    AnglesToAxis( angles, cg.testModelBarrelEntity.axis );
+  }
+}
+
+/*
+=================
+CG_TestGun_f
+
+Replaces the current view weapon with the given model
+=================
+*/
+void CG_TestGun_f( void )
+{
+  CG_TestModel_f( );
+  cg.testGun = qtrue;
+  cg.testModelEntity.renderfx = RF_MINLIGHT | RF_DEPTHHACK | RF_FIRST_PERSON;
+}
+
+
+void CG_TestModelNextFrame_f( void )
+{
+  cg.testModelEntity.frame++;
+  CG_Printf( "frame %i\n", cg.testModelEntity.frame );
+}
+
+void CG_TestModelPrevFrame_f( void )
+{
+  cg.testModelEntity.frame--;
+
+  if( cg.testModelEntity.frame < 0 )
+    cg.testModelEntity.frame = 0;
+
+  CG_Printf( "frame %i\n", cg.testModelEntity.frame );
+}
+
+void CG_TestModelNextSkin_f( void )
+{
+  cg.testModelEntity.skinNum++;
+  CG_Printf( "skin %i\n", cg.testModelEntity.skinNum );
+}
+
+void CG_TestModelPrevSkin_f( void )
+{
+  cg.testModelEntity.skinNum--;
+
+  if( cg.testModelEntity.skinNum < 0 )
+    cg.testModelEntity.skinNum = 0;
+
+  CG_Printf( "skin %i\n", cg.testModelEntity.skinNum );
+}
+
+static void CG_AddTestModel( void )
+{
+  int   i;
+
+  // re-register the model, because the level may have changed
+  cg.testModelEntity.hModel = trap_R_RegisterModel( cg.testModelName );
+  cg.testModelBarrelEntity.hModel = trap_R_RegisterModel( cg.testModelBarrelName );
+
+  if( !cg.testModelEntity.hModel )
+  {
+    CG_Printf( "Can't register model\n" );
+    return;
+  }
+
+  // if testing a gun, set the origin reletive to the view origin
+  if( cg.testGun )
+  {
+    VectorCopy( cg.refdef.vieworg, cg.testModelEntity.origin );
+    VectorCopy( cg.refdef.viewaxis[ 0 ], cg.testModelEntity.axis[ 0 ] );
+    VectorCopy( cg.refdef.viewaxis[ 1 ], cg.testModelEntity.axis[ 1 ] );
+    VectorCopy( cg.refdef.viewaxis[ 2 ], cg.testModelEntity.axis[ 2 ] );
+
+    // allow the position to be adjusted
+    for( i = 0; i < 3; i++ )
+    {
+      cg.testModelEntity.origin[ i ] += cg.refdef.viewaxis[ 0 ][ i ] * cg_gun_x.value;
+      cg.testModelEntity.origin[ i ] += cg.refdef.viewaxis[ 1 ][ i ] * cg_gun_y.value;
+      cg.testModelEntity.origin[ i ] += cg.refdef.viewaxis[ 2 ][ i ] * cg_gun_z.value;
+    }
+  }
+
+  trap_R_AddRefEntityToScene( &cg.testModelEntity );
+
+  if( cg.testModelBarrelEntity.hModel )
+  {
+    CG_PositionEntityOnTag( &cg.testModelBarrelEntity, &cg.testModelEntity,
+        cg.testModelEntity.hModel, "tag_barrel" );
+
+    trap_R_AddRefEntityToScene( &cg.testModelBarrelEntity );
+  }
+}
+
+
+
+//============================================================================
+
+
+/*
+=================
+CG_CalcVrect
+
+Sets the coordinates of the rendered window
+=================
+*/
+static void CG_CalcVrect( void )
+{
+  int   size;
+
+  // the intermission should allways be full screen
+  if( cg.snap->ps.pm_type == PM_INTERMISSION )
+    size = 100;
+  else
+    size = cg_viewsize.integer;
+
+  cg.refdef.width = cgs.glconfig.vidWidth * size / 100;
+  cg.refdef.width &= ~1;
+
+  cg.refdef.height = cgs.glconfig.vidHeight * size / 100;
+  cg.refdef.height &= ~1;
+
+  cg.refdef.x = ( cgs.glconfig.vidWidth - cg.refdef.width ) / 2;
+  cg.refdef.y = ( cgs.glconfig.vidHeight - cg.refdef.height ) / 2;
+}
+
+//==============================================================================
+
+/*
+===============
+CG_OffsetThirdPersonView
+
+===============
+*/
+void CG_OffsetThirdPersonView( void )
+{
+  int i;
+  vec3_t        forward, right, up;
+  vec3_t        view;
+  trace_t       trace;
+  static vec3_t mins = { -8, -8, -8 };
+  static vec3_t maxs = { 8, 8, 8 };
+  vec3_t        focusPoint;
+  vec3_t        surfNormal;
+  int           cmdNum;
+  usercmd_t     cmd, oldCmd;
+  float         range;
+  vec3_t        mouseInputAngles;
+  vec3_t        rotationAngles;
+  vec3_t        axis[ 3 ], rotaxis[ 3 ];
+  float         deltaPitch;
+  static float  pitch;
+  static vec3_t killerPos = { 0, 0, 0 };
+
+  // If cg_thirdpersonShoulderViewMode == 2, do shoulder view instead
+  // If cg_thirdpersonShoulderViewMode == 1, do shoulder view when chasing
+  //   a wallwalker because it's really erratic to watch
+  if( ( cg_thirdPersonShoulderViewMode.integer == 2 ) ||
+      ( ( cg_thirdPersonShoulderViewMode.integer == 1 ) &&
+        ( cg.snap->ps.stats[ STAT_STATE ] & SS_WALLCLIMBING ) &&
+        ( cg.snap->ps.stats[ STAT_HEALTH ] > 0 ) ) )
+  {
+    CG_OffsetShoulderView( );
+    return;
+  }
+
+  BG_GetClientNormal( &cg.predictedPlayerState, surfNormal );
+  // Set the view origin to the class's view height
+  VectorMA( cg.refdef.vieworg, cg.predictedPlayerState.viewheight, surfNormal, cg.refdef.vieworg );
+
+  // Set the focus point where the camera will look (at the player's vieworg)
+  VectorCopy( cg.refdef.vieworg, focusPoint );
+
+  // If player is dead, we want the player to be between us and the killer
+  // so pretend that the player was looking at the killer, then place cam behind them.
+  if( cg.predictedPlayerState.stats[ STAT_HEALTH ] <= 0 )
+  {
+    int killerEntNum = cg.predictedPlayerState.stats[ STAT_VIEWLOCK ];
+
+    // already looking at ourself
+    if( killerEntNum != cg.snap->ps.clientNum )
+    {
+      vec3_t lookDirection;
+      if( cg.wasDeadLastFrame == qfalse || !cg_staticDeathCam.integer )
+      {
+        VectorCopy( cg_entities[ killerEntNum ].lerpOrigin, killerPos );
+        cg.wasDeadLastFrame = qtrue;
+      }
+      VectorSubtract( killerPos, cg.refdef.vieworg, lookDirection );
+      vectoangles( lookDirection, cg.refdefViewAngles );
+    }
+  }
+
+  // get and rangecheck cg_thirdPersonRange
+  range = cg_thirdPersonRange.value;
+  if( range > 150.0f ) range = 150.0f;
+  if( range < 30.0f ) range = 30.0f;
+
+  // Calculate the angle of the camera's position around the player.
+  // Unless in demo, PLAYING in third person, or in dead-third-person cam, allow the player 
+  // to control camera position offsets using the mouse position.
+  if( cg.demoPlayback || 
+    ( ( cg.snap->ps.pm_flags & PMF_FOLLOW ) && 
+      ( cg.predictedPlayerState.stats[ STAT_HEALTH ] > 0 ) ) )
+  {
+    // Collect our input values from the mouse.
+    cmdNum = trap_GetCurrentCmdNumber();
+    trap_GetUserCmd( cmdNum, &cmd );
+    trap_GetUserCmd( cmdNum - 1, &oldCmd );
+
+    // Prevent pitch from wrapping and clamp it within a [-75, 90] range.
+    // Cgame has no access to ps.delta_angles[] here, so we need to reproduce
+    // it ourselves.
+    deltaPitch = SHORT2ANGLE( cmd.angles[ PITCH ] - oldCmd.angles[ PITCH ] );
+    if( fabs(deltaPitch) < 200.0f )
+    {
+      pitch += deltaPitch;
+    }
+
+    mouseInputAngles[ PITCH ] = pitch;
+    mouseInputAngles[ YAW ] = -1.0f * SHORT2ANGLE( cmd.angles[ YAW ] ); // yaw is inverted
+    mouseInputAngles[ ROLL ] = 0.0f;
+
+    for( i = 0; i < 3; i++ )
+      mouseInputAngles[ i ] = AngleNormalize180( mouseInputAngles[ i ] );
+
+    // Set the rotation angles to be the view angles offset by the mouse input
+    // Ignore the original pitch though; it's too jerky otherwise
+    if( !cg_thirdPersonPitchFollow.integer ) 
+      cg.refdefViewAngles[ PITCH ] = 0.0f;
+
+    for( i = 0; i < 3; i++ )
+    {
+      rotationAngles[ i ] = AngleNormalize180(cg.refdefViewAngles[ i ]) + mouseInputAngles[ i ];
+      AngleNormalize180( rotationAngles[ i ] );
+    }
+
+    // Don't let pitch go too high/too low or the camera flips around and
+    // that's really annoying.
+    // However, when we're not on the floor or ceiling (wallwalk) pitch 
+    // may not be pitch, so just let it go.
+    if( surfNormal[ 2 ] > 0.5f || surfNormal[ 2 ] < -0.5f ) 
+    {
+      if( rotationAngles[ PITCH ] > 85.0f )
+        rotationAngles[ PITCH ] = 85.0f;
+      else if( rotationAngles[ PITCH ] < -85.0f )
+        rotationAngles[ PITCH ] = -85.0f;
+    }
+
+    // Perform the rotations specified by rotationAngles.
+    AnglesToAxis( rotationAngles, axis );
+    if( !( cg.snap->ps.stats[ STAT_STATE ] & SS_WALLCLIMBING ) ||
+        !BG_RotateAxis( cg.snap->ps.grapplePoint, axis, rotaxis, qfalse,
+                        cg.snap->ps.eFlags & EF_WALLCLIMBCEILING ) )
+      AxisCopy( axis, rotaxis );
+
+    // Convert the new axis back to angles.
+    AxisToAngles( rotaxis, rotationAngles );
+  }
+  else 
+  {
+    if( cg.predictedPlayerState.stats[ STAT_HEALTH ] > 0 )
+    {
+      // If we're playing the game in third person, the viewangles already
+      // take care of our mouselook, so just use them.
+      for( i = 0; i < 3; i++ )
+        rotationAngles[ i ] = cg.refdefViewAngles[ i ];
+    }
+    else // dead
+    {
+      rotationAngles[ PITCH ] = 20.0f;
+      rotationAngles[ YAW ] = cg.refdefViewAngles[ YAW ];
+    }
+  }
+
+  rotationAngles[ YAW ] -= cg_thirdPersonAngle.value;
+
+  // Move the camera range distance back.
+  AngleVectors( rotationAngles, forward, right, up );
+  VectorCopy( cg.refdef.vieworg, view );
+  VectorMA( view, -range, forward, view );
+
+  // Ensure that the current camera position isn't out of bounds and that there
+  // is nothing between the camera and the player.
+  if( !cg_cameraMode.integer )
+  {
+    // Trace a ray from the origin to the viewpoint to make sure the view isn't
+    // in a solid block.  Use an 8 by 8 block to prevent the view from near clipping anything
+    CG_Trace( &trace, cg.refdef.vieworg, mins, maxs, view, cg.predictedPlayerState.clientNum, MASK_SOLID );
+
+    if( trace.fraction != 1.0f )
+    {
+      VectorCopy( trace.endpos, view );
+      view[ 2 ] += ( 1.0f - trace.fraction ) * 32;
+      // Try another trace to this position, because a tunnel may have the ceiling
+      // close enogh that this is poking out.
+
+      CG_Trace( &trace, cg.refdef.vieworg, mins, maxs, view, cg.predictedPlayerState.clientNum, MASK_SOLID );
+      VectorCopy( trace.endpos, view );
+    }
+  }
+
+  // Set the camera position to what we calculated.
+  VectorCopy( view, cg.refdef.vieworg );
+
+  // The above checks may have moved the camera such that the existing viewangles
+  // may not still face the player. Recalculate them to do so.
+  // but if we're dead, don't bother because we'd rather see what killed us
+  if( cg.predictedPlayerState.stats[ STAT_HEALTH ] > 0 )
+  {
+    VectorSubtract( focusPoint, cg.refdef.vieworg, focusPoint );
+    vectoangles( focusPoint, cg.refdefViewAngles );
+  }
+}
+
+
+/*
+===============
+CG_OffsetShoulderView
+
+===============
+*/
+void CG_OffsetShoulderView( void )
+{
+  int            i;
+  int            cmdNum;
+  usercmd_t      cmd, oldCmd;
+  vec3_t         rotationAngles;
+  vec3_t         axis[ 3 ], rotaxis[ 3 ];
+  float          deltaMousePitch;
+  static float   mousePitch;
+  vec3_t         forward, right, up;
+  classConfig_t* classConfig;
+
+  // Ignore following pitch; it's too jerky otherwise.
+  if( !cg_thirdPersonPitchFollow.integer ) 
+    cg.refdefViewAngles[ PITCH ] = 0.0f;
+    
+  AngleVectors( cg.refdefViewAngles, forward, right, up );
+
+  classConfig = BG_ClassConfig( cg.snap->ps.stats[ STAT_CLASS ] );
+  VectorMA( cg.refdef.vieworg, classConfig->shoulderOffsets[ 0 ], forward, cg.refdef.vieworg );
+  VectorMA( cg.refdef.vieworg, classConfig->shoulderOffsets[ 1 ], right, cg.refdef.vieworg );
+  VectorMA( cg.refdef.vieworg, classConfig->shoulderOffsets[ 2 ], up, cg.refdef.vieworg );
+
+  // If someone is playing like this, the rest is already taken care of
+  // so just get the firstperson effects and leave.
+  if( !cg.demoPlayback && !( cg.snap->ps.pm_flags & PMF_FOLLOW ) )
+  {
+    CG_OffsetFirstPersonView();
+    return;
+  }
+
+  // Get mouse input for camera rotation. 
+  cmdNum = trap_GetCurrentCmdNumber();
+  trap_GetUserCmd( cmdNum, &cmd );
+  trap_GetUserCmd( cmdNum - 1, &oldCmd );
+
+  // Prevent pitch from wrapping and clamp it within a [30, -50] range.
+  // Cgame has no access to ps.delta_angles[] here, so we need to reproduce
+  // it ourselves here.
+  deltaMousePitch = SHORT2ANGLE( cmd.angles[ PITCH ] - oldCmd.angles[ PITCH ] );
+  if( fabs(deltaMousePitch) < 200.0f )
+    mousePitch += deltaMousePitch;
+
+  // Handle pitch.
+  rotationAngles[ PITCH ] = mousePitch;
+
+  rotationAngles[ PITCH ] = AngleNormalize180( rotationAngles[ PITCH ] + AngleNormalize180( cg.refdefViewAngles[ PITCH ] ) );
+  if( rotationAngles [ PITCH ] < -90.0f ) rotationAngles [ PITCH ] = -90.0f;
+  if( rotationAngles [ PITCH ] > 90.0f ) rotationAngles [ PITCH ] = 90.0f;
+
+  // Yaw and Roll are much easier.
+  rotationAngles[ YAW ] = SHORT2ANGLE( cmd.angles[ YAW ] ) + cg.refdefViewAngles[ YAW ];
+  rotationAngles[ ROLL ] = 0.0f;
+
+  // Perform the rotations.
+  AnglesToAxis( rotationAngles, axis );
+  if( !( cg.snap->ps.stats[ STAT_STATE ] & SS_WALLCLIMBING ) ||
+      !BG_RotateAxis( cg.snap->ps.grapplePoint, axis, rotaxis, qfalse,
+                      cg.snap->ps.eFlags & EF_WALLCLIMBCEILING ) )
+    AxisCopy( axis, rotaxis );
+
+  AxisToAngles( rotaxis, rotationAngles );
+
+  // Actually set the viewangles.
+  for( i = 0; i < 3; i++ )
+    cg.refdefViewAngles[ i ] = rotationAngles[ i ];
+
+  // Now run the first person stuff so we get various effects added.
+  CG_OffsetFirstPersonView( );
+}
+
+// this causes a compiler bug on mac MrC compiler
+static void CG_StepOffset( void )
+{
+  float         steptime;
+  int           timeDelta;
+  vec3_t        normal;
+  playerState_t *ps = &cg.predictedPlayerState;
+
+  BG_GetClientNormal( ps, normal );
+
+  steptime = BG_Class( ps->stats[ STAT_CLASS ] )->steptime;
+
+  // smooth out stair climbing
+  timeDelta = cg.time - cg.stepTime;
+  if( timeDelta < steptime )
+  {
+    float stepChange = cg.stepChange
+      * (steptime - timeDelta) / steptime;
+
+    VectorMA( cg.refdef.vieworg, -stepChange, normal, cg.refdef.vieworg );
+  }
+}
+
+#define PCLOUD_ROLL_AMPLITUDE     25.0f
+#define PCLOUD_ROLL_FREQUENCY     0.4f
+#define PCLOUD_ZOOM_AMPLITUDE     15
+#define PCLOUD_ZOOM_FREQUENCY     0.625f // 2.5s / 4
+#define PCLOUD_DISORIENT_DURATION 2500
+
+
+/*
+===============
+CG_OffsetFirstPersonView
+
+===============
+*/
+void CG_OffsetFirstPersonView( void )
+{
+  float         *origin;
+  float         *angles;
+  float         bob;
+  float         ratio;
+  float         delta;
+  float         speed;
+  float         f;
+  vec3_t        predictedVelocity;
+  int           timeDelta;
+  float         bob2;
+  vec3_t        normal, baseOrigin;
+  playerState_t *ps = &cg.predictedPlayerState;
+
+  BG_GetClientNormal( ps, normal );
+
+  if( cg.snap->ps.pm_type == PM_INTERMISSION )
+    return;
+
+  origin = cg.refdef.vieworg;
+  angles = cg.refdefViewAngles;
+
+  VectorCopy( origin, baseOrigin );
+
+  // if dead, fix the angle and don't add any kick
+  if( cg.snap->ps.stats[ STAT_HEALTH ] <= 0 )
+  {
+    angles[ ROLL ] = 40;
+    angles[ PITCH ] = -15;
+    angles[ YAW ] = cg.snap->ps.stats[ STAT_VIEWLOCK ];
+    origin[ 2 ] += cg.predictedPlayerState.viewheight;
+    return;
+  }
+
+  // add angles based on damage kick
+  if( cg.damageTime )
+  {
+    ratio = cg.time - cg.damageTime;
+    if( ratio < DAMAGE_DEFLECT_TIME )
+    {
+      ratio /= DAMAGE_DEFLECT_TIME;
+      angles[ PITCH ] += ratio * cg.v_dmg_pitch;
+      angles[ ROLL ] += ratio * cg.v_dmg_roll;
+    }
+    else
+    {
+      ratio = 1.0 - ( ratio - DAMAGE_DEFLECT_TIME ) / DAMAGE_RETURN_TIME;
+      if( ratio > 0 )
+      {
+        angles[ PITCH ] += ratio * cg.v_dmg_pitch;
+        angles[ ROLL ] += ratio * cg.v_dmg_roll;
+      }
+    }
+  }
+
+  // add pitch based on fall kick
+#if 0
+  ratio = ( cg.time - cg.landTime) / FALL_TIME;
+  if (ratio < 0)
+    ratio = 0;
+  angles[PITCH] += ratio * cg.fall_value;
+#endif
+
+  // add angles based on velocity
+  VectorCopy( cg.predictedPlayerState.velocity, predictedVelocity );
+
+  delta = DotProduct( predictedVelocity, cg.refdef.viewaxis[ 0 ] );
+  angles[ PITCH ] += delta * cg_runpitch.value;
+
+  delta = DotProduct( predictedVelocity, cg.refdef.viewaxis[ 1 ] );
+  angles[ ROLL ] -= delta * cg_runroll.value;
+
+  // add angles based on bob
+  // bob amount is class dependant
+
+  if( cg.snap->ps.persistant[ PERS_SPECSTATE ] != SPECTATOR_NOT )
+    bob2 = 0.0f;
+  else
+    bob2 = BG_Class( cg.predictedPlayerState.stats[ STAT_CLASS ] )->bob;
+
+
+#define LEVEL4_FEEDBACK  10.0f
+
+  //give a charging player some feedback
+  if( ps->weapon == WP_ALEVEL4 )
+  {
+    if( ps->stats[ STAT_MISC ] > 0 )
+    {
+      float fraction = (float)ps->stats[ STAT_MISC ] /
+                       LEVEL4_TRAMPLE_CHARGE_MAX;
+
+      if( fraction > 1.0f )
+        fraction = 1.0f;
+
+      bob2 *= ( 1.0f + fraction * LEVEL4_FEEDBACK );
+    }
+  }
+
+  if( bob2 != 0.0f )
+  {
+    // make sure the bob is visible even at low speeds
+    speed = cg.xyspeed > 200 ? cg.xyspeed : 200;
+
+    delta = cg.bobfracsin * ( bob2 ) * speed;
+    if( cg.predictedPlayerState.pm_flags & PMF_DUCKED )
+      delta *= 3;   // crouching
+
+    angles[ PITCH ] += delta;
+    delta = cg.bobfracsin * ( bob2 ) * speed;
+    if( cg.predictedPlayerState.pm_flags & PMF_DUCKED )
+      delta *= 3;   // crouching accentuates roll
+
+    if( cg.bobcycle & 1 )
+      delta = -delta;
+
+    angles[ ROLL ] += delta;
+  }
+
+#define LEVEL3_FEEDBACK  20.0f
+
+  //provide some feedback for pouncing
+  if( ( cg.predictedPlayerState.weapon == WP_ALEVEL3 ||
+        cg.predictedPlayerState.weapon == WP_ALEVEL3_UPG ) &&
+      cg.predictedPlayerState.stats[ STAT_MISC ] > 0 )
+  {
+    float fraction1, fraction2;
+    vec3_t forward;
+
+    AngleVectors( angles, forward, NULL, NULL );
+    VectorNormalize( forward );
+
+    fraction1 = (float)cg.predictedPlayerState.stats[ STAT_MISC ] /
+                LEVEL3_POUNCE_TIME_UPG;
+    if( fraction1 > 1.0f )
+      fraction1 = 1.0f;
+
+    fraction2 = -sin( fraction1 * M_PI / 2 );
+
+    VectorMA( origin, LEVEL3_FEEDBACK * fraction2, forward, origin );
+  }
+  else if( ( cg.predictedPlayerState.weapon == WP_ALEVEL3 ||
+           cg.predictedPlayerState.weapon == WP_ALEVEL3_UPG ) &&
+           cg.predictedPlayerState.stats[ STAT_MISC ] > 0 )
+  {
+    float fraction1, fraction2;
+    vec3_t forward;
+
+    AngleVectors( angles, forward, NULL, NULL );
+    VectorNormalize( forward );
+
+    fraction1 = (float)cg.predictedPlayerState.stats[ STAT_MISC ] /
+                LEVEL3_POUNCE_TIME_UPG;
+    if( fraction1 > 1.0f )
+      fraction1 = 1.0f;
+
+    fraction2 = -sin( fraction1 * M_PI / 2 );
+
+    VectorMA( origin, LEVEL3_FEEDBACK * fraction2, forward, origin );
+  }
+
+#define STRUGGLE_DIST 5.0f
+#define STRUGGLE_TIME 250
+
+  //allow the player to struggle a little whilst grabbed
+  if( cg.predictedPlayerState.pm_type == PM_GRABBED )
+  {
+    vec3_t    forward, right, up;
+    usercmd_t cmd;
+    int       cmdNum;
+    float     fFraction, rFraction, uFraction;
+    float     fFraction2, rFraction2, uFraction2;
+
+    cmdNum = trap_GetCurrentCmdNumber();
+    trap_GetUserCmd( cmdNum, &cmd );
+
+    AngleVectors( angles, forward, right, up );
+
+    fFraction = (float)( cg.time - cg.forwardMoveTime ) / STRUGGLE_TIME;
+    rFraction = (float)( cg.time - cg.rightMoveTime ) / STRUGGLE_TIME;
+    uFraction = (float)( cg.time - cg.upMoveTime ) / STRUGGLE_TIME;
+
+    if( fFraction > 1.0f )
+      fFraction = 1.0f;
+    if( rFraction > 1.0f )
+      rFraction = 1.0f;
+    if( uFraction > 1.0f )
+      uFraction = 1.0f;
+
+    fFraction2 = -sin( fFraction * M_PI / 2 );
+    rFraction2 = -sin( rFraction * M_PI / 2 );
+    uFraction2 = -sin( uFraction * M_PI / 2 );
+
+    if( cmd.forwardmove > 0 )
+      VectorMA( origin, STRUGGLE_DIST * fFraction, forward, origin );
+    else if( cmd.forwardmove < 0 )
+      VectorMA( origin, -STRUGGLE_DIST * fFraction, forward, origin );
+    else
+      cg.forwardMoveTime = cg.time;
+
+    if( cmd.rightmove > 0 )
+      VectorMA( origin, STRUGGLE_DIST * rFraction, right, origin );
+    else if( cmd.rightmove < 0 )
+      VectorMA( origin, -STRUGGLE_DIST * rFraction, right, origin );
+    else
+      cg.rightMoveTime = cg.time;
+
+    if( cmd.upmove > 0 )
+      VectorMA( origin, STRUGGLE_DIST * uFraction, up, origin );
+    else if( cmd.upmove < 0 )
+      VectorMA( origin, -STRUGGLE_DIST * uFraction, up, origin );
+    else
+      cg.upMoveTime = cg.time;
+  }
+
+  if( ( cg.predictedPlayerEntity.currentState.eFlags & EF_POISONCLOUDED ) &&
+      ( cg.time - cg.poisonedTime < PCLOUD_DISORIENT_DURATION) &&
+      !( cg.snap->ps.pm_flags & PMF_FOLLOW ) )
+  {
+    float scale, fraction, pitchFraction;
+    
+    scale = 1.0f - (float)( cg.time - cg.poisonedTime ) /
+            BG_PlayerPoisonCloudTime( &cg.predictedPlayerState );
+    if( scale < 0.0f )
+      scale = 0.0f;
+
+    fraction = sin( ( cg.time - cg.poisonedTime ) / 500.0f * M_PI * PCLOUD_ROLL_FREQUENCY ) *
+               scale;
+    pitchFraction = sin( ( cg.time - cg.poisonedTime ) / 200.0f * M_PI * PCLOUD_ROLL_FREQUENCY ) *
+                    scale;
+
+    angles[ ROLL ] += fraction * PCLOUD_ROLL_AMPLITUDE;
+    angles[ YAW ] += fraction * PCLOUD_ROLL_AMPLITUDE;
+    angles[ PITCH ] += pitchFraction * PCLOUD_ROLL_AMPLITUDE / 2.0f;
+  }
+
+  // this *feels* more realisitic for humans
+  if( cg.predictedPlayerState.stats[ STAT_TEAM ] == TEAM_HUMANS &&
+      ( cg.predictedPlayerState.pm_type == PM_NORMAL ||
+        cg.predictedPlayerState.pm_type == PM_JETPACK ) )
+  {
+    angles[PITCH] += cg.bobfracsin * bob2 * 0.5;
+
+    // heavy breathing effects //FIXME: sound
+    if( cg.predictedPlayerState.stats[ STAT_STAMINA ] < STAMINA_BREATHING_LEVEL )
+    {
+      float deltaBreath = ( cg.predictedPlayerState.stats[ STAT_STAMINA ] -
+                            STAMINA_BREATHING_LEVEL ) / -250.0;
+      float deltaAngle = cos( (float)cg.time/150.0 ) * deltaBreath;
+
+      deltaAngle += ( deltaAngle < 0 ? -deltaAngle : deltaAngle ) * 0.5;
+
+      angles[ PITCH ] -= deltaAngle;
+    }
+  }
+
+//===================================
+
+  // add view height
+  VectorMA( origin, ps->viewheight, normal, origin );
+
+  // smooth out duck height changes
+  timeDelta = cg.time - cg.duckTime;
+  if( timeDelta < DUCK_TIME)
+  {
+    cg.refdef.vieworg[ 2 ] -= cg.duckChange
+      * ( DUCK_TIME - timeDelta ) / DUCK_TIME;
+  }
+
+  // add bob height
+  bob = cg.bobfracsin * cg.xyspeed * bob2;
+
+  if( bob > 6 )
+    bob = 6;
+
+  VectorMA( origin, bob, normal, origin );
+
+  // add fall height
+  delta = cg.time - cg.landTime;
+
+  if( delta < LAND_DEFLECT_TIME )
+  {
+    f = delta / LAND_DEFLECT_TIME;
+    cg.refdef.vieworg[ 2 ] += cg.landChange * f;
+  }
+  else if( delta < LAND_DEFLECT_TIME + LAND_RETURN_TIME )
+  {
+    delta -= LAND_DEFLECT_TIME;
+    f = 1.0 - ( delta / LAND_RETURN_TIME );
+    cg.refdef.vieworg[ 2 ] += cg.landChange * f;
+  }
+
+  // add step offset
+  CG_StepOffset( );
+}
+
+//======================================================================
+
+/*
+====================
+CG_CalcFov
+
+Fixed fov at intermissions, otherwise account for fov variable and zooms.
+====================
+*/
+#define WAVE_AMPLITUDE  1.0f
+#define WAVE_FREQUENCY  0.4f
+
+#define FOVWARPTIME     400.0f
+#define BASE_FOV_Y      73.739792f // atan2( 3, 4 / tan( 90 ) )
+#define MAX_FOV_Y       120.0f
+#define MAX_FOV_WARP_Y  127.5f
+
+static int CG_CalcFov( void )
+{
+  float     y;
+  float     phase;
+  float     v;
+  int       contents;
+  float     fov_x, fov_y;
+  float     zoomFov;
+  float     f;
+  int       inwater;
+  int       attribFov;
+  usercmd_t cmd;
+  usercmd_t oldcmd;
+  int       cmdNum;
+
+  cmdNum = trap_GetCurrentCmdNumber( );
+  trap_GetUserCmd( cmdNum, &cmd );
+  trap_GetUserCmd( cmdNum - 1, &oldcmd );
+
+  // switch follow modes if necessary: cycle between free -> follow -> third-person follow
+  if( cmd.buttons & BUTTON_USE_HOLDABLE && !( oldcmd.buttons & BUTTON_USE_HOLDABLE ) )
+  {
+    if ( cg.snap->ps.pm_flags & PMF_FOLLOW ) 
+    {
+      if( !cg.chaseFollow )
+        cg.chaseFollow = qtrue;
+      else
+      {
+        cg.chaseFollow = qfalse;
+        trap_SendClientCommand( "follow\n" );
+      }
+    }
+    else if ( cg.snap->ps.persistant[ PERS_SPECSTATE ] != SPECTATOR_NOT )
+      trap_SendClientCommand( "follow\n" );
+  }
+
+  if( cg.predictedPlayerState.pm_type == PM_INTERMISSION ||
+      ( cg.snap->ps.persistant[ PERS_SPECSTATE ] != SPECTATOR_NOT ) || 
+      ( cg.renderingThirdPerson ) )
+  {
+    // if in intermission or third person, use a fixed value
+    fov_y = BASE_FOV_Y;
+  }
+  else
+  {
+    // don't lock the fov globally - we need to be able to change it
+    attribFov = BG_Class( cg.predictedPlayerState.stats[ STAT_CLASS ] )->fov * 0.75f;
+    fov_y = attribFov;
+
+    if ( fov_y < 1.0f )
+      fov_y = 1.0f;
+    else if ( fov_y > MAX_FOV_Y )
+      fov_y = MAX_FOV_Y;
+
+    if( cg.spawnTime > ( cg.time - FOVWARPTIME ) &&
+        BG_ClassHasAbility( cg.predictedPlayerState.stats[ STAT_CLASS ], SCA_FOVWARPS ) )
+    {
+      float fraction = (float)( cg.time - cg.spawnTime ) / FOVWARPTIME;
+
+      fov_y = MAX_FOV_WARP_Y - ( ( MAX_FOV_WARP_Y - fov_y ) * fraction );
+    }
+
+    // account for zooms
+    zoomFov = BG_Weapon( cg.predictedPlayerState.weapon )->zoomFov * 0.75f;
+    if ( zoomFov < 1.0f )
+      zoomFov = 1.0f;
+    else if ( zoomFov > attribFov )
+      zoomFov = attribFov;
+
+    // only do all the zoom stuff if the client CAN zoom
+    // FIXME: zoom control is currently hard coded to BUTTON_ATTACK2
+    if( BG_Weapon( cg.predictedPlayerState.weapon )->canZoom )
+    {
+      if ( cg.zoomed )
+      {
+        f = ( cg.time - cg.zoomTime ) / (float)ZOOM_TIME;
+
+        if ( f > 1.0f )
+          fov_y = zoomFov;
+        else
+          fov_y = fov_y + f * ( zoomFov - fov_y );
+
+        // BUTTON_ATTACK2 isn't held so unzoom next time
+        if( !( cmd.buttons & BUTTON_ATTACK2 ) )
+        {
+          cg.zoomed   = qfalse;
+          cg.zoomTime = MIN( cg.time, 
+              cg.time + cg.time - cg.zoomTime - ZOOM_TIME );
+        }
+      }
+      else
+      {
+        f = ( cg.time - cg.zoomTime ) / (float)ZOOM_TIME;
+
+        if ( f > 1.0f )
+          fov_y = fov_y;
+        else
+          fov_y = zoomFov + f * ( fov_y - zoomFov );
+
+        // BUTTON_ATTACK2 is held so zoom next time
+        if( cmd.buttons & BUTTON_ATTACK2 )
+        {
+          cg.zoomed   = qtrue;
+          cg.zoomTime = MIN( cg.time, 
+              cg.time + cg.time - cg.zoomTime - ZOOM_TIME );
+        }
+      }
+    }
+  }
+
+  y = cg.refdef.height / tan( 0.5f * DEG2RAD( fov_y ) );
+  fov_x = atan2( cg.refdef.width, y );
+  fov_x = 2.0f * RAD2DEG( fov_x );
+
+  // warp if underwater
+  contents = CG_PointContents( cg.refdef.vieworg, -1 );
+
+  if( contents & ( CONTENTS_WATER | CONTENTS_SLIME | CONTENTS_LAVA ) )
+  {
+    phase = cg.time / 1000.0f * WAVE_FREQUENCY * M_PI * 2.0f;
+    v = WAVE_AMPLITUDE * sin( phase );
+    fov_x += v;
+    fov_y -= v;
+    inwater = qtrue;
+  }
+  else
+    inwater = qfalse;
+
+  if( ( cg.predictedPlayerEntity.currentState.eFlags & EF_POISONCLOUDED ) &&
+      ( cg.time - cg.poisonedTime < PCLOUD_DISORIENT_DURATION) &&
+      cg.predictedPlayerState.stats[ STAT_HEALTH ] > 0 &&
+      !( cg.snap->ps.pm_flags & PMF_FOLLOW ) )
+  {
+    float scale = 1.0f - (float)( cg.time - cg.poisonedTime ) /
+                  BG_PlayerPoisonCloudTime( &cg.predictedPlayerState );
+      
+    phase = ( cg.time - cg.poisonedTime ) / 1000.0f * PCLOUD_ZOOM_FREQUENCY * M_PI * 2.0f;
+    v = PCLOUD_ZOOM_AMPLITUDE * sin( phase ) * scale;
+    fov_x += v;
+    fov_y += v;
+  }
+
+
+  // set it
+  cg.refdef.fov_x = fov_x;
+  cg.refdef.fov_y = fov_y;
+
+  if( !cg.zoomed )
+    cg.zoomSensitivity = 1.0f;
+  else
+    cg.zoomSensitivity = cg.refdef.fov_y / 75.0f;
+
+  return inwater;
+}
+
+
+
+#define NORMAL_HEIGHT 64.0f
+#define NORMAL_WIDTH  6.0f
+
+/*
+===============
+CG_DrawSurfNormal
+
+Draws a vector against
+the surface player is looking at
+===============
+*/
+static void CG_DrawSurfNormal( void )
+{
+  trace_t     tr;
+  vec3_t      end, temp;
+  polyVert_t  normal[ 4 ];
+  vec4_t      color = { 0.0f, 255.0f, 0.0f, 128.0f };
+
+  VectorMA( cg.refdef.vieworg, 8192, cg.refdef.viewaxis[ 0 ], end );
+
+  CG_Trace( &tr, cg.refdef.vieworg, NULL, NULL, end, cg.predictedPlayerState.clientNum, MASK_SOLID );
+
+  VectorCopy( tr.endpos, normal[ 0 ].xyz );
+  normal[ 0 ].st[ 0 ] = 0;
+  normal[ 0 ].st[ 1 ] = 0;
+  Vector4Copy( color, normal[ 0 ].modulate );
+
+  VectorMA( tr.endpos, NORMAL_WIDTH, cg.refdef.viewaxis[ 1 ], temp );
+  VectorCopy( temp, normal[ 1 ].xyz);
+  normal[ 1 ].st[ 0 ] = 0;
+  normal[ 1 ].st[ 1 ] = 1;
+  Vector4Copy( color, normal[ 1 ].modulate );
+
+  VectorMA( tr.endpos, NORMAL_HEIGHT, tr.plane.normal, temp );
+  VectorMA( temp, NORMAL_WIDTH, cg.refdef.viewaxis[ 1 ], temp );
+  VectorCopy( temp, normal[ 2 ].xyz );
+  normal[ 2 ].st[ 0 ] = 1;
+  normal[ 2 ].st[ 1 ] = 1;
+  Vector4Copy( color, normal[ 2 ].modulate );
+
+  VectorMA( tr.endpos, NORMAL_HEIGHT, tr.plane.normal, temp );
+  VectorCopy( temp, normal[ 3 ].xyz );
+  normal[ 3 ].st[ 0 ] = 1;
+  normal[ 3 ].st[ 1 ] = 0;
+  Vector4Copy( color, normal[ 3 ].modulate );
+
+  trap_R_AddPolyToScene( cgs.media.outlineShader, 4, normal );
+}
+
+/*
+===============
+CG_addSmoothOp
+===============
+*/
+void CG_addSmoothOp( vec3_t rotAxis, float rotAngle, float timeMod )
+{
+  int i;
+
+  //iterate through smooth array
+  for( i = 0; i < MAXSMOOTHS; i++ )
+  {
+    //found an unused index in the smooth array
+    if( cg.sList[ i ].time + cg_wwSmoothTime.integer < cg.time )
+    {
+      //copy to array and stop
+      VectorCopy( rotAxis, cg.sList[ i ].rotAxis );
+      cg.sList[ i ].rotAngle = rotAngle;
+      cg.sList[ i ].time = cg.time;
+      cg.sList[ i ].timeMod = timeMod;
+      return;
+    }
+  }
+
+  //no free indices in the smooth array
+}
+
+/*
+===============
+CG_smoothWWTransitions
+===============
+*/
+static void CG_smoothWWTransitions( playerState_t *ps, const vec3_t in, vec3_t out )
+{
+  vec3_t    surfNormal, rotAxis, temp;
+  vec3_t    refNormal     = { 0.0f, 0.0f,  1.0f };
+  vec3_t    ceilingNormal = { 0.0f, 0.0f, -1.0f };
+  int       i;
+  float     stLocal, sFraction, rotAngle;
+  float     smoothTime, timeMod;
+  qboolean  performed = qfalse;
+  vec3_t    inAxis[ 3 ], lastAxis[ 3 ], outAxis[ 3 ];
+
+  if( cg.snap->ps.pm_flags & PMF_FOLLOW )
+  {
+    VectorCopy( in, out );
+    return;
+  }
+
+  //set surfNormal
+  BG_GetClientNormal( ps, surfNormal );
+
+  AnglesToAxis( in, inAxis );
+
+  //if we are moving from one surface to another smooth the transition
+  if( !VectorCompare( surfNormal, cg.lastNormal ) )
+  {
+    //if we moving from the ceiling to the floor special case
+    //( x product of colinear vectors is undefined)
+    if( VectorCompare( ceilingNormal, cg.lastNormal ) &&
+        VectorCompare( refNormal,     surfNormal ) )
+    {
+      AngleVectors( in, temp, NULL, NULL );
+      ProjectPointOnPlane( rotAxis, temp, refNormal );
+      VectorNormalize( rotAxis );
+      rotAngle = 180.0f;
+      timeMod = 1.5f;
+    }
+    else
+    {
+      AnglesToAxis( cg.lastVangles, lastAxis );
+      rotAngle = DotProduct( inAxis[ 0 ], lastAxis[ 0 ] ) +
+                 DotProduct( inAxis[ 1 ], lastAxis[ 1 ] ) +
+                 DotProduct( inAxis[ 2 ], lastAxis[ 2 ] );
+
+      rotAngle = RAD2DEG( acos( ( rotAngle - 1.0f ) / 2.0f ) );
+
+      CrossProduct( lastAxis[ 0 ], inAxis[ 0 ], temp );
+      VectorCopy( temp, rotAxis );
+      CrossProduct( lastAxis[ 1 ], inAxis[ 1 ], temp );
+      VectorAdd( rotAxis, temp, rotAxis );
+      CrossProduct( lastAxis[ 2 ], inAxis[ 2 ], temp );
+      VectorAdd( rotAxis, temp, rotAxis );
+
+      VectorNormalize( rotAxis );
+
+      timeMod = 1.0f;
+    }
+
+    //add the op
+    CG_addSmoothOp( rotAxis, rotAngle, timeMod );
+  }
+
+  //iterate through ops
+  for( i = MAXSMOOTHS - 1; i >= 0; i-- )
+  {
+    smoothTime = (int)( cg_wwSmoothTime.integer * cg.sList[ i ].timeMod );
+
+    //if this op has time remaining, perform it
+    if( cg.time < cg.sList[ i ].time + smoothTime )
+    {
+      stLocal = 1.0f - ( ( ( cg.sList[ i ].time + smoothTime ) - cg.time ) / smoothTime );
+      sFraction = -( cos( stLocal * M_PI ) + 1.0f ) / 2.0f;
+
+      RotatePointAroundVector( outAxis[ 0 ], cg.sList[ i ].rotAxis,
+        inAxis[ 0 ], sFraction * cg.sList[ i ].rotAngle );
+      RotatePointAroundVector( outAxis[ 1 ], cg.sList[ i ].rotAxis,
+        inAxis[ 1 ], sFraction * cg.sList[ i ].rotAngle );
+      RotatePointAroundVector( outAxis[ 2 ], cg.sList[ i ].rotAxis,
+        inAxis[ 2 ], sFraction * cg.sList[ i ].rotAngle );
+
+      AxisCopy( outAxis, inAxis );
+      performed = qtrue;
+    }
+  }
+
+  //if we performed any ops then return the smoothed angles
+  //otherwise simply return the in angles
+  if( performed )
+    AxisToAngles( outAxis, out );
+  else
+    VectorCopy( in, out );
+
+  //copy the current normal to the lastNormal
+  VectorCopy( in, cg.lastVangles );
+  VectorCopy( surfNormal, cg.lastNormal );
+}
+
+/*
+===============
+CG_smoothWJTransitions
+===============
+*/
+static void CG_smoothWJTransitions( playerState_t *ps, const vec3_t in, vec3_t out )
+{
+  int       i;
+  float     stLocal, sFraction;
+  qboolean  performed = qfalse;
+  vec3_t    inAxis[ 3 ], outAxis[ 3 ];
+
+  if( cg.snap->ps.pm_flags & PMF_FOLLOW )
+  {
+    VectorCopy( in, out );
+    return;
+  }
+
+  AnglesToAxis( in, inAxis );
+
+  //iterate through ops
+  for( i = MAXSMOOTHS - 1; i >= 0; i-- )
+  {
+    //if this op has time remaining, perform it
+    if( cg.time < cg.sList[ i ].time + cg_wwSmoothTime.integer )
+    {
+      stLocal = ( ( cg.sList[ i ].time + cg_wwSmoothTime.integer ) - cg.time ) / cg_wwSmoothTime.integer;
+      sFraction = 1.0f - ( ( cos( stLocal * M_PI * 2.0f ) + 1.0f ) / 2.0f );
+
+      RotatePointAroundVector( outAxis[ 0 ], cg.sList[ i ].rotAxis,
+        inAxis[ 0 ], sFraction * cg.sList[ i ].rotAngle );
+      RotatePointAroundVector( outAxis[ 1 ], cg.sList[ i ].rotAxis,
+        inAxis[ 1 ], sFraction * cg.sList[ i ].rotAngle );
+      RotatePointAroundVector( outAxis[ 2 ], cg.sList[ i ].rotAxis,
+        inAxis[ 2 ], sFraction * cg.sList[ i ].rotAngle );
+
+      AxisCopy( outAxis, inAxis );
+      performed = qtrue;
+    }
+  }
+
+  //if we performed any ops then return the smoothed angles
+  //otherwise simply return the in angles
+  if( performed )
+    AxisToAngles( outAxis, out );
+  else
+    VectorCopy( in, out );
+}
+
+
+/*
+===============
+CG_CalcViewValues
+
+Sets cg.refdef view values
+===============
+*/
+static int CG_CalcViewValues( void )
+{
+  playerState_t *ps;
+
+  memset( &cg.refdef, 0, sizeof( cg.refdef ) );
+
+  // calculate size of 3D view
+  CG_CalcVrect( );
+
+  ps = &cg.predictedPlayerState;
+
+  // intermission view
+  if( ps->pm_type == PM_INTERMISSION || ps->pm_type == PM_FREEZE || ps->pm_type == PM_SPECTATOR )
+  {
+    VectorCopy( ps->origin, cg.refdef.vieworg );
+    VectorCopy( ps->viewangles, cg.refdefViewAngles );
+    AnglesToAxis( cg.refdefViewAngles, cg.refdef.viewaxis );
+
+    return CG_CalcFov( );
+  }
+
+  cg.bobcycle = ( ps->bobCycle & 128 ) >> 7;
+  cg.bobfracsin = fabs( sin( ( ps->bobCycle & 127 ) / 127.0 * M_PI ) );
+  cg.xyspeed = sqrt( ps->velocity[ 0 ] * ps->velocity[ 0 ] +
+    ps->velocity[ 1 ] * ps->velocity[ 1 ] );
+
+  // the bob velocity should't get too fast to avoid jerking
+  if( cg.xyspeed > 300.0f )
+    cg.xyspeed = 300.0f;
+
+  VectorCopy( ps->origin, cg.refdef.vieworg );
+
+  if( BG_ClassHasAbility( ps->stats[ STAT_CLASS ], SCA_WALLCLIMBER ) )
+    CG_smoothWWTransitions( ps, ps->viewangles, cg.refdefViewAngles );
+  else if( BG_ClassHasAbility( ps->stats[ STAT_CLASS ], SCA_WALLJUMPER ) )
+    CG_smoothWJTransitions( ps, ps->viewangles, cg.refdefViewAngles );
+  else
+    VectorCopy( ps->viewangles, cg.refdefViewAngles );
+
+  //clumsy logic, but it needs to be this way round because the CS propogation
+  //delay screws things up otherwise
+  if( !BG_ClassHasAbility( ps->stats[ STAT_CLASS ], SCA_WALLJUMPER ) )
+  {
+    if( !( ps->stats[ STAT_STATE ] & SS_WALLCLIMBING ) )
+      VectorSet( cg.lastNormal, 0.0f, 0.0f, 1.0f );
+  }
+
+  // add error decay
+  if( cg_errorDecay.value > 0 )
+  {
+    int   t;
+    float f;
+
+    t = cg.time - cg.predictedErrorTime;
+    f = ( cg_errorDecay.value - t ) / cg_errorDecay.value;
+
+    if( f > 0 && f < 1 )
+      VectorMA( cg.refdef.vieworg, f, cg.predictedError, cg.refdef.vieworg );
+    else
+      cg.predictedErrorTime = 0;
+  }
+
+  //shut off the poison cloud effect if it's still on the go
+  if( cg.snap->ps.stats[ STAT_HEALTH ] <= 0 )
+  {
+    if( CG_IsParticleSystemValid( &cg.poisonCloudPS ) )
+      CG_DestroyParticleSystem( &cg.poisonCloudPS );
+  }
+  else
+    cg.wasDeadLastFrame = qfalse;
+
+  if( cg.renderingThirdPerson )
+  {
+    // back away from character
+    CG_OffsetThirdPersonView( );
+  }
+  else
+  {
+    // offset for local bobbing and kicks
+    CG_OffsetFirstPersonView( );
+  }
+
+  // position eye reletive to origin
+  AnglesToAxis( cg.refdefViewAngles, cg.refdef.viewaxis );
+
+  if( cg.hyperspace )
+    cg.refdef.rdflags |= RDF_NOWORLDMODEL | RDF_HYPERSPACE;
+
+  //draw the surface normal looking at
+  if( cg_drawSurfNormal.integer )
+    CG_DrawSurfNormal( );
+
+  // field of view
+  return CG_CalcFov( );
+}
+
+/*
+=====================
+CG_AddBufferedSound
+=====================
+*/
+void CG_AddBufferedSound( sfxHandle_t sfx )
+{
+  if( !sfx )
+    return;
+
+  cg.soundBuffer[ cg.soundBufferIn ] = sfx;
+  cg.soundBufferIn = ( cg.soundBufferIn + 1 ) % MAX_SOUNDBUFFER;
+
+  if( cg.soundBufferIn == cg.soundBufferOut )
+    cg.soundBufferOut++;
+}
+
+/*
+=====================
+CG_PlayBufferedSounds
+=====================
+*/
+static void CG_PlayBufferedSounds( void )
+{
+  if( cg.soundTime < cg.time )
+  {
+    if( cg.soundBufferOut != cg.soundBufferIn && cg.soundBuffer[ cg.soundBufferOut ] )
+    {
+      trap_S_StartLocalSound( cg.soundBuffer[ cg.soundBufferOut ], CHAN_ANNOUNCER );
+      cg.soundBuffer[ cg.soundBufferOut ] = 0;
+      cg.soundBufferOut = ( cg.soundBufferOut + 1 ) % MAX_SOUNDBUFFER;
+      cg.soundTime = cg.time + 750;
+    }
+  }
+}
+
+//=========================================================================
+
+/*
+=================
+CG_DrawActiveFrame
+
+Generates and draws a game scene and status information at the given time.
+=================
+*/
+void CG_DrawActiveFrame( int serverTime, stereoFrame_t stereoView, qboolean demoPlayback )
+{
+  int   inwater;
+
+  cg.time = serverTime;
+  cg.demoPlayback = demoPlayback;
+
+  // update cvars
+  CG_UpdateCvars( );
+
+  // if we are only updating the screen as a loading
+  // pacifier, don't even try to read snapshots
+  if( cg.infoScreenText[ 0 ] != 0 )
+  {
+    CG_DrawLoadingScreen( );
+    return;
+  }
+
+  // any looped sounds will be respecified as entities
+  // are added to the render list
+  trap_S_ClearLoopingSounds( qfalse );
+
+  // clear all the render lists
+  trap_R_ClearScene( );
+
+  // set up cg.snap and possibly cg.nextSnap
+  CG_ProcessSnapshots( );
+
+  // if we haven't received any snapshots yet, all
+  // we can draw is the information screen
+  if( !cg.snap || ( cg.snap->snapFlags & SNAPFLAG_NOT_ACTIVE ) )
+  {
+    CG_DrawLoadingScreen( );
+    return;
+  }
+
+  // let the client system know what our weapon and zoom settings are
+  trap_SetUserCmdValue( cg.weaponSelect, cg.zoomSensitivity );
+
+  // this counter will be bumped for every valid scene we generate
+  cg.clientFrame++;
+
+  // update cg.predictedPlayerState
+  CG_PredictPlayerState( );
+
+  // decide on third person view
+  cg.renderingThirdPerson = ( cg_thirdPerson.integer || ( cg.snap->ps.stats[ STAT_HEALTH ] <= 0 ) || 
+                            ( cg.chaseFollow && cg.snap->ps.pm_flags & PMF_FOLLOW) );
+
+  // update speedometer
+  CG_AddSpeed( );
+
+  // build cg.refdef
+  inwater = CG_CalcViewValues( );
+
+  // build the render lists
+  if( !cg.hyperspace )
+  {
+    CG_AddPacketEntities( );     // after calcViewValues, so predicted player state is correct
+    CG_AddMarks( );
+  }
+
+  CG_AddViewWeapon( &cg.predictedPlayerState );
+
+  //after CG_AddViewWeapon
+  if( !cg.hyperspace )
+  {
+    CG_AddParticles( );
+    CG_AddTrails( );
+  }
+
+  // add buffered sounds
+  CG_PlayBufferedSounds( );
+
+  // finish up the rest of the refdef
+  if( cg.testModelEntity.hModel )
+    CG_AddTestModel( );
+
+  cg.refdef.time = cg.time;
+  memcpy( cg.refdef.areamask, cg.snap->areamask, sizeof( cg.refdef.areamask ) );
+
+  //remove expired console lines
+  if( cg.consoleLines[ 0 ].time + cg_consoleLatency.integer < cg.time && cg_consoleLatency.integer > 0 )
+    CG_RemoveNotifyLine( );
+
+  // update audio positions
+  trap_S_Respatialize( cg.snap->ps.clientNum, cg.refdef.vieworg, cg.refdef.viewaxis, inwater );
+
+  // make sure the lagometerSample and frame timing isn't done twice when in stereo
+  if( stereoView != STEREO_RIGHT )
+  {
+    cg.frametime = cg.time - cg.oldTime;
+
+    if( cg.frametime < 0 )
+      cg.frametime = 0;
+
+    cg.oldTime = cg.time;
+    CG_AddLagometerFrameInfo( );
+  }
+
+  if( cg_timescale.value != cg_timescaleFadeEnd.value )
+  {
+    if( cg_timescale.value < cg_timescaleFadeEnd.value )
+    {
+      cg_timescale.value += cg_timescaleFadeSpeed.value * ( (float)cg.frametime ) / 1000;
+      if( cg_timescale.value > cg_timescaleFadeEnd.value )
+        cg_timescale.value = cg_timescaleFadeEnd.value;
+    }
+    else
+    {
+      cg_timescale.value -= cg_timescaleFadeSpeed.value * ( (float)cg.frametime ) / 1000;
+      if( cg_timescale.value < cg_timescaleFadeEnd.value )
+        cg_timescale.value = cg_timescaleFadeEnd.value;
+    }
+
+    if( cg_timescaleFadeSpeed.value )
+      trap_Cvar_Set( "timescale", va( "%f", cg_timescale.value ) );
+  }
+
+  // actually issue the rendering calls
+  CG_DrawActive( stereoView );
+
+  if( cg_stats.integer )
+    CG_Printf( "cg.clientFrame:%i\n", cg.clientFrame );
+}
+
diff --git a/src/cgame/cg_weapons.c b/src/cgame/cg_weapons.c
new file mode 100644
index 0000000..2b41d13
--- /dev/null
+++ b/src/cgame/cg_weapons.c
@@ -0,0 +1,2106 @@
+/*
+===========================================================================
+Copyright (C) 1999-2005 Id Software, Inc.
+Copyright (C) 2000-2009 Darklegion Development
+
+This file is part of Tremulous.
+
+Tremulous 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.
+
+Tremulous 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 Tremulous; if not, write to the Free Software
+Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
+===========================================================================
+*/
+
+// cg_weapons.c -- events and effects dealing with weapons
+
+
+#include "cg_local.h"
+
+/*
+=================
+CG_RegisterUpgrade
+
+The server says this item is used on this level
+=================
+*/
+void CG_RegisterUpgrade( int upgradeNum )
+{
+  upgradeInfo_t   *upgradeInfo;
+  char            *icon;
+
+  if( upgradeNum <= UP_NONE || upgradeNum >= UP_NUM_UPGRADES )
+  {
+    CG_Error( "CG_RegisterUpgrade: out of range: %d", upgradeNum );
+    return;
+  }
+
+  upgradeInfo = &cg_upgrades[ upgradeNum ];
+
+  if( upgradeInfo->registered )
+  {
+    CG_Printf( "CG_RegisterUpgrade: already registered: (%d) %s\n", upgradeNum,
+      BG_Upgrade( upgradeNum )->name );
+    return;
+  }
+
+  upgradeInfo->registered = qtrue;
+
+  if( !BG_Upgrade( upgradeNum )->name[ 0 ] )
+    CG_Error( "Couldn't find upgrade %i", upgradeNum );
+
+  upgradeInfo->humanName = BG_Upgrade( upgradeNum )->humanName;
+
+  //la la la la la, i'm not listening!
+  if( upgradeNum == UP_GRENADE )
+    upgradeInfo->upgradeIcon = cg_weapons[ WP_GRENADE ].weaponIcon;
+  else if( upgradeNum == UP_MINE )
+    upgradeInfo->upgradeIcon = cg_weapons[ WP_MINE ].weaponIcon;
+  else if( upgradeNum == UP_SMOKE )
+    upgradeInfo->upgradeIcon = cg_weapons[ WP_SMOKE ].weaponIcon;
+  else if( ( icon = BG_Upgrade( upgradeNum )->icon ) )
+    upgradeInfo->upgradeIcon = trap_R_RegisterShader( icon );
+}
+
+/*
+===============
+CG_InitUpgrades
+
+Precaches upgrades
+===============
+*/
+void CG_InitUpgrades( void )
+{
+  int   i;
+
+  Com_Memset( cg_upgrades, 0, sizeof( cg_upgrades ) );
+
+  for( i = UP_NONE + 1; i < UP_NUM_UPGRADES; i++ )
+    CG_RegisterUpgrade( i );
+}
+
+
+/*
+===============
+CG_ParseWeaponModeSection
+
+Parse a weapon mode section
+===============
+*/
+static qboolean CG_ParseWeaponModeSection( weaponInfoMode_t *wim, char **text_p )
+{
+  char  *token;
+  int   i;
+
+  // read optional parameters
+  while( 1 )
+  {
+    token = COM_Parse( text_p );
+
+    if( !token )
+      break;
+
+    if( !Q_stricmp( token, "" ) )
+      return qfalse;
+
+    if( !Q_stricmp( token, "missileModel" ) )
+    {
+      token = COM_Parse( text_p );
+      if( !token )
+        break;
+
+      wim->missileModel = trap_R_RegisterModel( token );
+
+      if( !wim->missileModel )
+        CG_Printf( S_COLOR_RED "ERROR: missile model not found %s\n", token );
+
+      continue;
+    }
+    else if( !Q_stricmp( token, "missileSprite" ) )
+    {
+      int size = 0;
+
+      token = COM_Parse( text_p );
+      if( !token )
+        break;
+
+      size = atoi( token );
+
+      if( size < 0 )
+        size = 0;
+
+      token = COM_Parse( text_p );
+      if( !token )
+        break;
+
+      wim->missileSprite = trap_R_RegisterShader( token );
+      wim->missileSpriteSize = size;
+      wim->usesSpriteMissle = qtrue;
+
+      if( !wim->missileSprite )
+        CG_Printf( S_COLOR_RED "ERROR: missile sprite not found %s\n", token );
+
+      continue;
+    }
+    else if( !Q_stricmp( token, "missileSpriteCharge" ) )
+    {
+      token = COM_Parse( text_p );
+      if( !token )
+        break;
+
+      wim->missileSpriteCharge = atof( token );
+
+      continue;
+    }
+    else if( !Q_stricmp( token, "missileRotates" ) )
+    {
+      wim->missileRotates = qtrue;
+
+      continue;
+    }
+    else if( !Q_stricmp( token, "missileAnimates" ) )
+    {
+      wim->missileAnimates = qtrue;
+
+      token = COM_Parse( text_p );
+      if( !token )
+        break;
+
+      wim->missileAnimStartFrame = atoi( token );
+
+      token = COM_Parse( text_p );
+      if( !token )
+        break;
+
+      wim->missileAnimNumFrames = atoi( token );
+
+      token = COM_Parse( text_p );
+      if( !token )
+        break;
+
+      wim->missileAnimFrameRate = atoi( token );
+
+      token = COM_Parse( text_p );
+      if( !token )
+        break;
+
+      wim->missileAnimLooping = atoi( token );
+
+      continue;
+    }
+    else if( !Q_stricmp( token, "missileParticleSystem" ) )
+    {
+      token = COM_Parse( text_p );
+      if( !token )
+        break;
+
+      wim->missileParticleSystem = CG_RegisterParticleSystem( token );
+
+      if( !wim->missileParticleSystem )
+        CG_Printf( S_COLOR_RED "ERROR: missile particle system not found %s\n", token );
+
+      continue;
+    }
+    else if( !Q_stricmp( token, "missileTrailSystem" ) )
+    {
+      token = COM_Parse( text_p );
+      if( !token )
+        break;
+
+      wim->missileTrailSystem = CG_RegisterTrailSystem( token );
+
+      if( !wim->missileTrailSystem )
+        CG_Printf( S_COLOR_RED "ERROR: missile trail system not found %s\n", token );
+
+      continue;
+    }
+    else if( !Q_stricmp( token, "muzzleParticleSystem" ) )
+    {
+      token = COM_Parse( text_p );
+      if( !token )
+        break;
+
+      wim->muzzleParticleSystem = CG_RegisterParticleSystem( token );
+
+      if( !wim->muzzleParticleSystem )
+        CG_Printf( S_COLOR_RED "ERROR: muzzle particle system not found %s\n", token );
+
+      continue;
+    }
+    else if( !Q_stricmp( token, "impactParticleSystem" ) )
+    {
+      token = COM_Parse( text_p );
+      if( !token )
+        break;
+
+      wim->impactParticleSystem = CG_RegisterParticleSystem( token );
+
+      if( !wim->impactParticleSystem )
+        CG_Printf( S_COLOR_RED "ERROR: impact particle system not found %s\n", token );
+
+      continue;
+    }
+    else if( !Q_stricmp( token, "impactMark" ) )
+    {
+      int size = 0;
+
+      token = COM_Parse( text_p );
+      if( !token )
+        break;
+
+      size = atoi( token );
+
+      if( size < 0 )
+        size = 0;
+
+      token = COM_Parse( text_p );
+      if( !token )
+        break;
+
+      wim->impactMark = trap_R_RegisterShader( token );
+      wim->impactMarkSize = size;
+
+      if( !wim->impactMark )
+        CG_Printf( S_COLOR_RED "ERROR: impact mark shader not found %s\n", token );
+
+      continue;
+    }
+    else if( !Q_stricmp( token, "impactSound" ) )
+    {
+      int index = 0;
+
+      token = COM_Parse( text_p );
+      if( !token )
+        break;
+
+      index = atoi( token );
+
+      if( index < 0 )
+        index = 0;
+      else if( index > 3 )
+        index = 3;
+
+      token = COM_Parse( text_p );
+      if( !token )
+        break;
+
+      wim->impactSound[ index ] = trap_S_RegisterSound( token, qfalse );
+
+      continue;
+    }
+    else if( !Q_stricmp( token, "impactFleshSound" ) )
+    {
+      int index = 0;
+
+      token = COM_Parse( text_p );
+      if( !token )
+        break;
+
+      index = atoi( token );
+
+      if( index < 0 )
+        index = 0;
+      else if( index > 3 )
+        index = 3;
+
+      token = COM_Parse( text_p );
+      if( !token )
+        break;
+
+      wim->impactFleshSound[ index ] = trap_S_RegisterSound( token, qfalse );
+
+      continue;
+    }
+    else if( !Q_stricmp( token, "alwaysImpact" ) )
+    {
+      wim->alwaysImpact = qtrue;
+
+      continue;
+    }
+    else if( !Q_stricmp( token, "flashDLightColor" ) )
+    {
+      for( i = 0 ; i < 3 ; i++ )
+      {
+        token = COM_Parse( text_p );
+        if( !token )
+          break;
+
+        wim->flashDlightColor[ i ] = atof( token );
+      }
+
+      continue;
+    }
+    else if( !Q_stricmp( token, "continuousFlash" ) )
+    {
+      wim->continuousFlash = qtrue;
+
+      continue;
+    }
+    else if( !Q_stricmp( token, "missileDlightColor" ) )
+    {
+      for( i = 0 ; i < 3 ; i++ )
+      {
+        token = COM_Parse( text_p );
+        if( !token )
+          break;
+
+        wim->missileDlightColor[ i ] = atof( token );
+      }
+
+      continue;
+    }
+    else if( !Q_stricmp( token, "missileDlight" ) )
+    {
+      int size = 0;
+
+      token = COM_Parse( text_p );
+      if( !token )
+        break;
+
+      size = atoi( token );
+
+      if( size < 0 )
+        size = 0;
+
+      wim->missileDlight = size;
+
+      continue;
+    }
+    else if( !Q_stricmp( token, "firingSound" ) )
+    {
+      token = COM_Parse( text_p );
+      if( !token )
+        break;
+
+      wim->firingSound = trap_S_RegisterSound( token, qfalse );
+
+      continue;
+    }
+    else if( !Q_stricmp( token, "missileSound" ) )
+    {
+      token = COM_Parse( text_p );
+      if( !token )
+        break;
+
+      wim->missileSound = trap_S_RegisterSound( token, qfalse );
+
+      continue;
+    }
+    else if( !Q_stricmp( token, "flashSound" ) )
+    {
+      int index = 0;
+
+      token = COM_Parse( text_p );
+      if( !token )
+        break;
+
+      index = atoi( token );
+
+      if( index < 0 )
+        index = 0;
+      else if( index > 3 )
+        index = 3;
+
+      token = COM_Parse( text_p );
+      if( !token )
+        break;
+
+      wim->flashSound[ index ] = trap_S_RegisterSound( token, qfalse );
+
+      continue;
+    }
+    else if( !Q_stricmp( token, "}" ) )
+      return qtrue; //reached the end of this weapon section
+    else
+    {
+      CG_Printf( S_COLOR_RED "ERROR: unknown token '%s' in weapon section\n", token );
+      return qfalse;
+    }
+  }
+
+  return qfalse;
+}
+
+
+/*
+======================
+CG_ParseWeaponFile
+
+Parses a configuration file describing a weapon
+======================
+*/
+static qboolean CG_ParseWeaponFile( const char *filename, weaponInfo_t *wi )
+{
+  char          *text_p;
+  int           len;
+  char          *token;
+  char          text[ 20000 ];
+  fileHandle_t  f;
+  weaponMode_t  weaponMode = WPM_NONE;
+
+  // load the file
+  len = trap_FS_FOpenFile( filename, &f, FS_READ );
+  if( len < 0 )
+    return qfalse;
+
+  if( len == 0 || len >= sizeof( text ) - 1 )
+  {
+    trap_FS_FCloseFile( f );
+    CG_Printf( "File %s is %s\n", filename, len == 0 ? "empty" : "too long" );
+    return qfalse;
+  }
+
+  trap_FS_Read( text, len, f );
+  text[ len ] = 0;
+  trap_FS_FCloseFile( f );
+
+  // parse the text
+  text_p = text;
+
+  // read optional parameters
+  while( 1 )
+  {
+    token = COM_Parse( &text_p );
+
+    if( !token )
+      break;
+
+    if( !Q_stricmp( token, "" ) )
+      break;
+
+    if( !Q_stricmp( token, "{" ) )
+    {
+      if( weaponMode == WPM_NONE )
+      {
+        CG_Printf( S_COLOR_RED "ERROR: weapon mode section started without a declaration\n" );
+        return qfalse;
+      }
+      else if( !CG_ParseWeaponModeSection( &wi->wim[ weaponMode ], &text_p ) )
+      {
+        CG_Printf( S_COLOR_RED "ERROR: failed to parse weapon mode section\n" );
+        return qfalse;
+      }
+
+      //start parsing ejectors again
+      weaponMode = WPM_NONE;
+
+      continue;
+    }
+    else if( !Q_stricmp( token, "primary" ) )
+    {
+      weaponMode = WPM_PRIMARY;
+      continue;
+    }
+    else if( !Q_stricmp( token, "secondary" ) )
+    {
+      weaponMode = WPM_SECONDARY;
+      continue;
+    }
+    else if( !Q_stricmp( token, "tertiary" ) )
+    {
+      weaponMode = WPM_TERTIARY;
+      continue;
+    }
+    else if( !Q_stricmp( token, "weaponModel" ) )
+    {
+      char path[ MAX_QPATH ];
+
+      token = COM_Parse( &text_p );
+      if( !token )
+        break;
+
+      wi->weaponModel = trap_R_RegisterModel( token );
+
+      if( !wi->weaponModel )
+        CG_Printf( S_COLOR_RED "ERROR: weapon model not found %s\n", token );
+
+      strcpy( path, token );
+      COM_StripExtension( path, path, MAX_QPATH );
+      strcat( path, "_flash.md3" );
+      wi->flashModel = trap_R_RegisterModel( path );
+
+      strcpy( path, token );
+      COM_StripExtension( path, path, MAX_QPATH );
+      strcat( path, "_barrel.md3" );
+      wi->barrelModel = trap_R_RegisterModel( path );
+
+      strcpy( path, token );
+      COM_StripExtension( path, path, MAX_QPATH );
+      strcat( path, "_hand.md3" );
+      wi->handsModel = trap_R_RegisterModel( path );
+
+      continue;
+    }
+    else if( !Q_stricmp( token, "weaponModel3rdPerson" ) )
+    {
+      char path[ MAX_QPATH ];
+
+      token = COM_Parse( &text_p );
+      if( !token )
+        break;
+
+      wi->weaponModel3rdPerson = trap_R_RegisterModel( token );
+
+      if( !wi->weaponModel3rdPerson )
+      {
+        CG_Printf( S_COLOR_RED "ERROR: 3rd person weapon "
+            "model not found %s\n", token );
+      }
+
+      strcpy( path, token );
+      COM_StripExtension( path, path, MAX_QPATH );
+      strcat( path, "_flash.md3" );
+      wi->flashModel3rdPerson = trap_R_RegisterModel( path );
+
+      strcpy( path, token );
+      COM_StripExtension( path, path, MAX_QPATH );
+      strcat( path, "_barrel.md3" );
+      wi->barrelModel3rdPerson = trap_R_RegisterModel( path );
+
+      continue;
+    }
+    else if( !Q_stricmp( token, "idleSound" ) )
+    {
+      token = COM_Parse( &text_p );
+      if( !token )
+        break;
+
+      wi->readySound = trap_S_RegisterSound( token, qfalse );
+
+      continue;
+    }
+    else if( !Q_stricmp( token, "icon" ) )
+    {
+      token = COM_Parse( &text_p );
+      if( !token )
+        break;
+
+      wi->weaponIcon = wi->ammoIcon = trap_R_RegisterShader( token );
+
+      if( !wi->weaponIcon )
+        CG_Printf( S_COLOR_RED "ERROR: weapon icon not found %s\n", token );
+
+      continue;
+    }
+    else if( !Q_stricmp( token, "crosshair" ) )
+    {
+      int size = 0;
+
+      token = COM_Parse( &text_p );
+      if( !token )
+        break;
+
+      size = atoi( token );
+
+      if( size < 0 )
+        size = 0;
+
+      token = COM_Parse( &text_p );
+      if( !token )
+        break;
+
+      wi->crossHair = trap_R_RegisterShader( token );
+      wi->crossHairSize = size;
+
+      if( !wi->crossHair )
+        CG_Printf( S_COLOR_RED "ERROR: weapon crosshair not found %s\n", token );
+
+      continue;
+    }
+    else if( !Q_stricmp( token, "disableIn3rdPerson" ) )
+    {
+      wi->disableIn3rdPerson = qtrue;
+
+      continue;
+    }
+
+    Com_Printf( S_COLOR_RED "ERROR: unknown token '%s'\n", token );
+    return qfalse;
+  }
+
+  return qtrue;
+}
+
+/*
+=================
+CG_RegisterWeapon
+=================
+*/
+void CG_RegisterWeapon( int weaponNum )
+{
+  weaponInfo_t  *weaponInfo;
+  char          path[ MAX_QPATH ];
+  vec3_t        mins, maxs;
+  int           i;
+
+  if( weaponNum <= WP_NONE || weaponNum >= WP_NUM_WEAPONS )
+  {
+    CG_Error( "CG_RegisterWeapon: out of range: %d", weaponNum );
+    return;
+  }
+
+  weaponInfo = &cg_weapons[ weaponNum ];
+
+  if( weaponInfo->registered )
+  {
+    CG_Printf( "CG_RegisterWeapon: already registered: (%d) %s\n", weaponNum,
+      BG_Weapon( weaponNum )->name );
+    return;
+  }
+
+  weaponInfo->registered = qtrue;
+
+  if( !BG_Weapon( weaponNum )->name[ 0 ] )
+    CG_Error( "Couldn't find weapon %i", weaponNum );
+
+  Com_sprintf( path, MAX_QPATH, "models/weapons/%s/weapon.cfg", BG_Weapon( weaponNum )->name );
+
+  weaponInfo->humanName = BG_Weapon( weaponNum )->humanName;
+
+  if( !CG_ParseWeaponFile( path, weaponInfo ) )
+    Com_Printf( S_COLOR_RED "ERROR: failed to parse %s\n", path );
+
+  // calc midpoint for rotation
+  trap_R_ModelBounds( weaponInfo->weaponModel, mins, maxs );
+  for( i = 0 ; i < 3 ; i++ )
+    weaponInfo->weaponMidpoint[ i ] = mins[ i ] + 0.5 * ( maxs[ i ] - mins[ i ] );
+}
+
+/*
+===============
+CG_InitWeapons
+
+Precaches weapons
+===============
+*/
+void CG_InitWeapons( void )
+{
+  int   i;
+
+  Com_Memset( cg_weapons, 0, sizeof( cg_weapons ) );
+
+  for( i = WP_NONE + 1; i < WP_NUM_WEAPONS; i++ )
+    CG_RegisterWeapon( i );
+
+  cgs.media.level2ZapTS = CG_RegisterTrailSystem( "models/weapons/lev2zap/lightning" );
+}
+
+
+/*
+========================================================================================
+
+VIEW WEAPON
+
+========================================================================================
+*/
+
+/*
+===============
+CG_SetWeaponLerpFrameAnimation
+
+may include ANIM_TOGGLEBIT
+===============
+*/
+static void CG_SetWeaponLerpFrameAnimation( weapon_t weapon, lerpFrame_t *lf, int newAnimation )
+{
+  animation_t *anim;
+
+  lf->animationNumber = newAnimation;
+  newAnimation &= ~ANIM_TOGGLEBIT;
+
+  if( newAnimation < 0 || newAnimation >= MAX_WEAPON_ANIMATIONS )
+    CG_Error( "Bad animation number: %i", newAnimation );
+
+  anim = &cg_weapons[ weapon ].animations[ newAnimation ];
+
+  lf->animation = anim;
+  lf->animationTime = lf->frameTime + anim->initialLerp;
+
+  if( cg_debugAnim.integer )
+    CG_Printf( "Anim: %i\n", newAnimation );
+}
+
+/*
+===============
+CG_WeaponAnimation
+===============
+*/
+static void CG_WeaponAnimation( centity_t *cent, int *old, int *now, float *backLerp )
+{
+  lerpFrame_t   *lf = &cent->pe.weapon;
+  entityState_t *es = &cent->currentState;
+
+  // see if the animation sequence is switching
+  if( es->weaponAnim != lf->animationNumber || !lf->animation )
+    CG_SetWeaponLerpFrameAnimation( es->weapon, lf, es->weaponAnim );
+
+  CG_RunLerpFrame( lf, 1.0f );
+
+  *old      = lf->oldFrame;
+  *now      = lf->frame;
+  *backLerp = lf->backlerp;
+}
+
+/*
+=================
+CG_MapTorsoToWeaponFrame
+
+=================
+*/
+static int CG_MapTorsoToWeaponFrame( clientInfo_t *ci, int frame )
+{
+
+  // change weapon
+  if( frame >= ci->animations[ TORSO_DROP ].firstFrame &&
+      frame < ci->animations[ TORSO_DROP ].firstFrame + 9 )
+    return frame - ci->animations[ TORSO_DROP ].firstFrame + 6;
+
+  // stand attack
+  if( frame >= ci->animations[ TORSO_ATTACK ].firstFrame &&
+      frame < ci->animations[ TORSO_ATTACK ].firstFrame + 6 )
+    return 1 + frame - ci->animations[ TORSO_ATTACK ].firstFrame;
+
+  // stand attack 2
+  if( frame >= ci->animations[ TORSO_ATTACK2 ].firstFrame &&
+      frame < ci->animations[ TORSO_ATTACK2 ].firstFrame + 6 )
+    return 1 + frame - ci->animations[ TORSO_ATTACK2 ].firstFrame;
+
+  return 0;
+}
+
+
+/*
+==============
+CG_CalculateWeaponPosition
+==============
+*/
+static void CG_CalculateWeaponPosition( vec3_t origin, vec3_t angles )
+{
+  float         scale;
+  int           delta;
+  float         fracsin;
+  float         bob;
+  weaponInfo_t  *weapon;
+
+  weapon = &cg_weapons[ cg.predictedPlayerState.weapon ];
+
+  VectorCopy( cg.refdef.vieworg, origin );
+  VectorCopy( cg.refdefViewAngles, angles );
+
+  // on odd legs, invert some angles
+  if( cg.bobcycle & 1 )
+    scale = -cg.xyspeed;
+  else
+    scale = cg.xyspeed;
+
+  // gun angles from bobbing
+  // bob amount is class dependant
+  bob = BG_Class( cg.predictedPlayerState.stats[ STAT_CLASS ] )->bob;
+
+  if( bob != 0 )
+  {
+    angles[ ROLL ] += scale * cg.bobfracsin * 0.005;
+    angles[ YAW ] += scale * cg.bobfracsin * 0.01;
+    angles[ PITCH ] += cg.xyspeed * cg.bobfracsin * 0.005;
+  }
+
+  // drop the weapon when landing
+  if( !weapon->noDrift )
+  {
+    delta = cg.time - cg.landTime;
+    if( delta < LAND_DEFLECT_TIME )
+      origin[ 2 ] += cg.landChange*0.25 * delta / LAND_DEFLECT_TIME;
+    else if( delta < LAND_DEFLECT_TIME + LAND_RETURN_TIME )
+      origin[ 2 ] += cg.landChange*0.25 *
+        ( LAND_DEFLECT_TIME + LAND_RETURN_TIME - delta ) / LAND_RETURN_TIME;
+
+    // idle drift
+    scale = cg.xyspeed + 40;
+    fracsin = sin( cg.time * 0.001 );
+    angles[ ROLL ] += scale * fracsin * 0.01;
+    angles[ YAW ] += scale * fracsin * 0.01;
+    angles[ PITCH ] += scale * fracsin * 0.01;
+  }
+}
+
+
+/*
+======================
+CG_MachinegunSpinAngle
+======================
+*/
+#define   SPIN_SPEED  0.9
+#define   COAST_TIME  1000
+static float CG_MachinegunSpinAngle( centity_t *cent, qboolean firing )
+{
+  int   delta;
+  float angle;
+  float speed;
+
+  delta = cg.time - cent->pe.barrelTime;
+  if( cent->pe.barrelSpinning )
+    angle = cent->pe.barrelAngle + delta * SPIN_SPEED;
+  else
+  {
+    if( delta > COAST_TIME )
+      delta = COAST_TIME;
+
+    speed = 0.5 * ( SPIN_SPEED + (float)( COAST_TIME - delta ) / COAST_TIME );
+    angle = cent->pe.barrelAngle + delta * speed;
+  }
+
+  if( cent->pe.barrelSpinning == !firing )
+  {
+    cent->pe.barrelTime = cg.time;
+    cent->pe.barrelAngle = AngleMod( angle );
+    cent->pe.barrelSpinning = firing;
+  }
+
+  return angle;
+}
+
+
+/*
+=============
+CG_AddPlayerWeapon
+
+Used for both the view weapon (ps is valid) and the world modelother character models (ps is NULL)
+The main player will have this called for BOTH cases, so effects like light and
+sound should only be done on the world model case.
+=============
+*/
+void CG_AddPlayerWeapon( refEntity_t *parent, playerState_t *ps, centity_t *cent )
+{
+  refEntity_t   gun;
+  refEntity_t   barrel;
+  refEntity_t   flash;
+  vec3_t        angles;
+  weapon_t      weaponNum;
+  weaponMode_t  weaponMode;
+  weaponInfo_t  *weapon;
+  qboolean      noGunModel;
+  qboolean      firing;
+
+  weaponNum = cent->currentState.weapon;
+  weaponMode = cent->currentState.generic1;
+
+  if( weaponMode <= WPM_NONE || weaponMode >= WPM_NUM_WEAPONMODES )
+    weaponMode = WPM_PRIMARY;
+
+  if( ( ( cent->currentState.eFlags & EF_FIRING ) && weaponMode == WPM_PRIMARY ) ||
+      ( ( cent->currentState.eFlags & EF_FIRING2 ) && weaponMode == WPM_SECONDARY ) ||
+      ( ( cent->currentState.eFlags & EF_FIRING3 ) && weaponMode == WPM_TERTIARY ) )
+    firing = qtrue;
+  else
+    firing = qfalse;
+
+  weapon = &cg_weapons[ weaponNum ];
+  if( !weapon->registered )
+  {
+    Com_Printf( S_COLOR_YELLOW "WARNING: CG_AddPlayerWeapon: weapon %d (%s) "
+        "is not registered\n", weaponNum, BG_Weapon( weaponNum )->name );
+    return;
+  }
+
+  // add the weapon
+  Com_Memset( &gun, 0, sizeof( gun ) );
+  Com_Memset( &barrel, 0, sizeof( barrel ) );
+  Com_Memset( &flash, 0, sizeof( flash ) );
+
+  VectorCopy( parent->lightingOrigin, gun.lightingOrigin );
+  gun.shadowPlane = parent->shadowPlane;
+  gun.renderfx = parent->renderfx;
+
+  if( ps )
+  {
+    gun.shaderRGBA[ 0 ] = 255;
+    gun.shaderRGBA[ 1 ] = 255;
+    gun.shaderRGBA[ 2 ] = 255;
+    gun.shaderRGBA[ 3 ] = 255;
+
+    //set weapon[1/2]Time when respective buttons change state
+    if( cg.weapon1Firing != ( cg.predictedPlayerState.eFlags & EF_FIRING ) )
+    {
+      cg.weapon1Time = cg.time;
+      cg.weapon1Firing = ( cg.predictedPlayerState.eFlags & EF_FIRING );
+    }
+
+    if( cg.weapon2Firing != ( cg.predictedPlayerState.eFlags & EF_FIRING2 ) )
+    {
+      cg.weapon2Time = cg.time;
+      cg.weapon2Firing = ( cg.predictedPlayerState.eFlags & EF_FIRING2 );
+    }
+
+    if( cg.weapon3Firing != ( cg.predictedPlayerState.eFlags & EF_FIRING3 ) )
+    {
+      cg.weapon3Time = cg.time;
+      cg.weapon3Firing = ( cg.predictedPlayerState.eFlags & EF_FIRING3 );
+    }
+  }
+
+  if( !ps )
+  {
+    gun.hModel = weapon->weaponModel3rdPerson;
+
+    if( !gun.hModel )
+      gun.hModel = weapon->weaponModel;
+  }
+  else
+    gun.hModel = weapon->weaponModel;
+
+  noGunModel = ( ( !ps || cg.renderingThirdPerson ) && weapon->disableIn3rdPerson ) || !gun.hModel;
+
+  if( !ps )
+  {
+    // add weapon ready sound
+    if( firing && weapon->wim[ weaponMode ].firingSound )
+    {
+      trap_S_AddLoopingSound( cent->currentState.number, cent->lerpOrigin, vec3_origin,
+                              weapon->wim[ weaponMode ].firingSound );
+    }
+    else if( weapon->readySound )
+      trap_S_AddLoopingSound( cent->currentState.number, cent->lerpOrigin, vec3_origin, weapon->readySound );
+  }
+
+  // Lucifer cannon charge warning beep
+  if( (weaponNum == WP_LUCIFER_CANNON ) &&
+
+      ( cent->currentState.eFlags & EF_WARN_CHARGE ) &&
+      cg.snap->ps.stats[ STAT_TEAM ] != TEAM_ALIENS )
+  {
+    trap_S_AddLoopingSound( cent->currentState.number, cent->lerpOrigin,
+                            vec3_origin, ps ? cgs.media.lCannonWarningSound :
+                                              cgs.media.lCannonWarningSound2 );
+  }
+  if( (weaponNum == WP_FLAMER) &&
+
+      ( cent->currentState.eFlags & EF_WARN_CHARGE ) &&
+      cg.snap->ps.stats[ STAT_TEAM ] != TEAM_ALIENS )
+  {
+    trap_S_AddLoopingSound( cent->currentState.number, cent->lerpOrigin,
+                            vec3_origin, ps ? cgs.media.FlamerWarningSound :
+                                              cgs.media.FlamerWarningSound2 );
+  }
+  if( !noGunModel )
+  {
+    CG_PositionEntityOnTag( &gun, parent, parent->hModel, "tag_weapon" );
+
+    if( cent->currentState.eFlags & EF_MOVER_STOP )
+    {
+      if( cg.snap->ps.stats[ STAT_TEAM ] != TEAM_HUMANS )
+        gun.customShader = cgs.media.invisibleShader;
+      else
+        gun.customShader = cgs.media.invisibleShaderTeam;
+    }
+
+    CG_WeaponAnimation( cent, &gun.oldframe, &gun.frame, &gun.backlerp );
+
+    trap_R_AddRefEntityToScene( &gun );
+
+    if( !ps )
+    {
+      barrel.hModel = weapon->barrelModel3rdPerson;
+
+      if( !barrel.hModel )
+        barrel.hModel = weapon->barrelModel;
+    }
+    else
+      barrel.hModel = weapon->barrelModel;
+
+    // add the spinning barrel
+    if( barrel.hModel )
+    {
+      VectorCopy( parent->lightingOrigin, barrel.lightingOrigin );
+      barrel.shadowPlane = parent->shadowPlane;
+      barrel.renderfx = parent->renderfx;
+
+      angles[ YAW ] = 0;
+      angles[ PITCH ] = 0;
+      angles[ ROLL ] = CG_MachinegunSpinAngle( cent, firing );
+      AnglesToAxis( angles, barrel.axis );
+
+      CG_PositionRotatedEntityOnTag( &barrel, &gun, gun.hModel, "tag_barrel" );
+
+      if( cent->currentState.eFlags & EF_MOVER_STOP )
+      {
+        if( cg.snap->ps.stats[ STAT_TEAM ] != TEAM_HUMANS )
+          barrel.customShader = cgs.media.invisibleShader;
+        else
+          barrel.customShader = cgs.media.invisibleShaderTeam;
+      }
+
+      trap_R_AddRefEntityToScene( &barrel );
+    }
+  }
+
+  if( CG_IsParticleSystemValid( &cent->muzzlePS ) )
+  {
+    if( ps || cg.renderingThirdPerson ||
+        cent->currentState.number != cg.predictedPlayerState.clientNum )
+    {
+      if( noGunModel )
+        CG_SetAttachmentTag( &cent->muzzlePS->attachment, *parent, parent->hModel, "tag_weapon" );
+      else
+        CG_SetAttachmentTag( &cent->muzzlePS->attachment, gun, gun.hModel, "tag_flash" );
+    }
+
+    //if the PS is infinite disable it when not firing
+    if( !firing && CG_IsParticleSystemInfinite( cent->muzzlePS ) )
+      CG_DestroyParticleSystem( &cent->muzzlePS );
+  }
+
+  // add the flash
+  if( !weapon->wim[ weaponMode ].continuousFlash || !firing )
+  {
+    // impulse flash
+    if( cg.time - cent->muzzleFlashTime > MUZZLE_FLASH_TIME )
+      return;
+  }
+
+  VectorCopy( parent->lightingOrigin, flash.lightingOrigin );
+  flash.shadowPlane = parent->shadowPlane;
+  flash.renderfx = parent->renderfx;
+
+  if( !ps )
+  {
+    flash.hModel = weapon->flashModel3rdPerson;
+
+    if( !flash.hModel )
+      flash.hModel = weapon->flashModel;
+  }
+  else
+    flash.hModel = weapon->flashModel;
+
+  if( flash.hModel )
+  {
+    angles[ YAW ] = 0;
+    angles[ PITCH ] = 0;
+    angles[ ROLL ] = crandom( ) * 10;
+    AnglesToAxis( angles, flash.axis );
+
+    if( noGunModel )
+      CG_PositionRotatedEntityOnTag( &flash, parent, parent->hModel, "tag_weapon" );
+    else
+      CG_PositionRotatedEntityOnTag( &flash, &gun, gun.hModel, "tag_flash" );
+
+    trap_R_AddRefEntityToScene( &flash );
+  }
+
+  if( ps || cg.renderingThirdPerson ||
+      cent->currentState.number != cg.predictedPlayerState.clientNum )
+  {
+    if( weapon->wim[ weaponMode ].muzzleParticleSystem && cent->muzzlePsTrigger )
+    {
+      cent->muzzlePS = CG_SpawnNewParticleSystem( weapon->wim[ weaponMode ].muzzleParticleSystem );
+
+      if( CG_IsParticleSystemValid( &cent->muzzlePS ) )
+      {
+        if( noGunModel )
+          CG_SetAttachmentTag( &cent->muzzlePS->attachment, *parent, parent->hModel, "tag_weapon" );
+        else
+          CG_SetAttachmentTag( &cent->muzzlePS->attachment, gun, gun.hModel, "tag_flash" );
+
+        CG_SetAttachmentCent( &cent->muzzlePS->attachment, cent );
+        CG_AttachToTag( &cent->muzzlePS->attachment );
+      }
+
+      cent->muzzlePsTrigger = qfalse;
+    }
+
+    // make a dlight for the flash
+    if( weapon->wim[ weaponMode ].flashDlightColor[ 0 ] ||
+        weapon->wim[ weaponMode ].flashDlightColor[ 1 ] ||
+        weapon->wim[ weaponMode ].flashDlightColor[ 2 ] )
+    {
+      trap_R_AddLightToScene( flash.origin, 300 + ( rand( ) & 31 ),
+          weapon->wim[ weaponMode ].flashDlightColor[ 0 ],
+          weapon->wim[ weaponMode ].flashDlightColor[ 1 ],
+          weapon->wim[ weaponMode ].flashDlightColor[ 2 ] );
+    }
+  }
+}
+
+/*
+==============
+CG_AddViewWeapon
+
+Add the weapon, and flash for the player's view
+==============
+*/
+
+#define WEAPON_CLICK_REPEAT 500
+
+void CG_AddViewWeapon( playerState_t *ps )
+{
+  refEntity_t   hand;
+  centity_t     *cent;
+  clientInfo_t  *ci;
+  float         fovOffset;
+  vec3_t        angles;
+  weaponInfo_t  *wi;
+  weapon_t      weapon = ps->weapon;
+  weaponMode_t  weaponMode = ps->generic1;
+
+
+  // no weapon carried - can't draw it
+  if( weapon == WP_NONE )
+    return;
+
+  if( weaponMode <= WPM_NONE || weaponMode >= WPM_NUM_WEAPONMODES )
+    weaponMode = WPM_PRIMARY;
+
+  wi = &cg_weapons[ weapon ];
+  if( !wi->registered )
+  {
+    Com_Printf( S_COLOR_YELLOW "WARNING: CG_AddViewWeapon: weapon %d (%s) "
+        "is not registered\n", weapon, BG_Weapon( weapon )->name );
+    return;
+  }
+  cent = &cg.predictedPlayerEntity; // &cg_entities[cg.snap->ps.clientNum];
+
+  if( ps->persistant[PERS_SPECSTATE] != SPECTATOR_NOT )
+    return;
+
+  if( ps->pm_type == PM_INTERMISSION )
+    return;
+
+  // draw a prospective buildable infront of the player
+  if( ( ps->stats[ STAT_BUILDABLE ] & ~SB_VALID_TOGGLEBIT ) > BA_NONE )
+    CG_GhostBuildable( ps->stats[ STAT_BUILDABLE ] & ~SB_VALID_TOGGLEBIT );
+
+  // no gun if in third person view
+  if( cg.renderingThirdPerson )
+    return;
+
+  // allow the gun to be completely removed
+  if( !cg_drawGun.integer )
+  {
+    vec3_t origin;
+
+    VectorCopy( cg.refdef.vieworg, origin );
+    VectorMA( origin, -8, cg.refdef.viewaxis[ 2 ], origin );
+
+    if( cent->muzzlePS )
+      CG_SetAttachmentPoint( &cent->muzzlePS->attachment, origin );
+
+    //check for particle systems
+    if( wi->wim[ weaponMode ].muzzleParticleSystem && cent->muzzlePsTrigger )
+    {
+      cent->muzzlePS = CG_SpawnNewParticleSystem( wi->wim[ weaponMode ].muzzleParticleSystem );
+
+      if( CG_IsParticleSystemValid( &cent->muzzlePS ) )
+      {
+        CG_SetAttachmentPoint( &cent->muzzlePS->attachment, origin );
+        CG_SetAttachmentCent( &cent->muzzlePS->attachment, cent );
+        CG_AttachToPoint( &cent->muzzlePS->attachment );
+      }
+      cent->muzzlePsTrigger = qfalse;
+    }
+
+    return;
+  }
+
+  // don't draw if testing a gun model
+  if( cg.testGun )
+    return;
+
+  // drop gun lower at higher fov
+  if( cg.refdef.fov_y > 90 )
+    fovOffset = -0.4 * ( cg.refdef.fov_y - 90 );
+  else
+    fovOffset = 0;
+
+  Com_Memset( &hand, 0, sizeof( hand ) );
+
+  // set up gun position
+  CG_CalculateWeaponPosition( hand.origin, angles );
+
+  VectorMA( hand.origin, cg_gun_x.value, cg.refdef.viewaxis[ 0 ], hand.origin );
+  VectorMA( hand.origin, cg_gun_y.value, cg.refdef.viewaxis[ 1 ], hand.origin );
+  VectorMA( hand.origin, ( cg_gun_z.value + fovOffset ), cg.refdef.viewaxis[ 2 ], hand.origin );
+
+  // Lucifer Cannon vibration effect
+  if( (weapon == WP_LUCIFER_CANNON || weapon == WP_FLAMER) && ps->stats[ STAT_MISC ] > 0 )
+
+  {
+    float fraction;
+
+    fraction = (float)ps->stats[ STAT_MISC ] / LCANNON_CHARGE_TIME_MAX;
+    VectorMA( hand.origin, random( ) * fraction, cg.refdef.viewaxis[ 0 ],
+              hand.origin );
+    VectorMA( hand.origin, random( ) * fraction, cg.refdef.viewaxis[ 1 ],
+              hand.origin );
+  }
+  
+  // Rifle FireMode2 vibration effect
+  
+
+  if( weapon == WP_MACHINEGUN && weaponMode == WPM_SECONDARY && ( cent->currentState.eFlags & EF_FIRING2 ) )
+
+  {
+    VectorMA( hand.origin, random( ) * 0, cg.refdef.viewaxis[ 0 ],
+              hand.origin );
+    VectorMA( hand.origin, random( ) * 3, cg.refdef.viewaxis[ 1 ],
+              hand.origin );	  
+			  
+  }
+
+  AnglesToAxis( angles, hand.axis );
+
+  // map torso animations to weapon animations
+  if( cg_gun_frame.integer )
+  {
+    // development tool
+    hand.frame = hand.oldframe = cg_gun_frame.integer;
+    hand.backlerp = 0;
+  }
+  else
+  {
+    // get clientinfo for animation map
+    ci = &cgs.clientinfo[ cent->currentState.clientNum ];
+    hand.frame = CG_MapTorsoToWeaponFrame( ci, cent->pe.torso.frame );
+    hand.oldframe = CG_MapTorsoToWeaponFrame( ci, cent->pe.torso.oldFrame );
+    hand.backlerp = cent->pe.torso.backlerp;
+  }
+
+  hand.hModel = wi->handsModel;
+  hand.renderfx = RF_DEPTHHACK | RF_FIRST_PERSON | RF_MINLIGHT;
+
+  // add everything onto the hand
+  CG_AddPlayerWeapon( &hand, ps, &cg.predictedPlayerEntity );
+}
+
+/*
+==============================================================================
+
+WEAPON SELECTION
+
+==============================================================================
+*/
+
+/*
+===============
+CG_WeaponSelectable
+===============
+*/
+static qboolean CG_WeaponSelectable( weapon_t weapon )
+{
+  if( !BG_InventoryContainsWeapon( weapon, cg.snap->ps.stats ) )
+    return qfalse;
+
+  return qtrue;
+}
+
+
+/*
+===============
+CG_UpgradeSelectable
+===============
+*/
+static qboolean CG_UpgradeSelectable( upgrade_t upgrade )
+{
+  if( !BG_InventoryContainsUpgrade( upgrade, cg.snap->ps.stats ) )
+    return qfalse;
+
+  return BG_Upgrade( upgrade )->usable;
+}
+
+
+#define ICON_BORDER 4
+
+/*
+===================
+CG_DrawItemSelect
+===================
+*/
+void CG_DrawItemSelect( rectDef_t *rect, vec4_t color )
+{
+  int           i;
+  float         x = rect->x;
+  float         y = rect->y;
+  float         width = rect->w;
+  float         height = rect->h;
+  float         iconWidth;
+  float         iconHeight;
+  int           items[ 64 ];
+  int           colinfo[ 64 ];
+  int           numItems = 0, selectedItem = 0;
+  int           length;
+  qboolean      vertical;
+  centity_t *cent;
+  playerState_t *ps;
+
+  cent = &cg_entities[ cg.snap->ps.clientNum ];
+  ps = &cg.snap->ps;
+
+  // don't display if dead
+  if( cg.predictedPlayerState.stats[ STAT_HEALTH ] <= 0 )
+    return;
+
+  if( !( cg.snap->ps.pm_flags & PMF_FOLLOW ) )
+  {
+    // first make sure that whatever it selected is actually selectable
+    if( cg.weaponSelect < 32 )
+    {
+      if( !CG_WeaponSelectable( cg.weaponSelect ) )
+        CG_NextWeapon_f( );
+    }
+    else
+    {
+      if( !CG_UpgradeSelectable( cg.weaponSelect - 32 ) )
+        CG_NextWeapon_f( );
+    }
+  }
+
+  // showing weapon select clears pickup item display, but not the blend blob
+  cg.itemPickupTime = 0;
+
+  // put all weapons in the items list
+  for( i = WP_NONE + 1; i < WP_NUM_WEAPONS; i++ )
+  {
+    if( !BG_InventoryContainsWeapon( i, cg.snap->ps.stats ) )
+      continue;
+
+    if( !ps->ammo && !ps->clips && !BG_Weapon( i )->infiniteAmmo )
+      colinfo[ numItems ] = 1;
+    else
+      colinfo[ numItems ] = 0;
+
+    if( i == cg.weaponSelect )
+      selectedItem = numItems;
+
+    if( !cg_weapons[ i ].registered )
+    {
+      Com_Printf( S_COLOR_YELLOW "WARNING: CG_DrawItemSelect: weapon %d (%s) "
+  	"is not registered\n", i, BG_Weapon( i )->name );
+      continue;
+    }
+    items[ numItems ] = i;
+    numItems++;
+  }
+
+  // put all upgrades in the weapons list
+  for( i = UP_NONE + 1; i < UP_NUM_UPGRADES; i++ )
+  {
+    if( !BG_InventoryContainsUpgrade( i, cg.snap->ps.stats ) )
+      continue;
+    colinfo[ numItems ] = 0;
+    if( !BG_Upgrade( i )->usable )
+      colinfo[ numItems ] = 2;
+
+    if( i == cg.weaponSelect - 32 )
+      selectedItem = numItems;
+
+    if( !cg_upgrades[ i ].registered )
+    {
+      Com_Printf( S_COLOR_YELLOW "WARNING: CG_DrawItemSelect: upgrade %d (%s) "
+  	"is not registered\n", i, BG_Upgrade( i )->name );
+      continue;
+    }
+    items[ numItems ] = i + 32;
+    numItems++;
+  }
+
+  // compute the length of the display window and determine orientation
+  vertical = height > width;
+  if( vertical )
+  {
+    iconWidth = width * cgDC.aspectScale;
+    iconHeight = width;
+    length = height / ( width * cgDC.aspectScale );
+  }
+  else
+  {
+    iconWidth = height * cgDC.aspectScale;
+    iconHeight = height;
+    length = width / ( height * cgDC.aspectScale );
+  }
+
+  // render icon ring
+  for( i = 0; i < length; i++ )
+  {
+    int item = i - length / 2 + selectedItem;
+
+    if( item < 0 )
+      item += length;
+    else if( item >= length )
+      item -= length;
+    if( item >= 0 && item < numItems )
+    {
+      switch( colinfo[ item ] )
+      {
+       case 0:
+         color = colorCyan;
+         break;
+       case 1:
+         color = colorRed;
+         break;
+       case 2:
+         color = colorMdGrey;
+         break;
+      }
+      color[3] = 0.5;
+      trap_R_SetColor( color );
+
+      if( items[ item ] < 32 )
+        CG_DrawPic( x, y, iconWidth, iconHeight,
+                    cg_weapons[ items[ item ] ].weaponIcon );
+      else
+        CG_DrawPic( x, y, iconWidth, iconHeight,
+                    cg_upgrades[ items[ item ] - 32 ].upgradeIcon );
+    }
+    if( vertical )
+      y += iconHeight;
+    else
+      x += iconWidth;
+  }
+  trap_R_SetColor( NULL );
+}
+
+
+/*
+===================
+CG_DrawItemSelectText
+===================
+*/
+void CG_DrawItemSelectText( rectDef_t *rect, float scale, int textStyle )
+{
+  int x, w;
+  char  *name;
+  float *color;
+
+  color = CG_FadeColor( cg.weaponSelectTime, WEAPON_SELECT_TIME );
+  if( !color )
+    return;
+
+  trap_R_SetColor( color );
+
+  // draw the selected name
+  if( cg.weaponSelect < 32 )
+  {
+    if( cg_weapons[ cg.weaponSelect ].registered &&
+        BG_InventoryContainsWeapon( cg.weaponSelect, cg.snap->ps.stats ) )
+    {
+      if( ( name = cg_weapons[ cg.weaponSelect ].humanName ) )
+      {
+        w = UI_Text_Width( name, scale );
+        x = rect->x + rect->w / 2;
+        UI_Text_Paint( x - w / 2, rect->y + rect->h, scale, color, name, 0, 0, textStyle );
+      }
+    }
+  }
+  else
+  {
+    if( cg_upgrades[ cg.weaponSelect - 32 ].registered &&
+        BG_InventoryContainsUpgrade( cg.weaponSelect - 32, cg.snap->ps.stats ) )
+    {
+      if( ( name = cg_upgrades[ cg.weaponSelect - 32 ].humanName ) )
+      {
+        w = UI_Text_Width( name, scale );
+        x = rect->x + rect->w / 2;
+        UI_Text_Paint( x - w / 2, rect->y + rect->h, scale, color, name, 0, 0, textStyle );
+      }
+    }
+  }
+
+  trap_R_SetColor( NULL );
+}
+
+
+/*
+===============
+CG_NextWeapon_f
+===============
+*/
+void CG_NextWeapon_f( void )
+{
+  int   i;
+  int   original;
+
+  if( !cg.snap )
+    return;
+
+  if( cg.snap->ps.pm_flags & PMF_FOLLOW )
+  {
+    trap_SendClientCommand( "followprev\n" );
+    return;
+  }
+
+  cg.weaponSelectTime = cg.time;
+  original = cg.weaponSelect;
+
+  for( i = 0; i < 64; i++ )
+  {
+    cg.weaponSelect++;
+    if( cg.weaponSelect == 64 )
+      cg.weaponSelect = 0;
+
+    if( cg.weaponSelect < 32 )
+    {
+      if( CG_WeaponSelectable( cg.weaponSelect ) )
+        break;
+    }
+    else
+    {
+      if( CG_UpgradeSelectable( cg.weaponSelect - 32 ) )
+        break;
+    }
+  }
+
+  if( i == 64 )
+    cg.weaponSelect = original;
+}
+
+/*
+===============
+CG_PrevWeapon_f
+===============
+*/
+void CG_PrevWeapon_f( void )
+{
+  int   i;
+  int   original;
+
+  if( !cg.snap )
+    return;
+
+  if( cg.snap->ps.pm_flags & PMF_FOLLOW )
+  {
+    trap_SendClientCommand( "follownext\n" );
+    return;
+  }
+
+  cg.weaponSelectTime = cg.time;
+  original = cg.weaponSelect;
+
+  for( i = 0; i < 64; i++ )
+  {
+    cg.weaponSelect--;
+    if( cg.weaponSelect == -1 )
+      cg.weaponSelect = 63;
+
+    if( cg.weaponSelect < 32 )
+    {
+      if( CG_WeaponSelectable( cg.weaponSelect ) )
+        break;
+    }
+    else
+    {
+      if( CG_UpgradeSelectable( cg.weaponSelect - 32 ) )
+        break;
+    }
+  }
+
+  if( i == 64 )
+    cg.weaponSelect = original;
+}
+
+/*
+===============
+CG_Weapon_f
+===============
+*/
+void CG_Weapon_f( void )
+{
+  int   num;
+
+  if( !cg.snap )
+    return;
+
+  if( cg.snap->ps.pm_flags & PMF_FOLLOW )
+    return;
+
+  num = atoi( CG_Argv( 1 ) );
+
+  if( num < 1 || num > 31 )
+    return;
+
+  cg.weaponSelectTime = cg.time;
+
+  if( !BG_InventoryContainsWeapon( num, cg.snap->ps.stats ) )
+    return;   // don't have the weapon
+
+  cg.weaponSelect = num;
+}
+
+
+/*
+===================================================================================================
+
+WEAPON EVENTS
+
+===================================================================================================
+*/
+
+/*
+================
+CG_FireWeapon
+
+Caused by an EV_FIRE_WEAPON event
+================
+*/
+void CG_FireWeapon( centity_t *cent, weaponMode_t weaponMode )
+{
+  entityState_t     *es;
+  int               c;
+  weaponInfo_t      *wi;
+  weapon_t          weaponNum;
+
+  es = &cent->currentState;
+
+  weaponNum = es->weapon;
+
+  if( weaponNum == WP_NONE )
+    return;
+
+  if( weaponMode <= WPM_NONE || weaponMode >= WPM_NUM_WEAPONMODES )
+    weaponMode = WPM_PRIMARY;
+
+  if( weaponNum >= WP_NUM_WEAPONS )
+  {
+    CG_Error( "CG_FireWeapon: ent->weapon >= WP_NUM_WEAPONS" );
+    return;
+  }
+
+  wi = &cg_weapons[ weaponNum ];
+
+  // mark the entity as muzzle flashing, so when it is added it will
+  // append the flash to the weapon model
+  cent->muzzleFlashTime = cg.time;
+
+  if( wi->wim[ weaponMode ].muzzleParticleSystem )
+  {
+    if( !CG_IsParticleSystemValid( &cent->muzzlePS ) ||
+        !CG_IsParticleSystemInfinite( cent->muzzlePS ) )
+      cent->muzzlePsTrigger = qtrue;
+  }
+
+  // play a sound
+  for( c = 0; c < 4; c++ )
+  {
+    if( !wi->wim[ weaponMode ].flashSound[ c ] )
+      break;
+  }
+
+  if( c > 0 )
+  {
+    c = rand( ) % c;
+    if( wi->wim[ weaponMode ].flashSound[ c ] )
+      trap_S_StartSound( NULL, es->number, CHAN_WEAPON, wi->wim[ weaponMode ].flashSound[ c ] );
+  }
+}
+
+
+/*
+=================
+CG_MissileHitWall
+
+Caused by an EV_MISSILE_MISS event, or directly by local bullet tracing
+=================
+*/
+void CG_MissileHitWall( weapon_t weaponNum, weaponMode_t weaponMode, int clientNum,
+                        vec3_t origin, vec3_t dir, impactSound_t soundType, int charge )
+{
+  qhandle_t           mark = 0;
+  qhandle_t           ps = 0;
+  int                 c;
+  float               radius = 1.0f;
+  weaponInfo_t        *weapon = &cg_weapons[ weaponNum ];
+
+  if( weaponMode <= WPM_NONE || weaponMode >= WPM_NUM_WEAPONMODES )
+    weaponMode = WPM_PRIMARY;
+
+  mark = weapon->wim[ weaponMode ].impactMark;
+  radius = weapon->wim[ weaponMode ].impactMarkSize;
+  ps = weapon->wim[ weaponMode ].impactParticleSystem;
+
+  if( soundType == IMPACTSOUND_FLESH )
+  {
+    //flesh sound
+    for( c = 0; c < 4; c++ )
+    {
+      if( !weapon->wim[ weaponMode ].impactFleshSound[ c ] )
+        break;
+    }
+
+    if( c > 0 )
+    {
+      c = rand( ) % c;
+      if( weapon->wim[ weaponMode ].impactFleshSound[ c ] )
+        trap_S_StartSound( origin, ENTITYNUM_WORLD, CHAN_AUTO, weapon->wim[ weaponMode ].impactFleshSound[ c ] );
+    }
+  }
+  else
+  {
+    //normal sound
+    for( c = 0; c < 4; c++ )
+    {
+      if( !weapon->wim[ weaponMode ].impactSound[ c ] )
+        break;
+    }
+
+    if( c > 0 )
+    {
+      c = rand( ) % c;
+      if( weapon->wim[ weaponMode ].impactSound[ c ] )
+        trap_S_StartSound( origin, ENTITYNUM_WORLD, CHAN_AUTO, weapon->wim[ weaponMode ].impactSound[ c ] );
+    }
+  }
+
+  //create impact particle system
+  if( ps )
+  {
+    particleSystem_t *partSystem = CG_SpawnNewParticleSystem( ps );
+
+    if( CG_IsParticleSystemValid( &partSystem ) )
+    {
+      CG_SetAttachmentPoint( &partSystem->attachment, origin );
+      CG_SetParticleSystemNormal( partSystem, dir );
+      CG_AttachToPoint( &partSystem->attachment );
+      partSystem->charge = charge;
+    }
+  }
+
+  //
+  // impact mark
+  //
+  if( radius > 0.0f )
+    CG_ImpactMark( mark, origin, dir, random( ) * 360, 1, 1, 1, 1, qfalse, radius, qfalse );
+}
+
+
+/*
+=================
+CG_MissileHitEntity
+=================
+*/
+void CG_MissileHitEntity( weapon_t weaponNum, weaponMode_t weaponMode,
+    vec3_t origin, vec3_t dir, int entityNum, int charge )
+{
+  vec3_t        normal;
+  weaponInfo_t  *weapon = &cg_weapons[ weaponNum ];
+
+  VectorCopy( dir, normal );
+  VectorInverse( normal );
+
+  CG_Bleed( origin, normal, entityNum );
+
+  if( weaponMode <= WPM_NONE || weaponMode >= WPM_NUM_WEAPONMODES )
+    weaponMode = WPM_PRIMARY;
+
+  if( weapon->wim[ weaponMode ].alwaysImpact )
+  {
+    int sound;
+
+    if( cg_entities[ entityNum ].currentState.eType == ET_PLAYER )
+    {
+      // Players
+      sound = IMPACTSOUND_FLESH;
+    }
+    else if( cg_entities[ entityNum ].currentState.eType == ET_BUILDABLE &&
+             BG_Buildable( cg_entities[ entityNum ].currentState.modelindex )->team == TEAM_ALIENS )
+    {
+      // Alien buildables
+      sound = IMPACTSOUND_FLESH;
+    }
+    else
+      sound = IMPACTSOUND_DEFAULT;
+          
+    CG_MissileHitWall( weaponNum, weaponMode, 0, origin, dir, sound, charge );
+  }
+}
+
+
+/*
+============================================================================
+
+BULLETS
+
+============================================================================
+*/
+
+
+/*
+===============
+CG_Tracer
+===============
+*/
+void CG_Tracer( vec3_t source, vec3_t dest )
+{
+  vec3_t      forward, right;
+  polyVert_t  verts[ 4 ];
+  vec3_t      line;
+  float       len, begin, end;
+  vec3_t      start, finish;
+  vec3_t      midpoint;
+
+  // tracer
+  VectorSubtract( dest, source, forward );
+  len = VectorNormalize( forward );
+
+  // start at least a little ways from the muzzle
+  if( len < 100 )
+    return;
+
+  begin = 50 + random( ) * ( len - 60 );
+  end = begin + cg_tracerLength.value;
+  if( end > len )
+    end = len;
+
+  VectorMA( source, begin, forward, start );
+  VectorMA( source, end, forward, finish );
+
+  line[ 0 ] = DotProduct( forward, cg.refdef.viewaxis[ 1 ] );
+  line[ 1 ] = DotProduct( forward, cg.refdef.viewaxis[ 2 ] );
+
+  VectorScale( cg.refdef.viewaxis[ 1 ], line[ 1 ], right );
+  VectorMA( right, -line[ 0 ], cg.refdef.viewaxis[ 2 ], right );
+  VectorNormalize( right );
+
+  VectorMA( finish, cg_tracerWidth.value, right, verts[ 0 ].xyz );
+  verts[ 0 ].st[ 0 ] = 0;
+  verts[ 0 ].st[ 1 ] = 1;
+  verts[ 0 ].modulate[ 0 ] = 255;
+  verts[ 0 ].modulate[ 1 ] = 255;
+  verts[ 0 ].modulate[ 2 ] = 255;
+  verts[ 0 ].modulate[ 3 ] = 255;
+
+  VectorMA( finish, -cg_tracerWidth.value, right, verts[ 1 ].xyz );
+  verts[ 1 ].st[ 0 ] = 1;
+  verts[ 1 ].st[ 1 ] = 0;
+  verts[ 1 ].modulate[ 0 ] = 255;
+  verts[ 1 ].modulate[ 1 ] = 255;
+  verts[ 1 ].modulate[ 2 ] = 255;
+  verts[ 1 ].modulate[ 3 ] = 255;
+
+  VectorMA( start, -cg_tracerWidth.value, right, verts[ 2 ].xyz );
+  verts[ 2 ].st[ 0 ] = 1;
+  verts[ 2 ].st[ 1 ] = 1;
+  verts[ 2 ].modulate[ 0 ] = 255;
+  verts[ 2 ].modulate[ 1 ] = 255;
+  verts[ 2 ].modulate[ 2 ] = 255;
+  verts[ 2 ].modulate[ 3 ] = 255;
+
+  VectorMA( start, cg_tracerWidth.value, right, verts[ 3 ].xyz );
+  verts[ 3 ].st[ 0 ] = 0;
+  verts[ 3 ].st[ 1 ] = 0;
+  verts[ 3 ].modulate[ 0 ] = 255;
+  verts[ 3 ].modulate[ 1 ] = 255;
+  verts[ 3 ].modulate[ 2 ] = 255;
+  verts[ 3 ].modulate[ 3 ] = 255;
+
+  trap_R_AddPolyToScene( cgs.media.tracerShader, 4, verts );
+
+  midpoint[ 0 ] = ( start[ 0 ] + finish[ 0 ] ) * 0.5;
+  midpoint[ 1 ] = ( start[ 1 ] + finish[ 1 ] ) * 0.5;
+  midpoint[ 2 ] = ( start[ 2 ] + finish[ 2 ] ) * 0.5;
+
+  // add the tracer sound
+  trap_S_StartSound( midpoint, ENTITYNUM_WORLD, CHAN_AUTO, cgs.media.tracerSound );
+}
+
+
+/*
+======================
+CG_CalcMuzzlePoint
+======================
+*/
+static qboolean CG_CalcMuzzlePoint( int entityNum, vec3_t muzzle )
+{
+  vec3_t    forward;
+  centity_t *cent;
+  int       anim;
+
+  if( entityNum == cg.snap->ps.clientNum )
+  {
+    VectorCopy( cg.snap->ps.origin, muzzle );
+    muzzle[ 2 ] += cg.snap->ps.viewheight;
+    AngleVectors( cg.snap->ps.viewangles, forward, NULL, NULL );
+    VectorMA( muzzle, 14, forward, muzzle );
+    return qtrue;
+  }
+
+  cent = &cg_entities[entityNum];
+
+  if( !cent->currentValid )
+    return qfalse;
+
+  VectorCopy( cent->currentState.pos.trBase, muzzle );
+
+  AngleVectors( cent->currentState.apos.trBase, forward, NULL, NULL );
+  anim = cent->currentState.legsAnim & ~ANIM_TOGGLEBIT;
+
+  if( anim == LEGS_WALKCR || anim == LEGS_IDLECR )
+    muzzle[ 2 ] += CROUCH_VIEWHEIGHT;
+  else
+    muzzle[ 2 ] += DEFAULT_VIEWHEIGHT;
+
+  VectorMA( muzzle, 14, forward, muzzle );
+
+  return qtrue;
+
+}
+
+
+/*
+======================
+CG_Bullet
+
+Renders bullet effects.
+======================
+*/
+void CG_Bullet( vec3_t end, int sourceEntityNum, vec3_t normal, qboolean flesh, int fleshEntityNum )
+{
+  vec3_t  start;
+
+  // if the shooter is currently valid, calc a source point and possibly
+  // do trail effects
+  if( sourceEntityNum >= 0 && cg_tracerChance.value > 0 )
+  {
+    if( CG_CalcMuzzlePoint( sourceEntityNum, start ) )
+    {
+      // draw a tracer
+      if( random( ) < cg_tracerChance.value )
+        CG_Tracer( start, end );
+    }
+  }
+
+  // impact splash and mark
+  if( flesh )
+    CG_Bleed( end, normal, fleshEntityNum );
+  else
+    CG_MissileHitWall( WP_MACHINEGUN, WPM_PRIMARY, 0, end, normal, IMPACTSOUND_DEFAULT, 0 );
+}
+
+/*
+============================================================================
+
+SHOTGUN TRACING
+
+============================================================================
+*/
+
+/*
+================
+CG_ShotgunPattern
+
+Perform the same traces the server did to locate the
+hit splashes
+================
+*/
+static void CG_ShotgunPattern( vec3_t origin, vec3_t origin2, int seed, int otherEntNum )
+{
+  int       i;
+  float     r, u;
+  vec3_t    end;
+  vec3_t    forward, right, up;
+  trace_t   tr;
+
+  // derive the right and up vectors from the forward vector, because
+  // the client won't have any other information
+  VectorNormalize2( origin2, forward );
+  PerpendicularVector( right, forward );
+  CrossProduct( forward, right, up );
+
+  // generate the "random" spread pattern
+  for( i = 0; i < SHOTGUN_PELLETS; i++ )
+  {
+    r = Q_crandom( &seed ) * SHOTGUN_SPREAD * 16;
+    u = Q_crandom( &seed ) * SHOTGUN_SPREAD * 16;
+    VectorMA( origin, 8192 * 16, forward, end );
+    VectorMA( end, r, right, end );
+    VectorMA( end, u, up, end );
+
+    CG_Trace( &tr, origin, NULL, NULL, end, otherEntNum, MASK_SHOT );
+
+    if( !( tr.surfaceFlags & SURF_NOIMPACT ) )
+    {
+      if( cg_entities[ tr.entityNum ].currentState.eType == ET_PLAYER ||
+          cg_entities[ tr.entityNum ].currentState.eType == ET_BUILDABLE )
+        CG_MissileHitEntity( WP_SHOTGUN, WPM_PRIMARY, tr.endpos, tr.plane.normal, tr.entityNum, 0 );
+      else if( tr.surfaceFlags & SURF_METALSTEPS )
+        CG_MissileHitWall( WP_SHOTGUN, WPM_PRIMARY, 0, tr.endpos, tr.plane.normal, IMPACTSOUND_METAL, 0 );
+      else
+        CG_MissileHitWall( WP_SHOTGUN, WPM_PRIMARY, 0, tr.endpos, tr.plane.normal, IMPACTSOUND_DEFAULT, 0 );
+    }
+  }
+}
+
+/*
+==============
+CG_ShotgunFire
+==============
+*/
+void CG_ShotgunFire( entityState_t *es )
+{
+  vec3_t  v;
+
+  VectorSubtract( es->origin2, es->pos.trBase, v );
+  VectorNormalize( v );
+  VectorScale( v, 32, v );
+  VectorAdd( es->pos.trBase, v, v );
+
+  CG_ShotgunPattern( es->pos.trBase, es->origin2, es->eventParm, es->otherEntityNum );
+}
+
+/*
+=================
+CG_Bleed
+
+This is the spurt of blood when a character gets hit
+=================
+*/
+void CG_Bleed( vec3_t origin, vec3_t normal, int entityNum )
+{
+  team_t            team;
+  qhandle_t         bleedPS;
+  particleSystem_t  *ps;
+
+  if( !cg_blood.integer )
+    return;
+
+  if( cg_entities[ entityNum ].currentState.eType == ET_PLAYER )
+  {
+    team = cgs.clientinfo[ entityNum ].team;
+    if( team == TEAM_ALIENS )
+      bleedPS = cgs.media.alienBleedPS;
+    else if( team == TEAM_HUMANS )
+      bleedPS = cgs.media.humanBleedPS;
+    else
+      return;
+  }
+  else if( cg_entities[ entityNum ].currentState.eType == ET_BUILDABLE )
+  {
+    //ew
+    team = BG_Buildable( cg_entities[ entityNum ].currentState.modelindex )->team;
+    if( team == TEAM_ALIENS )
+      bleedPS = cgs.media.alienBuildableBleedPS;
+    else if( team == TEAM_HUMANS )
+      bleedPS = cgs.media.humanBuildableBleedPS;
+    else
+      return;
+  }
+  else
+    return;
+
+  ps = CG_SpawnNewParticleSystem( bleedPS );
+
+  if( CG_IsParticleSystemValid( &ps ) )
+  {
+    CG_SetAttachmentPoint( &ps->attachment, origin );
+    CG_SetAttachmentCent( &ps->attachment, &cg_entities[ entityNum ] );
+    CG_AttachToPoint( &ps->attachment );
+
+    CG_SetParticleSystemNormal( ps, normal );
+  }
+}
+
-- 
cgit