/* =========================================================================== Copyright (C) 2000-2009 Darklegion Development This file is part of Tremulous. Tremulous is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. Tremulous is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Tremulous; if not, write to the Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA =========================================================================== */ // cg_trails.c -- the trail system #include "cg_local.h" static baseTrailSystem_t baseTrailSystems[ MAX_BASETRAIL_SYSTEMS ]; static baseTrailBeam_t baseTrailBeams[ MAX_BASETRAIL_BEAMS ]; static int numBaseTrailSystems = 0; static int numBaseTrailBeams = 0; static trailSystem_t trailSystems[ MAX_TRAIL_SYSTEMS ]; static trailBeam_t trailBeams[ MAX_TRAIL_BEAMS ]; /* =============== CG_CalculateBeamNodeProperties Fills in trailBeamNode_t.textureCoord =============== */ static void CG_CalculateBeamNodeProperties( trailBeam_t *tb ) { trailBeamNode_t *i = NULL; trailSystem_t *ts; baseTrailBeam_t *btb; float nodeDistances[ MAX_TRAIL_BEAM_NODES ]; float totalDistance = 0.0f, position = 0.0f; int j, numNodes = 0; float TCRange, widthRange, alphaRange; vec3_t colorRange; float fadeAlpha = 1.0f; if( !tb || !tb->nodes ) return; ts = tb->parent; btb = tb->class; if( ts->destroyTime > 0 && btb->fadeOutTime ) { fadeAlpha -= (float)( cg.time - ts->destroyTime ) / btb->fadeOutTime; if( fadeAlpha < 0.0f ) fadeAlpha = 0.0f; } TCRange = tb->class->backTextureCoord - tb->class->frontTextureCoord; widthRange = tb->class->backWidth - tb->class->frontWidth; alphaRange = tb->class->backAlpha - tb->class->frontAlpha; VectorSubtract( tb->class->backColor, tb->class->frontColor, colorRange ); for( i = tb->nodes; i && i->next; i = i->next ) { nodeDistances[ numNodes++ ] = Distance( i->position, i->next->position ); } for( j = 0; j < numNodes; j++ ) totalDistance += nodeDistances[ j ]; for( j = 0, i = tb->nodes; i; i = i->next, j++ ) { if( tb->class->textureType == TBTT_STRETCH ) { i->textureCoord = tb->class->frontTextureCoord + ( ( position / totalDistance ) * TCRange ); } else if( tb->class->textureType == TBTT_REPEAT ) { if( tb->class->clampToBack ) i->textureCoord = ( totalDistance - position ) / tb->class->repeatLength; else i->textureCoord = position / tb->class->repeatLength; } i->halfWidth = ( tb->class->frontWidth + ( ( position / totalDistance ) * widthRange ) ) / 2.0f; i->alpha = (byte)( (float)0xFF * ( tb->class->frontAlpha + ( ( position / totalDistance ) * alphaRange ) ) * fadeAlpha ); VectorMA( tb->class->frontColor, ( position / totalDistance ), colorRange, i->color ); position += nodeDistances[ j ]; } } /* =============== CG_LightVertex Lights a particular vertex =============== */ static void CG_LightVertex( vec3_t point, byte alpha, byte *rgba ) { int i; vec3_t alight, dlight, lightdir; trap_R_LightForPoint( point, alight, dlight, lightdir ); for( i = 0; i <= 2; i++ ) rgba[ i ] = (int)alight[ i ]; rgba[ 3 ] = alpha; } /* =============== CG_RenderBeam Renders a beam =============== */ static void CG_RenderBeam( trailBeam_t *tb ) { trailBeamNode_t *i = NULL; trailBeamNode_t *prev = NULL; trailBeamNode_t *next = NULL; vec3_t up; polyVert_t verts[ ( MAX_TRAIL_BEAM_NODES - 1 ) * 4 ]; int numVerts = 0; baseTrailBeam_t *btb; trailSystem_t *ts; baseTrailSystem_t *bts; if( !tb || !tb->nodes ) return; btb = tb->class; ts = tb->parent; bts = ts->class; if( bts->thirdPersonOnly && ( CG_AttachmentCentNum( &ts->frontAttachment ) == cg.snap->ps.clientNum || CG_AttachmentCentNum( &ts->backAttachment ) == cg.snap->ps.clientNum ) && !cg.renderingThirdPerson ) return; CG_CalculateBeamNodeProperties( tb ); i = tb->nodes; do { prev = i->prev; next = i->next; if( prev && next ) { //this node has two neighbours GetPerpendicularViewVector( cg.refdef.vieworg, next->position, prev->position, up ); } else if( !prev && next ) { //this is the front GetPerpendicularViewVector( cg.refdef.vieworg, next->position, i->position, up ); } else if( prev && !next ) { //this is the back GetPerpendicularViewVector( cg.refdef.vieworg, i->position, prev->position, up ); } else break; if( prev ) { VectorMA( i->position, i->halfWidth, up, verts[ numVerts ].xyz ); verts[ numVerts ].st[ 0 ] = i->textureCoord; verts[ numVerts ].st[ 1 ] = 1.0f; if( btb->realLight ) CG_LightVertex( verts[ numVerts ].xyz, i->alpha, verts[ numVerts ].modulate ); else { VectorCopy( i->color, verts[ numVerts ].modulate ); verts[ numVerts ].modulate[ 3 ] = i->alpha; } numVerts++; VectorMA( i->position, -i->halfWidth, up, verts[ numVerts ].xyz ); verts[ numVerts ].st[ 0 ] = i->textureCoord; verts[ numVerts ].st[ 1 ] = 0.0f; if( btb->realLight ) CG_LightVertex( verts[ numVerts ].xyz, i->alpha, verts[ numVerts ].modulate ); else { VectorCopy( i->color, verts[ numVerts ].modulate ); verts[ numVerts ].modulate[ 3 ] = i->alpha; } numVerts++; } if( next ) { VectorMA( i->position, -i->halfWidth, up, verts[ numVerts ].xyz ); verts[ numVerts ].st[ 0 ] = i->textureCoord; verts[ numVerts ].st[ 1 ] = 0.0f; if( btb->realLight ) CG_LightVertex( verts[ numVerts ].xyz, i->alpha, verts[ numVerts ].modulate ); else { VectorCopy( i->color, verts[ numVerts ].modulate ); verts[ numVerts ].modulate[ 3 ] = i->alpha; } numVerts++; VectorMA( i->position, i->halfWidth, up, verts[ numVerts ].xyz ); verts[ numVerts ].st[ 0 ] = i->textureCoord; verts[ numVerts ].st[ 1 ] = 1.0f; if( btb->realLight ) CG_LightVertex( verts[ numVerts ].xyz, i->alpha, verts[ numVerts ].modulate ); else { VectorCopy( i->color, verts[ numVerts ].modulate ); verts[ numVerts ].modulate[ 3 ] = i->alpha; } numVerts++; } i = i->next; } while( i ); trap_R_AddPolysToScene( tb->class->shader, 4, &verts[ 0 ], numVerts / 4 ); } /* =============== CG_AllocateBeamNode Allocates a trailBeamNode_t from a trailBeam_t's nodePool =============== */ static trailBeamNode_t *CG_AllocateBeamNode( trailBeam_t *tb ) { baseTrailBeam_t *btb = tb->class; int i; trailBeamNode_t *tbn; for( i = 0; i < MAX_TRAIL_BEAM_NODES; i++ ) { tbn = &tb->nodePool[ i ]; if( !tbn->used ) { tbn->timeLeft = btb->segmentTime; tbn->prev = NULL; tbn->next = NULL; tbn->used = qtrue; return tbn; } } // no space left return NULL; } /* =============== CG_DestroyBeamNode Removes a node from a beam Returns the new head =============== */ static trailBeamNode_t *CG_DestroyBeamNode( trailBeamNode_t *tbn ) { trailBeamNode_t *newHead = NULL; if( tbn->prev ) { if( tbn->next ) { // node is in the middle tbn->prev->next = tbn->next; tbn->next->prev = tbn->prev; } else // node is at the back tbn->prev->next = NULL; // find the new head (shouldn't have changed) newHead = tbn->prev; while( newHead->prev ) newHead = newHead->prev; } else if( tbn->next ) { //node is at the front tbn->next->prev = NULL; newHead = tbn->next; } tbn->prev = NULL; tbn->next = NULL; tbn->used = qfalse; return newHead; } /* =============== CG_FindLastBeamNode Returns the last beam node in a beam =============== */ static trailBeamNode_t *CG_FindLastBeamNode( trailBeam_t *tb ) { trailBeamNode_t *i = tb->nodes; while( i && i->next ) i = i->next; return i; } /* =============== CG_CountBeamNodes Returns the number of nodes in a beam =============== */ static int CG_CountBeamNodes( trailBeam_t *tb ) { trailBeamNode_t *i = tb->nodes; int numNodes = 0; while( i ) { numNodes++; i = i->next; } return numNodes; } /* =============== CG_PrependBeamNode Prepend a new beam node to the front of a beam Returns the new node =============== */ static trailBeamNode_t *CG_PrependBeamNode( trailBeam_t *tb ) { trailBeamNode_t *i; if( tb->nodes ) { // prepend another node i = CG_AllocateBeamNode( tb ); if( i ) { i->next = tb->nodes; tb->nodes->prev = i; tb->nodes = i; } } else //add first node { i = CG_AllocateBeamNode( tb ); if( i ) tb->nodes = i; } return i; } /* =============== CG_AppendBeamNode Append a new beam node to the back of a beam Returns the new node =============== */ static trailBeamNode_t *CG_AppendBeamNode( trailBeam_t *tb ) { trailBeamNode_t *last, *i; if( tb->nodes ) { // append another node last = CG_FindLastBeamNode( tb ); i = CG_AllocateBeamNode( tb ); if( i ) { last->next = i; i->prev = last; i->next = NULL; } } else //add first node { i = CG_AllocateBeamNode( tb ); if( i ) tb->nodes = i; } return i; } /* =============== CG_ApplyJitters =============== */ static void CG_ApplyJitters( trailBeam_t *tb ) { trailBeamNode_t *i = NULL; int j; baseTrailBeam_t *btb; trailSystem_t *ts; trailBeamNode_t *start; trailBeamNode_t *end; if( !tb || !tb->nodes ) return; btb = tb->class; ts = tb->parent; for( j = 0; j < btb->numJitters; j++ ) { if( tb->nextJitterTimes[ j ] <= cg.time ) { for( i = tb->nodes; i; i = i->next ) { i->jitters[ j ][ 0 ] = ( crandom( ) * btb->jitters[ j ].magnitude ); i->jitters[ j ][ 1 ] = ( crandom( ) * btb->jitters[ j ].magnitude ); } tb->nextJitterTimes[ j ] = cg.time + btb->jitters[ j ].period; } } start = tb->nodes; end = CG_FindLastBeamNode( tb ); if( !btb->jitterAttachments ) { if( CG_Attached( &ts->frontAttachment ) && start->next ) start = start->next; if( CG_Attached( &ts->backAttachment ) && end->prev ) end = end->prev; } for( i = start; i; i = i->next ) { vec3_t forward, right, up; trailBeamNode_t *prev; trailBeamNode_t *next; float upJitter = 0.0f, rightJitter = 0.0f; prev = i->prev; next = i->next; if( prev && next ) { //this node has two neighbours GetPerpendicularViewVector( cg.refdef.vieworg, next->position, prev->position, up ); VectorSubtract( next->position, prev->position, forward ); } else if( !prev && next ) { //this is the front GetPerpendicularViewVector( cg.refdef.vieworg, next->position, i->position, up ); VectorSubtract( next->position, i->position, forward ); } else if( prev && !next ) { //this is the back GetPerpendicularViewVector( cg.refdef.vieworg, i->position, prev->position, up ); VectorSubtract( i->position, prev->position, forward ); } VectorNormalize( forward ); CrossProduct( forward, up, right ); VectorNormalize( right ); for( j = 0; j < btb->numJitters; j++ ) { upJitter += i->jitters[ j ][ 0 ]; rightJitter += i->jitters[ j ][ 1 ]; } VectorMA( i->position, upJitter, up, i->position ); VectorMA( i->position, rightJitter, right, i->position ); if( i == end ) break; } } /* =============== CG_UpdateBeam Updates a beam =============== */ static void CG_UpdateBeam( trailBeam_t *tb ) { baseTrailBeam_t *btb; trailSystem_t *ts; trailBeamNode_t *i; int deltaTime; int nodesToAdd; int j; int numNodes; if( !tb ) return; btb = tb->class; ts = tb->parent; deltaTime = cg.time - tb->lastEvalTime; tb->lastEvalTime = cg.time; // first make sure this beam has enough nodes if( ts->destroyTime <= 0 || btb->fadeOutTime > 0 ) { nodesToAdd = btb->numSegments - CG_CountBeamNodes( tb ) + 1; while( nodesToAdd-- ) { i = CG_AppendBeamNode( tb ); if( !tb->nodes->next && CG_Attached( &ts->frontAttachment ) ) { // this is the first node to be added if( !CG_AttachmentPoint( &ts->frontAttachment, i->refPosition ) ) CG_DestroyTrailSystem( &ts ); } else VectorCopy( i->prev->refPosition, i->refPosition ); } } numNodes = CG_CountBeamNodes( tb ); for( i = tb->nodes; i; i = i->next ) VectorCopy( i->refPosition, i->position ); if( CG_Attached( &ts->frontAttachment ) && CG_Attached( &ts->backAttachment ) ) { // beam between two attachments vec3_t dir, front, back; if( ts->destroyTime > 0 && ( cg.time - ts->destroyTime ) >= btb->fadeOutTime ) { tb->valid = qfalse; return; } if( !CG_AttachmentPoint( &ts->frontAttachment, front ) ) CG_DestroyTrailSystem( &ts ); if( !CG_AttachmentPoint( &ts->backAttachment, back ) ) CG_DestroyTrailSystem( &ts ); VectorSubtract( back, front, dir ); for( j = 0, i = tb->nodes; i; i = i->next, j++ ) { float scale = (float)j / (float)( numNodes - 1 ); VectorMA( front, scale, dir, i->position ); } } else if( CG_Attached( &ts->frontAttachment ) ) { // beam from one attachment // cull the trail tail i = CG_FindLastBeamNode( tb ); if( i && i->timeLeft >= 0 ) { i->timeLeft -= deltaTime; if( i->timeLeft < 0 ) { tb->nodes = CG_DestroyBeamNode( i ); if( !tb->nodes ) { tb->valid = qfalse; return; } // if the ts has been destroyed, stop creating new nodes if( ts->destroyTime <= 0 ) CG_PrependBeamNode( tb ); } else if( i->timeLeft >= 0 && i->prev ) { vec3_t dir; float length; VectorSubtract( i->refPosition, i->prev->refPosition, dir ); length = VectorNormalize( dir ) * ( (float)i->timeLeft / (float)tb->class->segmentTime ); VectorMA( i->prev->refPosition, length, dir, i->position ); } } if( tb->nodes ) { if( !CG_AttachmentPoint( &ts->frontAttachment, tb->nodes->refPosition ) ) CG_DestroyTrailSystem( &ts ); VectorCopy( tb->nodes->refPosition, tb->nodes->position ); } } CG_ApplyJitters( tb ); } /* =============== CG_ParseTrailBeamColor =============== */ static qboolean CG_ParseTrailBeamColor( byte *c, char **text_p ) { char *token; int i; for( i = 0; i <= 2; i++ ) { token = COM_Parse( text_p ); if( !Q_stricmp( token, "" ) ) return qfalse; c[ i ] = (int)( (float)0xFF * atof_neg( token, qfalse ) ); } return qtrue; } /* =============== CG_ParseTrailBeam Parse a trail beam =============== */ static qboolean CG_ParseTrailBeam( baseTrailBeam_t *btb, char **text_p ) { char *token; // read optional parameters while( 1 ) { token = COM_Parse( text_p ); if( !Q_stricmp( token, "" ) ) return qfalse; if( !Q_stricmp( token, "segments" ) ) { token = COM_Parse( text_p ); if( !Q_stricmp( token, "" ) ) break; btb->numSegments = atoi_neg( token, qfalse ); if( btb->numSegments >= MAX_TRAIL_BEAM_NODES ) { btb->numSegments = MAX_TRAIL_BEAM_NODES - 1; CG_Printf( S_COLOR_YELLOW "WARNING: too many segments in trail beam\n" ); } continue; } else if( !Q_stricmp( token, "width" ) ) { token = COM_Parse( text_p ); if( !Q_stricmp( token, "" ) ) break; btb->frontWidth = atof_neg( token, qfalse ); token = COM_Parse( text_p ); if( !Q_stricmp( token, "" ) ) break; if( !Q_stricmp( token, "-" ) ) btb->backWidth = btb->frontWidth; else btb->backWidth = atof_neg( token, qfalse ); continue; } else if( !Q_stricmp( token, "alpha" ) ) { token = COM_Parse( text_p ); if( !Q_stricmp( token, "" ) ) break; btb->frontAlpha = atof_neg( token, qfalse ); token = COM_Parse( text_p ); if( !Q_stricmp( token, "" ) ) break; if( !Q_stricmp( token, "-" ) ) btb->backAlpha = btb->frontAlpha; else btb->backAlpha = atof_neg( token, qfalse ); continue; } else if( !Q_stricmp( token, "color" ) ) { token = COM_Parse( text_p ); if( !Q_stricmp( token, "" ) ) break; if( !Q_stricmp( token, "{" ) ) { if( !CG_ParseTrailBeamColor( btb->frontColor, text_p ) ) break; token = COM_Parse( text_p ); if( Q_stricmp( token, "}" ) ) { CG_Printf( S_COLOR_RED "ERROR: missing '}'\n" ); break; } token = COM_Parse( text_p ); if( !Q_stricmp( token, "" ) ) break; if( !Q_stricmp( token, "-" ) ) { btb->backColor[ 0 ] = btb->frontColor[ 0 ]; btb->backColor[ 1 ] = btb->frontColor[ 1 ]; btb->backColor[ 2 ] = btb->frontColor[ 2 ]; } else if( !Q_stricmp( token, "{" ) ) { if( !CG_ParseTrailBeamColor( btb->backColor, text_p ) ) break; token = COM_Parse( text_p ); if( Q_stricmp( token, "}" ) ) { CG_Printf( S_COLOR_RED "ERROR: missing '}'\n" ); break; } } else { CG_Printf( S_COLOR_RED "ERROR: missing '{'\n" ); break; } } else { CG_Printf( S_COLOR_RED "ERROR: missing '{'\n" ); break; } continue; } else if( !Q_stricmp( token, "segmentTime" ) ) { token = COM_Parse( text_p ); if( !Q_stricmp( token, "" ) ) break; btb->segmentTime = atoi_neg( token, qfalse ); continue; } else if( !Q_stricmp( token, "fadeOutTime" ) ) { token = COM_Parse( text_p ); if( !Q_stricmp( token, "" ) ) break; btb->fadeOutTime = atoi_neg( token, qfalse ); continue; } else if( !Q_stricmp( token, "shader" ) ) { token = COM_Parse( text_p ); if( !Q_stricmp( token, "" ) ) break; Q_strncpyz( btb->shaderName, token, MAX_QPATH ); continue; } else if( !Q_stricmp( token, "textureType" ) ) { token = COM_Parse( text_p ); if( !Q_stricmp( token, "" ) ) break; if( !Q_stricmp( token, "stretch" ) ) { btb->textureType = TBTT_STRETCH; token = COM_Parse( text_p ); if( !Q_stricmp( token, "" ) ) break; btb->frontTextureCoord = atof_neg( token, qfalse ); token = COM_Parse( text_p ); if( !Q_stricmp( token, "" ) ) break; btb->backTextureCoord = atof_neg( token, qfalse ); } else if( !Q_stricmp( token, "repeat" ) ) { btb->textureType = TBTT_REPEAT; token = COM_Parse( text_p ); if( !Q_stricmp( token, "" ) ) break; if( !Q_stricmp( token, "front" ) ) btb->clampToBack = qfalse; else if( !Q_stricmp( token, "back" ) ) btb->clampToBack = qtrue; else { CG_Printf( S_COLOR_RED "ERROR: unknown textureType clamp \"%s\"\n", token ); break; } token = COM_Parse( text_p ); if( !Q_stricmp( token, "" ) ) break; btb->repeatLength = atof_neg( token, qfalse ); } else { CG_Printf( S_COLOR_RED "ERROR: unknown textureType \"%s\"\n", token ); break; } continue; } else if( !Q_stricmp( token, "realLight" ) ) { btb->realLight = qtrue; continue; } else if( !Q_stricmp( token, "jitter" ) ) { if( btb->numJitters == MAX_TRAIL_BEAM_JITTERS ) { CG_Printf( S_COLOR_RED "ERROR: too many jitters\n", token ); break; } token = COM_Parse( text_p ); if( !Q_stricmp( token, "" ) ) break; btb->jitters[ btb->numJitters ].magnitude = atof_neg( token, qfalse ); token = COM_Parse( text_p ); if( !Q_stricmp( token, "" ) ) break; btb->jitters[ btb->numJitters ].period = atoi_neg( token, qfalse ); btb->numJitters++; continue; } else if( !Q_stricmp( token, "jitterAttachments" ) ) { btb->jitterAttachments = qtrue; continue; } else if( !Q_stricmp( token, "}" ) ) return qtrue; //reached the end of this trail beam else { CG_Printf( S_COLOR_RED "ERROR: unknown token '%s' in trail beam\n", token ); return qfalse; } } return qfalse; } /* =============== CG_InitialiseBaseTrailBeam =============== */ static void CG_InitialiseBaseTrailBeam( baseTrailBeam_t *btb ) { memset( btb, 0, sizeof( baseTrailBeam_t ) ); btb->numSegments = 1; btb->frontWidth = btb->backWidth = 1.0f; btb->frontAlpha = btb->backAlpha = 1.0f; memset( btb->frontColor, 0xFF, sizeof( btb->frontColor ) ); memset( btb->backColor, 0xFF, sizeof( btb->backColor ) ); btb->segmentTime = 100; btb->textureType = TBTT_STRETCH; btb->frontTextureCoord = 0.0f; btb->backTextureCoord = 1.0f; } /* =============== CG_ParseTrailSystem Parse a trail system section =============== */ static qboolean CG_ParseTrailSystem( baseTrailSystem_t *bts, char **text_p, const char *name ) { char *token; // read optional parameters while( 1 ) { token = COM_Parse( text_p ); if( !Q_stricmp( token, "" ) ) return qfalse; if( !Q_stricmp( token, "{" ) ) { CG_InitialiseBaseTrailBeam( &baseTrailBeams[ numBaseTrailBeams ] ); if( !CG_ParseTrailBeam( &baseTrailBeams[ numBaseTrailBeams ], text_p ) ) { CG_Printf( S_COLOR_RED "ERROR: failed to parse trail beam\n" ); return qfalse; } if( bts->numBeams == MAX_BEAMS_PER_SYSTEM ) { CG_Printf( S_COLOR_RED "ERROR: trail system has > %d beams\n", MAX_BEAMS_PER_SYSTEM ); return qfalse; } else if( numBaseTrailBeams == MAX_BASETRAIL_BEAMS ) { CG_Printf( S_COLOR_RED "ERROR: maximum number of trail beams (%d) reached\n", MAX_BASETRAIL_BEAMS ); return qfalse; } else { //start parsing beams again bts->beams[ bts->numBeams ] = &baseTrailBeams[ numBaseTrailBeams ]; bts->numBeams++; numBaseTrailBeams++; } continue; } else if( !Q_stricmp( token, "thirdPersonOnly" ) ) bts->thirdPersonOnly = qtrue; else if( !Q_stricmp( token, "lifeTime" ) ) { token = COM_Parse( text_p ); if( !Q_stricmp( token, "" ) ) break; bts->lifeTime = atoi_neg( token, qfalse ); continue; } else if( !Q_stricmp( token, "beam" ) ) //acceptable text continue; else if( !Q_stricmp( token, "}" ) ) { if( cg_debugTrails.integer >= 1 ) CG_Printf( "Parsed trail system %s\n", name ); return qtrue; //reached the end of this trail system } else { CG_Printf( S_COLOR_RED "ERROR: unknown token '%s' in trail system %s\n", token, bts->name ); return qfalse; } } return qfalse; } /* =============== CG_ParseTrailFile Load the trail systems from a trail file =============== */ static qboolean CG_ParseTrailFile( const char *fileName ) { char *text_p; int i; int len; char *token; char text[ 32000 ]; char tsName[ MAX_QPATH ]; qboolean tsNameSet = qfalse; fileHandle_t f; // load the file len = trap_FS_FOpenFile( fileName, &f, FS_READ ); if( len <= 0 ) return qfalse; if( len == 0 || len >= sizeof( text ) - 1 ) { trap_FS_FCloseFile( f ); CG_Printf( S_COLOR_RED "ERROR: trail file %s is %s\n", fileName, len == 0 ? "empty" : "too long" ); return qfalse; } trap_FS_Read( text, len, f ); text[ len ] = 0; trap_FS_FCloseFile( f ); // parse the text text_p = text; // read optional parameters while( 1 ) { token = COM_Parse( &text_p ); if( !Q_stricmp( token, "" ) ) break; if( !Q_stricmp( token, "{" ) ) { if( tsNameSet ) { //check for name space clashes for( i = 0; i < numBaseTrailSystems; i++ ) { if( !Q_stricmp( baseTrailSystems[ i ].name, tsName ) ) { CG_Printf( S_COLOR_RED "ERROR: a trail system is already named %s\n", tsName ); return qfalse; } } Q_strncpyz( baseTrailSystems[ numBaseTrailSystems ].name, tsName, MAX_QPATH ); if( !CG_ParseTrailSystem( &baseTrailSystems[ numBaseTrailSystems ], &text_p, tsName ) ) { CG_Printf( S_COLOR_RED "ERROR: %s: failed to parse trail system %s\n", fileName, tsName ); return qfalse; } //start parsing trail systems again tsNameSet = qfalse; if( numBaseTrailSystems == MAX_BASETRAIL_SYSTEMS ) { CG_Printf( S_COLOR_RED "ERROR: maximum number of trail systems (%d) reached\n", MAX_BASETRAIL_SYSTEMS ); return qfalse; } else numBaseTrailSystems++; continue; } else { CG_Printf( S_COLOR_RED "ERROR: unamed trail system\n" ); return qfalse; } } if( !tsNameSet ) { Q_strncpyz( tsName, token, sizeof( tsName ) ); tsNameSet = qtrue; } else { CG_Printf( S_COLOR_RED "ERROR: trail system already named\n" ); return qfalse; } } return qtrue; } /* =============== CG_LoadTrailSystems Load trail system templates =============== */ void CG_LoadTrailSystems( void ) { int i, numFiles, fileLen; char fileList[ MAX_TRAIL_FILES * MAX_QPATH ]; char fileName[ MAX_QPATH ]; char *filePtr; //clear out the old numBaseTrailSystems = 0; numBaseTrailBeams = 0; for( i = 0; i < MAX_BASETRAIL_SYSTEMS; i++ ) { baseTrailSystem_t *bts = &baseTrailSystems[ i ]; memset( bts, 0, sizeof( baseTrailSystem_t ) ); } for( i = 0; i < MAX_BASETRAIL_BEAMS; i++ ) { baseTrailBeam_t *btb = &baseTrailBeams[ i ]; memset( btb, 0, sizeof( baseTrailBeam_t ) ); } //and bring in the new numFiles = trap_FS_GetFileList( "scripts", ".trail", fileList, MAX_TRAIL_FILES * MAX_QPATH ); filePtr = fileList; for( i = 0; i < numFiles; i++, filePtr += fileLen + 1 ) { fileLen = strlen( filePtr ); strcpy( fileName, "scripts/" ); strcat( fileName, filePtr ); CG_Printf( "...loading '%s'\n", fileName ); CG_ParseTrailFile( fileName ); } } /* =============== CG_RegisterTrailSystem Load the media that a trail system needs =============== */ qhandle_t CG_RegisterTrailSystem( char *name ) { int i, j; baseTrailSystem_t *bts; baseTrailBeam_t *btb; for( i = 0; i < MAX_BASETRAIL_SYSTEMS; i++ ) { bts = &baseTrailSystems[ i ]; if( !Q_stricmp( bts->name, name ) ) { //already registered if( bts->registered ) return i + 1; for( j = 0; j < bts->numBeams; j++ ) { btb = bts->beams[ j ]; btb->shader = trap_R_RegisterShader( btb->shaderName ); } if( cg_debugTrails.integer >= 1 ) CG_Printf( "Registered trail system %s\n", name ); bts->registered = qtrue; //avoid returning 0 return i + 1; } } CG_Printf( S_COLOR_RED "ERROR: failed to register trail system %s\n", name ); return 0; } /* =============== CG_SpawnNewTrailBeam Allocate a new trail beam =============== */ static trailBeam_t *CG_SpawnNewTrailBeam( baseTrailBeam_t *btb, trailSystem_t *parent ) { int i; trailBeam_t *tb = NULL; trailSystem_t *ts = parent; for( i = 0; i < MAX_TRAIL_BEAMS; i++ ) { tb = &trailBeams[ i ]; if( !tb->valid ) { memset( tb, 0, sizeof( trailBeam_t ) ); //found a free slot tb->class = btb; tb->parent = ts; tb->valid = qtrue; if( cg_debugTrails.integer >= 1 ) CG_Printf( "TB %s created\n", ts->class->name ); 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; ts->birthTime = cg.time; for( j = 0; j < bts->numBeams; j++ ) CG_SpawnNewTrailBeam( bts->beams[ j ], ts ); if( cg_debugTrails.integer >= 1 ) CG_Printf( "TS %s created\n", bts->name ); 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 ); } // lifetime expired if( ts->destroyTime <= 0 && ts->class->lifeTime && ts->birthTime + ts->class->lifeTime < cg.time ) { trailSystem_t *tempTS = ts; CG_DestroyTrailSystem( &tempTS ); if( cg_debugTrails.integer >= 1 ) CG_Printf( "TS %s expired (born %d, lives %d, now %d)\n", ts->class->name, ts->birthTime, ts->class->lifeTime, cg.time ); } if( cg_debugTrails.integer >= 1 && !ts->valid ) CG_Printf( "TS %s garbage collected\n", ts->class->name ); } } /* =============== CG_AddTrails Add trails to the scene =============== */ void CG_AddTrails( void ) { int i; trailBeam_t *tb; int numTS = 0, numTB = 0; //remove expired trail systems CG_GarbageCollectTrailSystems( ); for( i = 0; i < MAX_TRAIL_BEAMS; i++ ) { tb = &trailBeams[ i ]; if( tb->valid ) { CG_UpdateBeam( tb ); CG_RenderBeam( tb ); } } if( cg_debugTrails.integer >= 2 ) { for( i = 0; i < MAX_TRAIL_SYSTEMS; i++ ) if( trailSystems[ i ].valid ) numTS++; for( i = 0; i < MAX_TRAIL_BEAMS; i++ ) if( trailBeams[ i ].valid ) numTB++; CG_Printf( "TS: %d TB: %d\n", numTS, numTB ); } } static trailSystem_t *testTS; static qhandle_t testTSHandle; /* =============== CG_DestroyTestTS_f Destroy the test a trail system =============== */ void CG_DestroyTestTS_f( void ) { if( CG_IsTrailSystemValid( &testTS ) ) CG_DestroyTrailSystem( &testTS ); } /* =============== CG_TestTS_f Test a trail system =============== */ void CG_TestTS_f( void ) { char tsName[ MAX_QPATH ]; if( trap_Argc( ) < 2 ) return; Q_strncpyz( tsName, CG_Argv( 1 ), MAX_QPATH ); testTSHandle = CG_RegisterTrailSystem( tsName ); if( testTSHandle ) { CG_DestroyTestTS_f( ); testTS = CG_SpawnNewTrailSystem( testTSHandle ); if( CG_IsTrailSystemValid( &testTS ) ) { CG_SetAttachmentCent( &testTS->frontAttachment, &cg_entities[ 0 ] ); CG_AttachToCent( &testTS->frontAttachment ); } } }