/*
===========================================================================
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 &&
       cent->currentState.eType != ET_BUILDABLE )
  {
    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( cent->currentState.eType == ET_MISSILE &&
      !( cg.snap->ps.pm_flags & PMF_FOLLOW ) )
  {
    //Default setting, people don't like having to see
    //projectilenudge with lagged as it screws up their aim 
    if(cg_projectileNudge.integer == 1 &&
      !cg.demoPlayback && cg_unlagged.integer)
    timeshift = cg.ping;
    //Previous default
    if(cg_projectileNudge.integer == 2 &&
      !cg.demoPlayback)
    timeshift = cg.ping;
    //Extrapolate using the value instead of ping
    //for those who think their ping isn't 'real'
    else if( cg_projectileNudge.integer > 2)
    timeshift = cg_projectileNudge.integer;

    else timeshift = 0;
  }

  // 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;
      }
    }
  }
}

