diff options
author | Jeff Kent <jeff@jkent.net> | 2017-04-13 11:30:00 +0000 |
---|---|---|
committer | /dev/humancontroller <devhc@example.com> | 2017-04-15 12:10:23 +0200 |
commit | 1a5afab2fcd1d43e5d43160ee601fa3bfa375a76 (patch) | |
tree | 7783e07f6dacd7f0dca6da2cfd787fbef1565dac /src/cgame | |
parent | 5ad9e26c3be1f2ebc6cdb340b685aef30ae16db7 (diff) |
import the Lakitu7 r181 cgame and ui modules
replacing the existing ones
Diffstat (limited to 'src/cgame')
-rw-r--r-- | src/cgame/cg_animation.c | 111 | ||||
-rw-r--r-- | src/cgame/cg_animmapobj.c | 227 | ||||
-rw-r--r-- | src/cgame/cg_attachment.c | 404 | ||||
-rw-r--r-- | src/cgame/cg_buildable.c | 1423 | ||||
-rw-r--r-- | src/cgame/cg_consolecmds.c | 325 | ||||
-rw-r--r-- | src/cgame/cg_draw.c | 3524 | ||||
-rw-r--r-- | src/cgame/cg_drawtools.c | 378 | ||||
-rw-r--r-- | src/cgame/cg_ents.c | 1256 | ||||
-rw-r--r-- | src/cgame/cg_event.c | 1034 | ||||
-rw-r--r-- | src/cgame/cg_local.h | 2072 | ||||
-rw-r--r-- | src/cgame/cg_main.c | 1842 | ||||
-rw-r--r-- | src/cgame/cg_marks.c | 289 | ||||
-rw-r--r-- | src/cgame/cg_mem.c | 202 | ||||
-rw-r--r-- | src/cgame/cg_particles.c | 2567 | ||||
-rw-r--r-- | src/cgame/cg_players.c | 2565 | ||||
-rw-r--r-- | src/cgame/cg_playerstate.c | 317 | ||||
-rw-r--r-- | src/cgame/cg_predict.c | 878 | ||||
-rw-r--r-- | src/cgame/cg_ptr.c | 81 | ||||
-rw-r--r-- | src/cgame/cg_scanner.c | 365 | ||||
-rw-r--r-- | src/cgame/cg_servercmds.c | 1016 | ||||
-rw-r--r-- | src/cgame/cg_snapshot.c | 411 | ||||
-rw-r--r-- | src/cgame/cg_syscalls.asm | 115 | ||||
-rw-r--r-- | src/cgame/cg_syscalls.c | 570 | ||||
-rw-r--r-- | src/cgame/cg_trails.c | 1500 | ||||
-rw-r--r-- | src/cgame/cg_tutorial.c | 663 | ||||
-rw-r--r-- | src/cgame/cg_view.c | 1340 | ||||
-rw-r--r-- | src/cgame/cg_weapons.c | 1845 |
27 files changed, 27320 insertions, 0 deletions
diff --git a/src/cgame/cg_animation.c b/src/cgame/cg_animation.c new file mode 100644 index 0000000..c370c53 --- /dev/null +++ b/src/cgame/cg_animation.c @@ -0,0 +1,111 @@ +/* +=========================================================================== +Copyright (C) 1999-2005 Id Software, Inc. +Copyright (C) 2000-2006 Tim Angus + +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 ) +{ + 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; + 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..2e80b7e --- /dev/null +++ b/src/cgame/cg_animmapobj.c @@ -0,0 +1,227 @@ +/* +=========================================================================== +Copyright (C) 1999-2005 Id Software, Inc. +Copyright (C) 2000-2006 Tim Angus + +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( ¢->lerpFrame ); + + *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 = ¢->lerpFrame; + + es = ¢->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->powerups; + 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( ¢->lerpFrame ); + 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 = ¢->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->powerups; + 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..0d3c8ea --- /dev/null +++ b/src/cgame/cg_attachment.c @@ -0,0 +1,404 @@ +/* +=========================================================================== +Copyright (C) 2000-2006 Tim Angus + +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..e7b4d85 --- /dev/null +++ b/src/cgame/cg_buildable.c @@ -0,0 +1,1423 @@ +/* +=========================================================================== +Copyright (C) 1999-2005 Id Software, Inc. +Copyright (C) 2000-2006 Tim Angus + +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 ) +{ + particleSystem_t *ps; + + trap_S_StartSound( origin, ENTITYNUM_WORLD, CHAN_AUTO, cgs.media.alienBuildableExplosion ); + + //particle system + ps = CG_SpawnNewParticleSystem( cgs.media.alienBuildableDestroyedPS ); + + 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 ) +{ + particleSystem_t *ps; + + trap_S_StartSound( origin, ENTITYNUM_WORLD, CHAN_AUTO, cgs.media.humanBuildableExplosion ); + + //particle system + 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 + +/* +================== +CG_Creep +================== +*/ +static void CG_Creep( centity_t *cent ) +{ + int msec; + float size, frac; + trace_t tr; + vec3_t temp, origin; + int scaleUpTime = BG_FindBuildTimeForBuildable( cent->currentState.modelindex ); + int time; + + 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, -4096, 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; + + if( size > 0.0f ) + CG_ImpactMark( cgs.media.creepShader, origin, cent->currentState.origin2, + 0.0f, 1.0f, 1.0f, 1.0f, 1.0f, qfalse, size, qtrue ); +} + +/* +====================== +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 >= sizeof( text ) - 1 ) + { + CG_Printf( "File %s too long\n", filename ); + 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 >= sizeof( text ) - 1 ) + { + CG_Printf( "File %s too long\n", filename ); + 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_FindNameForBuildable( i ); + + //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++ ) + { + if( ( modelFile = BG_FindModelsForBuildable( i, j ) ) ) + 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_FindTeamForBuildable( i ) == BIT_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" ); +} + +/* +=============== +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 ) +{ + int f, numFrames; + buildable_t buildable = cent->currentState.modelindex; + lerpFrame_t *lf = ¢->lerpFrame; + animation_t *anim; + buildableAnimNumber_t newAnimation = cent->buildableAnim & ~( ANIM_TOGGLEBIT|ANIM_FORCEBIT ); + + // debugging tool to get no animations + if( cg_animSpeed.integer == 0 ) + { + lf->oldFrame = lf->frame = lf->backlerp = 0; + return; + } + + // 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_FindHumanNameForBuildable( buildable ) ); + + 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 ); + + // 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; + 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; + cent->buildableAnim = cent->currentState.torsoAnim; + } + } + + 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 ); +} + +/* +=============== +CG_BuildableAnimation +=============== +*/ +static void CG_BuildableAnimation( centity_t *cent, int *old, int *now, float *backLerp ) +{ + entityState_t *es = ¢->currentState; + + //if no animation is set default to idle anim + if( cent->buildableAnim == BANIM_NONE ) + cent->buildableAnim = es->torsoAnim; + + //display the first frame of the construction anim if not yet spawned + if( !( es->generic1 & B_SPAWNED_TOGGLEBIT ) ) + { + 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_FindHumanNameForBuildable( es->modelindex ), es->number ); + + if( cent->buildableAnim == es->torsoAnim || es->legsAnim & ANIM_FORCEBIT ) + cent->buildableAnim = cent->oldBuildableAnim = es->legsAnim; + else + cent->buildableAnim = cent->oldBuildableAnim = 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; + + 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 ); + CG_CapTrace( &tr, start, mins, maxs, end, skipNumber, MASK_PLAYERSOLID ); + + if( tr.fraction == 1.0f ) + { + //erm we missed completely - try again with a box trace + CG_Trace( &tr, start, mins, maxs, end, skipNumber, MASK_PLAYERSOLID ); + } + + VectorMA( inOrigin, tr.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_FindBBoxForBuildable( 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_FindZOffsetForBuildable( buildable ), 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.greenBuildShader; + else + ent.customShader = cgs.media.redBuildShader; + + //rescale the model + scale = BG_FindModelScaleForBuildable( buildable ); + + 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 = ¢->currentState; + buildableTeam_t team = BG_FindTeamForBuildable( es->modelindex ); + int health = es->generic1 & B_HEALTH_MASK; + float healthFrac = (float)health / B_HEALTH_MASK; + + if( !( es->generic1 & B_SPAWNED_TOGGLEBIT ) ) + return; + + if( team == BIT_HUMANS ) + { + if( healthFrac < 0.33f && !CG_IsParticleSystemValid( ¢->buildablePS ) ) + { + cent->buildablePS = CG_SpawnNewParticleSystem( cgs.media.humanBuildableDamagedPS ); + + if( CG_IsParticleSystemValid( ¢->buildablePS ) ) + { + CG_SetAttachmentCent( ¢->buildablePS->attachment, cent ); + CG_AttachToCent( ¢->buildablePS->attachment ); + } + } + else if( healthFrac >= 0.33f && CG_IsParticleSystemValid( ¢->buildablePS ) ) + CG_DestroyParticleSystem( ¢->buildablePS ); + } + else if( team == BIT_ALIENS ) + { + if( healthFrac < 0.33f && !CG_IsParticleSystemValid( ¢->buildablePS ) ) + { + cent->buildablePS = CG_SpawnNewParticleSystem( cgs.media.alienBuildableDamagedPS ); + + if( CG_IsParticleSystemValid( ¢->buildablePS ) ) + { + CG_SetAttachmentCent( ¢->buildablePS->attachment, cent ); + CG_SetParticleSystemNormal( cent->buildablePS, es->origin2 ); + CG_AttachToCent( ¢->buildablePS->attachment ); + } + } + else if( healthFrac >= 0.33f && CG_IsParticleSystemValid( ¢->buildablePS ) ) + CG_DestroyParticleSystem( ¢->buildablePS ); + } +} + + +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; + return; + } + } + bs->loaded = qtrue; +} + +#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 = ¢->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; + + if( BG_FindTeamForBuildable( es->modelindex ) == BIT_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_FindBBoxForBuildable( es->modelindex, mins, maxs ); + + 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->generic1 & B_SPAWNED_TOGGLEBIT ) || + BG_FindTransparentTestForBuildable( hit->modelindex ) ) ) ) + { + 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_PTEAM ] == PTE_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 & B_HEALTH_MASK; + healthScale = (float)health / B_HEALTH_MASK; + + 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; + vec4_t frameColor; + + // this is fudged to get the width/height in the cfg to be more realistic + scale = ( picH / d ) * 3; + + powered = es->generic1 & B_POWERED_TOGGLEBIT; + marked = es->generic1 & B_MARKED_TOGGLEBIT; + + 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 ); + + 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 ); + 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_FindHealthForBuildable( es->modelindex ); + 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 ); + } +} + +static int QDECL 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_DrawBuildableStatus +================== +*/ +void CG_DrawBuildableStatus( void ) +{ + int i; + centity_t *cent; + entityState_t *es; + int buildableList[ MAX_ENTITIES_IN_SNAPSHOT ]; + int buildables = 0; + + switch( cg.predictedPlayerState.weapon ) + { + case WP_ABUILD: + case WP_ABUILD2: + case WP_HBUILD: + case WP_HBUILD2: + for( i = 0; i < cg.snap->numEntities; i++ ) + { + cent = &cg_entities[ cg.snap->entities[ i ].number ]; + es = ¢->currentState; + + if( es->eType == ET_BUILDABLE && + BG_FindTeamForBuildable( es->modelindex ) == + BG_FindTeamForWeapon( cg.predictedPlayerState.weapon ) ) + 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 ] ] ); + break; + + default: + break; + } +} + +#define BUILDABLE_SOUND_PERIOD 500 + +/* +================== +CG_Buildable +================== +*/ +void CG_Buildable( centity_t *cent ) +{ + refEntity_t ent; + entityState_t *es = ¢->currentState; + vec3_t angles; + vec3_t surfNormal, xNormal, mins, maxs; + vec3_t refNormal = { 0.0f, 0.0f, 1.0f }; + float rotAngle; + buildableTeam_t team = BG_FindTeamForBuildable( es->modelindex ); + float scale; + int health; + float healthScale; + + //must be before EF_NODRAW check + if( team == BIT_ALIENS ) + CG_Creep( cent ); + + // if set to invisible, skip + if( es->eFlags & EF_NODRAW ) + { + if( CG_IsParticleSystemValid( ¢->buildablePS ) ) + CG_DestroyParticleSystem( ¢->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_FindBBoxForBuildable( es->modelindex, mins, maxs ); + + if( es->pos.trType == TR_STATIONARY ) + CG_PositionAndOrientateBuildable( angles, ent.origin, surfNormal, es->number, + mins, maxs, ent.axis, ent.origin ); + + //offset on the Z axis if required + VectorMA( ent.origin, BG_FindZOffsetForBuildable( es->modelindex ), 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->generic1 & B_SPAWNED_TOGGLEBIT ) ) + { + sfxHandle_t prebuildSound = cgs.media.humanBuildablePrebuild; + + if( team == BIT_HUMANS ) + { + ent.customShader = cgs.media.humanSpawningShader; + prebuildSound = cgs.media.humanBuildablePrebuild; + } + else if( team == BIT_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_FindModelScaleForBuildable( es->modelindex ); + + 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 ); + + 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; + + 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; + + 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_FindProjTypeForBuildable( es->modelindex ) == 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 & B_HEALTH_MASK; + healthScale = (float)health / B_HEALTH_MASK; + + if( healthScale < cent->lastBuildableHealthScale && ( es->generic1 & B_SPAWNED_TOGGLEBIT ) ) + { + if( cent->lastBuildableDamageSoundTime + BUILDABLE_SOUND_PERIOD < cg.time ) + { + if( team == BIT_HUMANS ) + { + int i = rand( ) % 4; + trap_S_StartSound( NULL, es->number, CHAN_BODY, cgs.media.humanBuildableDamage[ i ] ); + } + else if( team == BIT_ALIENS ) + trap_S_StartSound( NULL, es->number, CHAN_BODY, cgs.media.alienBuildableDamage ); + + cent->lastBuildableDamageSoundTime = cg.time; + } + } + + cent->lastBuildableHealthScale = healthScale; + + //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..68aab6c --- /dev/null +++ b/src/cgame/cg_consolecmds.c @@ -0,0 +1,325 @@ +/* +=========================================================================== +Copyright (C) 1999-2005 Id Software, Inc. +Copyright (C) 2000-2006 Tim Angus + +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" + + + +void CG_TargetCommand_f( void ) +{ + int targetNum; + char test[ 4 ]; + + targetNum = CG_CrosshairPlayer( ); + if( !targetNum ) + return; + + trap_Argv( 1, test, 4 ); + trap_SendConsoleCommand( va( "gc %i %i", targetNum, atoi( test ) ) ); +} + + + +/* +================= +CG_SizeUp_f + +Keybinding command +================= +*/ +static void CG_SizeUp_f( void ) +{ + trap_Cvar_Set( "cg_viewsize", va( "%i", (int)( cg_viewsize.integer + 10 ) ) ); +} + + +/* +================= +CG_SizeDown_f + +Keybinding command +================= +*/ +static void CG_SizeDown_f( void ) +{ + trap_Cvar_Set( "cg_viewsize", va( "%i", (int)( cg_viewsize.integer - 10 ) ) ); +} + + +/* +============= +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; + //TA: added \n SendClientCommand doesn't call flush( )? + 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; + } +} + +static void CG_TellTarget_f( void ) +{ + int clientNum; + char command[ 128 ]; + char message[ 128 ]; + + clientNum = CG_CrosshairPlayer( ); + if( clientNum == -1 ) + return; + + trap_Args( message, 128 ); + Com_sprintf( command, 128, "tell %i %s", clientNum, message ); + trap_SendClientCommand( command ); +} + +static void CG_TellAttacker_f( void ) +{ + int clientNum; + char command[ 128 ]; + char message[ 128 ]; + + clientNum = CG_LastAttacker( ); + if( clientNum == -1 ) + return; + + trap_Args( message, 128 ); + Com_sprintf( command, 128, "tell %i %s", clientNum, message ); + trap_SendClientCommand( command ); +} + +typedef struct +{ + char *cmd; + void (*function)( void ); +} consoleCommand_t; + +static consoleCommand_t commands[ ] = +{ + { "testgun", CG_TestGun_f }, + { "testmodel", CG_TestModel_f }, + { "nextframe", CG_TestModelNextFrame_f }, + { "prevframe", CG_TestModelPrevFrame_f }, + { "nextskin", CG_TestModelNextSkin_f }, + { "prevskin", CG_TestModelPrevSkin_f }, + { "viewpos", CG_Viewpos_f }, + { "+scores", CG_ScoresDown_f }, + { "-scores", CG_ScoresUp_f }, + { "scoresUp", CG_scrollScoresUp_f }, + { "scoresDown", CG_scrollScoresDown_f }, + { "sizeup", CG_SizeUp_f }, + { "sizedown", CG_SizeDown_f }, + { "weapnext", CG_NextWeapon_f }, + { "weapprev", CG_PrevWeapon_f }, + { "weapon", CG_Weapon_f }, + { "tell_target", CG_TellTarget_f }, + { "tell_attacker", CG_TellAttacker_f }, + { "tcmd", CG_TargetCommand_f }, + { "testPS", CG_TestPS_f }, + { "destroyTestPS", CG_DestroyTestPS_f }, + { "testTS", CG_TestTS_f }, + { "destroyTestTS", CG_DestroyTestTS_f }, +}; + + +/* +================= +CG_ConsoleCommand + +The string has been tokenized and can be retrieved with +Cmd_Argc() / Cmd_Argv() +================= +*/ +qboolean CG_ConsoleCommand( void ) +{ + const char *cmd; + const char *arg1; + int i; + + cmd = CG_Argv( 0 ); + + //TA: ugly hacky special case + if( !Q_stricmp( cmd, "ui_menu" ) ) + { + arg1 = CG_Argv( 1 ); + trap_SendConsoleCommand( va( "menu %s\n", arg1 ) ); + return qtrue; + } + + for( i = 0; i < sizeof( commands ) / sizeof( commands[ 0 ] ); i++ ) + { + if( !Q_stricmp( cmd, commands[ i ].cmd ) ) + { + commands[ i ].function( ); + return qtrue; + } + } + + return qfalse; +} + + +/* +================= +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( "say" ); + trap_AddCommand( "say_team" ); + trap_AddCommand( "tell" ); + trap_AddCommand( "vsay" ); + trap_AddCommand( "vsay_team" ); + trap_AddCommand( "vtell" ); + trap_AddCommand( "vtaunt" ); + trap_AddCommand( "vosay" ); + trap_AddCommand( "vosay_team" ); + trap_AddCommand( "votell" ); + trap_AddCommand( "give" ); + trap_AddCommand( "god" ); + trap_AddCommand( "notarget" ); + trap_AddCommand( "noclip" ); + trap_AddCommand( "team" ); + trap_AddCommand( "follow" ); + trap_AddCommand( "levelshot" ); + trap_AddCommand( "addbot" ); + trap_AddCommand( "setviewpos" ); + trap_AddCommand( "callvote" ); + trap_AddCommand( "vote" ); + trap_AddCommand( "callteamvote" ); + trap_AddCommand( "teamvote" ); + trap_AddCommand( "stats" ); + trap_AddCommand( "teamtask" ); + 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( "menu" ); + trap_AddCommand( "ui_menu" ); + trap_AddCommand( "mapRotation" ); + trap_AddCommand( "stopMapRotation" ); + trap_AddCommand( "advanceMapRotation" ); + trap_AddCommand( "alienWin" ); + trap_AddCommand( "humanWin" ); +} diff --git a/src/cgame/cg_draw.c b/src/cgame/cg_draw.c new file mode 100644 index 0000000..82df413 --- /dev/null +++ b/src/cgame/cg_draw.c @@ -0,0 +1,3524 @@ +/* +=========================================================================== +Copyright (C) 1999-2005 Id Software, Inc. +Copyright (C) 2000-2006 Tim Angus + +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" + +// used for scoreboard +extern displayContextDef_t cgDC; +menuDef_t *menuScoreboard = NULL; + +int drawTeamOverlayModificationCount = -1; + +int sortedTeamPlayers[ TEAM_MAXOVERLAY ]; +int numSortedTeamPlayers; + +//TA UI +int CG_Text_Width( const char *text, float scale, int limit ) +{ + int count,len; + float out; + glyphInfo_t *glyph; + float useScale; +// FIXME: see ui_main.c, same problem +// const unsigned char *s = text; + const char *s = text; + fontInfo_t *font = &cgDC.Assets.textFont; + + if( scale <= cg_smallFont.value ) + font = &cgDC.Assets.smallFont; + else if( scale > cg_bigFont.value ) + font = &cgDC.Assets.bigFont; + + useScale = scale * font->glyphScale; + out = 0; + + if( text ) + { + len = strlen( text ); + if( limit > 0 && len > limit ) + len = limit; + + count = 0; + while( s && *s && count < len ) + { + if( Q_IsColorString( s ) ) + { + s += 2; + continue; + } + else + { + glyph = &font->glyphs[ (int)*s ]; + //TTimo: FIXME: getting nasty warnings without the cast, + //hopefully this doesn't break the VM build + out += glyph->xSkip; + s++; + count++; + } + } + } + + return out * useScale; +} + +int CG_Text_Height( const char *text, float scale, int limit ) +{ + int len, count; + float max; + glyphInfo_t *glyph; + float useScale; +// TTimo: FIXME +// const unsigned char *s = text; + const char *s = text; + fontInfo_t *font = &cgDC.Assets.textFont; + + if( scale <= cg_smallFont.value ) + font = &cgDC.Assets.smallFont; + else if( scale > cg_bigFont.value ) + font = &cgDC.Assets.bigFont; + + useScale = scale * font->glyphScale; + max = 0; + + if( text ) + { + len = strlen( text ); + if( limit > 0 && len > limit ) + len = limit; + + count = 0; + while( s && *s && count < len ) + { + if( Q_IsColorString( s ) ) + { + s += 2; + continue; + } + else + { + glyph = &font->glyphs[ (int)*s ]; + //TTimo: FIXME: getting nasty warnings without the cast, + //hopefully this doesn't break the VM build + if( max < glyph->height ) + max = glyph->height; + + s++; + count++; + } + } + } + + return max * useScale; +} + +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 ) +{ + float w, h; + w = width * scale; + h = height * scale; + CG_AdjustFrom640( &x, &y, &w, &h ); + trap_R_DrawStretchPic( x, y, w, h, s, t, s2, t2, hShader ); +} + +void CG_Text_Paint( float x, float y, float scale, vec4_t color, const char *text, + float adjust, int limit, int style ) +{ + int len, count; + vec4_t newColor; + glyphInfo_t *glyph; + float useScale; + fontInfo_t *font = &cgDC.Assets.textFont; + + if( scale <= cg_smallFont.value ) + font = &cgDC.Assets.smallFont; + else if( scale > cg_bigFont.value ) + font = &cgDC.Assets.bigFont; + + useScale = scale * font->glyphScale; + if( text ) + { +// TTimo: FIXME +// const unsigned char *s = text; + const char *s = text; + + trap_R_SetColor( color ); + memcpy( &newColor[ 0 ], &color[ 0 ], sizeof( vec4_t ) ); + len = strlen( text ); + + if( limit > 0 && len > limit ) + len = limit; + + count = 0; + while( s && *s && count < len ) + { + glyph = &font->glyphs[ (int)*s ]; + //TTimo: FIXME: getting nasty warnings without the cast, + //hopefully this doesn't break the VM build + + if( Q_IsColorString( s ) ) + { + memcpy( newColor, g_color_table[ ColorIndex( *( s + 1 ) ) ], sizeof( newColor ) ); + newColor[ 3 ] = color[ 3 ]; + trap_R_SetColor( newColor ); + s += 2; + continue; + } + else + { + float yadj = useScale * glyph->top; + if( style == ITEM_TEXTSTYLE_SHADOWED || + style == ITEM_TEXTSTYLE_SHADOWEDMORE ) + { + int ofs = style == ITEM_TEXTSTYLE_SHADOWED ? 1 : 2; + colorBlack[ 3 ] = newColor[ 3 ]; + trap_R_SetColor( colorBlack ); + CG_Text_PaintChar( x + ofs, y - yadj + ofs, + glyph->imageWidth, + glyph->imageHeight, + useScale, + glyph->s, + glyph->t, + glyph->s2, + glyph->t2, + glyph->glyph ); + + colorBlack[ 3 ] = 1.0; + trap_R_SetColor( newColor ); + } + else if( style == ITEM_TEXTSTYLE_NEON ) + { + vec4_t glow, outer, inner, white; + + glow[ 0 ] = newColor[ 0 ] * 0.5; + glow[ 1 ] = newColor[ 1 ] * 0.5; + glow[ 2 ] = newColor[ 2 ] * 0.5; + glow[ 3 ] = newColor[ 3 ] * 0.2; + + outer[ 0 ] = newColor[ 0 ]; + outer[ 1 ] = newColor[ 1 ]; + outer[ 2 ] = newColor[ 2 ]; + outer[ 3 ] = newColor[ 3 ]; + + inner[ 0 ] = newColor[ 0 ] * 1.5 > 1.0f ? 1.0f : newColor[ 0 ] * 1.5; + inner[ 1 ] = newColor[ 1 ] * 1.5 > 1.0f ? 1.0f : newColor[ 1 ] * 1.5; + inner[ 2 ] = newColor[ 2 ] * 1.5 > 1.0f ? 1.0f : newColor[ 2 ] * 1.5; + inner[ 3 ] = newColor[ 3 ]; + + white[ 0 ] = white[ 1 ] = white[ 2 ] = white[ 3 ] = 1.0f; + + trap_R_SetColor( glow ); + CG_Text_PaintChar( x - 3, y - yadj - 3, + glyph->imageWidth + 6, + glyph->imageHeight + 6, + useScale, + glyph->s, + glyph->t, + glyph->s2, + glyph->t2, + glyph->glyph ); + + trap_R_SetColor( outer ); + CG_Text_PaintChar( x - 1, y - yadj - 1, + glyph->imageWidth + 2, + glyph->imageHeight + 2, + useScale, + glyph->s, + glyph->t, + glyph->s2, + glyph->t2, + glyph->glyph ); + + trap_R_SetColor( inner ); + CG_Text_PaintChar( x - 0.5, y - yadj - 0.5, + glyph->imageWidth + 1, + glyph->imageHeight + 1, + useScale, + glyph->s, + glyph->t, + glyph->s2, + glyph->t2, + glyph->glyph ); + + trap_R_SetColor( white ); + } + + + CG_Text_PaintChar( x, y - yadj, + glyph->imageWidth, + glyph->imageHeight, + useScale, + glyph->s, + glyph->t, + glyph->s2, + glyph->t2, + glyph->glyph ); + + x += ( glyph->xSkip * useScale ) + adjust; + s++; + count++; + } + } + + trap_R_SetColor( NULL ); + } +} + +/* +============== +CG_DrawFieldPadded + +Draws large numbers for status bar and powerups +============== +*/ +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 ) ) + charWidth = 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; + + 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 and powerups +============== +*/ +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 ) ) + charWidth = 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 + 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 textStyle, int special, float progress ) +{ + float rimWidth = rect->h / 20.0f; + float doneWidth, leftWidth; + float tx, ty, tw, th; + char textBuffer[ 8 ]; + + if( rimWidth < 0.6f ) + rimWidth = 0.6f; + + if( special >= 0.0f ) + rimWidth = special; + + 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 == ITEM_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 ) ); + tw = CG_Text_Width( textBuffer, scale, 0 ); + th = scale * 40.0f; + + switch( align ) + { + case ITEM_ALIGN_LEFT: + tx = rect->x + ( rect->w / 10.0f ); + ty = rect->y + ( rect->h / 2.0f ) + ( th / 2.0f ); + break; + + case ITEM_ALIGN_RIGHT: + tx = rect->x + rect->w - ( rect->w / 10.0f ) - tw; + ty = rect->y + ( rect->h / 2.0f ) + ( th / 2.0f ); + break; + + case ITEM_ALIGN_CENTER: + tx = rect->x + ( rect->w / 2.0f ) - ( tw / 2.0f ); + ty = rect->y + ( rect->h / 2.0f ) + ( th / 2.0f ); + break; + + default: + tx = ty = 0.0f; + } + + CG_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_PTEAM ] == PTE_ALIENS && + !CG_AtHighestClass( ) ) + { + if( cg.time - cg.lastEvolveAttempt <= NO_CREDITS_TIME ) + { + if( ( ( cg.time - cg.lastEvolveAttempt ) / 300 ) % 2 ) + color[ 3 ] = 0.0f; + } + } + + 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, 1, rect->w, rect->h, value ); + + trap_R_SetColor( NULL ); + } +} + +static void CG_DrawPlayerBankValue( rectDef_t *rect, vec4_t color, qboolean padding ) +{ + int value; + playerState_t *ps; + + ps = &cg.snap->ps; + + value = ps->persistant[ PERS_BANK ]; + if( value > -1 ) + { + 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, 1, rect->w, rect->h, value ); + + trap_R_SetColor( NULL ); + } +} + +#define HH_MIN_ALPHA 0.2f +#define HH_MAX_ALPHA 0.8f +#define HH_ALPHA_DIFF (HH_MAX_ALPHA-HH_MIN_ALPHA) + +#define AH_MIN_ALPHA 0.2f +#define AH_MAX_ALPHA 0.8f +#define AH_ALPHA_DIFF (AH_MAX_ALPHA-AH_MIN_ALPHA) + +/* +============== +CG_DrawPlayerStamina1 +============== +*/ +static void CG_DrawPlayerStamina1( rectDef_t *rect, vec4_t color, qhandle_t shader ) +{ + playerState_t *ps = &cg.snap->ps; + float stamina = ps->stats[ STAT_STAMINA ]; + float maxStaminaBy3 = (float)MAX_STAMINA / 3.0f; + float progress; + + stamina -= ( 2 * (int)maxStaminaBy3 ); + progress = stamina / maxStaminaBy3; + + if( progress > 1.0f ) + progress = 1.0f; + else if( progress < 0.0f ) + progress = 0.0f; + + color[ 3 ] = HH_MIN_ALPHA + ( progress * HH_ALPHA_DIFF ); + + trap_R_SetColor( color ); + CG_DrawPic( rect->x, rect->y, rect->w, rect->h, shader ); + trap_R_SetColor( NULL ); +} + +/* +============== +CG_DrawPlayerStamina2 +============== +*/ +static void CG_DrawPlayerStamina2( rectDef_t *rect, vec4_t color, qhandle_t shader ) +{ + playerState_t *ps = &cg.snap->ps; + float stamina = ps->stats[ STAT_STAMINA ]; + float maxStaminaBy3 = (float)MAX_STAMINA / 3.0f; + float progress; + + stamina -= (int)maxStaminaBy3; + progress = stamina / maxStaminaBy3; + + if( progress > 1.0f ) + progress = 1.0f; + else if( progress < 0.0f ) + progress = 0.0f; + + color[ 3 ] = HH_MIN_ALPHA + ( progress * HH_ALPHA_DIFF ); + + trap_R_SetColor( color ); + CG_DrawPic( rect->x, rect->y, rect->w, rect->h, shader ); + trap_R_SetColor( NULL ); +} + +/* +============== +CG_DrawPlayerStamina3 +============== +*/ +static void CG_DrawPlayerStamina3( rectDef_t *rect, vec4_t color, qhandle_t shader ) +{ + playerState_t *ps = &cg.snap->ps; + float stamina = ps->stats[ STAT_STAMINA ]; + float maxStaminaBy3 = (float)MAX_STAMINA / 3.0f; + float progress; + + progress = stamina / maxStaminaBy3; + + if( progress > 1.0f ) + progress = 1.0f; + else if( progress < 0.0f ) + progress = 0.0f; + + color[ 3 ] = HH_MIN_ALPHA + ( progress * HH_ALPHA_DIFF ); + + trap_R_SetColor( color ); + CG_DrawPic( rect->x, rect->y, rect->w, rect->h, shader ); + trap_R_SetColor( NULL ); +} + +/* +============== +CG_DrawPlayerStamina4 +============== +*/ +static void CG_DrawPlayerStamina4( rectDef_t *rect, vec4_t color, qhandle_t shader ) +{ + playerState_t *ps = &cg.snap->ps; + float stamina = ps->stats[ STAT_STAMINA ]; + float progress; + + stamina += (float)MAX_STAMINA; + progress = stamina / (float)MAX_STAMINA; + + if( progress > 1.0f ) + progress = 1.0f; + else if( progress < 0.0f ) + progress = 0.0f; + + color[ 3 ] = HH_MIN_ALPHA + ( progress * HH_ALPHA_DIFF ); + + 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 color, qhandle_t shader ) +{ + playerState_t *ps = &cg.snap->ps; + float stamina = ps->stats[ STAT_STAMINA ]; + + if( stamina < 0 ) + color[ 3 ] = HH_MIN_ALPHA; + else + color[ 3 ] = HH_MAX_ALPHA; + + 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 color, qhandle_t shader ) +{ + playerState_t *ps = &cg.snap->ps; + centity_t *cent; + float buildTime = ps->stats[ STAT_MISC ]; + float progress; + float maxDelay; + + cent = &cg_entities[ cg.snap->ps.clientNum ]; + + switch( cent->currentState.weapon ) + { + case WP_ABUILD: + case WP_ABUILD2: + case WP_HBUILD: + case WP_HBUILD2: + maxDelay = (float)BG_FindBuildDelayForWeapon( cent->currentState.weapon ); + + if( buildTime > maxDelay ) + buildTime = maxDelay; + + progress = ( maxDelay - buildTime ) / maxDelay; + + color[ 3 ] = HH_MIN_ALPHA + ( progress * HH_ALPHA_DIFF ); + break; + + default: + if( ps->weaponstate == WEAPON_RELOADING ) + { + maxDelay = (float)BG_FindReloadTimeForWeapon( cent->currentState.weapon ); + progress = ( maxDelay - (float)ps->weaponTime ) / maxDelay; + + color[ 3 ] = HH_MIN_ALPHA + ( progress * HH_ALPHA_DIFF ); + } + 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 color, qhandle_t shader ) +{ + playerState_t *ps = &cg.snap->ps; + centity_t *cent; + float buildTime = ps->stats[ STAT_MISC ]; + float progress; + float maxDelay; + + cent = &cg_entities[ cg.snap->ps.clientNum ]; + + maxDelay = (float)BG_FindBuildDelayForWeapon( cent->currentState.weapon ); + + if( buildTime > maxDelay ) + buildTime = maxDelay; + + progress = ( maxDelay - buildTime ) / maxDelay; + + color[ 3 ] = AH_MIN_ALPHA + ( progress * AH_ALPHA_DIFF ); + + 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 color, qhandle_t shader ) +{ + playerState_t *ps = &cg.snap->ps; + qboolean boosted = ps->stats[ STAT_STATE ] & SS_BOOSTED; + + if( boosted ) + color[ 3 ] = AH_MAX_ALPHA; + else + color[ 3 ] = AH_MIN_ALPHA; + + trap_R_SetColor( color ); + 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 color, qhandle_t shader ) +{ + playerState_t *ps = &cg.snap->ps; + qboolean boosted = ps->stats[ STAT_STATE ] & SS_BOOSTED; + vec4_t localColor; + + Vector4Copy( color, localColor ); + + if( boosted ) + { + if( ps->stats[ STAT_BOOSTTIME ] > BOOST_TIME - 3000 ) + { + qboolean flash = ( ps->stats[ STAT_BOOSTTIME ] / 500 ) % 2; + + if( flash ) + localColor[ 3 ] = 1.0f; + } + } + + trap_R_SetColor( localColor ); + CG_DrawPic( rect->x, rect->y, rect->w, rect->h, shader ); + trap_R_SetColor( NULL ); +} + +/* +============== +CG_DrawPlayerPoisonBarbs +============== +*/ +static void CG_DrawPlayerPoisonBarbs( rectDef_t *rect, vec4_t color, qhandle_t shader ) +{ + playerState_t *ps = &cg.snap->ps; + int x = rect->x; + int y = rect->y; + int width = rect->w; + int height = rect->h; + qboolean vertical; + int iconsize, numBarbs, i; + + BG_UnpackAmmoArray( ps->weapon, ps->ammo, ps->powerups, &numBarbs, NULL ); + + if( height > width ) + { + vertical = qtrue; + iconsize = width; + } + else if( height <= width ) + { + vertical = qfalse; + iconsize = height; + } + + if( color[ 3 ] != 0.0 ) + trap_R_SetColor( color ); + + for( i = 0; i < numBarbs; i ++ ) + { + if( vertical ) + y += iconsize; + else + x += iconsize; + + CG_DrawPic( x, y, iconsize, iconsize, shader ); + } + + trap_R_SetColor( NULL ); +} + +/* +============== +CG_DrawPlayerWallclimbing +============== +*/ +static void CG_DrawPlayerWallclimbing( rectDef_t *rect, vec4_t color, qhandle_t shader ) +{ + playerState_t *ps = &cg.snap->ps; + qboolean ww = ps->stats[ STAT_STATE ] & SS_WALLCLIMBING; + + if( ww ) + color[ 3 ] = AH_MAX_ALPHA; + else + color[ 3 ] = AH_MIN_ALPHA; + + trap_R_SetColor( color ); + CG_DrawPic( rect->x, rect->y, rect->w, rect->h, shader ); + trap_R_SetColor( NULL ); +} + +static void CG_DrawPlayerStamina( rectDef_t *rect, vec4_t color, float scale, + int align, int textStyle, int special ) +{ + playerState_t *ps = &cg.snap->ps; + int stamina = ps->stats[ STAT_STAMINA ]; + float progress = ( (float)stamina + (float)MAX_STAMINA ) / ( (float)MAX_STAMINA * 2.0f ); + + CG_DrawProgressBar( rect, color, scale, align, textStyle, special, progress ); +} + +static void CG_DrawPlayerAmmoValue( rectDef_t *rect, vec4_t color ) +{ + int value; + centity_t *cent; + playerState_t *ps; + + cent = &cg_entities[ cg.snap->ps.clientNum ]; + ps = &cg.snap->ps; + + if( cent->currentState.weapon ) + { + switch( cent->currentState.weapon ) + { + case WP_ABUILD: + case WP_ABUILD2: + //percentage of BP remaining + value = cgs.alienBuildPoints; + break; + + case WP_HBUILD: + case WP_HBUILD2: + //percentage of BP remaining + value = cgs.humanBuildPoints; + break; + + default: + BG_UnpackAmmoArray( cent->currentState.weapon, ps->ammo, ps->powerups, &value, NULL ); + break; + } + + if( value > 999 ) + value = 999; + + if( value > -1 ) + { + trap_R_SetColor( color ); + CG_DrawField( rect->x, rect->y, 4, rect->w / 4, rect->h, value ); + trap_R_SetColor( NULL ); + } + } +} + + +/* +============== +CG_DrawAlienSense +============== +*/ +static void CG_DrawAlienSense( rectDef_t *rect ) +{ + if( BG_ClassHasAbility( cg.snap->ps.stats[ STAT_PCLASS ], 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_FindUsableForBuildable( es->modelindex ) && + cg.predictedPlayerState.stats[ STAT_PTEAM ] == BG_FindTeamForBuildable( es->modelindex ) ) + { + //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_FindUsesEnergyForWeapon( cg.snap->ps.weapon ) || + BG_FindInfinteAmmoForWeapon( cg.snap->ps.weapon ) ) ) + return; + + trap_R_SetColor( color ); + CG_DrawPic( rect->x, rect->y, rect->w, rect->h, shader ); + trap_R_SetColor( NULL ); + } +} + + +#define BUILD_DELAY_TIME 2000 + +static void CG_DrawPlayerBuildTimer( rectDef_t *rect, vec4_t color ) +{ + float progress; + int index; + centity_t *cent; + playerState_t *ps; + + cent = &cg_entities[ cg.snap->ps.clientNum ]; + ps = &cg.snap->ps; + + if( cent->currentState.weapon ) + { + switch( cent->currentState.weapon ) + { + case WP_ABUILD: + progress = (float)ps->stats[ STAT_MISC ] / (float)ABUILDER_BASE_DELAY; + break; + + case WP_ABUILD2: + progress = (float)ps->stats[ STAT_MISC ] / (float)ABUILDER_ADV_DELAY; + break; + + case WP_HBUILD: + progress = (float)ps->stats[ STAT_MISC ] / (float)HBUILD_DELAY; + break; + + case WP_HBUILD2: + progress = (float)ps->stats[ STAT_MISC ] / (float)HBUILD2_DELAY; + break; + + default: + return; + break; + } + + if( !ps->stats[ STAT_MISC ] ) + return; + + index = (int)( progress * 8.0f ); + + if( index > 7 ) + index = 7; + else if( index < 0 ) + index = 0; + + if( cg.time - cg.lastBuildAttempt <= BUILD_DELAY_TIME ) + { + if( ( ( 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; + centity_t *cent; + playerState_t *ps; + + cent = &cg_entities[ cg.snap->ps.clientNum ]; + ps = &cg.snap->ps; + + if( cent->currentState.weapon ) + { + switch( cent->currentState.weapon ) + { + case WP_ABUILD: + case WP_ABUILD2: + case WP_HBUILD: + case WP_HBUILD2: + break; + + default: + BG_UnpackAmmoArray( cent->currentState.weapon, ps->ammo, ps->powerups, NULL, &value ); + + 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 ) +{ + playerState_t *ps; + int value; + + ps = &cg.snap->ps; + + value = ps->stats[ STAT_HEALTH ]; + + trap_R_SetColor( color ); + CG_DrawField( rect->x, rect->y, 4, rect->w / 4, rect->h, value ); + trap_R_SetColor( NULL ); +} + +static void CG_DrawPlayerHealthBar( rectDef_t *rect, vec4_t color, float scale, + int align, int textStyle, int special ) +{ + playerState_t *ps; + float total; + + ps = &cg.snap->ps; + + total = ( (float)ps->stats[ STAT_HEALTH ] / (float)ps->stats[ STAT_MAX_HEALTH ] ); + CG_DrawProgressBar( rect, color, scale, align, textStyle, special, total ); +} + +/* +============== +CG_DrawPlayerHealthCross +============== +*/ +static void CG_DrawPlayerHealthCross( rectDef_t *rect, vec4_t color, qhandle_t shader ) +{ + playerState_t *ps = &cg.snap->ps; + int health = ps->stats[ STAT_HEALTH ]; + + if( health < 10 ) + { + color[ 0 ] = 1.0f; + color[ 1 ] = color[ 2 ] = 0.0f; + } + + trap_R_SetColor( color ); + CG_DrawPic( rect->x, rect->y, rect->w, rect->h, shader ); + trap_R_SetColor( NULL ); +} + +static void CG_DrawProgressLabel( rectDef_t *rect, float text_x, float text_y, vec4_t color, + float scale, int align, const char *s, float fraction ) +{ + vec4_t white = { 1.0f, 1.0f, 1.0f, 1.0f }; + float tx, tw = CG_Text_Width( s, scale, 0 ); + + switch( align ) + { + case ITEM_ALIGN_LEFT: + tx = 0.0f; + break; + + case ITEM_ALIGN_RIGHT: + tx = rect->w - tw; + break; + + case ITEM_ALIGN_CENTER: + tx = ( rect->w / 2.0f ) - ( tw / 2.0f ); + break; + + default: + tx = 0.0f; + } + + if( fraction < 1.0f ) + CG_Text_Paint( rect->x + text_x + tx, rect->y + text_y, scale, white, + s, 0, 0, ITEM_TEXTSTYLE_NORMAL ); + else + CG_Text_Paint( rect->x + text_x + tx, rect->y + text_y, scale, color, + s, 0, 0, ITEM_TEXTSTYLE_NEON ); +} + +static void CG_DrawMediaProgress( rectDef_t *rect, vec4_t color, float scale, + int align, int textStyle, int special ) +{ + CG_DrawProgressBar( rect, color, scale, align, textStyle, special, cg.mediaFraction ); +} + +static void CG_DrawMediaProgressLabel( rectDef_t *rect, float text_x, float text_y, + vec4_t color, float scale, int align ) +{ + CG_DrawProgressLabel( rect, text_x, text_y, color, scale, align, "Map and Textures", cg.mediaFraction ); +} + +static void CG_DrawBuildablesProgress( rectDef_t *rect, vec4_t color, float scale, + int align, int textStyle, int special ) +{ + CG_DrawProgressBar( rect, color, scale, align, textStyle, special, cg.buildablesFraction ); +} + +static void CG_DrawBuildablesProgressLabel( rectDef_t *rect, float text_x, float text_y, + vec4_t color, float scale, int align ) +{ + CG_DrawProgressLabel( rect, text_x, text_y, color, scale, align, "Buildable Models", cg.buildablesFraction ); +} + +static void CG_DrawCharModelProgress( rectDef_t *rect, vec4_t color, float scale, + int align, int textStyle, int special ) +{ + CG_DrawProgressBar( rect, color, scale, align, textStyle, special, cg.charModelFraction ); +} + +static void CG_DrawCharModelProgressLabel( rectDef_t *rect, float text_x, float text_y, + vec4_t color, float scale, int align ) +{ + CG_DrawProgressLabel( rect, text_x, text_y, color, scale, align, "Character Models", cg.charModelFraction ); +} + +static void CG_DrawOverallProgress( rectDef_t *rect, vec4_t color, float scale, + int align, int textStyle, int special ) +{ + float total; + + total = ( cg.charModelFraction + cg.buildablesFraction + cg.mediaFraction ) / 3.0f; + CG_DrawProgressBar( rect, color, scale, align, textStyle, special, total ); +} + +static void CG_DrawLevelShot( rectDef_t *rect ) +{ + const char *s; + const char *info; + qhandle_t levelshot; + qhandle_t detail; + + 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 ); + + // blend a detail texture over it + detail = trap_R_RegisterShader( "gfx/misc/detail" ); + CG_DrawPic( rect->x, rect->y, rect->w, rect->h, detail ); +} + +static void CG_DrawLoadingString( rectDef_t *rect, float text_x, float text_y, vec4_t color, + float scale, int align, int textStyle, const char *s ) +{ + float tw, th, tx; + int pos, i; + char buffer[ 1024 ]; + char *end; + + if( !s[ 0 ] ) + return; + + strcpy( buffer, s ); + tw = CG_Text_Width( s, scale, 0 ); + th = scale * 40.0f; + + pos = i = 0; + + while( pos < strlen( s ) ) + { + strcpy( buffer, &s[ pos ] ); + tw = CG_Text_Width( buffer, scale, 0 ); + + while( tw > rect->w ) + { + end = strrchr( buffer, ' ' ); + + if( end == NULL ) + break; + + *end = '\0'; + tw = CG_Text_Width( buffer, scale, 0 ); + } + + switch( align ) + { + case ITEM_ALIGN_LEFT: + tx = rect->x; + break; + + case ITEM_ALIGN_RIGHT: + tx = rect->x + rect->w - tw; + break; + + case ITEM_ALIGN_CENTER: + tx = rect->x + ( rect->w / 2.0f ) - ( tw / 2.0f ); + break; + + default: + tx = 0.0f; + } + + CG_Text_Paint( tx + text_x, rect->y + text_y + i * ( th + 3 ), scale, color, + buffer, 0, 0, textStyle ); + + pos += strlen( buffer ) + 1; + i++; + } +} + +static void CG_DrawLevelName( rectDef_t *rect, float text_x, float text_y, + vec4_t color, float scale, int align, int textStyle ) +{ + const char *s; + + s = CG_ConfigString( CS_MESSAGE ); + + CG_DrawLoadingString( rect, text_x, text_y, color, scale, align, textStyle, s ); +} + +static void CG_DrawMOTD( rectDef_t *rect, float text_x, float text_y, + vec4_t color, float scale, int align, int textStyle ) +{ + const char *s; + + s = CG_ConfigString( CS_MOTD ); + + CG_DrawLoadingString( rect, text_x, text_y, color, scale, align, textStyle, s ); +} + +static void CG_DrawHostname( rectDef_t *rect, float text_x, float text_y, + vec4_t color, float scale, int align, int textStyle ) +{ + char buffer[ 1024 ]; + const char *info; + + info = CG_ConfigString( CS_SERVERINFO ); + + Q_strncpyz( buffer, Info_ValueForKey( info, "sv_hostname" ), 1024 ); + Q_CleanStr( buffer ); + + CG_DrawLoadingString( rect, text_x, text_y, color, scale, align, 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 ) +{ + Menu_Paint( Menus_FindByName( "Loading" ), qtrue ); +} + +float CG_GetValue( int ownerDraw ) +{ + centity_t *cent; + playerState_t *ps; + + cent = &cg_entities[ cg.snap->ps.clientNum ]; + ps = &cg.snap->ps; + + switch( ownerDraw ) + { + case CG_PLAYER_AMMO_VALUE: + if( cent->currentState.weapon ) + { + int value; + + BG_UnpackAmmoArray( cent->currentState.weapon, ps->ammo, ps->powerups, + &value, NULL ); + + return value; + } + break; + case CG_PLAYER_CLIPS_VALUE: + if( cent->currentState.weapon ) + { + int value; + + BG_UnpackAmmoArray( cent->currentState.weapon, ps->ammo, ps->powerups, + NULL, &value ); + + return value; + } + 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; + CG_Text_Paint( x - CG_Text_Width( CG_GetKillerText( ), scale, 0 ) / 2, + rect->y + rect->h, scale, color, CG_GetKillerText( ), 0, 0, textStyle ); + } +} + + +static void CG_Text_Paint_Limit( float *maxX, float x, float y, float scale, + vec4_t color, const char* text, float adjust, int limit ) +{ + int len, count; + vec4_t newColor; + glyphInfo_t *glyph; + + if( text ) + { +// TTimo: FIXME +// const unsigned char *s = text; // bk001206 - unsigned + const char *s = text; + float max = *maxX; + float useScale; + fontInfo_t *font = &cgDC.Assets.textFont; + + if( scale <= cg_smallFont.value ) + font = &cgDC.Assets.smallFont; + else if( scale > cg_bigFont.value ) + font = &cgDC.Assets.bigFont; + + useScale = scale * font->glyphScale; + trap_R_SetColor( color ); + len = strlen( text ); + + if( limit > 0 && len > limit ) + len = limit; + + count = 0; + + while( s && *s && count < len ) + { + glyph = &font->glyphs[ (int)*s ]; + //TTimo: FIXME: getting nasty warnings without the cast, + //hopefully this doesn't break the VM build + + if( Q_IsColorString( s ) ) + { + memcpy( newColor, g_color_table[ ColorIndex( *(s+1) ) ], sizeof( newColor ) ); + newColor[ 3 ] = color[ 3 ]; + trap_R_SetColor( newColor ); + s += 2; + continue; + } + else + { + float yadj = useScale * glyph->top; + + if( CG_Text_Width( s, useScale, 1 ) + x > max ) + { + *maxX = 0; + break; + } + + CG_Text_PaintChar( x, y - yadj, + glyph->imageWidth, + glyph->imageHeight, + useScale, + glyph->s, + glyph->t, + glyph->s2, + glyph->t2, + glyph->glyph ); + x += ( glyph->xSkip * useScale ) + adjust; + *maxX = x; + count++; + s++; + } + } + + trap_R_SetColor( NULL ); + } +} + +static void CG_DrawTeamSpectators( rectDef_t *rect, float scale, vec4_t color, qhandle_t shader ) +{ + if( cg.spectatorLen ) + { + float maxX; + + if( cg.spectatorWidth == -1 ) + { + cg.spectatorWidth = 0; + cg.spectatorPaintX = rect->x + 1; + cg.spectatorPaintX2 = -1; + } + + if( cg.spectatorOffset > cg.spectatorLen ) + { + cg.spectatorOffset = 0; + cg.spectatorPaintX = rect->x + 1; + cg.spectatorPaintX2 = -1; + } + + if( cg.time > cg.spectatorTime ) + { + cg.spectatorTime = cg.time + 10; + + if( cg.spectatorPaintX <= rect->x + 2 ) + { + if( cg.spectatorOffset < cg.spectatorLen ) + { + //TA: skip colour directives + if( Q_IsColorString( &cg.spectatorList[ cg.spectatorOffset ] ) ) + cg.spectatorOffset += 2; + else + { + cg.spectatorPaintX += CG_Text_Width( &cg.spectatorList[ cg.spectatorOffset ], scale, 1 ) - 1; + cg.spectatorOffset++; + } + } + else + { + cg.spectatorOffset = 0; + + if( cg.spectatorPaintX2 >= 0 ) + cg.spectatorPaintX = cg.spectatorPaintX2; + else + cg.spectatorPaintX = rect->x + rect->w - 2; + + cg.spectatorPaintX2 = -1; + } + } + else + { + cg.spectatorPaintX--; + + if( cg.spectatorPaintX2 >= 0 ) + cg.spectatorPaintX2--; + } + } + + maxX = rect->x + rect->w - 2; + + CG_Text_Paint_Limit( &maxX, cg.spectatorPaintX, rect->y + rect->h - 3, scale, color, + &cg.spectatorList[ cg.spectatorOffset ], 0, 0 ); + + if( cg.spectatorPaintX2 >= 0 ) + { + float maxX2 = rect->x + rect->w - 2; + CG_Text_Paint_Limit( &maxX2, cg.spectatorPaintX2, rect->y + rect->h - 3, scale, + color, cg.spectatorList, 0, cg.spectatorOffset ); + } + + if( cg.spectatorOffset && maxX > 0 ) + { + // if we have an offset ( we are skipping the first part of the string ) and we fit the string + if( cg.spectatorPaintX2 == -1 ) + cg.spectatorPaintX2 = rect->x + rect->w - 2; + } + else + cg.spectatorPaintX2 = -1; + } +} + +/* +================== +CG_DrawStageReport +================== +*/ +static void CG_DrawStageReport( rectDef_t *rect, float text_x, float text_y, + vec4_t color, float scale, int align, int textStyle ) +{ + char s[ MAX_TOKEN_CHARS ]; + int tx, w, kills; + + if( cg.snap->ps.stats[ STAT_PTEAM ] == PTE_NONE && !cg.intermissionStarted ) + return; + + if( cg.intermissionStarted ) + { + Com_sprintf( s, MAX_TOKEN_CHARS, + "Stage %d" //PH34R MY MAD-LEET CODING SKILLZ + " " + "Stage %d", + cgs.alienStage + 1, cgs.humanStage + 1 ); + } + else if( cg.snap->ps.stats[ STAT_PTEAM ] == PTE_ALIENS ) + { + kills = cgs.alienNextStageThreshold - cgs.alienKills; + + 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, %d kill for next stage", + cgs.alienStage + 1, kills ); + else + Com_sprintf( s, MAX_TOKEN_CHARS, "Stage %d, %d kills for next stage", + cgs.alienStage + 1, kills ); + } + else if( cg.snap->ps.stats[ STAT_PTEAM ] == PTE_HUMANS ) + { + kills = cgs.humanNextStageThreshold - cgs.humanKills; + + if( cgs.humanNextStageThreshold < 0 ) + Com_sprintf( s, MAX_TOKEN_CHARS, "Stage %d", cgs.humanStage + 1 ); + else if( kills == 1 ) + Com_sprintf( s, MAX_TOKEN_CHARS, "Stage %d, %d kill for next stage", + cgs.humanStage + 1, kills ); + else + Com_sprintf( s, MAX_TOKEN_CHARS, "Stage %d, %d kills for next stage", + cgs.humanStage + 1, kills ); + } + + w = CG_Text_Width( s, scale, 0 ); + + switch( align ) + { + case ITEM_ALIGN_LEFT: + tx = rect->x; + break; + + case ITEM_ALIGN_RIGHT: + tx = rect->x + rect->w - w; + break; + + case ITEM_ALIGN_CENTER: + tx = rect->x + ( rect->w / 2.0f ) - ( w / 2.0f ); + break; + + default: + tx = 0.0f; + } + + CG_Text_Paint( text_x + tx, rect->y + text_y, scale, color, s, 0, 0, textStyle ); +} + +/* +================== +CG_DrawFPS +================== +*/ +//TA: personally i think this should be longer - it should really be a cvar +#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 align, int textStyle, + qboolean scalableText ) +{ + char *s; + int tx, w, totalWidth, 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 = CG_Text_Width( "0", scale, 0 ); + strLength = CG_DrawStrlen( s ); + totalWidth = CG_Text_Width( FPS_STRING, scale, 0 ) + w * strLength; + + switch( align ) + { + case ITEM_ALIGN_LEFT: + tx = rect->x; + break; + + case ITEM_ALIGN_RIGHT: + tx = rect->x + rect->w - totalWidth; + break; + + case ITEM_ALIGN_CENTER: + tx = rect->x + ( rect->w / 2.0f ) - ( totalWidth / 2.0f ); + break; + + default: + tx = 0.0f; + } + + if( scalableText ) + { + for( i = 0; i < strLength; i++ ) + { + char c[ 2 ]; + + c[ 0 ] = s[ i ]; + c[ 1 ] = '\0'; + + CG_Text_Paint( text_x + tx + i * w, rect->y + text_y, scale, color, c, 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 ); + } + + if( scalableText ) + CG_Text_Paint( text_x + tx + i * w, rect->y + text_y, scale, color, FPS_STRING, 0, 0, textStyle ); + } +} + + +/* +================= +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 align, int textStyle ) +{ + char *s; + int i, tx, w, totalWidth, strLength; + 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 = CG_Text_Width( "0", scale, 0 ); + strLength = CG_DrawStrlen( s ); + totalWidth = w * strLength; + + switch( align ) + { + case ITEM_ALIGN_LEFT: + tx = rect->x; + break; + + case ITEM_ALIGN_RIGHT: + tx = rect->x + rect->w - totalWidth; + break; + + case ITEM_ALIGN_CENTER: + tx = rect->x + ( rect->w / 2.0f ) - ( totalWidth / 2.0f ); + break; + + default: + tx = 0.0f; + } + + for( i = 0; i < strLength; i++ ) + { + char c[ 2 ]; + + c[ 0 ] = s[ i ]; + c[ 1 ] = '\0'; + + CG_Text_Paint( text_x + tx + i * w, rect->y + text_y, scale, color, c, 0, 0, textStyle ); + } +} + +/* +================= +CG_DrawClock +================= +*/ +static void CG_DrawClock( rectDef_t *rect, float text_x, float text_y, + float scale, vec4_t color, int align, int textStyle ) +{ + char *s; + int i, tx, w, totalWidth, strLength; + 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 = CG_Text_Width( "0", scale, 0 ); + strLength = CG_DrawStrlen( s ); + totalWidth = w * strLength; + + switch( align ) + { + case ITEM_ALIGN_LEFT: + tx = rect->x; + break; + + case ITEM_ALIGN_RIGHT: + tx = rect->x + rect->w - totalWidth; + break; + + case ITEM_ALIGN_CENTER: + tx = rect->x + ( rect->w / 2.0f ) - ( totalWidth / 2.0f ); + break; + + default: + tx = 0.0f; + } + + for( i = 0; i < strLength; i++ ) + { + char c[ 2 ]; + + c[ 0 ] = s[ i ]; + c[ 1 ] = '\0'; + + CG_Text_Paint( text_x + tx + i * w, rect->y + text_y, 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 align, int textStyle ) +{ + char *s; + int w, tx; + + if( !cg_drawSnapshot.integer ) + return; + + s = va( "time:%d snap:%d cmd:%d", cg.snap->serverTime, + cg.latestSnapshotNum, cgs.serverCommandSequence ); + w = CG_Text_Width( s, scale, 0 ); + + switch( align ) + { + case ITEM_ALIGN_LEFT: + tx = rect->x; + break; + + case ITEM_ALIGN_RIGHT: + tx = rect->x + rect->w - w; + break; + + case ITEM_ALIGN_CENTER: + tx = rect->x + ( rect->w / 2.0f ) - ( w / 2.0f ); + break; + + default: + tx = 0.0f; + } + + CG_Text_Paint( text_x + tx, rect->y + text_y, 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 = CG_Text_Width( s, 0.7f, 0 ); + CG_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; + vec4_t white = { 1.0f, 1.0f, 1.0f, 1.0f }; + + 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 ) + CG_Text_Paint( ax, ay, 0.5, white, "snc", 0, 0, ITEM_TEXTSTYLE_NORMAL ); + else + { + char *s; + + s = va( "%d", cg.ping ); + ax = rect->x + ( rect->w / 2.0f ) - ( CG_Text_Width( s, scale, 0 ) / 2.0f ) + text_x; + ay = rect->y + ( rect->h / 2.0f ) + ( CG_Text_Height( s, scale, 0 ) / 2.0f ) + text_y; + + Vector4Copy( textColor, adjustedColor ); + adjustedColor[ 3 ] = 0.5f; + CG_Text_Paint( ax, ay, scale, adjustedColor, s, 0, 0, ITEM_TEXTSTYLE_NORMAL ); + } + + CG_DrawDisconnect( ); +} + +/* +============== +CG_DrawTextBlock +============== +*/ +static void CG_DrawTextBlock( rectDef_t *rect, float text_x, float text_y, vec4_t color, + float scale, int align, int textStyle, const char *text, + menuDef_t *parent, itemDef_t *textItem ) +{ + float x, y, w, h; + + //offset the text + x = rect->x; + y = rect->y; + w = rect->w - ( 16 + ( 2 * text_x ) ); //16 to ensure text within frame + h = rect->h; + + textItem->text = text; + + textItem->parent = parent; + memcpy( textItem->window.foreColor, color, sizeof( vec4_t ) ); + textItem->window.flags = 0; + + switch( align ) + { + case ITEM_ALIGN_LEFT: + textItem->window.rect.x = x; + break; + + case ITEM_ALIGN_RIGHT: + textItem->window.rect.x = x + w; + break; + + case ITEM_ALIGN_CENTER: + textItem->window.rect.x = x + ( w / 2 ); + break; + + default: + textItem->window.rect.x = x; + break; + } + + textItem->window.rect.y = y; + textItem->window.rect.w = w; + textItem->window.rect.h = h; + textItem->window.borderSize = 0; + textItem->textRect.x = 0; + textItem->textRect.y = 0; + textItem->textRect.w = 0; + textItem->textRect.h = 0; + textItem->textalignment = align; + textItem->textalignx = text_x; + textItem->textaligny = text_y; + textItem->textscale = scale; + textItem->textStyle = textStyle; + + //hack to utilise existing autowrap code + Item_Text_AutoWrapped_Paint( textItem ); +} + +/* +=================== +CG_DrawConsole +=================== +*/ +static void CG_DrawConsole( rectDef_t *rect, float text_x, float text_y, vec4_t color, + float scale, int align, int textStyle ) +{ + static menuDef_t dummyParent; + static itemDef_t textItem; + + CG_DrawTextBlock( rect, text_x, text_y, color, scale, align, textStyle, + cg.consoleText, &dummyParent, &textItem ); +} + +/* +=================== +CG_DrawTutorial +=================== +*/ +static void CG_DrawTutorial( rectDef_t *rect, float text_x, float text_y, vec4_t color, + float scale, int align, int textStyle ) +{ + static menuDef_t dummyParent; + static itemDef_t textItem; + + if( !cg_tutorial.integer ) + return; + + CG_DrawTextBlock( rect, text_x, text_y, color, scale, align, textStyle, + CG_TutorialText( ), &dummyParent, &textItem ); +} + +/* +=================== +CG_DrawWeaponIcon +=================== +*/ +void CG_DrawWeaponIcon( rectDef_t *rect, vec4_t color ) +{ + int ammo, clips, maxAmmo; + centity_t *cent; + playerState_t *ps; + + cent = &cg_entities[ cg.snap->ps.clientNum ]; + ps = &cg.snap->ps; + + BG_UnpackAmmoArray( cent->currentState.weapon, ps->ammo, ps->powerups, &ammo, &clips ); + BG_FindAmmoForWeapon( cent->currentState.weapon, &maxAmmo, NULL ); + + // don't display if dead + if( cg.predictedPlayerState.stats[ STAT_HEALTH ] <= 0 ) + return; + + if( cent->currentState.weapon == 0 ) + return; + + CG_RegisterWeapon( cent->currentState.weapon ); + + if( clips == 0 && !BG_FindInfinteAmmoForWeapon( cent->currentState.weapon ) ) + { + float ammoPercent = (float)ammo / (float)maxAmmo; + + if( ammoPercent < 0.33f ) + { + color[ 0 ] = 1.0f; + color[ 1 ] = color[ 2 ] = 0.0f; + } + } + + if( cg.predictedPlayerState.stats[ STAT_PTEAM ] == PTE_ALIENS && CG_AtHighestClass( ) ) + { + 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[ cent->currentState.weapon ].weaponIcon ); + trap_R_SetColor( NULL ); +} + + + +/* +================================================================================ + +CROSSHAIR + +================================================================================ +*/ + + +/* +================= +CG_DrawCrosshair +================= +*/ +static void CG_DrawCrosshair( void ) +{ + float w, h; + qhandle_t hShader; + float x, y; + weaponInfo_t *wi; + + if( cg_drawCrosshair.integer == CROSSHAIR_ALWAYSOFF ) + return; + + if( cg_drawCrosshair.integer == CROSSHAIR_RANGEDONLY && + !BG_FindLongRangedForWeapon( cg.snap->ps.weapon ) ) + { + return; + } + + if( ( cg.snap->ps.persistant[ PERS_TEAM ] == TEAM_SPECTATOR ) || + ( cg.snap->ps.stats[ STAT_STATE ] & SS_INFESTING ) || + ( cg.snap->ps.stats[ STAT_STATE ] & SS_HOVELING ) ) + return; + + if( cg.renderingThirdPerson ) + return; + + wi = &cg_weapons[ cg.snap->ps.weapon ]; + + w = h = wi->crossHairSize; + + x = cg_crosshairX.integer; + y = cg_crosshairY.integer; + CG_AdjustFrom640( &x, &y, &w, &h ); + + hShader = wi->crossHair; + + if( hShader != 0 ) + { + trap_R_DrawStretchPic( x + cg.refdef.x + 0.5 * ( cg.refdef.width - w ), + y + cg.refdef.y + 0.5 * ( cg.refdef.height - h ), + w, h, 0, 0, 1, 1, hShader ); + } +} + + + +/* +================= +CG_ScanForCrosshairEntity +================= +*/ +static void CG_ScanForCrosshairEntity( void ) +{ + trace_t trace; + vec3_t start, end; + int content; + pTeam_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( trace.entityNum >= MAX_CLIENTS ) + return; + + // if the player is in fog, don't show it + content = trap_CM_PointContents( trace.endpos, 0 ); + if( content & CONTENTS_FOG ) + return; + + team = cgs.clientinfo[ trace.entityNum ].team; + + if( cg.snap->ps.persistant[ PERS_TEAM ] != TEAM_SPECTATOR ) + { + //only display team names of those on the same team as this player + if( team != cg.snap->ps.stats[ STAT_PTEAM ] ) + return; + } + + // update the fade timer + cg.crosshairClientNum = trace.entityNum; + cg.crosshairClientTime = cg.time; +} + + +/* +===================== +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, 1000 ); + if( !color ) + { + trap_R_SetColor( NULL ); + return; + } + + name = cgs.clientinfo[ cg.crosshairClientNum ].name; + w = CG_Text_Width( name, scale, 0 ); + x = rect->x + rect->w / 2; + CG_Text_Paint( x - w / 2, 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, float special, float scale, vec4_t color, + qhandle_t shader, int textStyle ) +{ + rectDef_t rect; + + if( cg_drawStatus.integer == 0 ) + return; + + rect.x = x; + rect.y = y; + rect.w = w; + rect.h = h; + + switch( ownerDraw ) + { + case CG_PLAYER_CREDITS_VALUE: + CG_DrawPlayerCreditsValue( &rect, color, qtrue ); + break; + case CG_PLAYER_BANK_VALUE: + CG_DrawPlayerBankValue( &rect, color, qtrue ); + break; + case CG_PLAYER_CREDITS_VALUE_NOPAD: + CG_DrawPlayerCreditsValue( &rect, color, qfalse ); + break; + case CG_PLAYER_BANK_VALUE_NOPAD: + CG_DrawPlayerBankValue( &rect, color, qfalse ); + break; + case CG_PLAYER_STAMINA: + CG_DrawPlayerStamina( &rect, color, scale, align, textStyle, special ); + break; + case CG_PLAYER_STAMINA_1: + CG_DrawPlayerStamina1( &rect, color, shader ); + break; + case CG_PLAYER_STAMINA_2: + CG_DrawPlayerStamina2( &rect, color, shader ); + break; + case CG_PLAYER_STAMINA_3: + CG_DrawPlayerStamina3( &rect, color, shader ); + break; + case CG_PLAYER_STAMINA_4: + CG_DrawPlayerStamina4( &rect, color, shader ); + break; + case CG_PLAYER_STAMINA_BOLT: + CG_DrawPlayerStaminaBolt( &rect, color, shader ); + break; + case CG_PLAYER_AMMO_VALUE: + CG_DrawPlayerAmmoValue( &rect, color ); + break; + case CG_PLAYER_CLIPS_VALUE: + CG_DrawPlayerClipsValue( &rect, color ); + break; + case CG_PLAYER_BUILD_TIMER: + CG_DrawPlayerBuildTimer( &rect, color ); + break; + case CG_PLAYER_HEALTH: + CG_DrawPlayerHealthValue( &rect, color ); + break; + case CG_PLAYER_HEALTH_BAR: + CG_DrawPlayerHealthBar( &rect, color, scale, align, textStyle, special ); + break; + case CG_PLAYER_HEALTH_CROSS: + CG_DrawPlayerHealthCross( &rect, color, shader ); + break; + case CG_PLAYER_CLIPS_RING: + CG_DrawPlayerClipsRing( &rect, color, shader ); + break; + case CG_PLAYER_BUILD_TIMER_RING: + CG_DrawPlayerBuildTimerRing( &rect, color, shader ); + break; + case CG_PLAYER_WALLCLIMBING: + CG_DrawPlayerWallclimbing( &rect, color, shader ); + break; + case CG_PLAYER_BOOSTED: + CG_DrawPlayerBoosted( &rect, color, shader ); + break; + case CG_PLAYER_BOOST_BOLT: + CG_DrawPlayerBoosterBolt( &rect, color, shader ); + break; + case CG_PLAYER_POISON_BARBS: + CG_DrawPlayerPoisonBarbs( &rect, color, shader ); + break; + case CG_PLAYER_ALIEN_SENSE: + CG_DrawAlienSense( &rect ); + break; + case CG_PLAYER_HUMAN_SCANNER: + CG_DrawHumanScanner( &rect, shader, color ); + break; + case CG_PLAYER_USABLE_BUILDABLE: + CG_DrawUsableBuildable( &rect, shader, color ); + break; + case CG_KILLER: + CG_DrawKiller( &rect, scale, color, shader, textStyle ); + break; + case CG_PLAYER_SELECT: + CG_DrawItemSelect( &rect, color ); + break; + case CG_PLAYER_WEAPONICON: + CG_DrawWeaponIcon( &rect, color ); + break; + case CG_PLAYER_SELECTTEXT: + CG_DrawItemSelectText( &rect, scale, textStyle ); + break; + case CG_SPECTATORS: + CG_DrawTeamSpectators( &rect, scale, color, shader ); + break; + case CG_PLAYER_CROSSHAIRNAMES: + CG_DrawCrosshairNames( &rect, scale, textStyle ); + break; + case CG_STAGE_REPORT_TEXT: + CG_DrawStageReport( &rect, text_x, text_y, color, scale, align, textStyle ); + break; + + //loading screen + case CG_LOAD_LEVELSHOT: + CG_DrawLevelShot( &rect ); + break; + case CG_LOAD_MEDIA: + CG_DrawMediaProgress( &rect, color, scale, align, textStyle, special ); + break; + case CG_LOAD_MEDIA_LABEL: + CG_DrawMediaProgressLabel( &rect, text_x, text_y, color, scale, align ); + break; + case CG_LOAD_BUILDABLES: + CG_DrawBuildablesProgress( &rect, color, scale, align, textStyle, special ); + break; + case CG_LOAD_BUILDABLES_LABEL: + CG_DrawBuildablesProgressLabel( &rect, text_x, text_y, color, scale, align ); + break; + case CG_LOAD_CHARMODEL: + CG_DrawCharModelProgress( &rect, color, scale, align, textStyle, special ); + break; + case CG_LOAD_CHARMODEL_LABEL: + CG_DrawCharModelProgressLabel( &rect, text_x, text_y, color, scale, align ); + break; + case CG_LOAD_OVERALL: + CG_DrawOverallProgress( &rect, color, scale, align, textStyle, special ); + break; + case CG_LOAD_LEVELNAME: + CG_DrawLevelName( &rect, text_x, text_y, color, scale, align, textStyle ); + break; + case CG_LOAD_MOTD: + CG_DrawMOTD( &rect, text_x, text_y, color, scale, align, textStyle ); + break; + case CG_LOAD_HOSTNAME: + CG_DrawHostname( &rect, text_x, text_y, color, scale, align, textStyle ); + break; + + case CG_FPS: + CG_DrawFPS( &rect, text_x, text_y, scale, color, align, textStyle, qtrue ); + break; + case CG_FPS_FIXED: + CG_DrawFPS( &rect, text_x, text_y, scale, color, align, textStyle, qfalse ); + break; + case CG_TIMER: + CG_DrawTimer( &rect, text_x, text_y, scale, color, align, textStyle ); + break; + case CG_CLOCK: + CG_DrawClock( &rect, text_x, text_y, scale, color, align, textStyle ); + break; + case CG_TIMER_MINS: + CG_DrawTimerMins( &rect, color ); + break; + case CG_TIMER_SECS: + CG_DrawTimerSecs( &rect, color ); + break; + case CG_SNAPSHOT: + CG_DrawSnapshot( &rect, text_x, text_y, scale, color, align, textStyle ); + break; + case CG_LAGOMETER: + CG_DrawLagometer( &rect, text_x, text_y, scale, color ); + break; + + case CG_DEMO_PLAYBACK: + CG_DrawDemoPlayback( &rect, color, shader ); + break; + case CG_DEMO_RECORDING: + CG_DrawDemoRecording( &rect, color, shader ); + break; + + case CG_CONSOLE: + CG_DrawConsole( &rect, text_x, text_y, color, scale, align, textStyle ); + break; + + case CG_TUTORIAL: + CG_DrawTutorial( &rect, text_x, text_y, color, scale, align, 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_OpenByName( "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 ) +{ +} + + +void CG_GetTeamColor( vec4_t *color ) +{ + (*color)[ 0 ] = (*color)[ 2 ] = 0.0f; + (*color)[ 1 ] = 0.17f; + (*color)[ 3 ] = 0.25f; +} +//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 ] < -800 ) && + ( cg.snap->ps.stats[ STAT_PTEAM ] == PTE_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; + + Q_strncpyz( cg.centerPrint, str, 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[ 1024 ]; + + for( l = 0; l < 50; l++ ) + { + if( !start[ l ] || start[ l ] == '\n' ) + break; + + linebuffer[ l ] = start[ l ]; + } + + linebuffer[ l ] = 0; + + w = CG_Text_Width( linebuffer, 0.5, 0 ); + h = CG_Text_Height( linebuffer, 0.5, 0 ); + x = ( SCREEN_WIDTH - w ) / 2; + CG_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( void ) +{ + char *s; + int sec; + vec4_t white = { 1.0f, 1.0f, 1.0f, 1.0f }; + char yeskey[ 32 ], nokey[ 32 ]; + + if( !cgs.voteTime ) + return; + + // play a talk beep whenever it is modified + if( cgs.voteModified ) + { + cgs.voteModified = qfalse; + trap_S_StartLocalSound( cgs.media.talkSound, CHAN_LOCAL_SOUND ); + } + + sec = ( VOTE_TIME - ( cg.time - cgs.voteTime ) ) / 1000; + + if( sec < 0 ) + sec = 0; + Q_strncpyz( yeskey, CG_KeyBinding( "vote yes" ), sizeof( yeskey ) ); + Q_strncpyz( nokey, CG_KeyBinding( "vote no" ), sizeof( nokey ) ); + s = va( "VOTE(%i): \"%s\" [%s]Yes:%i [%s]No:%i", sec, cgs.voteString, + yeskey, cgs.voteYes, nokey, cgs.voteNo ); + CG_Text_Paint( 8, 340, 0.3f, white, s, 0, 0, ITEM_TEXTSTYLE_NORMAL ); +} + +/* +================= +CG_DrawTeamVote +================= +*/ +static void CG_DrawTeamVote( void ) +{ + char *s; + int sec, cs_offset; + vec4_t white = { 1.0f, 1.0f, 1.0f, 1.0f }; + char yeskey[ 32 ], nokey[ 32 ]; + + if( cg.predictedPlayerState.stats[ STAT_PTEAM ] == PTE_HUMANS ) + cs_offset = 0; + else if( cg.predictedPlayerState.stats[ STAT_PTEAM ] == PTE_ALIENS ) + cs_offset = 1; + else + return; + + if( !cgs.teamVoteTime[ cs_offset ] ) + return; + + // play a talk beep whenever it is modified + if ( cgs.teamVoteModified[ cs_offset ] ) + { + cgs.teamVoteModified[ cs_offset ] = qfalse; + trap_S_StartLocalSound( cgs.media.talkSound, CHAN_LOCAL_SOUND ); + } + + sec = ( VOTE_TIME - ( cg.time - cgs.teamVoteTime[ cs_offset ] ) ) / 1000; + + if( sec < 0 ) + sec = 0; + + Q_strncpyz( yeskey, CG_KeyBinding( "teamvote yes" ), sizeof( yeskey ) ); + Q_strncpyz( nokey, CG_KeyBinding( "teamvote no" ), sizeof( nokey ) ); + s = va( "TEAMVOTE(%i): \"%s\" [%s]Yes:%i [%s]No:%i", sec, + cgs.teamVoteString[ cs_offset ], + yeskey, cgs.teamVoteYes[cs_offset], + nokey, cgs.teamVoteNo[ cs_offset ] ); + + CG_Text_Paint( 8, 360, 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; + } + + + if( menuScoreboard == NULL ) + menuScoreboard = Menus_FindByName( "teamscore_menu" ); + + if( menuScoreboard ) + { + if( firstTime ) + { + CG_SetScoreSelection( menuScoreboard ); + firstTime = qfalse; + } + + Menu_Paint( menuScoreboard, qtrue ); + } + + return qtrue; +} + +/* +================= +CG_DrawIntermission +================= +*/ +static void CG_DrawIntermission( void ) +{ + if( cg_drawStatus.integer ) + Menu_Paint( Menus_FindByName( "default_hud" ), qtrue ); + + cg.scoreFadeTime = cg.time; + cg.scoreBoardShowing = CG_DrawScoreboard( ); +} + +#define FOLLOWING_STRING "following " + +/* +================= +CG_DrawFollow +================= +*/ +static qboolean CG_DrawFollow( void ) +{ + float w; + vec4_t color; + char buffer[ MAX_STRING_CHARS ]; + + if( !( cg.snap->ps.pm_flags & PMF_FOLLOW ) ) + return qfalse; + + color[ 0 ] = 1; + color[ 1 ] = 1; + color[ 2 ] = 1; + color[ 3 ] = 1; + + strcpy( buffer, FOLLOWING_STRING ); + strcat( buffer, cgs.clientinfo[ cg.snap->ps.clientNum ].name ); + + w = CG_Text_Width( buffer, 0.7f, 0 ); + CG_Text_Paint( 320 - w / 2, 400, 0.7f, color, buffer, 0, 0, ITEM_TEXTSTYLE_SHADOWED ); + + return qtrue; +} + +/* +================= +CG_DrawQueue +================= +*/ +static qboolean CG_DrawQueue( void ) +{ + float w; + vec4_t color; + char 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; + + Com_sprintf( buffer, MAX_STRING_CHARS, "You are in position %d of the spawn queue.", + cg.snap->ps.persistant[ PERS_QUEUEPOS ] + 1 ); + + w = CG_Text_Width( buffer, 0.7f, 0 ); + CG_Text_Paint( 320 - w / 2, 360, 0.7f, color, buffer, 0, 0, ITEM_TEXTSTYLE_SHADOWED ); + + if( cg.snap->ps.stats[ STAT_PTEAM ] == PTE_ALIENS ) + { + if( cgs.numAlienSpawns == 1 ) + Com_sprintf( buffer, MAX_STRING_CHARS, "There is 1 spawn remaining." ); + else + Com_sprintf( buffer, MAX_STRING_CHARS, "There are %d spawns remaining.", + cgs.numAlienSpawns ); + } + else if( cg.snap->ps.stats[ STAT_PTEAM ] == PTE_HUMANS ) + { + if( cgs.numHumanSpawns == 1 ) + Com_sprintf( buffer, MAX_STRING_CHARS, "There is 1 spawn remaining." ); + else + Com_sprintf( buffer, MAX_STRING_CHARS, "There are %d spawns remaining.", + cgs.numHumanSpawns ); + } + + w = CG_Text_Width( buffer, 0.7f, 0 ); + CG_Text_Paint( 320 - w / 2, 400, 0.7f, color, buffer, 0, 0, ITEM_TEXTSTYLE_SHADOWED ); + + return qtrue; +} + +//================================================================================== + +#define SPECTATOR_STRING "SPECTATOR" +/* +================= +CG_Draw2D +================= +*/ +static void CG_Draw2D( void ) +{ + vec4_t color; + float w; + menuDef_t *menu = NULL, *defaultMenu; + + color[ 0 ] = color[ 1 ] = color[ 2 ] = color[ 3 ] = 1.0f; + + // if we are taking a levelshot for the menu, don't draw anything + if( cg.levelShot ) + return; + + if( cg_draw2D.integer == 0 ) + return; + + if( cg.snap->ps.pm_type == PM_INTERMISSION ) + { + CG_DrawIntermission( ); + return; + } + + //TA: draw the lighting effects e.g. nvg + CG_DrawLighting( ); + + + defaultMenu = Menus_FindByName( "default_hud" ); + + if( cg.snap->ps.persistant[ PERS_TEAM ] == TEAM_SPECTATOR ) + { + w = CG_Text_Width( SPECTATOR_STRING, 0.7f, 0 ); + CG_Text_Paint( 320 - w / 2, 440, 0.7f, color, SPECTATOR_STRING, 0, 0, ITEM_TEXTSTYLE_SHADOWED ); + } + else + menu = Menus_FindByName( BG_FindHudNameForClass( cg.predictedPlayerState.stats[ STAT_PCLASS ] ) ); + + if( !( cg.snap->ps.stats[ STAT_STATE ] & SS_INFESTING ) && + !( cg.snap->ps.stats[ STAT_STATE ] & SS_HOVELING ) && menu && + ( cg.snap->ps.stats[ STAT_HEALTH ] > 0 ) ) + { + CG_DrawBuildableStatus( ); + if( cg_drawStatus.integer ) + Menu_Paint( menu, qtrue ); + + CG_DrawCrosshair( ); + } + else if( cg_drawStatus.integer ) + Menu_Paint( defaultMenu, qtrue ); + + CG_DrawVote( ); + CG_DrawTeamVote( ); + CG_DrawFollow( ); + 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_TEAM ] == TEAM_SPECTATOR || 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_PTEAM ] == PTE_ALIENS ) + VectorSet( color, 0.43f, 0.8f, 0.37f ); + else if( cg.snap->ps.stats[ STAT_PTEAM ] == PTE_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..06ae071 --- /dev/null +++ b/src/cgame/cg_drawtools.c @@ -0,0 +1,378 @@ +/* +=========================================================================== +Copyright (C) 1999-2005 Id Software, Inc. +Copyright (C) 2000-2006 Tim Angus + +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 ) +{ + CG_AdjustFrom640( &x, &y, &w, &h ); + size *= cgs.screenXScale; + trap_R_DrawStretchPic( x, y, size, h, 0, 0, 0, 0, cgs.media.whiteShader ); + trap_R_DrawStretchPic( x + w - size, y, size, h, 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_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.0 ); + py = tan( cg.refdef.fov_y * M_PI / 360.0 ); + + VectorSubtract( point, cg.refdef.vieworg, trans ); + + xc = 640.0f / 2.0f; + yc = 480.0f / 2.0f; + + z = DotProduct( trans, cg.refdef.viewaxis[ 0 ] ); + if( z <= 0.001f ) + return qfalse; + + if( x ) + *x = xc - DotProduct( trans, cg.refdef.viewaxis[ 1 ] ) * xc / ( z * px ); + + if( y ) + *y = yc - 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; +} diff --git a/src/cgame/cg_ents.c b/src/cgame/cg_ents.c new file mode 100644 index 0000000..2d2e808 --- /dev/null +++ b/src/cgame/cg_ents.c @@ -0,0 +1,1256 @@ +/* +=========================================================================== +Copyright (C) 1999-2005 Id Software, Inc. +Copyright (C) 2000-2006 Tim Angus + +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( ¢->muzzleTS ) ) + { + //FIXME hack to prevent tesla trails reaching too far + if( cent->currentState.eType == ET_BUILDABLE ) + { + vec3_t front, back; + + CG_AttachmentPoint( ¢->muzzleTS->frontAttachment, front ); + CG_AttachmentPoint( ¢->muzzleTS->backAttachment, back ); + + if( Distance( front, back ) > ( TESLAGEN_RANGE * M_ROOT3 ) ) + CG_DestroyTrailSystem( ¢->muzzleTS ); + } + + if( cg.time > cent->muzzleTSDeathTime && CG_IsTrailSystemValid( ¢->muzzleTS ) ) + CG_DestroyTrailSystem( ¢->muzzleTS ); + } +} + + +/* +================== +CG_General +================== +*/ +static void CG_General( centity_t *cent ) +{ + refEntity_t ent; + entityState_t *s1; + + s1 = ¢->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 = ¢->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 ); + } + } + + 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 = ¢->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( ¢->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; + 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 = ¢->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 = ¢->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 = ¢->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->powerups; + 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 = ¢->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; + + es = ¢->currentState; + + for( i = 0; i <= 2; i++ ) + { + switch( i ) + { + case 0: + if( es->time <= 0 ) + continue; + + source = &cg_entities[ es->powerups ]; + target = &cg_entities[ es->time ]; + break; + + case 1: + if( es->time2 <= 0 ) + continue; + + source = &cg_entities[ es->time ]; + target = &cg_entities[ es->time2 ]; + break; + + case 2: + if( es->constantLight <= 0 ) + continue; + + source = &cg_entities[ es->time2 ]; + target = &cg_entities[ es->constantLight ]; + break; + } + + if( !CG_IsTrailSystemValid( ¢->level2ZapTS[ i ] ) ) + cent->level2ZapTS[ i ] = CG_SpawnNewTrailSystem( cgs.media.level2ZapTS ); + + if( CG_IsTrailSystemValid( ¢->level2ZapTS[ i ] ) ) + { + CG_SetAttachmentCent( ¢->level2ZapTS[ i ]->frontAttachment, source ); + CG_SetAttachmentCent( ¢->level2ZapTS[ i ]->backAttachment, target ); + CG_AttachToCent( ¢->level2ZapTS[ i ]->frontAttachment ); + CG_AttachToCent( ¢->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( ¢->currentState.pos, fromTime, oldOrigin ); + BG_EvaluateTrajectory( ¢->currentState.apos, fromTime, oldAngles ); + + BG_EvaluateTrajectory( ¢->currentState.pos, toTime, origin ); + BG_EvaluateTrajectory( ¢->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( ¢->currentState.pos, cg.snap->serverTime, current ); + BG_EvaluateTrajectory( ¢->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( ¢->currentState.apos, cg.snap->serverTime, current ); + BG_EvaluateTrajectory( ¢->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 > 0 && + 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( ¢->currentState.pos, + ( cg.time + timeshift ), cent->lerpOrigin ); + BG_EvaluateTrajectory( ¢->currentState.apos, + ( cg.time + timeshift ), cent->lerpAngles ); + + if( timeshift ) + { + trace_t tr; + vec3_t lastOrigin; + + BG_EvaluateTrajectory( ¢->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 = ¢->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; + } + + //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 = ¢->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 <= 2; i++ ) + { + if( CG_IsTrailSystemValid( ¢->level2ZapTS[ i ] ) ) + CG_DestroyTrailSystem( ¢->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: + 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 = ¢->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..07bcea9 --- /dev/null +++ b/src/cgame/cg_event.c @@ -0,0 +1,1034 @@ +/* +=========================================================================== +Copyright (C) 1999-2005 Id Software, Inc. +Copyright (C) 2000-2006 Tim Angus + +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[ 32 ]; + char attackerName[ 32 ]; + 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 ]; + + 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 ) - 2 ); + strcat( targetName, S_COLOR_WHITE ); + + message2 = ""; + + // check for single client messages + + switch( mod ) + { + case MOD_SUICIDE: + message = "suicides"; + break; + case MOD_FALLING: + message = "fell fowl to gravity"; + break; + case MOD_CRUSH: + message = "was squished"; + break; + case MOD_WATER: + message = "forgot to pack a snorkel"; + break; + case MOD_SLIME: + message = "melted"; + break; + case MOD_LAVA: + message = "does a back flip into the lava"; + break; + case MOD_TARGET_LASER: + message = "saw the light"; + break; + case MOD_TRIGGER_HURT: + message = "was in the wrong place"; + break; + case MOD_HSPAWN: + message = "should have run further"; + break; + case MOD_ASPAWN: + message = "shouldn't have trod in the acid"; + break; + case MOD_MGTURRET: + message = "was gunned down by a turret"; + break; + case MOD_TESLAGEN: + message = "was zapped by a tesla generator"; + break; + case MOD_ATUBE: + message = "was melted by an acid tube"; + break; + case MOD_OVERMIND: + message = "got too close to the overmind"; + break; + case MOD_REACTOR: + message = "got too close to the reactor"; + break; + case MOD_SLOWBLOB: + message = "should have visited a medical station"; + break; + case MOD_SWARM: + message = "was hunted down by the swarm"; + break; + default: + message = NULL; + break; + } + + if( attacker == target ) + { + gender = ci->gender; + switch( mod ) + { + case MOD_FLAMER_SPLASH: + if( gender == GENDER_FEMALE ) + message = "toasted herself"; + else if( gender == GENDER_NEUTER ) + message = "toasted itself"; + else + message = "toasted himself"; + break; + + case MOD_LCANNON_SPLASH: + if( gender == GENDER_FEMALE ) + message = "irradiated herself"; + else if( gender == GENDER_NEUTER ) + message = "irradiated itself"; + else + message = "irradiated himself"; + break; + + case MOD_GRENADE: + if( gender == GENDER_FEMALE ) + message = "blew herself up"; + else if( gender == GENDER_NEUTER ) + message = "blew itself up"; + else + message = "blew himself up"; + break; + + default: + if( gender == GENDER_FEMALE ) + message = "killed herself"; + else if( gender == GENDER_NEUTER ) + message = "killed itself"; + else + message = "killed himself"; + break; + } + } + + if( message ) + { + CG_Printf( "%s %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 ) - 2); + strcat( attackerName, S_COLOR_WHITE ); + // 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 = "was sawn by"; + break; + case MOD_BLASTER: + message = "was blasted by"; + break; + case MOD_MACHINEGUN: + message = "was machinegunned by"; + break; + case MOD_CHAINGUN: + message = "was chaingunned by"; + break; + case MOD_SHOTGUN: + message = "was gunned down by"; + break; + case MOD_PRIFLE: + message = "was pulse rifled by"; + break; + case MOD_MDRIVER: + message = "was mass driven by"; + break; + case MOD_LASGUN: + message = "was lasgunned by"; + break; + case MOD_FLAMER: + message = "was grilled by"; + message2 = "'s flamer"; + break; + case MOD_FLAMER_SPLASH: + message = "was toasted by"; + message2 = "'s flamer"; + break; + case MOD_LCANNON: + message = "felt the full force of"; + message2 = "'s lucifer cannon"; + break; + case MOD_LCANNON_SPLASH: + message = "was caught in the fallout of"; + message2 = "'s lucifer cannon"; + break; + case MOD_GRENADE: + message = "couldn't escape"; + message2 = "'s grenade"; + break; + + case MOD_ABUILDER_CLAW: + message = "should leave"; + message2 = "'s buildings alone"; + break; + case MOD_LEVEL0_BITE: + message = "was bitten by"; + break; + case MOD_LEVEL1_CLAW: + message = "was swiped by"; + Com_sprintf( className, 64, "'s %s", + BG_FindHumanNameForClassNum( PCL_ALIEN_LEVEL1 ) ); + message2 = className; + break; + case MOD_LEVEL2_CLAW: + message = "was clawed by"; + Com_sprintf( className, 64, "'s %s", + BG_FindHumanNameForClassNum( PCL_ALIEN_LEVEL2 ) ); + message2 = className; + break; + case MOD_LEVEL2_ZAP: + message = "was zapped by"; + Com_sprintf( className, 64, "'s %s", + BG_FindHumanNameForClassNum( PCL_ALIEN_LEVEL2 ) ); + message2 = className; + break; + case MOD_LEVEL3_CLAW: + message = "was chomped by"; + Com_sprintf( className, 64, "'s %s", + BG_FindHumanNameForClassNum( PCL_ALIEN_LEVEL3 ) ); + message2 = className; + break; + case MOD_LEVEL3_POUNCE: + message = "was pounced upon by"; + Com_sprintf( className, 64, "'s %s", + BG_FindHumanNameForClassNum( PCL_ALIEN_LEVEL3 ) ); + message2 = className; + break; + case MOD_LEVEL3_BOUNCEBALL: + message = "was sniped by"; + Com_sprintf( className, 64, "'s %s", + BG_FindHumanNameForClassNum( PCL_ALIEN_LEVEL3 ) ); + message2 = className; + break; + case MOD_LEVEL4_CLAW: + message = "was mauled by"; + Com_sprintf( className, 64, "'s %s", + BG_FindHumanNameForClassNum( PCL_ALIEN_LEVEL4 ) ); + message2 = className; + break; + case MOD_LEVEL4_CHARGE: + message = "should have gotten out of the way of"; + Com_sprintf( className, 64, "'s %s", + BG_FindHumanNameForClassNum( PCL_ALIEN_LEVEL4 ) ); + message2 = className; + break; + + case MOD_POISON: + message = "should have used a medkit against"; + message2 = "'s poison"; + break; + case MOD_LEVEL1_PCLOUD: + message = "was gassed by"; + Com_sprintf( className, 64, "'s %s", + BG_FindHumanNameForClassNum( PCL_ALIEN_LEVEL1 ) ); + message2 = className; + break; + + + case MOD_TELEFRAG: + message = "tried to invade"; + message2 = "'s personal space"; + break; + default: + message = "was killed by"; + break; + } + + if( message ) + { + CG_Printf( "%s %s %s%s%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 died.\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_EntityEvent + +An entity has an event value +also called by CG_CheckPlayerstateEvents +============== +*/ +#define DEBUGNAME(x) if(cg_debugEvents.integer){CG_Printf(x"\n");} +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_TEAM ] == TEAM_SPECTATOR ) + steptime = 200; + else + steptime = BG_FindSteptimeForClass( cg.snap->ps.stats[ STAT_PCLASS ] ); + + es = ¢->currentState; + event = es->event & ~EV_EVENT_BITS; + + if( cg_debugEvents.integer ) + CG_Printf( "ent:%3i event:%3i ", es->number, event ); + + if( !event ) + { + DEBUGNAME("ZEROEVENT"); + return; + } + + clientNum = es->clientNum; + if( clientNum < 0 || clientNum >= MAX_CLIENTS ) + clientNum = 0; + + ci = &cgs.clientinfo[ clientNum ]; + + switch( event ) + { + // + // movement generated events + // + case EV_FOOTSTEP: + DEBUGNAME( "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: + DEBUGNAME( "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: + DEBUGNAME( "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: + DEBUGNAME( "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: + DEBUGNAME( "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: + DEBUGNAME( "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: + DEBUGNAME( "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: + DEBUGNAME( "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: + DEBUGNAME( "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: + DEBUGNAME( "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 + DEBUGNAME( "EV_STEP" ); + { + 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: + DEBUGNAME( "EV_JUMP" ); + trap_S_StartSound( NULL, es->number, CHAN_VOICE, CG_CustomSound( es->number, "*jump1.wav" ) ); + + if( BG_ClassHasAbility( cg.predictedPlayerState.stats[ STAT_PCLASS ], SCA_WALLJUMPER ) ) + { + vec3_t surfNormal, refNormal = { 0.0f, 0.0f, 1.0f }; + vec3_t rotAxis; + + 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, rotAxis ); + VectorNormalize( rotAxis ); + + //add the op + CG_addSmoothOp( rotAxis, 15.0f, 1.0f ); + } + + //copy the current normal to the lastNormal + VectorCopy( surfNormal, cg.lastNormal ); + } + + break; + + case EV_LEV1_GRAB: + DEBUGNAME( "EV_LEV1_GRAB" ); + trap_S_StartSound( NULL, es->number, CHAN_VOICE, cgs.media.alienL1Grab ); + break; + + case EV_LEV4_CHARGE_PREPARE: + DEBUGNAME( "EV_LEV4_CHARGE_PREPARE" ); + trap_S_StartSound( NULL, es->number, CHAN_VOICE, cgs.media.alienL4ChargePrepare ); + break; + + case EV_LEV4_CHARGE_START: + DEBUGNAME( "EV_LEV4_CHARGE_START" ); + //FIXME: stop cgs.media.alienL4ChargePrepare playing here + trap_S_StartSound( NULL, es->number, CHAN_VOICE, cgs.media.alienL4ChargeStart ); + break; + + case EV_TAUNT: + DEBUGNAME( "EV_TAUNT" ); + if( !cg_noTaunt.integer ) + trap_S_StartSound( NULL, es->number, CHAN_VOICE, CG_CustomSound( es->number, "*taunt.wav" ) ); + break; + + case EV_WATER_TOUCH: + DEBUGNAME( "EV_WATER_TOUCH" ); + trap_S_StartSound( NULL, es->number, CHAN_AUTO, cgs.media.watrInSound ); + break; + + case EV_WATER_LEAVE: + DEBUGNAME( "EV_WATER_LEAVE" ); + trap_S_StartSound( NULL, es->number, CHAN_AUTO, cgs.media.watrOutSound ); + break; + + case EV_WATER_UNDER: + DEBUGNAME( "EV_WATER_UNDER" ); + trap_S_StartSound( NULL, es->number, CHAN_AUTO, cgs.media.watrUnSound ); + break; + + case EV_WATER_CLEAR: + DEBUGNAME( "EV_WATER_CLEAR" ); + trap_S_StartSound( NULL, es->number, CHAN_AUTO, CG_CustomSound( es->number, "*gasp.wav" ) ); + break; + + // + // weapon events + // + case EV_NOAMMO: + DEBUGNAME( "EV_NOAMMO" ); + { + } + break; + + case EV_CHANGE_WEAPON: + DEBUGNAME( "EV_CHANGE_WEAPON" ); + trap_S_StartSound( NULL, es->number, CHAN_AUTO, cgs.media.selectSound ); + break; + + case EV_FIRE_WEAPON: + DEBUGNAME( "EV_FIRE_WEAPON" ); + CG_FireWeapon( cent, WPM_PRIMARY ); + break; + + case EV_FIRE_WEAPON2: + DEBUGNAME( "EV_FIRE_WEAPON2" ); + CG_FireWeapon( cent, WPM_SECONDARY ); + break; + + case EV_FIRE_WEAPON3: + DEBUGNAME( "EV_FIRE_WEAPON3" ); + CG_FireWeapon( cent, WPM_TERTIARY ); + break; + + //================================================================= + + // + // other events + // + case EV_PLAYER_TELEPORT_IN: + DEBUGNAME( "EV_PLAYER_TELEPORT_IN" ); + //deprecated + break; + + case EV_PLAYER_TELEPORT_OUT: + DEBUGNAME( "EV_PLAYER_TELEPORT_OUT" ); + CG_PlayerDisconnect( position ); + break; + + case EV_BUILD_CONSTRUCT: + DEBUGNAME( "EV_BUILD_CONSTRUCT" ); + //do something useful here + break; + + case EV_BUILD_DESTROY: + DEBUGNAME( "EV_BUILD_DESTROY" ); + //do something useful here + break; + + case EV_RPTUSE_SOUND: + DEBUGNAME( "EV_RPTUSE_SOUND" ); + trap_S_StartSound( NULL, es->number, CHAN_AUTO, cgs.media.repeaterUseSound ); + break; + + case EV_GRENADE_BOUNCE: + DEBUGNAME( "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; + + // + // missile impacts + // + case EV_MISSILE_HIT: + DEBUGNAME( "EV_MISSILE_HIT" ); + ByteToDir( es->eventParm, dir ); + CG_MissileHitPlayer( es->weapon, es->generic1, position, dir, es->otherEntityNum ); + break; + + case EV_MISSILE_MISS: + DEBUGNAME( "EV_MISSILE_MISS" ); + ByteToDir( es->eventParm, dir ); + CG_MissileHitWall( es->weapon, es->generic1, 0, position, dir, IMPACTSOUND_DEFAULT ); + break; + + case EV_MISSILE_MISS_METAL: + DEBUGNAME( "EV_MISSILE_MISS_METAL" ); + ByteToDir( es->eventParm, dir ); + CG_MissileHitWall( es->weapon, es->generic1, 0, position, dir, IMPACTSOUND_METAL ); + break; + + case EV_HUMAN_BUILDABLE_EXPLOSION: + DEBUGNAME( "EV_HUMAN_BUILDABLE_EXPLOSION" ); + ByteToDir( es->eventParm, dir ); + CG_HumanBuildableExplosion( position, dir ); + break; + + case EV_ALIEN_BUILDABLE_EXPLOSION: + DEBUGNAME( "EV_ALIEN_BUILDABLE_EXPLOSION" ); + ByteToDir( es->eventParm, dir ); + CG_AlienBuildableExplosion( position, dir ); + break; + + case EV_TESLATRAIL: + DEBUGNAME( "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: + DEBUGNAME( "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: + DEBUGNAME( "EV_BULLET_HIT_FLESH" ); + CG_Bullet( es->pos.trBase, es->otherEntityNum, dir, qtrue, es->eventParm ); + break; + + case EV_SHOTGUN: + DEBUGNAME( "EV_SHOTGUN" ); + CG_ShotgunFire( es ); + break; + + case EV_GENERAL_SOUND: + DEBUGNAME( "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 + DEBUGNAME( "EV_GLOBAL_SOUND" ); + 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 + DEBUGNAME( "EV_PAIN" ); + if( cent->currentState.number != cg.snap->ps.clientNum ) + CG_PainEvent( cent, es->eventParm ); + break; + + case EV_DEATH1: + case EV_DEATH2: + case EV_DEATH3: + DEBUGNAME( "EV_DEATHx" ); + trap_S_StartSound( NULL, es->number, CHAN_VOICE, + CG_CustomSound( es->number, va( "*death%i.wav", event - EV_DEATH1 + 1 ) ) ); + break; + + case EV_OBITUARY: + DEBUGNAME( "EV_OBITUARY" ); + CG_Obituary( es ); + break; + + case EV_GIB_PLAYER: + DEBUGNAME( "EV_GIB_PLAYER" ); + // no gibbing + break; + + case EV_STOPLOOPINGSOUND: + DEBUGNAME( "EV_STOPLOOPINGSOUND" ); + trap_S_StopLoopingSound( es->number ); + es->loopSound = 0; + break; + + case EV_DEBUG_LINE: + DEBUGNAME( "EV_DEBUG_LINE" ); + CG_Beam( cent ); + break; + + case EV_BUILD_DELAY: + DEBUGNAME( "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: + DEBUGNAME( "EV_BUILD_REPAIR" ); + trap_S_StartSound( NULL, es->number, CHAN_AUTO, cgs.media.buildableRepairSound ); + break; + + case EV_BUILD_REPAIRED: + DEBUGNAME( "EV_BUILD_REPAIRED" ); + trap_S_StartSound( NULL, es->number, CHAN_AUTO, cgs.media.buildableRepairedSound ); + break; + + case EV_OVERMIND_ATTACK: + DEBUGNAME( "EV_OVERMIND_ATTACK" ); + if( cg.predictedPlayerState.stats[ STAT_PTEAM ] == PTE_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: + DEBUGNAME( "EV_OVERMIND_DYING" ); + if( cg.predictedPlayerState.stats[ STAT_PTEAM ] == PTE_ALIENS ) + { + trap_S_StartLocalSound( cgs.media.alienOvermindDying, CHAN_ANNOUNCER ); + CG_CenterPrint( "The Overmind is dying!", 200, GIANTCHAR_WIDTH * 4 ); + } + break; + + case EV_DCC_ATTACK: + DEBUGNAME( "EV_DCC_ATTACK" ); + if( cg.predictedPlayerState.stats[ STAT_PTEAM ] == PTE_HUMANS ) + { + //trap_S_StartLocalSound( cgs.media.humanDCCAttack, CHAN_ANNOUNCER ); + CG_CenterPrint( "Our base is under attack!", 200, GIANTCHAR_WIDTH * 4 ); + } + break; + + case EV_OVERMIND_SPAWNS: + DEBUGNAME( "EV_OVERMIND_SPAWNS" ); + if( cg.predictedPlayerState.stats[ STAT_PTEAM ] == PTE_ALIENS ) + { + trap_S_StartLocalSound( cgs.media.alienOvermindSpawns, CHAN_ANNOUNCER ); + CG_CenterPrint( "The Overmind needs spawns!", 200, GIANTCHAR_WIDTH * 4 ); + } + break; + + case EV_ALIEN_EVOLVE: + DEBUGNAME( "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: + DEBUGNAME( "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: + DEBUGNAME( "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_MEDKIT_USED: + DEBUGNAME( "EV_MEDKIT_USED" ); + trap_S_StartSound( NULL, es->number, CHAN_AUTO, cgs.media.medkitUseSound ); + break; + + case EV_PLAYER_RESPAWN: + DEBUGNAME( "EV_PLAYER_RESPAWN" ); + if( es->number == cg.clientNum ) + cg.spawnTime = cg.time; + break; + + default: + DEBUGNAME( "UNKNOWN" ); + 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( ¢->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..74b7a4d --- /dev/null +++ b/src/cgame/cg_local.h @@ -0,0 +1,2072 @@ +/* +=========================================================================== +Copyright (C) 1999-2005 Id Software, Inc. +Copyright (C) 2000-2006 Tim Angus + +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 10 +#define MAX_MARK_POLYS 256 + +#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 + +#define DEFAULT_MODEL "sarge" +#define DEFAULT_TEAM_MODEL "sarge" +#define DEFAULT_TEAM_HEAD "sarge" + +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 4 +#define MAX_PARTICLES_PER_EJECTOR 4 + +#define MAX_BASEPARTICLE_SYSTEMS 192 +#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 48 +#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; + float 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; + 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; +} 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; +} 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; + + 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 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 + +//TA: 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, flag, nonseg; + 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; + +//================================================= + +// 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; + + //TA: + buildableAnimNumber_t buildableAnim; //persistant anim number + buildableAnimNumber_t oldBuildableAnim; //to detect when new anims are set + particleSystem_t *buildablePS; + buildableStatus_t buildableStatus; + float lastBuildableHealthScale; + 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 *entityPS; + qboolean entityPSMissing; + + trailSystem_t *level2ZapTS[ 3 ]; + + trailSystem_t *muzzleTS; //used for the tesla and reactor + int muzzleTSDeathTime; + + qboolean valid; + qboolean oldValid; +} 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 ]; + pTeam_t team; + + int botSkill; // 0 = not bot, 1-5 = bot + + vec3_t color1; + vec3_t color2; + + int score; // updated by score servercmds + int location; // location index for team mode + int health; // you only get this info about your teammates + int armor; + int curWeapon; + + int handicap; + int wins, losses; // in tourney mode + + int teamTask; // task in teamplay (offence/defence) + qboolean teamLeader; // true when this is a team leader + + int powerups; // so can display quad/flag status + + int medkitUsageTime; + int invulnerabilityStartTime; + int invulnerabilityStopTime; + + int breathPuffTime; + + // 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 ]; + char headModelName[ MAX_QPATH ]; + char headSkinName[ 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 ]; +} 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; + qhandle_t missileParticleSystem; + qhandle_t missileTrailSystem; + qboolean missileRotates; + qboolean missileAnimates; + int missileAnimStartFrame; + int missileAnimNumFrames; + int missileAnimFrameRate; + int missileAnimLooping; + + sfxHandle_t firingSound; + qboolean loopFireSound; + + 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; + + 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 + +//====================================================================== + +//TA: +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 8192 +#define MAX_CONSOLE_LINES 32 + +// 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 ) + +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 spectatorLen; // length of list + float spectatorWidth; // width in device units + int spectatorTime; // next time to offset + int spectatorPaintX; // current paint x + int spectatorPaintX2; // current paint x + int spectatorOffset; // current offset from start + int spectatorPaintLen; // current offset from start + + // centerprinting + int centerPrintTime; + int centerPrintCharWidth; + int centerPrintY; + char centerPrint[ 1024 ]; + int centerPrintLines; + + // low ammo warning state + int lowAmmoWarning; // 1 = low, 2 = empty + + // kill timers for carnage reward + int lastKillTime; + + // crosshair client ID + int crosshairClientNum; + int crosshairClientTime; + + // powerup active flashing + int powerupActive; + int powerupTime; + + // attacking player + int attackerTime; + int voiceTime; + + // 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 warmup; + int warmupCount; + + //========================== + + 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; + + vec3_t kick_angles; // weapon kicks + vec3_t kick_origin; + + // 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; //TA: fovwarp + int weapon1Time; //TA: time when BUTTON_ATTACK went t->f f->t + int weapon2Time; //TA: time when BUTTON_ATTACK2 went t->f f->t + int weapon3Time; //TA: time when BUTTON_USE_HOLDABLE went t->f f->t + qboolean weapon1Firing; + qboolean weapon2Firing; + qboolean weapon3Firing; + + int poisonedTime; + + vec3_t lastNormal; //TA: view smoothage + vec3_t lastVangles; //TA: view smoothage + smooth_t sList[ MAXSMOOTHS ]; //TA: WW smoothing + + int forwardMoveTime; //TA: for struggling + int rightMoveTime; + int upMoveTime; + + float charModelFraction; //TA: 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; + + float painBlendValue; + float painBlendTarget; + int lastHealth; + + int lastPredictedCommand; + int lastServerTime; + playerState_t savedPmoveStates[ NUM_SAVED_STATES ]; + int stateHead, stateTail; + int ping; +} 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 balloonShader; + 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 scannerLineShader; + + + qhandle_t numberShaders[ 11 ]; + + qhandle_t shadowMarkShader; + qhandle_t wakeMarkShader; + + // buildable shaders + qhandle_t greenBuildShader; + qhandle_t redBuildShader; + qhandle_t humanSpawningShader; + + // disconnect + qhandle_t disconnectPS; + qhandle_t disconnectSound; + + // sounds + sfxHandle_t tracerSound; + 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 hardBounceSound1; + sfxHandle_t hardBounceSound2; + + sfxHandle_t voteNow; + sfxHandle_t votePassed; + sfxHandle_t voteFailed; + + 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 alienStageTransition; + sfxHandle_t humanStageTransition; + + sfxHandle_t alienOvermindAttack; + sfxHandle_t alienOvermindDying; + sfxHandle_t alienOvermindSpawns; + + 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 alienEvolvePS; + qhandle_t alienAcidTubePS; + + sfxHandle_t alienEvolveSound; + + qhandle_t humanBuildableDamagedPS; + qhandle_t humanBuildableDestroyedPS; + qhandle_t alienBuildableDamagedPS; + qhandle_t alienBuildableDestroyedPS; + + qhandle_t alienBleedPS; + qhandle_t humanBleedPS; + + qhandle_t teslaZapTS; + + sfxHandle_t lCannonWarningSound; + + qhandle_t buildWeaponTimerPie[ 8 ]; + qhandle_t upgradeClassIconShader; +} 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; + int voteYes; + int voteNo; + qboolean voteModified; // beep whenever changed + char voteString[ MAX_STRING_TOKENS ]; + + int teamVoteTime[ 2 ]; + int teamVoteYes[ 2 ]; + int teamVoteNo[ 2 ]; + qboolean teamVoteModified[ 2 ]; // beep whenever changed + char teamVoteString[ 2 ][ MAX_STRING_TOKENS ]; + + int levelStartTime; + + int scores1, scores2; // from configstrings + + qboolean newHud; + + int alienBuildPoints; + int alienBuildPointsTotal; + int humanBuildPoints; + int humanBuildPointsTotal; + int humanBuildPointsPowered; + + int alienStage; + int humanStage; + int alienKills; + int humanKills; + int alienNextStageThreshold; + int humanNextStageThreshold; + + int numAlienSpawns; + int numHumanSpawns; + + // + // 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 ]; + + //TA: 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; +} cgs_t; + +//============================================================================== + +extern cgs_t cgs; +extern cg_t cg; +extern centity_t cg_entities[ MAX_GENTITIES ]; + +//TA: weapon limit expanded: +//extern weaponInfo_t cg_weapons[MAX_WEAPONS]; +extern weaponInfo_t cg_weapons[ 32 ]; +//TA: upgrade infos: +extern upgradeInfo_t cg_upgrades[ 32 ]; + +//TA: buildable infos: +extern buildableInfo_t cg_buildables[ BA_NUM_BUILDABLES ]; + +extern markPoly_t cg_markPolys[ MAX_MARK_POLYS ]; + +extern vmCvar_t cg_centertime; +extern vmCvar_t cg_runpitch; +extern vmCvar_t cg_runroll; +extern vmCvar_t cg_bobup; +extern vmCvar_t cg_bobpitch; +extern vmCvar_t cg_bobroll; +extern vmCvar_t cg_swingSpeed; +extern vmCvar_t cg_shadows; +extern vmCvar_t cg_gibs; +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_draw3dIcons; +extern vmCvar_t cg_drawIcons; +extern vmCvar_t cg_drawAmmoWarning; +extern vmCvar_t cg_drawCrosshair; +extern vmCvar_t cg_drawCrosshairNames; +extern vmCvar_t cg_drawRewards; +extern vmCvar_t cg_drawTeamOverlay; +extern vmCvar_t cg_teamOverlayUserinfo; +extern vmCvar_t cg_crosshairX; +extern vmCvar_t cg_crosshairY; +extern vmCvar_t cg_drawStatus; +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_teslaTrailTime; +extern vmCvar_t cg_railTrailTime; +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_brassTime; +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_drawGun; +extern vmCvar_t cg_viewsize; +extern vmCvar_t cg_tracerChance; +extern vmCvar_t cg_tracerWidth; +extern vmCvar_t cg_tracerLength; +extern vmCvar_t cg_autoswitch; +extern vmCvar_t cg_ignore; +extern vmCvar_t cg_simpleItems; +extern vmCvar_t cg_fov; +extern vmCvar_t cg_zoomFov; +extern vmCvar_t cg_thirdPersonRange; +extern vmCvar_t cg_thirdPersonAngle; +extern vmCvar_t cg_thirdPerson; +extern vmCvar_t cg_stereoSeparation; +extern vmCvar_t cg_lagometer; +extern vmCvar_t cg_drawAttacker; +extern vmCvar_t cg_synchronousClients; +extern vmCvar_t cg_stats; +extern vmCvar_t cg_forceModel; +extern vmCvar_t cg_buildScript; +extern vmCvar_t cg_paused; +extern vmCvar_t cg_blood; +extern vmCvar_t cg_predictItems; +extern vmCvar_t cg_deferPlayers; +extern vmCvar_t cg_drawFriend; +extern vmCvar_t cg_teamChatsOnly; +extern vmCvar_t cg_noVoiceChats; +extern vmCvar_t cg_noVoiceText; +extern vmCvar_t cg_scorePlum; +extern vmCvar_t cg_smoothClients; +extern vmCvar_t pmove_fixed; +extern vmCvar_t pmove_msec; +//extern vmCvar_t cg_pmove_fixed; +extern vmCvar_t cg_cameraOrbit; +extern vmCvar_t cg_cameraOrbitDelay; +extern vmCvar_t cg_timescaleFadeEnd; +extern vmCvar_t cg_timescaleFadeSpeed; +extern vmCvar_t cg_timescale; +extern vmCvar_t cg_cameraMode; +extern vmCvar_t cg_smallFont; +extern vmCvar_t cg_bigFont; +extern vmCvar_t cg_noTaunt; +extern vmCvar_t cg_noProjectileTrail; +extern vmCvar_t cg_oldRail; +extern vmCvar_t cg_oldRocket; +extern vmCvar_t cg_oldPlasma; +extern vmCvar_t cg_trueLightning; +extern vmCvar_t cg_creepRes; +extern vmCvar_t cg_drawSurfNormal; +extern vmCvar_t cg_drawBBOX; +extern vmCvar_t cg_debugAlloc; +extern vmCvar_t cg_wwSmoothTime; +extern vmCvar_t cg_wwFollow; +extern vmCvar_t cg_wwToggle; +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_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; + +//TA: hack to get class an carriage through to UI module +extern vmCvar_t ui_currentClass; +extern vmCvar_t ui_carriage; +extern vmCvar_t ui_stages; +extern vmCvar_t ui_dialog; +extern vmCvar_t ui_loading; +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_unlagged; + +// +// 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 ); +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 ); //TA +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 ); + + +// +// 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 ); + +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_GetColorForHealth( int health, int armor, 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 ); + + +// +// cg_draw.c +// +extern int sortedTeamPlayers[ TEAM_MAXOVERLAY ]; +extern int numSortedTeamPlayers; + +void CG_AddLagometerFrameInfo( void ); +void CG_AddLagometerSnapshotInfo( snapshot_t *snap ); +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, float special, + float scale, vec4_t color, qhandle_t shader, int textStyle); +void CG_Text_Paint( float x, float y, float scale, vec4_t color, const char *text, float adjust, int limit, int style ); +int CG_Text_Width( const char *text, float scale, int limit ); +int CG_Text_Height( const char *text, float scale, int limit ); +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_AddRefEntityWithPowerups( refEntity_t *ent, int powerups, int team ); +void CG_NewClientInfo( int clientNum ); +void CG_PrecacheClientInfo( pClass_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 ); +qboolean CG_AtHighestClass( 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 ); +void CG_AlienBuildableExplosion( vec3_t origin, vec3_t dir ); + +// +// cg_animation.c +// +void CG_RunLerpFrame( lerpFrame_t *lf ); + +// +// 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 ); +void CG_MissileHitPlayer( weapon_t weapon, weaponMode_t weaponMode, vec3_t origin, vec3_t dir, int entityNum ); +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_mem.c +// +void CG_InitMemory( void ); +void *CG_Alloc( int size ); +void CG_Free( void *ptr ); +void CG_DefragmentMemory( void ); + +// +// 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 ); +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_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 ); + +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 + diff --git a/src/cgame/cg_main.c b/src/cgame/cg_main.c new file mode 100644 index 0000000..d303b30 --- /dev/null +++ b/src/cgame/cg_main.c @@ -0,0 +1,1842 @@ +/* +=========================================================================== +Copyright (C) 1999-2005 Id Software, Inc. +Copyright (C) 2000-2006 Tim Angus + +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; + +int forceModelModificationCount = -1; + +void CG_Init( int serverMessageNum, int serverCommandSequence, int clientNum ); +void CG_Shutdown( void ); + +/* +================ +vmMain + +This is the only way control passes into the module. +This must be the very first function compiled into the .q3vm file +================ +*/ +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: + cgDC.cursorx = cgs.cursorX; + cgDC.cursory = cgs.cursorY; + CG_MouseEvent( arg0, arg1 ); + return 0; + + case CG_EVENT_HANDLING: + CG_EventHandling( arg0 ); + return 0; + + default: + CG_Error( "vmMain: unknown command %i", command ); + break; + } + + return -1; +} + + +cg_t cg; +cgs_t cgs; +centity_t cg_entities[ MAX_GENTITIES ]; + +//TA: weapons limit expanded: +//weaponInfo_t cg_weapons[MAX_WEAPONS]; +weaponInfo_t cg_weapons[ 32 ]; +upgradeInfo_t cg_upgrades[ 32 ]; + +buildableInfo_t cg_buildables[ BA_NUM_BUILDABLES ]; + +vmCvar_t cg_teslaTrailTime; +vmCvar_t cg_railTrailTime; +vmCvar_t cg_centertime; +vmCvar_t cg_runpitch; +vmCvar_t cg_runroll; +vmCvar_t cg_bobup; +vmCvar_t cg_bobpitch; +vmCvar_t cg_bobroll; +vmCvar_t cg_swingSpeed; +vmCvar_t cg_shadows; +vmCvar_t cg_gibs; +vmCvar_t cg_drawTimer; +vmCvar_t cg_drawClock; +vmCvar_t cg_drawFPS; +vmCvar_t cg_drawDemoState; +vmCvar_t cg_drawSnapshot; +vmCvar_t cg_draw3dIcons; +vmCvar_t cg_drawIcons; +vmCvar_t cg_drawAmmoWarning; +vmCvar_t cg_drawCrosshair; +vmCvar_t cg_drawCrosshairNames; +vmCvar_t cg_drawRewards; +vmCvar_t cg_crosshairX; +vmCvar_t cg_crosshairY; +vmCvar_t cg_draw2D; +vmCvar_t cg_drawStatus; +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_brassTime; +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_autoswitch; +vmCvar_t cg_ignore; +vmCvar_t cg_simpleItems; +vmCvar_t cg_fov; +vmCvar_t cg_zoomFov; +vmCvar_t cg_thirdPerson; +vmCvar_t cg_thirdPersonRange; +vmCvar_t cg_thirdPersonAngle; +vmCvar_t cg_stereoSeparation; +vmCvar_t cg_lagometer; +vmCvar_t cg_drawAttacker; +vmCvar_t cg_synchronousClients; +vmCvar_t cg_stats; +vmCvar_t cg_buildScript; +vmCvar_t cg_forceModel; +vmCvar_t cg_paused; +vmCvar_t cg_blood; +vmCvar_t cg_predictItems; +vmCvar_t cg_deferPlayers; +vmCvar_t cg_drawTeamOverlay; +vmCvar_t cg_teamOverlayUserinfo; +vmCvar_t cg_drawFriend; +vmCvar_t cg_teamChatsOnly; +vmCvar_t cg_noVoiceChats; +vmCvar_t cg_noVoiceText; +vmCvar_t cg_hudFiles; +vmCvar_t cg_scorePlum; +vmCvar_t cg_smoothClients; +vmCvar_t pmove_fixed; +//vmCvar_t cg_pmove_fixed; +vmCvar_t pmove_msec; +vmCvar_t cg_pmove_msec; +vmCvar_t cg_cameraMode; +vmCvar_t cg_cameraOrbit; +vmCvar_t cg_cameraOrbitDelay; +vmCvar_t cg_timescaleFadeEnd; +vmCvar_t cg_timescaleFadeSpeed; +vmCvar_t cg_timescale; +vmCvar_t cg_smallFont; +vmCvar_t cg_bigFont; +vmCvar_t cg_noTaunt; +vmCvar_t cg_noProjectileTrail; +vmCvar_t cg_oldRail; +vmCvar_t cg_oldRocket; +vmCvar_t cg_oldPlasma; +vmCvar_t cg_trueLightning; +vmCvar_t cg_creepRes; +vmCvar_t cg_drawSurfNormal; +vmCvar_t cg_drawBBOX; +vmCvar_t cg_debugAlloc; +vmCvar_t cg_wwSmoothTime; +vmCvar_t cg_wwFollow; +vmCvar_t cg_wwToggle; +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_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; + +//TA: hack to get class and carriage through to UI module +vmCvar_t ui_currentClass; +vmCvar_t ui_carriage; +vmCvar_t ui_stages; +vmCvar_t ui_dialog; +vmCvar_t ui_loading; +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_unlagged; + + +typedef struct +{ + vmCvar_t *vmCvar; + char *cvarName; + char *defaultString; + int cvarFlags; +} cvarTable_t; + +static cvarTable_t cvarTable[ ] = +{ + { &cg_ignore, "cg_ignore", "0", 0 }, // used for debugging + { &cg_autoswitch, "cg_autoswitch", "1", CVAR_ARCHIVE }, + { &cg_drawGun, "cg_drawGun", "1", CVAR_ARCHIVE }, + { &cg_zoomFov, "cg_zoomfov", "22.5", CVAR_ARCHIVE }, + { &cg_fov, "cg_fov", "90", 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_gibs, "cg_gibs", "1", CVAR_ARCHIVE }, + { &cg_draw2D, "cg_draw2D", "1", CVAR_ARCHIVE }, + { &cg_drawStatus, "cg_drawStatus", "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_draw3dIcons, "cg_draw3dIcons", "1", CVAR_ARCHIVE }, + { &cg_drawIcons, "cg_drawIcons", "1", CVAR_ARCHIVE }, + { &cg_drawAmmoWarning, "cg_drawAmmoWarning", "1", CVAR_ARCHIVE }, + { &cg_drawAttacker, "cg_drawAttacker", "1", CVAR_ARCHIVE }, + { &cg_drawCrosshair, "cg_drawCrosshair", "1", CVAR_ARCHIVE }, + { &cg_drawCrosshairNames, "cg_drawCrosshairNames", "1", CVAR_ARCHIVE }, + { &cg_drawRewards, "cg_drawRewards", "1", CVAR_ARCHIVE }, + { &cg_crosshairX, "cg_crosshairX", "0", CVAR_ARCHIVE }, + { &cg_crosshairY, "cg_crosshairY", "0", CVAR_ARCHIVE }, + { &cg_brassTime, "cg_brassTime", "2500", CVAR_ARCHIVE }, + { &cg_simpleItems, "cg_simpleItems", "0", CVAR_ARCHIVE }, + { &cg_addMarks, "cg_marks", "1", CVAR_ARCHIVE }, + { &cg_lagometer, "cg_lagometer", "0", CVAR_ARCHIVE }, + { &cg_teslaTrailTime, "cg_teslaTrailTime", "250", CVAR_ARCHIVE }, + { &cg_railTrailTime, "cg_railTrailTime", "400", 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_bobup , "cg_bobup", "0.005", CVAR_CHEAT }, + { &cg_bobpitch, "cg_bobpitch", "0.002", CVAR_ARCHIVE }, + { &cg_bobroll, "cg_bobroll", "0.002", 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", "40", CVAR_CHEAT }, + { &cg_thirdPersonAngle, "cg_thirdPersonAngle", "0", CVAR_CHEAT }, + { &cg_thirdPerson, "cg_thirdPerson", "0", CVAR_CHEAT }, + { &cg_forceModel, "cg_forceModel", "0", CVAR_ARCHIVE }, + { &cg_predictItems, "cg_predictItems", "1", CVAR_ARCHIVE }, + { &cg_deferPlayers, "cg_deferPlayers", "1", CVAR_ARCHIVE }, + { &cg_drawTeamOverlay, "cg_drawTeamOverlay", "0", CVAR_ARCHIVE }, + { &cg_teamOverlayUserinfo, "teamoverlay", "0", CVAR_ROM | CVAR_USERINFO }, + { &cg_stats, "cg_stats", "0", 0 }, + { &cg_drawFriend, "cg_drawFriend", "1", CVAR_ARCHIVE }, + { &cg_teamChatsOnly, "cg_teamChatsOnly", "0", CVAR_ARCHIVE }, + { &cg_noVoiceChats, "cg_noVoiceChats", "0", CVAR_ARCHIVE }, + { &cg_noVoiceText, "cg_noVoiceText", "0", CVAR_ARCHIVE }, + { &cg_creepRes, "cg_creepRes", "16", CVAR_ARCHIVE }, + { &cg_drawSurfNormal, "cg_drawSurfNormal", "0", CVAR_CHEAT }, + { &cg_drawBBOX, "cg_drawBBOX", "0", CVAR_CHEAT }, + { &cg_debugAlloc, "cg_debugAlloc", "0", 0 }, + { &cg_wwSmoothTime, "cg_wwSmoothTime", "300", CVAR_ARCHIVE }, + { &cg_wwFollow, "cg_wwFollow", "1", CVAR_ARCHIVE|CVAR_USERINFO }, + { &cg_wwToggle, "cg_wwToggle", "1", CVAR_ARCHIVE|CVAR_USERINFO }, + { &cg_unlagged, "cg_unlagged", "1", 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_disableScannerPlane, "cg_disableScannerPlane", "0", CVAR_ARCHIVE }, + { &cg_tutorial, "cg_tutorial", "1", CVAR_ARCHIVE }, + { &cg_hudFiles, "cg_hudFiles", "ui/hud.txt", 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 }, + + { &ui_currentClass, "ui_currentClass", "0", 0 }, + { &ui_carriage, "ui_carriage", "", 0 }, + { &ui_stages, "ui_stages", "0 0", 0 }, + { &ui_dialog, "ui_dialog", "Text not set", 0 }, + { &ui_loading, "ui_loading", "0", 0 }, + { &ui_voteActive, "ui_voteActive", "0", 0 }, + { &ui_humanTeamVoteActive, "ui_humanTeamVoteActive", "0", 0 }, + { &ui_alienTeamVoteActive, "ui_alienTeamVoteActive", "0", 0 }, + + { &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_buildScript, "com_buildScript", "0", 0 }, // force loading of all possible data amd error on failures + { &cg_paused, "cl_paused", "0", CVAR_ROM }, + { &cg_blood, "com_blood", "1", CVAR_ARCHIVE }, + { &cg_synchronousClients, "g_synchronousClients", "0", 0 }, // communicated by systeminfo + { &cg_cameraOrbit, "cg_cameraOrbit", "0", CVAR_CHEAT}, + { &cg_cameraOrbitDelay, "cg_cameraOrbitDelay", "50", CVAR_ARCHIVE}, + { &cg_timescaleFadeEnd, "cg_timescaleFadeEnd", "1", 0}, + { &cg_timescaleFadeSpeed, "cg_timescaleFadeSpeed", "0", 0}, + { &cg_timescale, "timescale", "1", 0}, + { &cg_scorePlum, "cg_scorePlums", "1", CVAR_USERINFO | CVAR_ARCHIVE}, + { &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_noProjectileTrail, "cg_noProjectileTrail", "0", CVAR_ARCHIVE}, + { &cg_smallFont, "ui_smallFont", "0.2", CVAR_ARCHIVE}, + { &cg_bigFont, "ui_bigFont", "0.5", CVAR_ARCHIVE}, + { &cg_oldRail, "cg_oldRail", "1", CVAR_ARCHIVE}, + { &cg_oldRocket, "cg_oldRocket", "1", CVAR_ARCHIVE}, + { &cg_oldPlasma, "cg_oldPlasma", "1", CVAR_ARCHIVE}, + { &cg_trueLightning, "cg_trueLightning", "0.0", CVAR_ARCHIVE} +// { &cg_pmove_fixed, "cg_pmove_fixed", "0", CVAR_USERINFO | CVAR_ARCHIVE } +}; + +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 ); + } + + //repress standard Q3 console + trap_Cvar_Set( "con_notifytime", "-2" ); + + // see if we are also running the server on this machine + trap_Cvar_VariableStringBuffer( "sv_running", var, sizeof( var ) ); + cgs.localServer = atoi( var ); + forceModelModificationCount = cg_forceModel.modificationCount; + + trap_Cvar_Register( NULL, "model", DEFAULT_MODEL, CVAR_USERINFO | CVAR_ARCHIVE ); + trap_Cvar_Register( NULL, "headmodel", DEFAULT_MODEL, CVAR_USERINFO | CVAR_ARCHIVE ); + trap_Cvar_Register( NULL, "team_model", DEFAULT_TEAM_MODEL, CVAR_USERINFO | CVAR_ARCHIVE ); + trap_Cvar_Register( NULL, "team_headmodel", DEFAULT_TEAM_HEAD, CVAR_USERINFO | CVAR_ARCHIVE ); +} + + +/* +=================== +CG_ForceModelChange +=================== +*/ +static void CG_ForceModelChange( void ) +{ + int i; + + for( i = 0; i < MAX_CLIENTS; i++ ) + { + const char *clientInfo; + + clientInfo = CG_ConfigString( CS_PLAYERS + i ); + + if( !clientInfo[ 0 ] ) + continue; + + CG_NewClientInfo( i ); + } +} + + +/* +================= +CG_UpdateCvars +================= +*/ +void CG_UpdateCvars( void ) +{ + int i; + cvarTable_t *cv; + + for( i = 0, cv = cvarTable; i < cvarTableSize; i++, cv++ ) + trap_Cvar_Update( cv->vmCvar ); + + // check for modications here + + // if force model changed + if( forceModelModificationCount != cg_forceModel.modificationCount ) + { + forceModelModificationCount = cg_forceModel.modificationCount; + CG_ForceModelChange( ); + } +} + + +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 ]; + + trap_LiteralArgs( buffer, BIG_INFO_STRING ); + + if( !buffer[ 0 ] ) + { + cg.consoleText[ 0 ] = '\0'; + cg.numConsoleLines = 0; + 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 = strlen( buffer ); + cg.numConsoleLines++; +} + +void QDECL CG_Printf( const char *msg, ... ) +{ + va_list argptr; + char text[ 1024 ]; + + va_start( argptr, msg ); + vsprintf( 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 ); + vsprintf( 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 ); + vsprintf( 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); + vsprintf (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 ) +{ + fileHandle_t f; + + if( trap_FS_FOpenFile( filename, &f, FS_READ ) > 0 ) + { + //file exists so close it + trap_FS_FCloseFile( f ); + + return qtrue; + } + else + return qfalse; +} + +/* +================= +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.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.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.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.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.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.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 ); +} + + +//=================================================================================== + + +/* +================= +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.scannerLineShader = trap_R_RegisterShader( "gfx/2d/stalk" ); + + cgs.media.tracerShader = trap_R_RegisterShader( "gfx/misc/tracer" ); + + cgs.media.backTileShader = trap_R_RegisterShader( "console" ); + + + //TA: 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 ] ); + + cgs.media.upgradeClassIconShader = trap_R_RegisterShader( "icons/icona_upgrade.tga" ); + + cgs.media.balloonShader = trap_R_RegisterShader( "gfx/sprites/chatballoon" ); + + 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.alienEvolvePS = CG_RegisterParticleSystem( "alienEvolvePS" ); + cgs.media.alienAcidTubePS = CG_RegisterParticleSystem( "alienAcidTubePS" ); + + cgs.media.jetPackDescendPS = CG_RegisterParticleSystem( "jetPackDescendPS" ); + cgs.media.jetPackHoverPS = CG_RegisterParticleSystem( "jetPackHoverPS" ); + cgs.media.jetPackAscendPS = CG_RegisterParticleSystem( "jetPackAscendPS" ); + + 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.alienBleedPS = CG_RegisterParticleSystem( "alienBleedPS" ); + cgs.media.humanBleedPS = CG_RegisterParticleSystem( "humanBleedPS" ); + + 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 == PTE_NONE ) + Q_strcat( cg.spectatorList, sizeof( cg.spectatorList ), va( "%s " S_COLOR_WHITE, cgs.clientinfo[ i ].name ) ); + } + + i = strlen( cg.spectatorList ); + + if( i != cg.spectatorLen ) + { + cg.spectatorLen = i; + cg.spectatorWidth = -1; + } +} + + + +/* +=================== +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_FindModelNameForClass( i ), + BG_FindSkinNameForClass( i ) ); + + 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 == PTE_ALIENS || + cg.scores[ i ].team == PTE_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; // bk001204 - why not? +} + +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 flags, float *special, int key ) +{ + return qfalse; +} + + +static int CG_FeederCount( float feederID ) +{ + int i, count = 0; + + if( feederID == FEEDER_ALIENTEAM_LIST ) + { + for( i = 0; i < cg.numScores; i++ ) + { + if( cg.scores[ i ].team == PTE_ALIENS ) + count++; + } + } + else if( feederID == FEEDER_HUMANTEAM_LIST ) + { + for( i = 0; i < cg.numScores; i++ ) + { + if( cg.scores[ i ].team == PTE_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 == PTE_ALIENS ) + alien++; + else if( cg.scores[ i ].team == PTE_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 == PTE_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 ]; +} + +static const char *CG_FeederItemText( float 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 = PTE_ALIENS; + else if( feederID == FEEDER_HUMANTEAM_LIST ) + team = PTE_HUMANS; + + info = CG_InfoFromScoreIndex( index, team, &scoreIndex ); + sp = &cg.scores[ scoreIndex ]; + + if( ( atoi( CG_ConfigString( CS_CLIENTS_READY ) ) & ( 1 << sp->client ) ) && + cg.intermissionStarted ) + showIcons = qfalse; + else if( cg.snap->ps.pm_type == PM_SPECTATOR || cg.snap->ps.pm_flags & PMF_FOLLOW || + team == cg.snap->ps.stats[ STAT_PTEAM ] || 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 == PTE_HUMANS && sp->upgrade != UP_NONE ) + *handle = cg_upgrades[ sp->upgrade ].upgradeIcon; + else if( sp->team == PTE_ALIENS ) + { + switch( sp->weapon ) + { + case WP_ABUILD2: + case WP_ALEVEL1_UPG: + case WP_ALEVEL2_UPG: + case WP_ALEVEL3_UPG: + *handle = cgs.media.upgradeClassIconShader; + break; + + default: + break; + } + } + } + break; + + case 2: + if( ( atoi( CG_ConfigString( CS_CLIENTS_READY ) ) & ( 1 << sp->client ) ) && + cg.intermissionStarted ) + return "Ready"; + break; + + case 3: + return info->name; + break; + + case 4: + return va( "%d", info->score ); + break; + + case 5: + return va( "%4d", sp->time ); + break; + + case 6: + if( sp->ping == -1 ) + return "connecting"; + + return va( "%4d", sp->ping ); + break; + } + } + + return ""; +} + +static qhandle_t CG_FeederItemImage( float feederID, int index ) +{ + return 0; +} + +static void CG_FeederSelection( float feederID, int index ) +{ + int i, count; + int team = ( feederID == FEEDER_ALIENTEAM_LIST ) ? PTE_ALIENS : PTE_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 ) +{ + CG_Text_Paint( x, y, scale, color, text, 0, limit, style ); +} + +static int CG_OwnerDrawWidth( int ownerDraw, float scale ) +{ + switch( ownerDraw ) + { + case CG_KILLER: + return CG_Text_Width( CG_GetKillerText( ), scale, 0 ); + 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 ); +} + +//TA: 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.registerShaderNoMip = &trap_R_RegisterShaderNoMip; + cgDC.setColor = &trap_R_SetColor; + cgDC.drawHandlePic = &CG_DrawPic; + cgDC.drawStretchPic = &trap_R_DrawStretchPic; + cgDC.drawText = &CG_Text_Paint; + cgDC.textWidth = &CG_Text_Width; + cgDC.textHeight = &CG_Text_Height; + 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.getTeamColor = &CG_GetTeamColor; + cgDC.setCVar = trap_Cvar_Set; + cgDC.getCVarString = trap_Cvar_VariableStringBuffer; + cgDC.getCVarValue = CG_Cvar_Get; + cgDC.drawTextWithCursor = &CG_Text_PaintWithCursor; + //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.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( hudSet[ 0 ] == '\0' ) + hudSet = "ui/hud.txt"; + + CG_LoadMenus( hudSet ); +} + +void CG_AssetCache( void ) +{ + 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 ); +} + +/* +================= +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; + + // 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" ); + + //inform UI to repress cursor whilst loading + trap_Cvar_Set( "ui_loading", "1" ); + + //TA: load overrides + BG_InitClassOverrides( ); + BG_InitBuildableOverrides( ); + BG_InitAllowedGameElements( ); + + //TA: dyn memory + CG_InitMemory( ); + + CG_RegisterCvars( ); + + CG_InitConsoleCommands( ); + + //TA: moved up for LoadHudMenu + String_Init( ); + + //TA: TA UI + CG_AssetCache( ); + CG_LoadHudMenu( ); // load new hud stuff + + cg.weaponSelect = WP_NONE; + + // old servers + + // get the rendering configuration from the client system + trap_GetGlconfig( &cgs.glconfig ); + cgs.screenXScale = cgs.glconfig.vidWidth / 640.0; + cgs.screenYScale = cgs.glconfig.vidHeight / 480.0; + + // get the gamestate from the client system + trap_GetGameState( &cgs.gameState ); + + // 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 ); + + //TA: + CG_InitBuildables( ); + + 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 ); + + trap_Cvar_Set( "ui_loading", "0" ); +} + +/* +================= +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 +} diff --git a/src/cgame/cg_marks.c b/src/cgame/cg_marks.c new file mode 100644 index 0000000..380f1f0 --- /dev/null +++ b/src/cgame/cg_marks.c @@ -0,0 +1,289 @@ +/* +=========================================================================== +Copyright (C) 1999-2005 Id Software, Inc. +Copyright (C) 2000-2006 Tim Angus + +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 128 +#define MAX_MARK_POINTS 384 + +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" ); + + //if ( markTotal >= MAX_MARK_POLYS ) { + // return; + //} + + // 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; + + // 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_POLY ]; + markPoly_t *mark; + + // we have an upper limit on the complexity of polygons + // that we store persistantly + if( mf->numPoints > MAX_VERTS_ON_POLY ) + mf->numPoints = MAX_VERTS_ON_POLY; + + 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 10000 +#define MARK_FADE_TIME 1000 + +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_mem.c b/src/cgame/cg_mem.c new file mode 100644 index 0000000..6cf5ddd --- /dev/null +++ b/src/cgame/cg_mem.c @@ -0,0 +1,202 @@ +/* +=========================================================================== +Copyright (C) 1999-2005 Id Software, Inc. +Copyright (C) 2000-2006 Tim Angus + +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" + +#define POOLSIZE (256 * 1024) +#define FREEMEMCOOKIE ((int)0xDEADBE3F) // Any unlikely to be used value +#define ROUNDBITS 31 // Round to 32 bytes + +struct freememnode +{ + // Size of ROUNDBITS + int cookie, size; // Size includes node (obviously) + struct freememnode *prev, *next; +}; + +static char memoryPool[ POOLSIZE ]; +static struct freememnode *freehead; +static int freemem; + +void *CG_Alloc( int size ) +{ + // Find a free block and allocate. + // Does two passes, attempts to fill same-sized free slot first. + + struct freememnode *fmn, *prev, *next, *smallest; + int allocsize, smallestsize; + char *endptr; + int *ptr; + + allocsize = ( size + sizeof(int) + ROUNDBITS ) & ~ROUNDBITS; // Round to 32-byte boundary + ptr = NULL; + + smallest = NULL; + smallestsize = POOLSIZE + 1; // Guaranteed not to miss any slots :) + for( fmn = freehead; fmn; fmn = fmn->next ) + { + if( fmn->cookie != FREEMEMCOOKIE ) + CG_Error( "CG_Alloc: Memory corruption detected!\n" ); + + if( fmn->size >= allocsize ) + { + // We've got a block + if( fmn->size == allocsize ) + { + // Same size, just remove + + prev = fmn->prev; + next = fmn->next; + if( prev ) + prev->next = next; // Point previous node to next + if( next ) + next->prev = prev; // Point next node to previous + if( fmn == freehead ) + freehead = next; // Set head pointer to next + ptr = (int *) fmn; + break; // Stop the loop, this is fine + } + else + { + // Keep track of the smallest free slot + if( fmn->size < smallestsize ) + { + smallest = fmn; + smallestsize = fmn->size; + } + } + } + } + + if( !ptr && smallest ) + { + // We found a slot big enough + smallest->size -= allocsize; + endptr = (char *) smallest + smallest->size; + ptr = (int *) endptr; + } + + if( ptr ) + { + freemem -= allocsize; + if( cg_debugAlloc.integer ) + CG_Printf( "CG_Alloc of %i bytes (%i left)\n", allocsize, freemem ); + memset( ptr, 0, allocsize ); + *ptr++ = allocsize; // Store a copy of size for deallocation + return( (void *) ptr ); + } + + CG_Error( "CG_Alloc: failed on allocation of %i bytes\n", size ); + return( NULL ); +} + +void CG_Free( void *ptr ) +{ + // Release allocated memory, add it to the free list. + + struct freememnode *fmn; + char *freeend; + int *freeptr; + + freeptr = ptr; + freeptr--; + + freemem += *freeptr; + if( cg_debugAlloc.integer ) + CG_Printf( "CG_Free of %i bytes (%i left)\n", *freeptr, freemem ); + + for( fmn = freehead; fmn; fmn = fmn->next ) + { + freeend = ((char *) fmn) + fmn->size; + if( freeend == (char *) freeptr ) + { + // Released block can be merged to an existing node + + fmn->size += *freeptr; // Add size of node. + return; + } + } + // No merging, add to head of list + + fmn = (struct freememnode *) freeptr; + fmn->size = *freeptr; // Set this first to avoid corrupting *freeptr + fmn->cookie = FREEMEMCOOKIE; + fmn->prev = NULL; + fmn->next = freehead; + freehead->prev = fmn; + freehead = fmn; +} + +void CG_InitMemory( void ) +{ + // Set up the initial node + + freehead = (struct freememnode *) memoryPool; + freehead->cookie = FREEMEMCOOKIE; + freehead->size = POOLSIZE; + freehead->next = NULL; + freehead->prev = NULL; + freemem = sizeof(memoryPool); +} + +void CG_DefragmentMemory( void ) +{ + // If there's a frenzy of deallocation and we want to + // allocate something big, this is useful. Otherwise... + // not much use. + + struct freememnode *startfmn, *endfmn, *fmn; + + for( startfmn = freehead; startfmn; ) + { + endfmn = (struct freememnode *)(((char *) startfmn) + startfmn->size); + for( fmn = freehead; fmn; ) + { + if( fmn->cookie != FREEMEMCOOKIE ) + CG_Error( "CG_DefragmentMemory: Memory corruption detected!\n" ); + + if( fmn == endfmn ) + { + // We can add fmn onto startfmn. + + if( fmn->prev ) + fmn->prev->next = fmn->next; + if( fmn->next ) + { + if( !(fmn->next->prev = fmn->prev) ) + freehead = fmn->next; // We're removing the head node + } + startfmn->size += fmn->size; + memset( fmn, 0, sizeof(struct freememnode) ); // A redundant call, really. + + startfmn = freehead; + endfmn = fmn = NULL; // Break out of current loop + } + else + fmn = fmn->next; + } + + if( endfmn ) + startfmn = startfmn->next; // endfmn acts as a 'restart' flag here + } +} diff --git a/src/cgame/cg_particles.c b/src/cgame/cg_particles.c new file mode 100644 index 0000000..80c4b23 --- /dev/null +++ b/src/cgame/cg_particles.c @@ -0,0 +1,2567 @@ +/* +=========================================================================== +Copyright (C) 2000-2006 Tim Angus + +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->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 ); + + 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 varianceBuffer[ 16 ]; + char *variancePtr = NULL, *varEndPointer = NULL; + float localValue = 0.0f; + float localVariance = 0.0f; + + Q_strncpyz( valueBuffer, token, sizeof( valueBuffer ) ); + Q_strncpyz( varianceBuffer, token, sizeof( varianceBuffer ) ); + + 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; + + bp->displacement[ i ] = atof_neg( token, qtrue ); + } + + token = COM_Parse( text_p ); + if( !token ) + break; + + CG_ParseValueAndVariance( token, NULL, &randFrac, qfalse ); + + bp->randDisplacement = 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, "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, "}" ) ) + 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 >= sizeof( text ) - 1 ) + { + CG_Printf( S_COLOR_RED "ERROR: particle file %s too long\n", fileName ); + 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 ); + } + + 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, 1, 1, 1, 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 ); + + 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 = ¢->currentState; + + if( es->eFlags & EF_NODRAW ) + { + if( CG_IsParticleSystemValid( ¢->entityPS ) && CG_IsParticleSystemInfinite( cent->entityPS ) ) + CG_DestroyParticleSystem( ¢->entityPS ); + + return; + } + + if( !CG_IsParticleSystemValid( ¢->entityPS ) && !cent->entityPSMissing ) + { + cent->entityPS = CG_SpawnNewParticleSystem( cgs.gameParticleSystems[ es->modelindex ] ); + + if( CG_IsParticleSystemValid( ¢->entityPS ) ) + { + CG_SetAttachmentPoint( ¢->entityPS->attachment, cent->lerpOrigin ); + CG_SetAttachmentCent( ¢->entityPS->attachment, cent ); + CG_AttachToPoint( ¢->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..b594330 --- /dev/null +++ b/src/cgame/cg_players.c @@ -0,0 +1,2565 @@ +/* +=========================================================================== +Copyright (C) 1999-2005 Id Software, Inc. +Copyright (C) 2000-2006 Tim Angus + +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 >= sizeof( text ) - 1 ) + { + CG_Printf( "File %s too long\n", filename ); + 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; + } + + //FIXME: skins do not load without icon present. do we want icons anyway? +/* Com_sprintf( filename, sizeof( filename ), "models/players/%s/icon_%s.tga", modelName, skinName ); + ci->modelIcon = trap_R_RegisterShaderNoMip( filename ); + if( !ci->modelIcon ) + { + Com_Printf( "Failed to load icon file: %s\n", filename ); + return qfalse; + }*/ + + return qtrue; +} + +/* +==================== +CG_ColorFromString +==================== +*/ +static void CG_ColorFromString( const char *v, vec3_t color ) +{ + int val; + + VectorClear( color ); + + val = atoi( v ); + + if( val < 1 || val > 7 ) + { + VectorSet( color, 1, 1, 1 ); + return; + } + + if( val & 1 ) + color[ 2 ] = 1.0f; + + if( val & 2 ) + color[ 1 ] = 1.0f; + + if( val & 4 ) + color[ 0 ] = 1.0f; +} + + +/* +=================== +CG_LoadClientInfo + +Load it now, taking the disk hits +=================== +*/ +static void CG_LoadClientInfo( clientInfo_t *ci ) +{ + const char *dir, *fallback; + int i; + const char *s; + int clientNum; + + if( !CG_RegisterClientModelname( ci, ci->modelName, ci->skinName ) ) + { + if( cg_buildScript.integer ) + CG_Error( "CG_RegisterClientModelname( %s, %s ) failed", ci->modelName, ci->skinName ); + + // fall back + if( !CG_RegisterClientModelname( ci, DEFAULT_MODEL, "default" ) ) + CG_Error( "DEFAULT_MODEL (%s) failed to register", DEFAULT_MODEL ); + } + + // sounds + dir = ci->modelName; + fallback = DEFAULT_MODEL; + + 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 ); + if( !ci->sounds[ i ] ) + ci->sounds[ i ] = trap_S_RegisterSound( va( "sound/player/%s/%s", fallback, s + 1 ), qfalse ); + } + } + else + { + ci->sounds[ i ] = trap_S_RegisterSound( va( "sound/player/%s/%s", dir, s + 1 ), qfalse ); + if( !ci->sounds[ i ] ) + ci->sounds[ i ] = trap_S_RegisterSound( va( "sound/player/%s/%s", fallback, 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( pClass_t class ) +{ + int i; + clientInfo_t *match; + char *modelName; + char *skinName; + + modelName = BG_FindModelNameForClass( class ); + skinName = BG_FindSkinNameForClass( class ); + + 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( pClass_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 ) ); + Q_strncpyz( newInfo.headModelName, model, sizeof( newInfo.headModelName ) ); + + // modelName didn not include a skin name + if( !skin ) + { + Q_strncpyz( newInfo.skinName, "default", sizeof( newInfo.skinName ) ); + Q_strncpyz( newInfo.headSkinName, "default", sizeof( newInfo.headSkinName ) ); + } + else + { + Q_strncpyz( newInfo.skinName, skin, sizeof( newInfo.skinName ) ); + Q_strncpyz( newInfo.headSkinName, skin, sizeof( newInfo.headSkinName ) ); + } + + newInfo.infoValid = qtrue; + + // actually register the models + *ci = newInfo; + CG_LoadClientInfo( ci ); +} + + +/* +====================== +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 ) ); + + // isolate the player's name + v = Info_ValueForKey( configstring, "n" ); + Q_strncpyz( newInfo.name, v, sizeof( newInfo.name ) ); + + // colors + v = Info_ValueForKey( configstring, "c1" ); + CG_ColorFromString( v, newInfo.color1 ); + + v = Info_ValueForKey( configstring, "c2" ); + CG_ColorFromString( v, newInfo.color2 ); + + // bot skill + v = Info_ValueForKey( configstring, "skill" ); + newInfo.botSkill = atoi( v ); + + // handicap + v = Info_ValueForKey( configstring, "hc" ); + newInfo.handicap = atoi( v ); + + // wins + v = Info_ValueForKey( configstring, "w" ); + newInfo.wins = atoi( v ); + + // losses + v = Info_ValueForKey( configstring, "l" ); + newInfo.losses = atoi( v ); + + // team + v = Info_ValueForKey( configstring, "t" ); + newInfo.team = atoi( v ); + + // team task + v = Info_ValueForKey( configstring, "tt" ); + newInfo.teamTask = atoi( v ); + + // team leader + v = Info_ValueForKey( configstring, "tl" ); + newInfo.teamLeader = atoi( v ); + + // 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; + } + + //CG_Printf( "NCI: %s\n", v ); + + // head model + v = Info_ValueForKey( configstring, "hmodel" ); + Q_strncpyz( newInfo.headModelName, v, sizeof( newInfo.headModelName ) ); + + slash = strchr( newInfo.headModelName, '/' ); + + if( !slash ) + { + // modelName didn not include a skin name + Q_strncpyz( newInfo.headSkinName, "default", sizeof( newInfo.headSkinName ) ); + } + else + { + Q_strncpyz( newInfo.headSkinName, slash + 1, sizeof( newInfo.headSkinName ) ); + // truncate modelName + *slash = 0; + } + + // 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 ) +{ + 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; + } + + // see if the animation sequence is switching + if( newAnimation != lf->animationNumber || !lf->animation ) + { + CG_SetLerpFrameAnimation( ci, lf, newAnimation ); + } + + // 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 *= speedScale; // adjust for haste, etc + 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 ); +} + + +/* +=============== +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, ¢->pe.legs, LEGS_TURN, speedScale ); + else + CG_RunPlayerLerpFrame( ci, ¢->pe.legs, cent->currentState.legsAnim, speedScale ); + + *legsOld = cent->pe.legs.oldFrame; + *legs = cent->pe.legs.frame; + *legsBackLerp = cent->pe.legs.backlerp; + + CG_RunPlayerLerpFrame( ci, ¢->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, ¢->pe.nonseg, NSPA_TURN, speedScale ); + else + CG_RunPlayerLerpFrame( ci, ¢->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, + ¢->pe.torso.yawAngle, ¢->pe.torso.yawing ); + CG_SwingAngles( legsAngles[ YAW ], 0, 0, cg_swingSpeed.value, + ¢->pe.legs.yawAngle, ¢->pe.legs.yawing ); + } + else + { + CG_SwingAngles( torsoAngles[ YAW ], 25, 90, cg_swingSpeed.value, + ¢->pe.torso.yawAngle, ¢->pe.torso.yawing ); + CG_SwingAngles( legsAngles[ YAW ], 40, 90, cg_swingSpeed.value, + ¢->pe.legs.yawAngle, ¢->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, ¢->pe.torso.pitchAngle, ¢->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 = ¢->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 = ¢->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, + ¢->pe.nonseg.yawAngle, ¢->pe.nonseg.yawing ); + } + else + { + CG_SwingAngles( localAngles[ YAW ], 40, 90, cg_swingSpeed.value, + ¢->pe.nonseg.yawAngle, ¢->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 = ¢->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( ¢->jetPackPS ) ) + CG_DestroyParticleSystem( ¢->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( ¢->jetPackPS ) ) + CG_DestroyParticleSystem( ¢->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( ¢->jetPackPS ) ) + CG_DestroyParticleSystem( ¢->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( ¢->jetPackPS ) ) + { + CG_SetAttachmentTag( ¢->jetPackPS->attachment, + jetpack, jetpack.hModel, "tag_flash" ); + CG_SetAttachmentCent( ¢->jetPackPS->attachment, cent ); + CG_AttachToTag( ¢->jetPackPS->attachment ); + } + } + else if( CG_IsParticleSystemValid( ¢->jetPackPS ) ) + { + CG_DestroyParticleSystem( ¢->jetPackPS ); + cent->jetPackState = JPS_OFF; + } + } + else if( CG_IsParticleSystemValid( ¢->jetPackPS ) ) + { + CG_DestroyParticleSystem( ¢->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; + } + + if( cent->currentState.eFlags & EF_TALK ) + { + // the masses have decreed this to be wrong +/* CG_PlayerFloatSprite( cent, cgs.media.balloonShader ); + 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, pClass_t class ) +{ + vec3_t end, mins, maxs; + trace_t trace; + float alpha; + entityState_t *es = ¢->currentState; + vec3_t surfNormal = { 0.0f, 0.0f, 1.0f }; + + BG_FindBBoxForClass( class, mins, maxs, NULL, NULL, NULL ); + mins[ 2 ] = 0.0f; + maxs[ 2 ] = 2.0f; + + 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_FindShadowScaleForClass( class ), qtrue ); + + return qtrue; +} + + +/* +=============== +CG_PlayerSplash + +Draw a mark at the water surface +=============== +*/ +static void CG_PlayerSplash( centity_t *cent, pClass_t class ) +{ + vec3_t start, end; + vec3_t mins, maxs; + trace_t trace; + int contents; + + if( !cg_shadows.integer ) + return; + + BG_FindBBoxForClass( 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_FindShadowScaleForClass( class ), 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; + entityState_t *es = ¢->currentState; + pClass_t class = ( es->powerups >> 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_FindBBoxForClass( 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; + } + else + { + legs.hModel = ci->nonSegModel; + legs.customSkin = ci->nonSegSkin; + } + + 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_FindBBoxForClass( 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_FindModelScaleForClass( class ); + + 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_FindZOffsetForClass( class ), 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; + + 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; + + 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 ); + } + + // + // 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( ¢->muzzlePS ) ) + CG_DestroyParticleSystem( ¢->muzzlePS ); + + if( CG_IsParticleSystemValid( ¢->jetPackPS ) ) + CG_DestroyParticleSystem( ¢->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 = ¢->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_FindBBoxForClass( 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( ¢->pe.legs, 0, sizeof( lerpFrame_t ) ); + CG_RunPlayerLerpFrame( ci, ¢->pe.legs, es->legsAnim, 1 ); + legs.oldframe = cent->pe.legs.oldFrame; + legs.frame = cent->pe.legs.frame; + legs.backlerp = cent->pe.legs.backlerp; + + memset( ¢->pe.torso, 0, sizeof( lerpFrame_t ) ); + CG_RunPlayerLerpFrame( ci, ¢->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( ¢->pe.nonseg, 0, sizeof( lerpFrame_t ) ); + CG_RunPlayerLerpFrame( ci, ¢->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_FindZOffsetForClass( es->clientNum ); + VectorCopy( legs.origin, legs.oldorigin ); // don't positionally lerp at all + + //rescale the model + scale = BG_FindModelScaleForClass( es->clientNum ); + + 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; + } + + //CG_AddRefEntityWithPowerups( &legs, es->powerups, ci->team ); + 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; + + //CG_AddRefEntityWithPowerups( &torso, es->powerups, ci->team ); + 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; + + //CG_AddRefEntityWithPowerups( &head, es->powerups, ci->team ); + 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 ], + ¢->pe.legs, cent->currentState.legsAnim ); + CG_ClearLerpFrame( &cgs.clientinfo[ cent->currentState.clientNum ], + ¢->pe.torso, cent->currentState.torsoAnim ); + CG_ClearLerpFrame( &cgs.clientinfo[ cent->currentState.clientNum ], + ¢->pe.nonseg, cent->currentState.legsAnim ); + + BG_EvaluateTrajectory( ¢->currentState.pos, cg.time, cent->lerpOrigin ); + BG_EvaluateTrajectory( ¢->currentState.apos, cg.time, cent->lerpAngles ); + + VectorCopy( cent->lerpOrigin, cent->rawOrigin ); + VectorCopy( cent->lerpAngles, cent->rawAngles ); + + memset( ¢->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( ¢->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( ¢->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 ); + } +} + +/* +================= +CG_Bleed + +This is the spurt of blood when a character gets hit +================= +*/ +void CG_Bleed( vec3_t origin, vec3_t normal, int entityNum ) +{ + pTeam_t team = cgs.clientinfo[ entityNum ].team; + qhandle_t bleedPS; + particleSystem_t *ps; + + if( !cg_blood.integer ) + return; + + if( team == PTE_ALIENS ) + bleedPS = cgs.media.alienBleedPS; + else if( team == PTE_HUMANS ) + bleedPS = cgs.media.humanBleedPS; + 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 ); + } +} + +/* +=============== +CG_AtHighestClass + +Is the local client at the highest class possible? +=============== +*/ +qboolean CG_AtHighestClass( void ) +{ + int i; + qboolean superiorClasses = qfalse; + + for( i = PCL_NONE + 1; i < PCL_NUM_CLASSES; i++ ) + { + if( BG_ClassCanEvolveFromTo( + cg.predictedPlayerState.stats[ STAT_PCLASS ], i, + ALIEN_MAX_KILLS, 0 ) >= 0 && + BG_FindStagesForClass( i, cgs.alienStage ) && + BG_ClassIsAllowed( i ) ) + { + superiorClasses = qtrue; + break; + } + } + + return !superiorClasses; +} + diff --git a/src/cgame/cg_playerstate.c b/src/cgame/cg_playerstate.c new file mode 100644 index 0000000..e1bcb09 --- /dev/null +++ b/src/cgame/cg_playerstate.c @@ -0,0 +1,317 @@ +/* +=========================================================================== +Copyright (C) 1999-2005 Id Software, Inc. +Copyright (C) 2000-2006 Tim Angus + +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 changed teams + if( ps->persistant[ PERS_TEAM ] != ops->persistant[ PERS_TEAM ] ) + 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_TEAM ] != TEAM_SPECTATOR ) + 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; + } +} + diff --git a/src/cgame/cg_predict.c b/src/cgame/cg_predict.c new file mode 100644 index 0000000..34f00c4 --- /dev/null +++ b/src/cgame/cg_predict.c @@ -0,0 +1,878 @@ +/* +=========================================================================== +Copyright (C) 1999-2005 Id Software, Inc. +Copyright (C) 2000-2006 Tim Angus + +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]; + +/* +==================== +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 = ¢->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 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; + + for( i = 0; i < j; i++ ) + { + if( i < cg_numSolidEntities ) + cent = cg_solidEntities[ i ]; + else + cent = &cg.predictedPlayerEntity; + + ent = ¢->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( ¢->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_FindBBoxForClass( ( ent->powerups >> 8 ) & 0xFF, bmins, bmaxs, NULL, NULL, NULL ); + + cmodel = trap_CM_TempBoxModel( bmins, bmaxs ); + VectorCopy( vec3_origin, angles ); + 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; + + 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 = ¢->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 = ¢->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; + } + + for( i = 0; i < MAX_WEAPONS; i++ ) + { + if( pps->ammo[ i ] != ps->ammo[ i ] ) + return 18; + } + + 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_PLAYERSOLID & ~CONTENTS_BODY; + else + cg_pmove.tracemask = MASK_PLAYERSOLID; + + if( cg.snap->ps.persistant[ PERS_TEAM ] == TEAM_SPECTATOR ) + cg_pmove.tracemask &= ~CONTENTS_BODY; // 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_ptr.c b/src/cgame/cg_ptr.c new file mode 100644 index 0000000..1881087 --- /dev/null +++ b/src/cgame/cg_ptr.c @@ -0,0 +1,81 @@ +/* +=========================================================================== +Copyright (C) 2000-2006 Tim Angus + +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_ptr.c -- post timeout restoration handling + + +#include "cg_local.h" + +#define PTRC_FILE "ptrc.cfg" + +/* +=============== +CG_ReadPTRCode + +Read a PTR code from disk +=============== +*/ +int CG_ReadPTRCode( void ) +{ + int len; + char text[ 16 ]; + fileHandle_t f; + + // load the file + len = trap_FS_FOpenFile( PTRC_FILE, &f, FS_READ ); + if( len <= 0 ) + return 0; + + // should never happen - malformed write + if( len >= sizeof( text ) - 1 ) + return 0; + + trap_FS_Read( text, len, f ); + text[ len ] = 0; + trap_FS_FCloseFile( f ); + + return atoi( text ); +} + +/* +=============== +CG_WritePTRCode + +Write a PTR code to disk +=============== +*/ +void CG_WritePTRCode( int code ) +{ + char text[ 16 ]; + fileHandle_t f; + + Com_sprintf( text, 16, "%d", code ); + + // open file + if( trap_FS_FOpenFile( PTRC_FILE, &f, FS_WRITE ) < 0 ) + return; + + // write the code + trap_FS_Write( text, strlen( text ), f ); + + trap_FS_FCloseFile( f ); +} diff --git a/src/cgame/cg_scanner.c b/src/cgame/cg_scanner.c new file mode 100644 index 0000000..ab98b23 --- /dev/null +++ b/src/cgame/cg_scanner.c @@ -0,0 +1,365 @@ +/* +=========================================================================== +Copyright (C) 2000-2006 Tim Angus + +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 700 + +/* +============= +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_PTEAM ] == PTE_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 ]; + + if( cent->currentState.eType == ET_BUILDABLE ) + { + //TA: add to list of item positions (for creep) + if( cent->currentState.modelindex2 == BIT_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 == BIT_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.powerups & 0x00FF; + + if( team == PTE_ALIENS ) + { + VectorCopy( cent->lerpOrigin, entityPositions.alienClientPos[ + entityPositions.numAlienClients ] ); + + if( entityPositions.numAlienClients < MAX_CLIENTS ) + entityPositions.numAlienClients++; + } + else if( team == PTE_HUMANS ) + { + VectorCopy( cent->lerpOrigin, entityPositions.humanClientPos[ + entityPositions.numHumanClients ] ); + + if( entityPositions.numHumanClients < MAX_CLIENTS ) + entityPositions.numHumanClients++; + } + } + } +} + +#define STALKWIDTH 2.0f +#define BLIPX 16.0f +#define BLIPY 8.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 ); +} + +#define BLIPX2 24.0f +#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; + + if( ps->stats[ STAT_STATE ] & SS_WALLCLIMBING ) + { + if( ps->stats[ STAT_STATE ] & SS_WALLCLIMBINGCEILING ) + VectorSet( normal, 0.0f, 0.0f, -1.0f ); + else + VectorCopy( ps->grapplePoint, normal ); + } + else + VectorSet( normal, 0.0f, 0.0f, 1.0f ); + + 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_AlienSense +============= +*/ +void CG_AlienSense( rectDef_t *rect ) +{ + int i; + vec3_t origin; + vec3_t relOrigin; + vec4_t buildable = { 1.0f, 0.0f, 0.0f, 0.7f }; + vec4_t client = { 0.0f, 0.0f, 1.0f, 0.7f }; + + 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, buildable ); + } + + //draw human clients + for( i = 0; i < entityPositions.numHumanClients; i++ ) + { + VectorClear( relOrigin ); + VectorSubtract( entityPositions.humanClientPos[ i ], origin, relOrigin ); + + if( VectorLength( relOrigin ) < ALIENSENSE_RANGE ) + CG_DrawDir( 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; + vec4_t hIbelow; + vec4_t aIabove = { 1.0f, 0.0f, 0.0f, 0.75f }; + vec4_t aIbelow = { 1.0f, 0.0f, 0.0f, 0.5f }; + + Vector4Copy( color, hIabove ); + hIabove[ 3 ] *= 1.5f; + Vector4Copy( color, hIbelow ); + + 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_DrawBlips( rect, relOrigin, hIbelow ); + } + + //draw alien buildables 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_DrawBlips( 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_DrawBlips( 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_DrawBlips( rect, relOrigin, aIabove ); + } +} diff --git a/src/cgame/cg_servercmds.c b/src/cgame/cg_servercmds.c new file mode 100644 index 0000000..7fb3e06 --- /dev/null +++ b/src/cgame/cg_servercmds.c @@ -0,0 +1,1016 @@ +/* +=========================================================================== +Copyright (C) 1999-2005 Id Software, Inc. +Copyright (C) 2000-2006 Tim Angus + +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 = atoi( CG_Argv( 1 ) ); + + if( cg.numScores > MAX_CLIENTS ) + cg.numScores = MAX_CLIENTS; + + cg.teamScores[ 0 ] = atoi( CG_Argv( 2 ) ); + cg.teamScores[ 1 ] = atoi( CG_Argv( 3 ) ); + + 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 + 4 ) ); + cg.scores[ i ].score = atoi( CG_Argv( i * 6 + 5 ) ); + cg.scores[ i ].ping = atoi( CG_Argv( i * 6 + 6 ) ); + cg.scores[ i ].time = atoi( CG_Argv( i * 6 + 7 ) ); + cg.scores[ i ].weapon = atoi( CG_Argv( i * 6 + 8 ) ); + cg.scores[ i ].upgrade = atoi( CG_Argv( i * 6 + 9 ) ); + + 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; + cgs.clientinfo[ cg.scores[ i ].client ].powerups = 0; + + cg.scores[ i ].team = cgs.clientinfo[ cg.scores[ i ].client ].team; + } +} + +/* +================= +CG_ParseTeamInfo + +================= +*/ +static void CG_ParseTeamInfo( void ) +{ + int i; + int client; + + numSortedTeamPlayers = atoi( CG_Argv( 1 ) ); + + for( i = 0; i < numSortedTeamPlayers; i++ ) + { + client = atoi( CG_Argv( i * 6 + 2 ) ); + + sortedTeamPlayers[ i ] = client; + + cgs.clientinfo[ client ].location = atoi( CG_Argv( i * 6 + 3 ) ); + cgs.clientinfo[ client ].health = atoi( CG_Argv( i * 6 + 4 ) ); + cgs.clientinfo[ client ].armor = atoi( CG_Argv( i * 6 + 5 ) ); + cgs.clientinfo[ client ].curWeapon = atoi( CG_Argv( i * 6 + 6 ) ); + cgs.clientinfo[ client ].powerups = atoi( CG_Argv( i * 6 + 7 ) ); + } +} + + +/* +================ +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.warmupCount = -1; + + if( warmup == 0 && cg.warmup ) + { + } + + cg.warmup = warmup; +} + +/* +================ +CG_SetConfigValues + +Called on load to set the initial values from configure strings +================ +*/ +void CG_SetConfigValues( void ) +{ + sscanf( CG_ConfigString( CS_BUILDPOINTS ), + "%d %d %d %d %d", &cgs.alienBuildPoints, + &cgs.alienBuildPointsTotal, + &cgs.humanBuildPoints, + &cgs.humanBuildPointsTotal, + &cgs.humanBuildPointsPowered ); + + sscanf( CG_ConfigString( CS_STAGES ), "%d %d %d %d %d %d", &cgs.alienStage, &cgs.humanStage, + &cgs.alienKills, &cgs.humanKills, &cgs.alienNextStageThreshold, &cgs.humanNextStageThreshold ); + sscanf( CG_ConfigString( CS_SPAWNS ), "%d %d", &cgs.numAlienSpawns, &cgs.numHumanSpawns ); + + cgs.levelStartTime = atoi( CG_ConfigString( CS_LEVEL_START_TIME ) ); + cg.warmup = 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_PTEAM ] != PTE_ALIENS ) + return; + + trap_S_StartLocalSound( cgs.media.alienStageTransition, CHAN_ANNOUNCER ); + CG_CenterPrint( "We have evolved!", 200, GIANTCHAR_WIDTH * 4 ); +} + +/* +================ +CG_AnnounceHumanStageTransistion +================ +*/ +static void CG_AnnounceHumanStageTransistion( stage_t from, stage_t to ) +{ + if( cg.predictedPlayerState.stats[ STAT_PTEAM ] != PTE_HUMANS ) + return; + + trap_S_StartLocalSound( cgs.media.humanStageTransition, CHAN_ANNOUNCER ); + CG_CenterPrint( "Reinforcements 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_BUILDPOINTS ) + sscanf( str, "%d %d %d %d %d", &cgs.alienBuildPoints, + &cgs.alienBuildPointsTotal, + &cgs.humanBuildPoints, + &cgs.humanBuildPointsTotal, + &cgs.humanBuildPointsPowered ); + else if( num == CS_STAGES ) + { + stage_t oldAlienStage = cgs.alienStage; + stage_t oldHumanStage = cgs.humanStage; + + sscanf( str, "%d %d %d %d %d %d", + &cgs.alienStage, &cgs.humanStage, + &cgs.alienKills, &cgs.humanKills, + &cgs.alienNextStageThreshold, &cgs.humanNextStageThreshold ); + + if( cgs.alienStage != oldAlienStage ) + CG_AnnounceAlienStageTransistion( oldAlienStage, cgs.alienStage ); + + if( cgs.humanStage != oldHumanStage ) + CG_AnnounceHumanStageTransistion( oldHumanStage, cgs.humanStage ); + } + else if( num == CS_SPAWNS ) + sscanf( str, "%d %d", &cgs.numAlienSpawns, &cgs.numHumanSpawns ); + else if( num == CS_LEVEL_START_TIME ) + cgs.levelStartTime = atoi( str ); + else if( num == CS_VOTE_TIME ) + { + cgs.voteTime = atoi( str ); + cgs.voteModified = qtrue; + + if( cgs.voteTime ) + trap_Cvar_Set( "ui_voteActive", "1" ); + else + trap_Cvar_Set( "ui_voteActive", "0" ); + } + else if( num == CS_VOTE_YES ) + { + cgs.voteYes = atoi( str ); + cgs.voteModified = qtrue; + } + else if( num == CS_VOTE_NO ) + { + cgs.voteNo = atoi( str ); + cgs.voteModified = qtrue; + } + else if( num == CS_VOTE_STRING ) + Q_strncpyz( cgs.voteString, str, sizeof( cgs.voteString ) ); + else if( num >= CS_TEAMVOTE_TIME && num <= CS_TEAMVOTE_TIME + 1 ) + { + int cs_offset = num - CS_TEAMVOTE_TIME; + + cgs.teamVoteTime[ cs_offset ] = atoi( str ); + cgs.teamVoteModified[ cs_offset ] = qtrue; + + if( cs_offset == 0 ) + { + if( cgs.teamVoteTime[ cs_offset ] ) + trap_Cvar_Set( "ui_humanTeamVoteActive", "1" ); + else + trap_Cvar_Set( "ui_humanTeamVoteActive", "0" ); + } + else if( cs_offset == 1 ) + { + if( cgs.teamVoteTime[ cs_offset ] ) + trap_Cvar_Set( "ui_alienTeamVoteActive", "1" ); + else + trap_Cvar_Set( "ui_alienTeamVoteActive", "0" ); + } + } + else if( num >= CS_TEAMVOTE_YES && num <= CS_TEAMVOTE_YES + 1 ) + { + cgs.teamVoteYes[ num - CS_TEAMVOTE_YES ] = atoi( str ); + cgs.teamVoteModified[ num - CS_TEAMVOTE_YES ] = qtrue; + } + else if( num >= CS_TEAMVOTE_NO && num <= CS_TEAMVOTE_NO + 1 ) + { + cgs.teamVoteNo[ num - CS_TEAMVOTE_NO ] = atoi( str ); + cgs.teamVoteModified[ num - CS_TEAMVOTE_NO ] = qtrue; + } + else if( num >= CS_TEAMVOTE_STRING && num <= CS_TEAMVOTE_STRING + 1 ) + { + Q_strncpyz( cgs.teamVoteString[ num - CS_TEAMVOTE_STRING ], str, + sizeof( cgs.teamVoteString[ num - CS_TEAMVOTE_STRING ] ) ); + } + 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 = 0; + + cg.mapRestart = qtrue; + + CG_StartMusic( ); + + trap_S_ClearLoopingSounds( qtrue ); + + // we really should clear more parts of cg here and stop sounds + + // play the "fight" sound if this is a restart without warmup + if( cg.warmup == 0 ) + CG_CenterPrint( "FIGHT!", 120, GIANTCHAR_WIDTH * 2 ); + + trap_Cvar_Set( "cg_thirdPerson", "0" ); +} + +/* +================= +CG_RemoveChatEscapeChar +================= +*/ +static void CG_RemoveChatEscapeChar( char *text ) +{ + int i, l; + + l = 0; + for( i = 0; text[ i ]; i++ ) + { + if( text[ i ] == '\x19' ) + continue; + + text[ l++ ] = text[ i ]; + } + + text[ l ] = '\0'; +} + +/* +=============== +CG_SetUIVars + +Set some cvars used by the UI +=============== +*/ +static void CG_SetUIVars( void ) +{ + int i; + char carriageCvar[ MAX_TOKEN_CHARS ]; + + *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_FindPurchasableForWeapon( i ) ) + 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_FindPurchasableForUpgrade( i ) ) + 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_Menu +============== +*/ +void CG_Menu( int menu ) +{ + const char *cmd = NULL; // command to send + const char *longMsg = NULL; // command parameter + const char *shortMsg = NULL; // non-modal version of message + CG_SetUIVars( ); + + // string literals have static storage duration, this is safe, + // cleaner and much more readable. + switch( menu ) + { + case MN_TEAM: + cmd = "menu tremulous_teamselect\n"; + break; + + case MN_A_CLASS: + cmd = "menu tremulous_alienclass\n"; + break; + + case MN_H_SPAWN: + cmd = "menu tremulous_humanitem\n"; + break; + + case MN_A_BUILD: + cmd = "menu tremulous_alienbuild\n"; + break; + + case MN_H_BUILD: + cmd = "menu tremulous_humanbuild\n"; + break; + + case MN_H_ARMOURY: + cmd = "menu tremulous_humanarmoury\n"; + 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 = "The alien team has too many players\n"; + cmd = "menu tremulous_alien_dialog\n"; + 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 = "The human team has too many players\n"; + cmd = "menu tremulous_human_dialog\n"; + break; + + case MN_A_TEAMCHANGEBUILDTIMER: + longMsg = "You cannot leave the Alien team until your build timer " + "has expired."; + shortMsg = "You cannot change teams until your build timer expires.\n"; + cmd = "menu tremulous_alien_dialog\n"; + break; + + case MN_H_TEAMCHANGEBUILDTIMER: + longMsg = "You cannot leave the Human team until your build timer " + "has expired."; + shortMsg = "You cannot change teams until your build timer expires.\n"; + cmd = "menu tremulous_human_dialog\n"; + break; + + //=============================== + + case MN_H_NOROOM: + longMsg = "There is no room to build here. Move until the buildable turns " + "translucent green indicating a valid build location."; + shortMsg = "There is no room to build here\n"; + cmd = "menu tremulous_human_dialog\n"; + break; + + case MN_H_NOPOWER: + longMsg = "There is no power remaining. Free up power by destroying " + "existing buildable objects."; + shortMsg = "There is no power remaining\n"; + cmd = "menu tremulous_human_dialog\n"; + break; + + case MN_H_NOTPOWERED: + longMsg = "This buildable is not powered. Build a Reactor and/or Repeater " + "in order to power it."; + shortMsg = "This buildable is not powered\n"; + cmd = "menu tremulous_human_dialog\n"; + break; + + case MN_H_NORMAL: + longMsg = "Cannot build on this surface. The surface is too steep or " + "unsuitable to build on. Please choose another site for this " + "structure."; + shortMsg = "Cannot build on this surface\n"; + cmd = "menu tremulous_human_dialog\n"; + break; + + case MN_H_REACTOR: + longMsg = "There can only be one Reactor. Destroy the existing one if you " + "wish to move it."; + shortMsg = "There can only be one Reactor\n"; + cmd = "menu tremulous_human_dialog\n"; + break; + + case MN_H_REPEATER: + longMsg = "There is no power here. If available, a Repeater may be used to " + "transmit power to this location."; + shortMsg = "There is no power here\n"; + cmd = "menu tremulous_human_dialog\n"; + break; + + case MN_H_NODCC: + longMsg = "There is no Defense Computer. A Defense Computer is needed to " + "build this."; + shortMsg = "There is no Defense Computer\n"; + cmd = "menu tremulous_human_dialog\n"; + break; + + case MN_H_TNODEWARN: + longMsg = "WARNING: This Telenode will not be powered. Build near a power " + "structure to prevent seeing this message again."; + shortMsg = "This Telenode will not be powered\n"; + cmd = "menu tremulous_human_dialog\n"; + break; + + case MN_H_RPTWARN: + longMsg = "WARNING: This Repeater will not be powered as there is no parent " + "Reactor providing power. Build a Reactor."; + shortMsg = "This Repeater will not be powered\n"; + cmd = "menu tremulous_human_dialog\n"; + break; + + case MN_H_RPTWARN2: + longMsg = "This area already has power. A Repeater is not required here."; + shortMsg = "This area already has power\n"; + cmd = "menu tremulous_human_dialog\n"; + break; + + case MN_H_NOSLOTS: + longMsg = "You have no room to carry this. Please sell any conflicting " + "upgrades before purchasing this item."; + shortMsg = "You have no room to carry this\n"; + cmd = "menu tremulous_human_dialog\n"; + break; + + case MN_H_NOFUNDS: + longMsg = "Insufficient funds. You do not have enough credits to perform " + "this action."; + shortMsg = "Insufficient funds\n"; + cmd = "menu tremulous_human_dialog\n"; + break; + + case MN_H_ITEMHELD: + longMsg = "You already hold this item. It is not possible to carry multiple " + "items of the same type."; + shortMsg = "You already hold this item\n"; + cmd = "menu tremulous_human_dialog\n"; + break; + + case MN_H_NOARMOURYHERE: + longMsg = "You must be near a powered Armoury in order to purchase " + "weapons, upgrades or non-energy ammunition."; + shortMsg = "You must be near a powered Armoury\n"; + cmd = "menu tremulous_human_dialog\n"; + break; + + case MN_H_NOENERGYAMMOHERE: + longMsg = "You must be near an Armoury, Reactor or Repeater in order " + "to purchase energy ammunition."; + shortMsg = "You must be near an Armoury, Reactor or Repeater\n"; + cmd = "menu tremulous_human_dialog\n"; + 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 = "Not enough room here to put on a Battle Suit\n"; + cmd = "menu tremulous_human_dialog\n"; + 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 = "Not enough room here to take off your Battle Suit\n"; + cmd = "menu tremulous_human_dialog\n"; + break; + + case MN_H_ARMOURYBUILDTIMER: + longMsg = "You are not allowed to buy or sell weapons until your " + "build timer has expired."; + shortMsg = "You can not buy or sell weapos until your build timer " + "expires\n"; + cmd = "menu tremulous_human_dialog\n"; + break; + + + //=============================== + + case MN_A_NOROOM: + longMsg = "There is no room to build here. Move until the structure turns " + "translucent green indicating a valid build location."; + shortMsg = "There is no room to build here\n"; + cmd = "menu tremulous_alien_dialog\n"; + 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 = "There is no creep here\n"; + cmd = "menu tremulous_alien_dialog\n"; + break; + + case MN_A_NOOVMND: + longMsg = "There is no Overmind. An Overmind must be built to control " + "the structure you tried to place"; + shortMsg = "There is no Overmind\n"; + cmd = "menu tremulous_alien_dialog\n"; + break; + + case MN_A_OVERMIND: + longMsg = "There can only be one Overmind. Destroy the existing one if you " + "wish to move it."; + shortMsg = "There can only be one Overmind\n"; + cmd = "menu tremulous_alien_dialog\n"; + break; + + case MN_A_HOVEL: + longMsg = "There can only be one Hovel. Destroy the existing one if you " + "wish to move it."; + shortMsg = "There can only be one Hovel\n"; + cmd = "menu tremulous_alien_dialog\n"; + break; + + case MN_A_NOASSERT: + longMsg = "The Overmind cannot control any more structures. Destroy existing " + "structures to build more."; + shortMsg = "The Overmind cannot control any more structures\n"; + cmd = "menu tremulous_alien_dialog\n"; + break; + + case MN_A_SPWNWARN: + longMsg = "WARNING: This spawn will not be controlled by an Overmind. " + "Build an Overmind to prevent seeing this message again."; + shortMsg = "This spawn will not be controlled by an Overmind\n"; + cmd = "menu tremulous_alien_dialog\n"; + break; + + case MN_A_NORMAL: + longMsg = "Cannot build on this surface. This surface is too steep or " + "unsuitable to build on. Please choose another site for this " + "structure."; + shortMsg = "Cannot build on this surface\n"; + cmd = "menu tremulous_alien_dialog\n"; + break; + + case MN_A_NOEROOM: + longMsg = "There is no room to evolve here. Move away from walls or other " + "nearby objects and try again."; + cmd = "menu tremulous_alien_dialog\n"; + shortMsg = "There is no room to evolve here\n"; + break; + + case MN_A_TOOCLOSE: + longMsg = "This location is too close to the enemy to evolve. Move away " + "until you are no longer aware of the enemy's presence and try " + "again."; + shortMsg = "This location is too close to the enemy to evolve\n"; + cmd = "menu tremulous_alien_dialog\n"; + break; + + case MN_A_NOOVMND_EVOLVE: + longMsg = "There is no Overmind. An Overmind must be built to allow " + "you to upgrade."; + shortMsg = "There is no Overmind\n"; + cmd = "menu tremulous_alien_dialog\n"; + break; + + case MN_A_EVOLVEBUILDTIMER: + longMsg = "You cannot Evolve until your build timer has expired."; + shortMsg = "You cannot Evolve until your build timer expires\n"; + cmd = "menu tremulous_alien_dialog\n"; + break; + + case MN_A_HOVEL_OCCUPIED: + longMsg = "This Hovel is already occupied by another builder."; + shortMsg = "This Hovel is already occupied by another builder\n"; + cmd = "menu tremulous_alien_dialog\n"; + break; + + case MN_A_HOVEL_BLOCKED: + longMsg = "The exit to this Hovel is currently blocked. Please wait until it " + "becomes clear then try again."; + shortMsg = "The exit to this Hovel is currently blocked\n"; + cmd = "menu tremulous_alien_dialog\n"; + break; + + case MN_A_HOVEL_EXIT: + longMsg = "The exit to this Hovel would always be blocked. Please choose " + "a more suitable location."; + shortMsg = "The exit to this Hovel would always be blocked\n"; + cmd = "menu tremulous_alien_dialog\n"; + break; + + case MN_A_INFEST: + trap_Cvar_Set( "ui_currentClass", va( "%d %d", cg.snap->ps.stats[ STAT_PCLASS ], + cg.snap->ps.persistant[ PERS_CREDIT ] ) ); + cmd = "menu tremulous_alienupgrade\n"; + break; + + default: + Com_Printf( "cgame: debug: no such menu %d\n", menu ); + } + + if( !cg_disableWarningDialogs.integer || !shortMsg ) + { + // Player either wants dialog window or there's no short message + if( cmd ) + { + if( longMsg ) + trap_Cvar_Set( "ui_dialog", longMsg ); + + trap_SendConsoleCommand( cmd ); + } + } + else + { + // There is short message and player wants it + CG_Printf( shortMsg ); + } +} + +/* +================= +CG_ServerCommand + +The string has been tokenized and can be retrieved with +Cmd_Argc() / Cmd_Argv() +================= +*/ +static void CG_ServerCommand( void ) +{ + const char *cmd; + char text[ MAX_SAY_TEXT ]; + + cmd = CG_Argv( 0 ); + + if( !cmd[ 0 ] ) + { + // server claimed the command + return; + } + + if( !strcmp( cmd, "cp" ) ) + { + CG_CenterPrint( CG_Argv( 1 ), SCREEN_HEIGHT * 0.30, BIGCHAR_WIDTH ); + return; + } + + if( !strcmp( cmd, "cs" ) ) + { + CG_ConfigStringModified( ); + return; + } + + if( !strcmp( cmd, "print" ) ) + { + CG_Printf( "%s", CG_Argv( 1 ) ); + return; + } + + if( !strcmp( cmd, "chat" ) ) + { + if( !cg_teamChatsOnly.integer ) + { + Q_strncpyz( text, CG_Argv( 1 ), MAX_SAY_TEXT ); + if( Q_stricmpn( text, "[skipnotify]", 12 ) ) + trap_S_StartLocalSound( cgs.media.talkSound, CHAN_LOCAL_SOUND ); + CG_RemoveChatEscapeChar( text ); + CG_Printf( "%s\n", text ); + } + + return; + } + + if( !strcmp( cmd, "tchat" ) ) + { + Q_strncpyz( text, CG_Argv( 1 ), MAX_SAY_TEXT ); + if( Q_stricmpn( text, "[skipnotify]", 12 ) ) + { + if( cg.snap->ps.stats[ STAT_PTEAM ] == PTE_ALIENS ) + trap_S_StartLocalSound( cgs.media.alienTalkSound, CHAN_LOCAL_SOUND ); + else if( cg.snap->ps.stats[ STAT_PTEAM ] == PTE_HUMANS ) + trap_S_StartLocalSound( cgs.media.humanTalkSound, CHAN_LOCAL_SOUND ); + else + trap_S_StartLocalSound( cgs.media.talkSound, CHAN_LOCAL_SOUND ); + } + CG_RemoveChatEscapeChar( text ); + CG_Printf( "%s\n", text ); + return; + } + + if( !strcmp( cmd, "scores" ) ) + { + CG_ParseScores( ); + return; + } + + if( !strcmp( cmd, "tinfo" ) ) + { + CG_ParseTeamInfo( ); + return; + } + + if( !strcmp( cmd, "map_restart" ) ) + { + CG_MapRestart( ); + return; + } + + if( Q_stricmp( cmd, "remapShader" ) == 0 ) + { + if( trap_Argc( ) == 4 ) + trap_R_RemapShader( CG_Argv( 1 ), CG_Argv( 2 ), CG_Argv( 3 ) ); + } + + // clientLevelShot is sent before taking a special screenshot for + // the menu system during development + if( !strcmp( cmd, "clientLevelShot" ) ) + { + cg.levelShot = qtrue; + return; + } + + //the server has triggered a menu + if( !strcmp( cmd, "servermenu" ) ) + { + if( trap_Argc( ) == 2 && !cg.demoPlayback ) + CG_Menu( atoi( CG_Argv( 1 ) ) ); + + return; + } + + //the server thinks this client should close all menus + if( !strcmp( cmd, "serverclosemenus" ) ) + { + trap_SendConsoleCommand( "closemenus\n" ); + return; + } + + //poison cloud effect needs to be reliable + if( !strcmp( cmd, "poisoncloud" ) ) + { + 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 ); + } + + return; + } + + if( !strcmp( cmd, "weaponswitch" ) ) + { + CG_Printf( "client weaponswitch\n" ); + if( trap_Argc( ) == 2 ) + { + cg.weaponSelect = atoi( CG_Argv( 1 ) ); + cg.weaponSelectTime = cg.time; + } + + return; + } + + // server requests a ptrc + if( !strcmp( cmd, "ptrcrequest" ) ) + { + int code = CG_ReadPTRCode( ); + + trap_SendClientCommand( va( "ptrcverify %d", code ) ); + return; + } + + // server issues a ptrc + if( !strcmp( cmd, "ptrcissue" ) ) + { + if( trap_Argc( ) == 2 ) + { + int code = atoi( CG_Argv( 1 ) ); + + CG_WritePTRCode( code ); + } + + return; + } + + // reply to ptrcverify + if( !strcmp( cmd, "ptrcconfirm" ) ) + { + trap_SendConsoleCommand( "menu ptrc_popmenu\n" ); + + 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..f439a69 --- /dev/null +++ b/src/cgame/cg_snapshot.c @@ -0,0 +1,411 @@ +/* +=========================================================================== +Copyright (C) 1999-2005 Id Software, Inc. +Copyright (C) 2000-2006 Tim Angus + +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( ¢->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 ); + + // if we had a map_restart, set everthing with initial + if( !cg.snap ) { } //TA: ? + + // 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( ¢->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", + 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..0893ebc --- /dev/null +++ b/src/cgame/cg_syscalls.asm @@ -0,0 +1,115 @@ +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_DrawStretchPic -47 +equ trap_R_ModelBounds -48 +equ trap_R_LerpTag -49 +equ trap_GetGlconfig -50 +equ trap_GetGameState -51 +equ trap_GetCurrentSnapshotNumber -52 +equ trap_GetSnapshot -53 +equ trap_GetServerCommand -54 +equ trap_GetCurrentCmdNumber -55 +equ trap_GetUserCmd -56 +equ trap_SetUserCmdValue -57 +equ trap_R_RegisterShaderNoMip -58 +equ trap_MemoryRemaining -59 +equ trap_R_RegisterFont -60 +equ trap_Key_IsDown -61 +equ trap_Key_GetCatcher -62 +equ trap_Key_SetCatcher -63 +equ trap_Key_GetKey -64 +equ trap_Parse_AddGlobalDefine -65 +equ trap_Parse_LoadSource -66 +equ trap_Parse_FreeSource -67 +equ trap_Parse_ReadToken -68 +equ trap_Parse_SourceFileAndLine -69 +equ trap_S_StopBackgroundTrack -70 +equ trap_RealTime -71 +equ trap_SnapVector -72 +equ trap_RemoveCommand -73 +equ trap_R_LightForPoint -74 +equ trap_CIN_PlayCinematic -75 +equ trap_CIN_StopCinematic -76 +equ trap_CIN_RunCinematic -77 +equ trap_CIN_DrawCinematic -78 +equ trap_CIN_SetExtents -79 +equ trap_R_RemapShader -80 +equ trap_S_AddRealLoopingSound -81 +equ trap_S_StopLoopingSound -82 +equ trap_CM_TempCapsuleModel -83 +equ trap_CM_CapsuleTrace -84 +equ trap_CM_TransformedCapsuleTrace -85 +equ trap_R_AddAdditiveLightToScene -86 +equ trap_GetEntityToken -87 +equ trap_R_AddPolysToScene -88 +equ trap_R_inPVS -89 +equ trap_FS_Seek -90 +equ trap_FS_GetFileList -91 +equ trap_LiteralArgs -92 +equ trap_CM_BiSphereTrace -93 +equ trap_CM_TransformedBiSphereTrace -94 +equ trap_GetDemoState -95 +equ trap_GetDemoPos -96 +equ trap_GetDemoName -97 +equ trap_Key_KeynumToStringBuf -98 +equ trap_Key_GetBindingBuf -99 +equ trap_Key_SetBinding -100 + +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..43afc2c --- /dev/null +++ b/src/cgame/cg_syscalls.c @@ -0,0 +1,570 @@ +/* +=========================================================================== +Copyright (C) 1999-2005 Id Software, Inc. +Copyright (C) 2000-2006 Tim Angus + +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; + + +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 ); +} + +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 ); +} + +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_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 ); +} + diff --git a/src/cgame/cg_trails.c b/src/cgame/cg_trails.c new file mode 100644 index 0000000..c8943ed --- /dev/null +++ b/src/cgame/cg_trails.c @@ -0,0 +1,1500 @@ +/* +=========================================================================== +Copyright (C) 2000-2006 Tim Angus + +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 -= ( 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 ) + { + 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, "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 >= sizeof( text ) - 1 ) + { + CG_Printf( S_COLOR_RED "ERROR: trail file %s too long\n", fileName ); + 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 ); + + break; + } + } + + return tb; +} + + +/* +=============== +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; + + 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 ); + + break; + } + } + + return ts; +} + +/* +=============== +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 ); + } + + 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..5ad29b7 --- /dev/null +++ b/src/cgame/cg_tutorial.c @@ -0,0 +1,663 @@ +/* +=========================================================================== +Copyright (C) 2000-2006 Tim Angus + +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 } }, + { "boost", "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 + { + Q_strncpyz( buffer, va( "\"%s\"", bindings[ i ].humanName ), + MAX_STRING_CHARS ); + Q_strcat( buffer, MAX_STRING_CHARS, " (unbound)" ); + } + + 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 & B_HEALTH_MASK; + *healthFraction = (float)health / B_HEALTH_MASK; + } + + if( es->eType == ET_BUILDABLE && + ps->stats[ STAT_PTEAM ] == BG_FindTeamForBuildable( es->modelindex ) ) + 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_FindHumanNameForBuildable( buildable ) ) ); + + Q_strcat( text, MAX_TUTORIAL_TEXT, + va( "Press %s to cancel placing the %s\n", + CG_KeyNameForCommand( "+button5" ), + BG_FindHumanNameForBuildable( buildable ) ) ); + } + 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->generic1 & B_MARKED_TOGGLEBIT ) + { + 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" ) ) ); + } + } + } + + if( ps->stats[ STAT_PCLASS ] == PCL_ALIEN_BUILDER0_UPG ) + { + 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" ) ) ); + } + + 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 a human to damage it\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 a human to grab it\n" ); + + Q_strcat( text, MAX_TUTORIAL_TEXT, + va( "Press %s to swipe\n", + CG_KeyNameForCommand( "+attack" ) ) ); + + if( ps->stats[ STAT_PCLASS ] == 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_PCLASS ] == 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_PCLASS ] == 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 charge\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; + float health; + + if( buildable > BA_NONE ) + { + Q_strcat( text, MAX_TUTORIAL_TEXT, + va( "Press %s to place the %s\n", + CG_KeyNameForCommand( "+attack" ), + BG_FindHumanNameForBuildable( buildable ) ) ); + + Q_strcat( text, MAX_TUTORIAL_TEXT, + va( "Press %s to cancel placing the %s\n", + CG_KeyNameForCommand( "+button5" ), + BG_FindHumanNameForBuildable( buildable ) ) ); + } + else + { + Q_strcat( text, MAX_TUTORIAL_TEXT, + va( "Press %s to build a structure\n", + CG_KeyNameForCommand( "+attack" ) ) ); + + if( CG_BuildableInRange( ps, &health ) ) + { + if( health < 1.0f ) + { + Q_strcat( text, MAX_TUTORIAL_TEXT, + va( "Hold %s to repair this structure\n", + CG_KeyNameForCommand( "+button5" ) ) ); + } + + 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; + int ammo, clips; + upgrade_t upgrade = UP_NONE; + + if( cg.weaponSelect <= 32 ) + name = cg_weapons[ cg.weaponSelect ].humanName; + else if( cg.weaponSelect > 32 ) + { + name = cg_upgrades[ cg.weaponSelect - 32 ].humanName; + upgrade = cg.weaponSelect - 32; + } + + BG_UnpackAmmoArray( ps->weapon, ps->ammo, ps->powerups, &ammo, &clips ); + + if( !ammo && !clips && !BG_FindInfinteAmmoForWeapon( ps->weapon ) ) + { + //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 a 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_FindHumanNameForWeapon( ps->weapon ) ) ); + break; + + case WP_MASS_DRIVER: + Q_strcat( text, MAX_TUTORIAL_TEXT, + va( "Press %s to fire the %s\n", + CG_KeyNameForCommand( "+attack" ), + BG_FindHumanNameForWeapon( ps->weapon ) ) ); + + 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_FindHumanNameForWeapon( ps->weapon ) ) ); + 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_FindHumanNameForWeapon( ps->weapon ) ) ); + break; + + case WP_HBUILD: + case WP_HBUILD2: + 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_FindUsableForUpgrade( upgrade ) ) ) + { + 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_FindHumanNameForUpgrade( UP_MEDKIT ) ) ); + } + + Q_strcat( text, MAX_TUTORIAL_TEXT, + va( "Press %s to use a structure\n", + CG_KeyNameForCommand( "+button7" ) ) ); + + Q_strcat( text, MAX_TUTORIAL_TEXT, + va( "Press %s to sprint\n", + CG_KeyNameForCommand( "boost" ) ) ); +} + +/* +=============== +CG_SpectatorText +=============== +*/ +static void CG_SpectatorText( char *text, playerState_t *ps ) +{ + if( cgs.clientinfo[ cg.clientNum ].team != PTE_NONE ) + { + 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 ) + { + 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 %s\n", CG_KeyNameForCommand( "+button2" ), + ( cgs.clientinfo[ cg.clientNum ].team == PTE_NONE ) + ? "player" : "teammate" ) ); + } +} + +/* +=============== +CG_TutorialText + +Returns context help for the current class/weapon +=============== +*/ +const char *CG_TutorialText( void ) +{ + playerState_t *ps; + static char text[ MAX_TUTORIAL_TEXT ]; + + CG_GetBindings( ); + + text[ 0 ] = '\0'; + ps = &cg.snap->ps; + + if( !cg.intermissionStarted && !cg.demoPlayback ) + { + if( ps->persistant[ PERS_TEAM ] == TEAM_SPECTATOR || + ps->pm_flags & PMF_FOLLOW ) + { + CG_SpectatorText( text, ps ); + } + else if( ps->stats[ STAT_HEALTH ] > 0 ) + { + switch( ps->stats[ STAT_PCLASS ] ) + { + 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: + CG_HumanText( text, ps ); + break; + + default: + break; + } + + if( ps->stats[ STAT_PTEAM ] == PTE_ALIENS ) + { + entityState_t *es = CG_BuildableInRange( ps, NULL ); + + if( ps->stats[ STAT_STATE ] & SS_HOVELING ) + { + Q_strcat( text, MAX_TUTORIAL_TEXT, + va( "Press %s to exit the hovel\n", + CG_KeyNameForCommand( "+button7" ) ) ); + } + else if( es && es->modelindex == BA_A_HOVEL && + es->generic1 & B_SPAWNED_TOGGLEBIT && + ( ps->stats[ STAT_PCLASS ] == PCL_ALIEN_BUILDER0 || + ps->stats[ STAT_PCLASS ] == PCL_ALIEN_BUILDER0_UPG ) ) + { + Q_strcat( text, MAX_TUTORIAL_TEXT, + va( "Press %s to enter the hovel\n", + CG_KeyNameForCommand( "+button7" ) ) ); + } + else if( BG_UpgradeClassAvailable( ps ) ) + { + Q_strcat( text, MAX_TUTORIAL_TEXT, + va( "Press %s to evolve\n", + CG_KeyNameForCommand( "+button7" ) ) ); + } + } + } + + 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..477196c --- /dev/null +++ b/src/cgame/cg_view.c @@ -0,0 +1,1340 @@ +/* +=========================================================================== +Copyright (C) 1999-2005 Id Software, Inc. +Copyright (C) 2000-2006 Tim Angus + +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 + { + // bound normal viewsize + if( cg_viewsize.integer < 30 ) + { + trap_Cvar_Set( "cg_viewsize", "30" ); + size = 30; + } + else if( cg_viewsize.integer > 100 ) + { + trap_Cvar_Set( "cg_viewsize","100" ); + 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 + +=============== +*/ +#define FOCUS_DISTANCE 512 +static void CG_OffsetThirdPersonView( void ) +{ + vec3_t forward, right, up; + vec3_t view; + vec3_t focusAngles; + trace_t trace; + static vec3_t mins = { -8, -8, -8 }; + static vec3_t maxs = { 8, 8, 8 }; + vec3_t focusPoint; + float focusDist; + float forwardScale, sideScale; + vec3_t surfNormal; + + if( cg.predictedPlayerState.stats[ STAT_STATE ] & SS_WALLCLIMBING ) + { + if( cg.predictedPlayerState.stats[ STAT_STATE ] & SS_WALLCLIMBINGCEILING ) + VectorSet( surfNormal, 0.0f, 0.0f, -1.0f ); + else + VectorCopy( cg.predictedPlayerState.grapplePoint, surfNormal ); + } + else + VectorSet( surfNormal, 0.0f, 0.0f, 1.0f ); + + VectorMA( cg.refdef.vieworg, cg.predictedPlayerState.viewheight, surfNormal, cg.refdef.vieworg ); + + VectorCopy( cg.refdefViewAngles, focusAngles ); + + // if dead, look at killer + if( cg.predictedPlayerState.stats[ STAT_HEALTH ] <= 0 ) + { + focusAngles[ YAW ] = cg.predictedPlayerState.stats[ STAT_VIEWLOCK ]; + cg.refdefViewAngles[ YAW ] = cg.predictedPlayerState.stats[ STAT_VIEWLOCK ]; + } + + //if ( focusAngles[PITCH] > 45 ) { + // focusAngles[PITCH] = 45; // don't go too far overhead + //} + AngleVectors( focusAngles, forward, NULL, NULL ); + + VectorMA( cg.refdef.vieworg, FOCUS_DISTANCE, forward, focusPoint ); + + VectorCopy( cg.refdef.vieworg, view ); + + VectorMA( view, 12, surfNormal, view ); + + //cg.refdefViewAngles[PITCH] *= 0.5; + + AngleVectors( cg.refdefViewAngles, forward, right, up ); + + forwardScale = cos( cg_thirdPersonAngle.value / 180 * M_PI ); + sideScale = sin( cg_thirdPersonAngle.value / 180 * M_PI ); + VectorMA( view, -cg_thirdPersonRange.value * forwardScale, forward, view ); + VectorMA( view, -cg_thirdPersonRange.value * sideScale, right, view ); + + // 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 + + if( !cg_cameraMode.integer ) + { + CG_Trace( &trace, cg.refdef.vieworg, mins, maxs, view, cg.predictedPlayerState.clientNum, MASK_SOLID ); + + if( trace.fraction != 1.0 ) + { + VectorCopy( trace.endpos, view ); + view[ 2 ] += ( 1.0 - 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 ); + } + } + + VectorCopy( view, cg.refdef.vieworg ); + + // select pitch to look at focus point from vieword + VectorSubtract( focusPoint, cg.refdef.vieworg, focusPoint ); + focusDist = sqrt( focusPoint[ 0 ] * focusPoint[ 0 ] + focusPoint[ 1 ] * focusPoint[ 1 ] ); + if ( focusDist < 1 ) { + focusDist = 1; // should never happen + } + cg.refdefViewAngles[ PITCH ] = -180 / M_PI * atan2( focusPoint[ 2 ], focusDist ); + cg.refdefViewAngles[ YAW ] -= cg_thirdPersonAngle.value; +} + + +// 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; + + if( ps->stats[ STAT_STATE ] & SS_WALLCLIMBING ) + { + if( ps->stats[ STAT_STATE ] & SS_WALLCLIMBINGCEILING ) + VectorSet( normal, 0.0f, 0.0f, -1.0f ); + else + VectorCopy( ps->grapplePoint, normal ); + } + else + VectorSet( normal, 0.0f, 0.0f, 1.0f ); + + steptime = BG_FindSteptimeForClass( ps->stats[ STAT_PCLASS ] ); + + // smooth out stair climbing + timeDelta = cg.time - cg.stepTime; + if( timeDelta < steptime ) + { + float stepChange = cg.stepChange + * (steptime - timeDelta) / steptime; + + if( ps->stats[ STAT_STATE ] & SS_WALLCLIMBING ) + VectorMA( cg.refdef.vieworg, -stepChange, normal, cg.refdef.vieworg ); + else + cg.refdef.vieworg[ 2 ] -= stepChange; + } +} + +#define PCLOUD_ROLL_AMPLITUDE 25.0f +#define PCLOUD_ROLL_FREQUENCY 0.4f +#define PCLOUD_ZOOM_AMPLITUDE 15 +#define PCLOUD_ZOOM_FREQUENCY 0.7f + + +/* +=============== +CG_OffsetFirstPersonView + +=============== +*/ +static 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; + + if( ps->stats[ STAT_STATE ] & SS_WALLCLIMBING ) + { + if( ps->stats[ STAT_STATE ] & SS_WALLCLIMBINGCEILING ) + VectorSet( normal, 0.0f, 0.0f, -1.0f ); + else + VectorCopy( ps->grapplePoint, normal ); + } + else + VectorSet( normal, 0.0f, 0.0f, 1.0f ); + + + 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 weapon kick + VectorAdd( angles, cg.kick_angles, angles ); + + // 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_TEAM ] == TEAM_SPECTATOR ) + bob2 = 0.0f; + else + bob2 = BG_FindBobForClass( cg.predictedPlayerState.stats[ STAT_PCLASS ] ); + + +#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 ] / (float)LEVEL4_CHARGE_TIME; + + 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 ) + { + if( cg.predictedPlayerState.stats[ STAT_MISC ] > 0 ) + { + float fraction1, fraction2; + vec3_t forward; + + AngleVectors( angles, forward, NULL, NULL ); + VectorNormalize( forward ); + + fraction1 = (float)( cg.time - cg.weapon2Time ) / (float)LEVEL3_POUNCE_CHARGE_TIME; + + 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.predictedPlayerState.stats[ STAT_STATE ] & SS_POISONCLOUDED && + !( cg.snap->ps.pm_flags & PMF_FOLLOW ) ) + { + float fraction = sin( ( (float)cg.time / 1000.0f ) * M_PI * 2 * PCLOUD_ROLL_FREQUENCY ); + float pitchFraction = sin( ( (float)cg.time / 1000.0f ) * M_PI * 5 * PCLOUD_ROLL_FREQUENCY ); + + fraction *= 1.0f - ( ( cg.time - cg.poisonedTime ) / (float)LEVEL1_PCLOUD_TIME ); + pitchFraction *= 1.0f - ( ( cg.time - cg.poisonedTime ) / (float)LEVEL1_PCLOUD_TIME ); + + 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_PTEAM ] == PTE_HUMANS ) + { + angles[PITCH] += cg.bobfracsin * bob2 * 0.5; + + // heavy breathing effects //FIXME: sound + if( cg.predictedPlayerState.stats[ STAT_STAMINA ] < 0 ) + { + float deltaBreath = (float)( + cg.predictedPlayerState.stats[ STAT_STAMINA ] < 0 ? + -cg.predictedPlayerState.stats[ STAT_STAMINA ] : + cg.predictedPlayerState.stats[ STAT_STAMINA ] ) / 200.0; + float deltaAngle = cos( (float)cg.time/150.0 ) * deltaBreath; + + deltaAngle += ( deltaAngle < 0 ? -deltaAngle : deltaAngle ) * 0.5; + + angles[ PITCH ] -= deltaAngle; + } + } + +//=================================== + + // add view height + // when wall climbing the viewheight is not straight up + if( cg.predictedPlayerState.stats[ STAT_STATE ] & SS_WALLCLIMBING ) + VectorMA( origin, ps->viewheight, normal, origin ); + else + origin[ 2 ] += cg.predictedPlayerState.viewheight; + + // 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; + + // likewise for bob + if( cg.predictedPlayerState.stats[ STAT_STATE ] & SS_WALLCLIMBING ) + VectorMA( origin, bob, normal, origin ); + else + origin[ 2 ] += bob; + + + // 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( ); + + // add kick offset + + VectorAdd (origin, cg.kick_origin, origin); +} + +//====================================================================== + +/* +==================== +CG_CalcFov + +Fixed fov at intermissions, otherwise account for fov variable and zooms. +==================== +*/ +#define WAVE_AMPLITUDE 1 +#define WAVE_FREQUENCY 0.4 + +#define FOVWARPTIME 400.0 + +static int CG_CalcFov( void ) +{ + float x; + float phase; + float v; + int contents; + float fov_x, fov_y; + float zoomFov; + float f; + int inwater; + int attribFov; + usercmd_t cmd; + int cmdNum; + + cmdNum = trap_GetCurrentCmdNumber( ); + trap_GetUserCmd( cmdNum, &cmd ); + + if( cg.predictedPlayerState.pm_type == PM_INTERMISSION || + ( cg.snap->ps.persistant[ PERS_TEAM ] == TEAM_SPECTATOR ) ) + { + // if in intermission, use a fixed value + fov_x = 90; + } + else + { + // don't lock the fov globally - we need to be able to change it + attribFov = BG_FindFovForClass( cg.predictedPlayerState.stats[ STAT_PCLASS ] ); + fov_x = attribFov; + + if ( fov_x < 1 ) + fov_x = 1; + else if ( fov_x > 160 ) + fov_x = 160; + + if( cg.spawnTime > ( cg.time - FOVWARPTIME ) && + BG_ClassHasAbility( cg.predictedPlayerState.stats[ STAT_PCLASS ], SCA_FOVWARPS ) ) + { + float temp, temp2; + + temp = (float)( cg.time - cg.spawnTime ) / FOVWARPTIME; + temp2 = ( 170 - fov_x ) * temp; + + //Com_Printf( "%f %f\n", temp*100, temp2*100 ); + + fov_x = 170 - temp2; + } + + // account for zooms + zoomFov = BG_FindZoomFovForWeapon( cg.predictedPlayerState.weapon ); + if ( zoomFov < 1 ) + zoomFov = 1; + 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_WeaponCanZoom( cg.predictedPlayerState.weapon ) ) + { + if ( cg.zoomed ) + { + f = ( cg.time - cg.zoomTime ) / (float)ZOOM_TIME; + + if ( f > 1.0 ) + fov_x = zoomFov; + else + fov_x = fov_x + f * ( zoomFov - fov_x ); + + // BUTTON_ATTACK2 isn't held so unzoom next time + if( !( cmd.buttons & BUTTON_ATTACK2 ) ) + { + cg.zoomed = qfalse; + cg.zoomTime = cg.time; + } + } + else + { + f = ( cg.time - cg.zoomTime ) / (float)ZOOM_TIME; + + if ( f > 1.0 ) + fov_x = fov_x; + else + fov_x = zoomFov + f * ( fov_x - zoomFov ); + + // BUTTON_ATTACK2 is held so zoom next time + if( cmd.buttons & BUTTON_ATTACK2 ) + { + cg.zoomed = qtrue; + cg.zoomTime = cg.time; + } + } + } + } + + x = cg.refdef.width / tan( fov_x / 360 * M_PI ); + fov_y = atan2( cg.refdef.height, x ); + fov_y = fov_y * 360 / M_PI; + + // warp if underwater + contents = CG_PointContents( cg.refdef.vieworg, -1 ); + + if( contents & ( CONTENTS_WATER | CONTENTS_SLIME | CONTENTS_LAVA ) ) + { + phase = cg.time / 1000.0 * WAVE_FREQUENCY * M_PI * 2; + v = WAVE_AMPLITUDE * sin( phase ); + fov_x += v; + fov_y -= v; + inwater = qtrue; + } + else + inwater = qfalse; + + if( cg.predictedPlayerState.stats[ STAT_STATE ] & SS_POISONCLOUDED && + cg.predictedPlayerState.stats[ STAT_HEALTH ] > 0 && + !( cg.snap->ps.pm_flags & PMF_FOLLOW ) ) + { + phase = cg.time / 1000.0 * PCLOUD_ZOOM_FREQUENCY * M_PI * 2; + v = PCLOUD_ZOOM_AMPLITUDE * sin( phase ); + v *= 1.0f - ( ( cg.time - cg.poisonedTime ) / (float)LEVEL1_PCLOUD_TIME ); + 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; + else + cg.zoomSensitivity = cg.refdef.fov_y / 75.0; + + 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 + if( !( ps->stats[ STAT_STATE ] & SS_WALLCLIMBINGCEILING ) ) + VectorCopy( ps->grapplePoint, surfNormal ); + else + VectorCopy( ceilingNormal, 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 ) + { + 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 ] ); + + VectorCopy( ps->origin, cg.refdef.vieworg ); + + if( BG_ClassHasAbility( ps->stats[ STAT_PCLASS ], SCA_WALLCLIMBER ) ) + CG_smoothWWTransitions( ps, ps->viewangles, cg.refdefViewAngles ); + else if( BG_ClassHasAbility( ps->stats[ STAT_PCLASS ], 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_PCLASS ], 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 ); + } + + 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 ); + + // 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..deae4e0 --- /dev/null +++ b/src/cgame/cg_weapons.c @@ -0,0 +1,1845 @@ +/* +=========================================================================== +Copyright (C) 1999-2005 Id Software, Inc. +Copyright (C) 2000-2006 Tim Angus + +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; + + upgradeInfo = &cg_upgrades[ upgradeNum ]; + + if( upgradeNum == 0 ) + return; + + if( upgradeInfo->registered ) + return; + + memset( upgradeInfo, 0, sizeof( *upgradeInfo ) ); + upgradeInfo->registered = qtrue; + + if( !BG_FindNameForUpgrade( upgradeNum ) ) + CG_Error( "Couldn't find upgrade %i", upgradeNum ); + + upgradeInfo->humanName = BG_FindHumanNameForUpgrade( upgradeNum ); + + //la la la la la, i'm not listening! + if( upgradeNum == UP_GRENADE ) + upgradeInfo->upgradeIcon = cg_weapons[ WP_GRENADE ].weaponIcon; + else if( ( icon = BG_FindIconForUpgrade( upgradeNum ) ) ) + upgradeInfo->upgradeIcon = trap_R_RegisterShader( icon ); +} + +/* +=============== +CG_InitUpgrades + +Precaches upgrades +=============== +*/ +void CG_InitUpgrades( void ) +{ + int i; + + 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, "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 >= sizeof( text ) - 1 ) + { + CG_Printf( "File %s too long\n", filename ); + 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 ); + + if( !wi->handsModel ) + wi->handsModel = trap_R_RegisterModel( "models/weapons2/shotgun/shotgun_hand.md3" ); + + 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; + + weaponInfo = &cg_weapons[ weaponNum ]; + + if( weaponNum == 0 ) + return; + + if( weaponInfo->registered ) + return; + + memset( weaponInfo, 0, sizeof( *weaponInfo ) ); + weaponInfo->registered = qtrue; + + if( !BG_FindNameForWeapon( weaponNum ) ) + CG_Error( "Couldn't find weapon %i", weaponNum ); + + Com_sprintf( path, MAX_QPATH, "models/weapons/%s/weapon.cfg", BG_FindNameForWeapon( weaponNum ) ); + + weaponInfo->humanName = BG_FindHumanNameForWeapon( weaponNum ); + + 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 ] ); + + //FIXME: + for( i = WPM_NONE + 1; i < WPM_NUM_WEAPONMODES; i++ ) + weaponInfo->wim[ i ].loopFireSound = qfalse; +} + +/* +=============== +CG_InitWeapons + +Precaches weapons +=============== +*/ +void CG_InitWeapons( void ) +{ + int i; + + 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_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; + + 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_FindBobForClass( cg.predictedPlayerState.stats[ STAT_PCLASS ] ); + + 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( !BG_ClassHasAbility( cg.predictedPlayerState.stats[ STAT_PCLASS ], SCA_NOWEAPONDRIFT ) ) + { + 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; + + CG_RegisterWeapon( weaponNum ); + weapon = &cg_weapons[ weaponNum ]; + + // add the weapon + memset( &gun, 0, sizeof( gun ) ); + VectorCopy( parent->lightingOrigin, gun.lightingOrigin ); + gun.shadowPlane = parent->shadowPlane; + gun.renderfx = parent->renderfx; + + // set custom shading for railgun refire rate + 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 ); + } + } + + 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 ); + } + + if( !noGunModel ) + { + CG_PositionEntityOnTag( &gun, parent, parent->hModel, "tag_weapon" ); + + trap_R_AddRefEntityToScene( &gun ); + + // add the spinning barrel + if( weapon->barrelModel ) + { + memset( &barrel, 0, sizeof( barrel ) ); + VectorCopy( parent->lightingOrigin, barrel.lightingOrigin ); + barrel.shadowPlane = parent->shadowPlane; + barrel.renderfx = parent->renderfx; + + barrel.hModel = weapon->barrelModel; + angles[ YAW ] = 0; + angles[ PITCH ] = 0; + angles[ ROLL ] = CG_MachinegunSpinAngle( cent, firing ); + AnglesToAxis( angles, barrel.axis ); + + CG_PositionRotatedEntityOnTag( &barrel, &gun, weapon->weaponModel, "tag_barrel" ); + + trap_R_AddRefEntityToScene( &barrel ); + } + } + + if( CG_IsParticleSystemValid( ¢->muzzlePS ) ) + { + if( ps || cg.renderingThirdPerson || + cent->currentState.number != cg.predictedPlayerState.clientNum ) + { + if( noGunModel ) + CG_SetAttachmentTag( ¢->muzzlePS->attachment, *parent, parent->hModel, "tag_weapon" ); + else + CG_SetAttachmentTag( ¢->muzzlePS->attachment, gun, weapon->weaponModel, "tag_flash" ); + } + + //if the PS is infinite disable it when not firing + if( !firing && CG_IsParticleSystemInfinite( cent->muzzlePS ) ) + CG_DestroyParticleSystem( ¢->muzzlePS ); + } + + // add the flash + if( !weapon->wim[ weaponMode ].continuousFlash || !firing ) + { + // impulse flash + if( cg.time - cent->muzzleFlashTime > MUZZLE_FLASH_TIME ) + return; + } + + memset( &flash, 0, sizeof( flash ) ); + VectorCopy( parent->lightingOrigin, flash.lightingOrigin ); + flash.shadowPlane = parent->shadowPlane; + flash.renderfx = parent->renderfx; + + 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, weapon->weaponModel, "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( ¢->muzzlePS ) ) + { + if( noGunModel ) + CG_SetAttachmentTag( ¢->muzzlePS->attachment, *parent, parent->hModel, "tag_weapon" ); + else + CG_SetAttachmentTag( ¢->muzzlePS->attachment, gun, weapon->weaponModel, "tag_flash" ); + + CG_SetAttachmentCent( ¢->muzzlePS->attachment, cent ); + CG_AttachToTag( ¢->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 +============== +*/ +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; + + if( weaponMode <= WPM_NONE || weaponMode >= WPM_NUM_WEAPONMODES ) + weaponMode = WPM_PRIMARY; + + CG_RegisterWeapon( weapon ); + wi = &cg_weapons[ weapon ]; + cent = &cg.predictedPlayerEntity; // &cg_entities[cg.snap->ps.clientNum]; + + if( ( ps->persistant[PERS_TEAM] == TEAM_SPECTATOR ) || + ( ps->stats[ STAT_STATE ] & SS_INFESTING ) || + ( ps->stats[ STAT_STATE ] & SS_HOVELING ) ) + return; + + // no weapon carried - can't draw it + if( weapon == WP_NONE ) + 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 ); + + if( weapon == WP_LUCIFER_CANNON && ps->stats[ STAT_MISC ] > 0 ) + { + if( ps->stats[ STAT_MISC ] > ( LCANNON_TOTAL_CHARGE - ( LCANNON_TOTAL_CHARGE / 3 ) ) ) + trap_S_AddLoopingSound( ps->clientNum, ps->origin, vec3_origin, cgs.media.lCannonWarningSound ); + } + + // 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( ¢->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( ¢->muzzlePS ) ) + { + CG_SetAttachmentPoint( ¢->muzzlePS->attachment, origin ); + CG_SetAttachmentCent( ¢->muzzlePS->attachment, cent ); + CG_AttachToPoint( ¢->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; + + 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 ); + + if( weapon == WP_LUCIFER_CANNON && ps->stats[ STAT_MISC ] > 0 ) + { + float fraction = (float)ps->stats[ STAT_MISC ] / (float)LCANNON_TOTAL_CHARGE; + + VectorMA( hand.origin, random( ) * fraction, cg.refdef.viewaxis[ 0 ], hand.origin ); + VectorMA( hand.origin, random( ) * fraction, 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 ) +{ + //int ammo, clips; + // + //BG_UnpackAmmoArray( i, cg.snap->ps.ammo, cg.snap->ps.powerups, &ammo, &clips ); + // + // this is a pain in the ass + //if( !ammo && !clips && !BG_FindInfinteAmmoForWeapon( i ) ) + // return qfalse; + + 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_FindUsableForUpgrade( upgrade ); +} + + +#define ICON_BORDER 4 + +/* +=================== +CG_DrawItemSelect +=================== +*/ +void CG_DrawItemSelect( rectDef_t *rect, vec4_t color ) +{ + int i; + int x = rect->x; + int y = rect->y; + int width = rect->w; + int height = rect->h; + int iconsize; + int items[ 64 ]; + int numItems = 0, selectedItem = 0; + int length; + int selectWindow; + qboolean vertical; + centity_t *cent; + playerState_t *ps; + + int colinfo[ 64 ]; + + 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 && !CG_WeaponSelectable( cg.weaponSelect ) ) + CG_NextWeapon_f( ); + else if( cg.weaponSelect > 32 && !CG_UpgradeSelectable( cg.weaponSelect - 32 ) ) + CG_NextWeapon_f( ); + } + + // showing weapon select clears pickup item display, but not the blend blob + cg.itemPickupTime = 0; + + if( height > width ) + { + vertical = qtrue; + iconsize = width; + length = height / width; + } + else if( height <= width ) + { + vertical = qfalse; + iconsize = height; + length = width / height; + } + + selectWindow = length / 2; + + for( i = WP_NONE + 1; i < WP_NUM_WEAPONS; i++ ) + { + if( !BG_InventoryContainsWeapon( i, cg.snap->ps.stats ) ) + continue; + + { + int ammo, clips; + + BG_UnpackAmmoArray( i, cg.snap->ps.ammo, cg.snap->ps.powerups, &ammo, &clips ); + + if( !ammo && !clips && !BG_FindInfinteAmmoForWeapon( i ) ) + colinfo[ numItems ] = 1; + else + colinfo[ numItems ] = 0; + + } + + if( i == cg.weaponSelect ) + selectedItem = numItems; + + CG_RegisterWeapon( i ); + items[ numItems ] = i; + numItems++; + } + + for( i = UP_NONE + 1; i < UP_NUM_UPGRADES; i++ ) + { + if( !BG_InventoryContainsUpgrade( i, cg.snap->ps.stats ) ) + continue; + colinfo[ numItems ] = 0; + if( !BG_FindUsableForUpgrade ( i ) ) + colinfo[ numItems ] = 2; + + + if( i == cg.weaponSelect - 32 ) + selectedItem = numItems; + + CG_RegisterUpgrade( i ); + items[ numItems ] = i + 32; + numItems++; + } + + for( i = 0; i < length; i++ ) + { + int displacement = i - selectWindow; + int item = displacement + selectedItem; + + 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, iconsize, iconsize, cg_weapons[ items[ item ] ].weaponIcon ); + else if( items[ item ] > 32 ) + CG_DrawPic( x, y, iconsize, iconsize, cg_upgrades[ items[ item ] - 32 ].upgradeIcon ); + + trap_R_SetColor( NULL ); + } + + if( vertical ) + y += iconsize; + else + x += iconsize; + } +} + + +/* +=================== +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 = CG_Text_Width( name, scale, 0 ); + x = rect->x + rect->w / 2; + CG_Text_Paint( x - w / 2, rect->y + rect->h, scale, color, name, 0, 0, textStyle ); + } + } + } + else if( cg.weaponSelect > 32 ) + { + 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 = CG_Text_Width( name, scale, 0 ); + x = rect->x + rect->w / 2; + CG_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.weaponSelect > 32 ) + { + 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.weaponSelect > 32 ) + { + 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 = ¢->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( ¢->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 ) +{ + 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 ); + } + } + + // + // impact mark + // + if( radius > 0.0f ) + CG_ImpactMark( mark, origin, dir, random( ) * 360, 1, 1, 1, 1, qfalse, radius, qfalse ); +} + + +/* +================= +CG_MissileHitPlayer +================= +*/ +void CG_MissileHitPlayer( weapon_t weaponNum, weaponMode_t weaponMode, + vec3_t origin, vec3_t dir, int entityNum ) +{ + 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 ) + CG_MissileHitWall( weaponNum, weaponMode, 0, origin, dir, IMPACTSOUND_FLESH ); +} + + +/* +============================================================================ + +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 ); +} + +/* +============================================================================ + +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_MissileHitPlayer( WP_SHOTGUN, WPM_PRIMARY, tr.endpos, tr.plane.normal, tr.entityNum ); + else if( tr.surfaceFlags & SURF_METALSTEPS ) + CG_MissileHitWall( WP_SHOTGUN, WPM_PRIMARY, 0, tr.endpos, tr.plane.normal, IMPACTSOUND_METAL ); + else + CG_MissileHitWall( WP_SHOTGUN, WPM_PRIMARY, 0, tr.endpos, tr.plane.normal, IMPACTSOUND_DEFAULT ); + } + } +} + +/* +============== +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 ); +} + |