diff options
author | Tim Angus <tim@ngus.net> | 2001-01-03 22:43:20 +0000 |
---|---|---|
committer | Tim Angus <tim@ngus.net> | 2001-01-03 22:43:20 +0000 |
commit | 7cc6c9cbe613b7022ad3b5ae1a8a9bd7811e5e6a (patch) | |
tree | d244805f389a78ce6f3d78fe2ea89ecd7a4ef9bf /src | |
parent | 6f175bfee2d373b67a94261d26e3106d483099e8 (diff) |
1.27 upgrade
Diffstat (limited to 'src')
53 files changed, 43871 insertions, 0 deletions
diff --git a/src/cgame/cg_consolecmds.c b/src/cgame/cg_consolecmds.c new file mode 100644 index 00000000..ef5a79ee --- /dev/null +++ b/src/cgame/cg_consolecmds.c @@ -0,0 +1,307 @@ +// Copyright (C) 1999-2000 Id Software, Inc. +// +// cg_consolecmds.c -- text commands typed in at the local console, or +// executed by a key binding + +/* + * Portions Copyright (C) 2000-2001 Tim Angus + * + * This program 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, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ + +/* To assertain which portions are licensed under the GPL and which are + * licensed by Id Software, Inc. please run a diff between the equivalent + * versions of the "Tremulous" modification and the unmodified "Quake3" + * game source code. + */ + +#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]); +} + + +static void CG_ScoresDown_f( void ) { + if ( cg.scoresRequestTime + 2000 < cg.time ) { + // the scores are more than two seconds out of data, + // so request new ones + cg.scoresRequestTime = cg.time; + trap_SendClientCommand( "score" ); + + // leave the current scores up if they were already + // displayed, but if this is the first hit, clear them out + if ( !cg.showScores ) { + 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 ); +} + +static void CG_VoiceTellTarget_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, "vtell %i %s", clientNum, message ); + trap_SendClientCommand( command ); +} + +static void CG_VoiceTellAttacker_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, "vtell %i %s", clientNum, message ); + trap_SendClientCommand( command ); +} + +/* +================== +CG_StartOrbit_f +================== +*/ + +static void CG_StartOrbit_f( void ) { + if (cg_cameraOrbit.value != 0) { + trap_Cvar_Set ("cg_cameraOrbit", "0"); + trap_Cvar_Set("cg_thirdPerson", "0"); + } else { + trap_Cvar_Set("cg_cameraOrbit", "5"); + trap_Cvar_Set("cg_thirdPerson", "1"); + trap_Cvar_Set("cg_thirdPersonAngle", "0"); + trap_Cvar_Set("cg_thirdPersonRange", "100"); + } +} + + +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 }, + { "+zoom", CG_ZoomDown_f }, + { "-zoom", CG_ZoomUp_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 }, + { "vtell_target", CG_VoiceTellTarget_f }, + { "vtell_attacker", CG_VoiceTellAttacker_f }, + { "tcmd", CG_TargetCommand_f }, + { "startOrbit", CG_StartOrbit_f }, + { "loaddeferred", CG_LoadDeferredPlayers } +}; + + +/* +================= +CG_ConsoleCommand + +The string has been tokenized and can be retrieved with +Cmd_Argc() / Cmd_Argv() +================= +*/ +qboolean CG_ConsoleCommand( void ) { + const char *cmd; + int i; + + cmd = CG_Argv(0); + + 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 ("itemact"); + trap_AddCommand ("itemdeact"); + trap_AddCommand ("itemtoggle"); + trap_AddCommand ("destroy"); + trap_AddCommand ("torch"); + trap_AddCommand ("menu"); + trap_AddCommand ("defmenu"); + trap_AddCommand ("undefmenu"); + trap_AddCommand ("loaddefered"); // spelled wrong, but not changing for demo +} diff --git a/src/cgame/cg_draw.c b/src/cgame/cg_draw.c new file mode 100644 index 00000000..7dad03c3 --- /dev/null +++ b/src/cgame/cg_draw.c @@ -0,0 +1,2355 @@ +// Copyright (C) 1999-2000 Id Software, Inc. +// +// cg_draw.c -- draw all of the graphical elements during +// active (after loading) gameplay + +/* + * Portions Copyright (C) 2000-2001 Tim Angus + * + * This program 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, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ + +/* To assertain which portions are licensed under the GPL and which are + * licensed by Id Software, Inc. please run a diff between the equivalent + * versions of the "Tremulous" modification and the unmodified "Quake3" + * game source code. + */ + +#include "cg_local.h" + +int drawTeamOverlayModificationCount = -1; +int sortedTeamPlayers[TEAM_MAXOVERLAY]; +int numSortedTeamPlayers; + +char systemChat[256]; +char teamChat1[256]; +char teamChat2[256]; + +/* +============== +CG_DrawField + +Draws large numbers for status bar and powerups +============== +*/ +static void CG_DrawField (int x, int y, int width, int value) { + char num[16], *ptr; + int l; + int frame; + + if ( width < 1 ) { + return; + } + + // draw number string + if ( width > 5 ) { + width = 5; + } + + 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), "%i", value); + l = strlen(num); + if (l > width) + l = width; + x += 2 + CHAR_WIDTH*(width - l); + + ptr = num; + while (*ptr && l) + { + if (*ptr == '-') + frame = STAT_MINUS; + else + frame = *ptr -'0'; + + CG_DrawPic( x,y, CHAR_WIDTH, CHAR_HEIGHT, cgs.media.numberShaders[frame] ); + x += CHAR_WIDTH; + ptr++; + l--; + } +} + + +/* +================ +CG_Draw3DModel + +================ +*/ +void CG_Draw3DModel( float x, float y, float w, float h, qhandle_t model, qhandle_t skin, vec3_t origin, vec3_t angles ) { + refdef_t refdef; + refEntity_t ent; + + if ( !cg_draw3dIcons.integer || !cg_drawIcons.integer ) { + return; + } + + CG_AdjustFrom640( &x, &y, &w, &h ); + + memset( &refdef, 0, sizeof( refdef ) ); + + memset( &ent, 0, sizeof( ent ) ); + AnglesToAxis( angles, ent.axis ); + VectorCopy( origin, ent.origin ); + ent.hModel = model; + ent.customSkin = skin; + ent.renderfx = RF_NOSHADOW; // no stencil shadows + + refdef.rdflags = RDF_NOWORLDMODEL; + + AxisClear( refdef.viewaxis ); + + refdef.fov_x = 30; + refdef.fov_y = 30; + + refdef.x = x; + refdef.y = y; + refdef.width = w; + refdef.height = h; + + refdef.time = cg.time; + + trap_R_ClearScene(); + trap_R_AddRefEntityToScene( &ent ); + trap_R_RenderScene( &refdef ); +} + +/* +================ +CG_DrawHead + +Used for both the status bar and the scoreboard +================ +*/ +void CG_DrawHead( float x, float y, float w, float h, int clientNum, vec3_t headAngles ) { + clipHandle_t cm; + clientInfo_t *ci; + float len; + vec3_t origin; + vec3_t mins, maxs; + + ci = &cgs.clientinfo[ clientNum ]; + + if ( cg_draw3dIcons.integer ) { + cm = ci->headModel; + if ( !cm ) { + return; + } + + // offset the origin y and z to center the head + trap_R_ModelBounds( cm, mins, maxs ); + + origin[2] = -0.5 * ( mins[2] + maxs[2] ); + origin[1] = 0.5 * ( mins[1] + maxs[1] ); + + // calculate distance so the head nearly fills the box + // assume heads are taller than wide + len = 0.7 * ( maxs[2] - mins[2] ); + origin[0] = len / 0.268; // len / tan( fov/2 ) + + // allow per-model tweaking + VectorAdd( origin, ci->headOffset, origin ); + + CG_Draw3DModel( x, y, w, h, ci->headModel, ci->headSkin, origin, headAngles ); + } else if ( cg_drawIcons.integer ) { + CG_DrawPic( x, y, w, h, ci->modelIcon ); + } + + // if they are deferred, draw a cross out + if ( ci->deferred ) { + CG_DrawPic( x, y, w, h, cgs.media.deferShader ); + } +} + +/* +================ +CG_DrawFlagModel + +Used for both the status bar and the scoreboard +================ +*/ +void CG_DrawFlagModel( float x, float y, float w, float h, int team, qboolean force2D ) { + qhandle_t cm; + float len; + vec3_t origin, angles; + vec3_t mins, maxs; + qhandle_t handle; + + if ( !force2D && cg_draw3dIcons.integer ) { + + VectorClear( angles ); + + cm = cgs.media.redFlagModel; + + // offset the origin y and z to center the flag + trap_R_ModelBounds( cm, mins, maxs ); + + origin[2] = -0.5 * ( mins[2] + maxs[2] ); + origin[1] = 0.5 * ( mins[1] + maxs[1] ); + + // calculate distance so the flag nearly fills the box + // assume heads are taller than wide + len = 0.5 * ( maxs[2] - mins[2] ); + origin[0] = len / 0.268; // len / tan( fov/2 ) + + angles[YAW] = 60 * sin( cg.time / 2000.0 );; + + CG_Draw3DModel( x, y, w, h, + team == TEAM_HUMANS ? cgs.media.redFlagModel : cgs.media.blueFlagModel, + 0, origin, angles ); + } else if ( cg_drawIcons.integer ) { + gitem_t *item = BG_FindItemForPowerup( team == TEAM_HUMANS ? PW_REDFLAG : PW_BLUEFLAG ); + + CG_DrawPic( x, y, w, h, cg_items[ ITEM_INDEX(item) ].icon ); + } +} + +/* +================ +CG_DrawStatusBarHead + +================ +*/ +static void CG_DrawStatusBarHead( float x ) { + vec3_t angles; + float size, stretch; + float frac; + + VectorClear( angles ); + + if ( cg.damageTime && cg.time - cg.damageTime < DAMAGE_TIME ) { + frac = (float)(cg.time - cg.damageTime ) / DAMAGE_TIME; + size = ICON_SIZE * 1.25 * ( 1.5 - frac * 0.5 ); + + stretch = size - ICON_SIZE * 1.25; + // kick in the direction of damage + x -= stretch * 0.5 + cg.damageX * stretch * 0.5; + + cg.headStartYaw = 180 + cg.damageX * 45; + + cg.headEndYaw = 180 + 20 * cos( crandom()*M_PI ); + cg.headEndPitch = 5 * cos( crandom()*M_PI ); + + cg.headStartTime = cg.time; + cg.headEndTime = cg.time + 100 + random() * 2000; + } else { + if ( cg.time >= cg.headEndTime ) { + // select a new head angle + cg.headStartYaw = cg.headEndYaw; + cg.headStartPitch = cg.headEndPitch; + cg.headStartTime = cg.headEndTime; + cg.headEndTime = cg.time + 100 + random() * 2000; + + cg.headEndYaw = 180 + 20 * cos( crandom()*M_PI ); + cg.headEndPitch = 5 * cos( crandom()*M_PI ); + } + + size = ICON_SIZE * 1.25; + } + + // if the server was frozen for a while we may have a bad head start time + if ( cg.headStartTime > cg.time ) { + cg.headStartTime = cg.time; + } + + frac = ( cg.time - cg.headStartTime ) / (float)( cg.headEndTime - cg.headStartTime ); + frac = frac * frac * ( 3 - 2 * frac ); + angles[YAW] = cg.headStartYaw + ( cg.headEndYaw - cg.headStartYaw ) * frac; + angles[PITCH] = cg.headStartPitch + ( cg.headEndPitch - cg.headStartPitch ) * frac; + + CG_DrawHead( x, 480 - size, size, size, + cg.snap->ps.clientNum, angles ); +} + +/* +================ +CG_DrawStatusBarFlag + +================ +*/ +static void CG_DrawStatusBarFlag( float x, int team ) { + CG_DrawFlagModel( x, 480 - ICON_SIZE, ICON_SIZE, ICON_SIZE, team, qfalse ); +} + + +/* +================ +CG_DrawTeamBackground + +================ +*/ +void CG_DrawTeamBackground( int x, int y, int w, int h, float alpha, int team ) +{ + vec4_t hcolor; + + hcolor[3] = alpha; + if ( team == TEAM_HUMANS ) { + hcolor[0] = 1; + hcolor[1] = 0; + hcolor[2] = 0; + } else if ( team == TEAM_DROIDS ) { + hcolor[0] = 0; + hcolor[1] = 0; + hcolor[2] = 1; + } else { + return; + } + trap_R_SetColor( hcolor ); + CG_DrawPic( x, y, w, h, cgs.media.teamStatusBar ); + trap_R_SetColor( NULL ); +} + +/* +================ +CG_DrawLighting + +================ +*/ +static void CG_DrawLighting( void ) +{ + centity_t *cent; + byte lum; + static byte lastLum; + vec3_t point, direction; + + cent = &cg_entities[cg.snap->ps.clientNum]; + + VectorCopy( cent->lerpOrigin, point ); + //TA: when wall climbing the viewheight is not straight up + if( cg.predictedPlayerState.stats[ STAT_STATE ] & SS_WALLCLIMBING ) + VectorMA( point, 32, cg.predictedPlayerState.grapplePoint, point ); + else + point[ 2 ] += 32; + + AngleVectors( cg.predictedPlayerState.viewangles, direction, NULL, NULL ); + + lum = CG_LightFromDirection( point, direction ); + //CG_Printf( "%d\n", lum ); + if( abs( lastLum - lum ) > 4 ) + lastLum = lum; + + if( BG_activated( UP_NVG, cg.snap->ps.stats ) ) + CG_DrawPic( 0, 0, 640, 480, cgs.media.humanNV ); + + switch( cg.snap->ps.stats[ STAT_PCLASS ] ) + { + case PCL_D_BASE: + case PCL_D_BUILDER: + if( lastLum < 10 ) + CG_DrawPic( -4, -4, 648, 488, cgs.media.droidNav80 ); + else if( lastLum >= 10 && lastLum < 16 ) + CG_DrawPic( -4, -4, 648, 488, cgs.media.droidNav75 ); + else if( lastLum >= 16 && lastLum < 22 ) + CG_DrawPic( -4, -4, 648, 488, cgs.media.droidNav70 ); + else if( lastLum >= 22 && lastLum < 28 ) + CG_DrawPic( -4, -4, 648, 488, cgs.media.droidNav65 ); + else if( lastLum >= 28 && lastLum < 34 ) + CG_DrawPic( -4, -4, 648, 488, cgs.media.droidNav60 ); + else if( lastLum >= 34 && lastLum < 40 ) + CG_DrawPic( -4, -4, 648, 488, cgs.media.droidNav55 ); + else if( lastLum >= 40 && lastLum < 46 ) + CG_DrawPic( -4, -4, 648, 488, cgs.media.droidNav50 ); + else if( lastLum >= 46 && lastLum < 53 ) + CG_DrawPic( -4, -4, 648, 488, cgs.media.droidNav45 ); + else if( lastLum >= 53 && lastLum < 61 ) + CG_DrawPic( -4, -4, 648, 488, cgs.media.droidNav40 ); + else if( lastLum >= 61 && lastLum < 70 ) + CG_DrawPic( -4, -4, 648, 488, cgs.media.droidNav35 ); + else if( lastLum >= 70 && lastLum < 80 ) + CG_DrawPic( -4, -4, 648, 488, cgs.media.droidNav30 ); + else if( lastLum >= 80 && lastLum < 100 ) + CG_DrawPic( -4, -4, 648, 488, cgs.media.droidNav25 ); + else if( lastLum >= 100 && lastLum < 130 ) + CG_DrawPic( -4, -4, 648, 488, cgs.media.droidNav20 ); + else if( lastLum >= 130 && lastLum < 180 ) + CG_DrawPic( -4, -4, 648, 488, cgs.media.droidNav15 ); + else if( lastLum >= 180 ) + CG_DrawPic( -4, -4, 648, 488, cgs.media.droidNav10 ); + break; + } + + //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 ); + } +} + +/* +================ +CG_DrawStatusBar + +================ +*/ +static void CG_DrawStatusBar( void ) { + int color; + centity_t *cent; + playerState_t *ps; + int value; + int ammo, clips, maxclips; + vec4_t hcolor; + vec3_t angles; + vec3_t origin; + byte lum; + static byte lastLum; + static float colors[4][4] = { +// { 0.2, 1.0, 0.2, 1.0 } , { 1.0, 0.2, 0.2, 1.0 }, {0.5, 0.5, 0.5, 1} }; + { 0.3f, 0.4f, 0.3f, 1.0f } , // normal + { 1.0f, 0.2f, 0.2f, 1.0f }, // low health + {0.2f, 0.3f, 0.2f, 1.0f}, // weapon firing + { 1.0f, 1.0f, 1.0f, 1.0f } }; // health > 100 + + + if ( cg_drawStatus.integer == 0 ) { + return; + } + + // draw the team background + CG_DrawTeamBackground( 0, 420, 640, 60, 0.33f, cg.snap->ps.persistant[PERS_TEAM] ); + + cent = &cg_entities[cg.snap->ps.clientNum]; + ps = &cg.snap->ps; + + VectorClear( angles ); + + //TA: stop drawing all these silly 3d models on the hud. Saves space and is more realistic. + + // draw any 3D icons first, so the changes back to 2D are minimized + /*if ( cent->currentState.weapon && cg_weapons[ cent->currentState.weapon ].ammoModel ) { + origin[0] = 70; + origin[1] = 0; + origin[2] = 0; + angles[YAW] = 90 + 20 * sin( cg.time / 1000.0 ); + CG_Draw3DModel( CHAR_WIDTH*3 + TEXT_ICON_SPACE, 432, ICON_SIZE, ICON_SIZE, + cg_weapons[ cent->currentState.weapon ].ammoModel, 0, origin, angles ); + }*/ + + //CG_DrawStatusBarHead( 185 + CHAR_WIDTH*3 + TEXT_ICON_SPACE ); + + /*if (cg.predictedPlayerState.powerups[PW_REDFLAG]) + CG_DrawStatusBarFlag( 185 + CHAR_WIDTH*3 + TEXT_ICON_SPACE + ICON_SIZE, TEAM_HUMANS); + else if (cg.predictedPlayerState.powerups[PW_BLUEFLAG]) + CG_DrawStatusBarFlag( 185 + CHAR_WIDTH*3 + TEXT_ICON_SPACE + ICON_SIZE, TEAM_DROIDS);*/ + + /*if ( ps->stats[ STAT_ARMOR ] ) { + origin[0] = 90; + origin[1] = 0; + origin[2] = -10; + angles[YAW] = ( cg.time & 2047 ) * 360 / 2048.0; + CG_Draw3DModel( 370 + CHAR_WIDTH*3 + TEXT_ICON_SPACE, 432, ICON_SIZE, ICON_SIZE, + cgs.media.armorModel, 0, origin, angles ); + }*/ + + // + // ammo + // + if ( cent->currentState.weapon ) { + //TA: must mask off clips and maxClips + if( !BG_infiniteAmmo( cent->currentState.weapon ) ) + BG_unpackAmmoArray( cent->currentState.weapon, ps->ammo, ps->powerups, &ammo, &clips, &maxclips ); + else + ammo = -1; + + if ( ammo > -1 ) { + if ( cg.predictedPlayerState.weaponstate == WEAPON_FIRING + && cg.predictedPlayerState.weaponTime > 100 ) { + // draw as dark grey when reloading + color = 2; // dark grey + } else { + if ( ammo >= 0 ) { + color = 0; // green + } else { + color = 1; // red + } + } + trap_R_SetColor( colors[color] ); + + CG_DrawField( 85, 432, 3, ammo); + + if( maxclips ) + CG_DrawField( 20, 432, 1, clips ); + + trap_R_SetColor( NULL ); + + // if we didn't draw a 3D icon, draw a 2D icon for ammo + /*if ( !cg_draw3dIcons.integer && cg_drawIcons.integer ) { + qhandle_t icon; + + icon = cg_weapons[ cg.predictedPlayerState.weapon ].ammoIcon; + if ( icon ) { + CG_DrawPic( CHAR_WIDTH*3 + TEXT_ICON_SPACE, 432, ICON_SIZE, ICON_SIZE, icon ); + } + }*/ + } + } + + // + // stamina + // + #define STAM_HEIGHT 20 + #define STAM_WIDTH 10 + #define STAM_X 5 + #define STAM_Y 435 + if( ps->stats[ STAT_PTEAM ] == PTE_HUMANS ) + { + int stamina = ps->stats[ STAT_STAMINA ]; + int height = (int)( (float)stamina / ( 1000 / STAM_HEIGHT ) ); + vec4_t bcolor = { 0.5, 0.5, 0.5, 0.5 }; + + trap_R_SetColor( bcolor ); // white + CG_DrawPic( STAM_X, STAM_Y, STAM_WIDTH, STAM_HEIGHT * 2, cgs.media.whiteShader ); + + if( stamina > 0 ) + { + trap_R_SetColor( colors[0] ); // green + CG_DrawPic( STAM_X, STAM_Y + ( STAM_HEIGHT - height ), + STAM_WIDTH, height, cgs.media.whiteShader ); + } + + if( stamina < 0 ) + { + trap_R_SetColor( colors[1] ); // red + CG_DrawPic( STAM_X, STAM_Y + STAM_HEIGHT , STAM_WIDTH, -height, cgs.media.whiteShader ); + } + } + + // + // health+armor + // + if( ps->stats[ STAT_PTEAM ] == PTE_DROIDS ) + { + vec4_t fcolor = { 1, 0, 0, 0.5 }; //red half alpha + vec4_t tcolor = { 0.3, 0.8, 1, 1 }; //cyan no alpha + + value = (int)( (float)( (float)ps->stats[STAT_HEALTH] / ps->stats[STAT_MAX_HEALTH] ) * 100 ); + + CG_DrawFadePic( 20, 0, 30, 440, fcolor, tcolor, value, cgs.media.droidHealth ); + + value = (int)( (float)( (float)ps->stats[STAT_ARMOR] / ps->stats[STAT_MAX_HEALTH] ) * 100 ); + + if( value > 0 ) + CG_DrawFadePic( 580, 0, 30, 440, fcolor, tcolor, value, cgs.media.droidHealth ); + } + else + { + value = ps->stats[STAT_HEALTH]; + if ( value > 100 ) { + trap_R_SetColor( colors[0] ); // white + } else if (value > 25) { + trap_R_SetColor( colors[0] ); // green + } else if (value > 0) { + color = (cg.time >> 8) & 1; // flash + trap_R_SetColor( colors[color] ); + } else { + trap_R_SetColor( colors[1] ); // red + } + + // stretch the health up when taking damage + CG_DrawField ( 300, 432, 3, value); + CG_ColorForHealth( hcolor ); + trap_R_SetColor( hcolor ); + + value = ps->stats[STAT_ARMOR]; + if (value > 0 ) + { + trap_R_SetColor( colors[0] ); + CG_DrawField (541, 432, 3, value); + trap_R_SetColor( NULL ); + // if we didn't draw a 3D icon, draw a 2D icon for armor + /*if ( !cg_draw3dIcons.integer && cg_drawIcons.integer ) { + CG_DrawPic( 370 + CHAR_WIDTH*3 + TEXT_ICON_SPACE, 432, ICON_SIZE, ICON_SIZE, cgs.media.armorIcon ); + }*/ + } + } + +} + +/* +=========================================================================================== + + UPPER RIGHT CORNER + +=========================================================================================== +*/ + +/* +================ +CG_DrawAttacker + +================ +*/ +static float CG_DrawAttacker( float y ) { + int t; + float size; + vec3_t angles; + const char *info; + const char *name; + int clientNum; + + if ( cg.predictedPlayerState.stats[STAT_HEALTH] <= 0 ) { + return y; + } + + if ( !cg.attackerTime ) { + return y; + } + + clientNum = cg.predictedPlayerState.persistant[PERS_ATTACKER]; + if ( clientNum < 0 || clientNum >= MAX_CLIENTS || clientNum == cg.snap->ps.clientNum ) { + return y; + } + + t = cg.time - cg.attackerTime; + if ( t > ATTACKER_HEAD_TIME ) { + cg.attackerTime = 0; + return y; + } + + size = ICON_SIZE * 1.25; + + angles[PITCH] = 0; + angles[YAW] = 180; + angles[ROLL] = 0; + CG_DrawHead( 640 - size, y, size, size, clientNum, angles ); + + info = CG_ConfigString( CS_PLAYERS + clientNum ); + name = Info_ValueForKey( info, "n" ); + y += size; + CG_DrawBigString( 640 - ( Q_PrintStrlen( name ) * BIGCHAR_WIDTH), y, name, 0.5 ); + + return y + BIGCHAR_HEIGHT + 2; +} + +/* +================== +CG_DrawSnapshot +================== +*/ +static float CG_DrawSnapshot( float y ) { + char *s; + int w; + + s = va( "time:%i snap:%i cmd:%i", cg.snap->serverTime, + cg.latestSnapshotNum, cgs.serverCommandSequence ); + w = CG_DrawStrlen( s ) * BIGCHAR_WIDTH; + + CG_DrawBigString( 635 - w, y + 2, s, 1.0F); + + return y + BIGCHAR_HEIGHT + 4; +} + +/* +================== +CG_DrawFPS +================== +*/ +#define FPS_FRAMES 4 +static float CG_DrawFPS( float y ) { + char *s; + int w; + static int previousTimes[FPS_FRAMES]; + static int index; + int i, total; + int fps; + static int previous; + int t, frameTime; + + // 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( "%ifps", fps ); + w = CG_DrawStrlen( s ) * BIGCHAR_WIDTH; + + CG_DrawBigString( 635 - w, y + 2, s, 1.0F); + } + + return y + BIGCHAR_HEIGHT + 4; +} + +/* +================= +CG_DrawTimer +================= +*/ +static float CG_DrawTimer( float y ) { + char *s; + int w; + int mins, seconds, tens; + int msec; + + msec = cg.time - cgs.levelStartTime; + + seconds = msec / 1000; + mins = seconds / 60; + seconds -= mins * 60; + tens = seconds / 10; + seconds -= tens * 10; + + s = va( "%i:%i%i", mins, tens, seconds ); + w = CG_DrawStrlen( s ) * BIGCHAR_WIDTH; + + CG_DrawBigString( 635 - w, y + 2, s, 1.0F); + + return y + BIGCHAR_HEIGHT + 4; +} + + +/* +================= +CG_DrawTeamOverlay +================= +*/ + +static float CG_DrawTeamOverlay( float y, qboolean right, qboolean upper ) { + int x, w, h, xx; + int i, j, len; + const char *p; + vec4_t hcolor; + int pwidth, lwidth; + int plyrs; + char st[16]; + clientInfo_t *ci; + gitem_t *item; + int ret_y, count; + + if ( !cg_drawTeamOverlay.integer ) { + return y; + } + + if ( cg.snap->ps.persistant[PERS_TEAM] != TEAM_HUMANS && + cg.snap->ps.persistant[PERS_TEAM] != TEAM_DROIDS ) { + return y; // Not on any team + } + + plyrs = 0; + + // max player name width + pwidth = 0; + count = (numSortedTeamPlayers > 8) ? 8 : numSortedTeamPlayers; + for (i = 0; i < count; i++) { + ci = cgs.clientinfo + sortedTeamPlayers[i]; + if ( ci->infoValid && ci->team == cg.snap->ps.persistant[PERS_TEAM]) { + plyrs++; + len = CG_DrawStrlen(ci->name); + if (len > pwidth) + pwidth = len; + } + } + + if (!plyrs) + return y; + + if (pwidth > TEAM_OVERLAY_MAXNAME_WIDTH) + pwidth = TEAM_OVERLAY_MAXNAME_WIDTH; + + // max location name width + lwidth = 0; + for (i = 1; i < MAX_LOCATIONS; i++) { + p = CG_ConfigString(CS_LOCATIONS + i); + if (p && *p) { + len = CG_DrawStrlen(p); + if (len > lwidth) + lwidth = len; + } + } + + if (lwidth > TEAM_OVERLAY_MAXLOCATION_WIDTH) + lwidth = TEAM_OVERLAY_MAXLOCATION_WIDTH; + + w = (pwidth + lwidth + 4 + 7) * TINYCHAR_WIDTH; + + if ( right ) + x = 640 - w; + else + x = 0; + + h = plyrs * TINYCHAR_HEIGHT; + + if ( upper ) { + ret_y = y + h; + } else { + y -= h; + ret_y = y; + } + + if ( cg.snap->ps.persistant[PERS_TEAM] == TEAM_HUMANS ) { + hcolor[0] = 1; + hcolor[1] = 0; + hcolor[2] = 0; + hcolor[3] = 0.33f; + } else { // if ( cg.snap->ps.persistant[PERS_TEAM] == TEAM_DROIDS ) + hcolor[0] = 0; + hcolor[1] = 0; + hcolor[2] = 1; + hcolor[3] = 0.33f; + } + trap_R_SetColor( hcolor ); + CG_DrawPic( x, y, w, h, cgs.media.teamStatusBar ); + trap_R_SetColor( NULL ); + + for (i = 0; i < count; i++) { + ci = cgs.clientinfo + sortedTeamPlayers[i]; + if ( ci->infoValid && ci->team == cg.snap->ps.persistant[PERS_TEAM]) { + + hcolor[0] = hcolor[1] = hcolor[2] = hcolor[3] = 1.0; + + xx = x + TINYCHAR_WIDTH; + + CG_DrawStringExt( xx, y, + ci->name, hcolor, qfalse, qfalse, + TINYCHAR_WIDTH, TINYCHAR_HEIGHT, TEAM_OVERLAY_MAXNAME_WIDTH); + + if (lwidth) { + p = CG_ConfigString(CS_LOCATIONS + ci->location); + if (!p || !*p) + p = "unknown"; + len = CG_DrawStrlen(p); + if (len > lwidth) + len = lwidth; + +// xx = x + TINYCHAR_WIDTH * 2 + TINYCHAR_WIDTH * pwidth + +// ((lwidth/2 - len/2) * TINYCHAR_WIDTH); + xx = x + TINYCHAR_WIDTH * 2 + TINYCHAR_WIDTH * pwidth; + CG_DrawStringExt( xx, y, + p, hcolor, qfalse, qfalse, TINYCHAR_WIDTH, TINYCHAR_HEIGHT, + TEAM_OVERLAY_MAXLOCATION_WIDTH); + } + + CG_GetColorForHealth( ci->health, ci->armor, hcolor ); + + Com_sprintf (st, sizeof(st), "%3i %3i", ci->health, ci->armor); + + xx = x + TINYCHAR_WIDTH * 3 + + TINYCHAR_WIDTH * pwidth + TINYCHAR_WIDTH * lwidth; + + CG_DrawStringExt( xx, y, + st, hcolor, qfalse, qfalse, + TINYCHAR_WIDTH, TINYCHAR_HEIGHT, 0 ); + + // draw weapon icon + xx += TINYCHAR_WIDTH * 3; + + if ( cg_weapons[ci->curWeapon].weaponIcon ) { + CG_DrawPic( xx, y, TINYCHAR_WIDTH, TINYCHAR_HEIGHT, + cg_weapons[ci->curWeapon].weaponIcon ); + } else { + CG_DrawPic( xx, y, TINYCHAR_WIDTH, TINYCHAR_HEIGHT, + cgs.media.deferShader ); + } + + // Draw powerup icons + if (right) { + xx = x; + } else { + xx = x + w - TINYCHAR_WIDTH; + } + /*for (j = 0; j < PW_NUM_POWERUPS; j++) { + if (ci->powerups & (1 << j)) { + gitem_t *item; + + item = BG_FindItemForPowerup( j ); + + CG_DrawPic( xx, y, TINYCHAR_WIDTH, TINYCHAR_HEIGHT, + trap_R_RegisterShader( item->icon ) ); + if (right) { + xx -= TINYCHAR_WIDTH; + } else { + xx += TINYCHAR_WIDTH; + } + } + }*/ + + y += TINYCHAR_HEIGHT; + } + } + + return ret_y; +} + + +/* +===================== +CG_DrawUpperRight + +===================== +*/ +static void CG_DrawUpperRight( void ) { + float y; + + y = 0; + + if ( cgs.gametype >= GT_TEAM && cg_drawTeamOverlay.integer == 1 ) { + y = CG_DrawTeamOverlay( y, qtrue, qtrue ); + } + if ( cg_drawSnapshot.integer ) { + y = CG_DrawSnapshot( y ); + } + if ( cg_drawFPS.integer ) { + y = CG_DrawFPS( y ); + } + if ( cg_drawTimer.integer ) { + y = CG_DrawTimer( y ); + } + if ( cg_drawAttacker.integer ) { + y = CG_DrawAttacker( y ); + } + +} + +/* +=========================================================================================== + + LOWER RIGHT CORNER + +=========================================================================================== +*/ + + +/* +================= +CG_DrawPoints + +Draw the small two score display +================= +*/ +static float CG_DrawPoints( float y ) +{ + const char *s; + int points, totalpoints, buildpoints; + int team; + int x, w; + float y1; + qboolean spectator; + + y -= BIGCHAR_HEIGHT + 8; + + y1 = y; + + + x = 640; + points = cg.snap->ps.persistant[PERS_POINTS]; + totalpoints = cg.snap->ps.persistant[PERS_TOTALPOINTS]; + + team = cg.snap->ps.stats[ STAT_PTEAM ]; + + if( team == PTE_DROIDS ) + buildpoints = cgs.aBuildPoints; + else if( team == PTE_HUMANS ) + buildpoints = cgs.hBuildPoints; + + spectator = ( team == PTE_NONE ); + + if( !spectator ) + { + s = va( "%2i", points ); + w = CG_DrawStrlen( s ) * BIGCHAR_WIDTH + 8; + x -= w; + + CG_DrawBigString( x + 2, y, s, 1.0F ); + + s = va( "%2i", totalpoints ); + w = CG_DrawStrlen( s ) * BIGCHAR_WIDTH + 8; + x -= w; + + CG_DrawBigString( x + 2, y, s, 1.0F ); + + s = va( "%2i", buildpoints ); + w = CG_DrawStrlen( s ) * BIGCHAR_WIDTH + 8; + x -= w; + + CG_DrawBigString( x + 2, y, s, 1.0F ); + } + + return y1 - 8; +} + + +/* +================= +CG_DrawScores + +Draw the small two score display +================= +*/ +static float CG_DrawScores( float y ) { + const char *s; + int s1, s2, score; + int x, w; + int v; + vec4_t color; + float y1; + gitem_t *item; + + s1 = cgs.scores1; + s2 = cgs.scores2; + + y -= BIGCHAR_HEIGHT + 8; + + y1 = y; + + // draw from the right side to left + if ( cgs.gametype >= GT_TEAM ) { + x = 640; + + color[0] = 0; + color[1] = 0; + color[2] = 1; + color[3] = 0.33f; + s = va( "%2i", s2 ); + w = CG_DrawStrlen( s ) * BIGCHAR_WIDTH + 8; + x -= w; + CG_FillRect( x, y-4, w, BIGCHAR_HEIGHT+8, color ); + if ( cg.snap->ps.persistant[PERS_TEAM] == TEAM_DROIDS ) { + CG_DrawPic( x, y-4, w, BIGCHAR_HEIGHT+8, cgs.media.selectShader ); + } + CG_DrawBigString( x + 4, y, s, 1.0F); + + if ( cgs.gametype == GT_CTF ) { + // Display flag status + item = BG_FindItemForPowerup( PW_BLUEFLAG ); + + if (item) { + y1 = y - BIGCHAR_HEIGHT - 8; + if( cgs.blueflag >= 0 && cgs.blueflag <= 2 ) { + CG_DrawPic( x, y1-4, w, BIGCHAR_HEIGHT+8, cgs.media.blueFlagShader[cgs.blueflag] ); + } + } + } + + color[0] = 1; + color[1] = 0; + color[2] = 0; + color[3] = 0.33f; + s = va( "%2i", s1 ); + w = CG_DrawStrlen( s ) * BIGCHAR_WIDTH + 8; + x -= w; + CG_FillRect( x, y-4, w, BIGCHAR_HEIGHT+8, color ); + if ( cg.snap->ps.persistant[PERS_TEAM] == TEAM_HUMANS ) { + CG_DrawPic( x, y-4, w, BIGCHAR_HEIGHT+8, cgs.media.selectShader ); + } + CG_DrawBigString( x + 4, y, s, 1.0F); + + if ( cgs.gametype == GT_CTF ) { + // Display flag status + item = BG_FindItemForPowerup( PW_REDFLAG ); + + if (item) { + y1 = y - BIGCHAR_HEIGHT - 8; + if( cgs.redflag >= 0 && cgs.redflag <= 2 ) { + CG_DrawPic( x, y1-4, w, BIGCHAR_HEIGHT+8, cgs.media.redFlagShader[cgs.redflag] ); + } + } + } + + + if ( cgs.gametype >= GT_CTF ) { + v = cgs.capturelimit; + } else { + v = cgs.fraglimit; + } + if ( v ) { + s = va( "%2i", v ); + w = CG_DrawStrlen( s ) * BIGCHAR_WIDTH + 8; + x -= w; + CG_DrawBigString( x + 4, y, s, 1.0F); + } + + } else { + qboolean spectator; + + x = 640; + score = cg.snap->ps.persistant[PERS_SCORE]; + spectator = ( cg.snap->ps.persistant[PERS_TEAM] == TEAM_SPECTATOR ); + + // always show your score in the second box if not in first place + if ( s1 != score ) { + s2 = score; + } + if ( s2 != SCORE_NOT_PRESENT ) { + s = va( "%2i", s2 ); + w = CG_DrawStrlen( s ) * BIGCHAR_WIDTH + 8; + x -= w; + if ( !spectator && score == s2 && score != s1 ) { + color[0] = 1; + color[1] = 0; + color[2] = 0; + color[3] = 0.33f; + CG_FillRect( x, y-4, w, BIGCHAR_HEIGHT+8, color ); + CG_DrawPic( x, y-4, w, BIGCHAR_HEIGHT+8, cgs.media.selectShader ); + } else { + color[0] = 0.5f; + color[1] = 0.5f; + color[2] = 0.5f; + color[3] = 0.33f; + CG_FillRect( x, y-4, w, BIGCHAR_HEIGHT+8, color ); + } + CG_DrawBigString( x + 4, y, s, 1.0F); + } + + // first place + if ( s1 != SCORE_NOT_PRESENT ) { + s = va( "%2i", s1 ); + w = CG_DrawStrlen( s ) * BIGCHAR_WIDTH + 8; + x -= w; + if ( !spectator && score == s1 ) { + color[0] = 0; + color[1] = 0; + color[2] = 1; + color[3] = 0.33f; + CG_FillRect( x, y-4, w, BIGCHAR_HEIGHT+8, color ); + CG_DrawPic( x, y-4, w, BIGCHAR_HEIGHT+8, cgs.media.selectShader ); + } else { + color[0] = 0.5f; + color[1] = 0.5f; + color[2] = 0.5f; + color[3] = 0.33f; + CG_FillRect( x, y-4, w, BIGCHAR_HEIGHT+8, color ); + } + CG_DrawBigString( x + 4, y, s, 1.0F); + } + + if ( cgs.fraglimit ) { + s = va( "%2i", cgs.fraglimit ); + w = CG_DrawStrlen( s ) * BIGCHAR_WIDTH + 8; + x -= w; + CG_DrawBigString( x + 4, y, s, 1.0F); + } + + } + + return y1 - 8; +} + +/* +================ +CG_DrawPowerups +================ +*/ +static float CG_DrawPowerups( float y ) { + int sorted[MAX_POWERUPS]; + int sortedTime[MAX_POWERUPS]; + int i, j, k; + int active; + playerState_t *ps; + int t; + gitem_t *item; + int x; + int color; + float size; + float f; + static float colors[2][4] = { + { 0.2f, 1.0f, 0.2f, 1.0f } , { 1.0f, 0.2f, 0.2f, 1.0f } }; + + ps = &cg.snap->ps; + + if ( ps->stats[STAT_HEALTH] <= 0 ) { + return y; + } + + // sort the list by time remaining + active = 0; + for ( i = 0 ; i < MAX_POWERUPS ; i++ ) { + if ( !ps->powerups[ i ] ) { + continue; + } + t = ps->powerups[ i ] - cg.time; + // ZOID--don't draw if the power up has unlimited time (999 seconds) + // This is true of the CTF flags + if ( t < 0 || t > 999000) { + continue; + } + + // insert into the list + for ( j = 0 ; j < active ; j++ ) { + if ( sortedTime[j] >= t ) { + for ( k = active - 1 ; k >= j ; k-- ) { + sorted[k+1] = sorted[k]; + sortedTime[k+1] = sortedTime[k]; + } + break; + } + } + sorted[j] = i; + sortedTime[j] = t; + active++; + } + + // draw the icons and timers + x = 640 - ICON_SIZE - CHAR_WIDTH * 2; + for ( i = 0 ; i < active ; i++ ) { + item = BG_FindItemForPowerup( sorted[i] ); + + if (item) { + + color = 1; + + y -= ICON_SIZE; + + trap_R_SetColor( colors[color] ); + CG_DrawField( x, y, 2, sortedTime[ i ] / 1000 ); + + t = ps->powerups[ sorted[i] ]; + if ( t - cg.time >= POWERUP_BLINKS * POWERUP_BLINK_TIME ) { + trap_R_SetColor( NULL ); + } else { + vec4_t modulate; + + f = (float)( t - cg.time ) / POWERUP_BLINK_TIME; + f -= (int)f; + modulate[0] = modulate[1] = modulate[2] = modulate[3] = f; + trap_R_SetColor( modulate ); + } + + if ( cg.powerupActive == sorted[i] && + cg.time - cg.powerupTime < PULSE_TIME ) { + f = 1.0 - ( ( (float)cg.time - cg.powerupTime ) / PULSE_TIME ); + size = ICON_SIZE * ( 1.0 + ( PULSE_SCALE - 1.0 ) * f ); + } else { + size = ICON_SIZE; + } + + CG_DrawPic( 640 - size, y + ICON_SIZE / 2 - size / 2, + size, size, trap_R_RegisterShader( item->icon ) ); + } + } + trap_R_SetColor( NULL ); + + return y; +} + + +/* +===================== +CG_DrawLowerRight + +===================== +*/ +static void CG_DrawLowerRight( void ) { + float y; + + y = 480 - ICON_SIZE; + + if ( cgs.gametype >= GT_TEAM && cg_drawTeamOverlay.integer == 2 ) { + y = CG_DrawTeamOverlay( y, qtrue, qfalse ); + } + + //y = CG_DrawScores( y ); + y = CG_DrawPoints( y ); + y = CG_DrawPowerups( y ); +} + +/* +=================== +CG_DrawPickupItem +=================== +*/ +static int CG_DrawPickupItem( int y ) { + int value; + float *fadeColor; + + if ( cg.snap->ps.stats[STAT_HEALTH] <= 0 ) { + return y; + } + + y -= ICON_SIZE; + + value = cg.itemPickup; + if ( value ) { + fadeColor = CG_FadeColor( cg.itemPickupTime, 3000 ); + if ( fadeColor ) { + CG_RegisterItemVisuals( value ); + trap_R_SetColor( fadeColor ); + CG_DrawPic( 8, y, ICON_SIZE, ICON_SIZE, cg_items[ value ].icon ); + CG_DrawBigString( ICON_SIZE + 16, y + (ICON_SIZE/2 - BIGCHAR_HEIGHT/2), bg_itemlist[ value ].pickup_name, fadeColor[0] ); + trap_R_SetColor( NULL ); + } + } + + return y; +} + +/* +===================== +CG_DrawLowerLeft + +===================== +*/ +static void CG_DrawLowerLeft( void ) { + float y; + + y = 480 - ICON_SIZE; + + if ( cgs.gametype >= GT_TEAM && cg_drawTeamOverlay.integer == 3 ) { + y = CG_DrawTeamOverlay( y, qfalse, qfalse ); + } + + + y = CG_DrawPickupItem( y ); +} + + + +//=========================================================================================== + +/* +================= +CG_DrawTeamInfo +================= +*/ +static void CG_DrawTeamInfo( void ) { + int w, h; + int i, len; + vec4_t hcolor; + int chatHeight; + +#define CHATLOC_Y 420 // bottom end +#define CHATLOC_X 0 + + if (cg_teamChatHeight.integer < TEAMCHAT_HEIGHT) + chatHeight = cg_teamChatHeight.integer; + else + chatHeight = TEAMCHAT_HEIGHT; + if (chatHeight <= 0) + return; // disabled + + if (cgs.teamLastChatPos != cgs.teamChatPos) { + if (cg.time - cgs.teamChatMsgTimes[cgs.teamLastChatPos % chatHeight] > cg_teamChatTime.integer) { + cgs.teamLastChatPos++; + } + + h = (cgs.teamChatPos - cgs.teamLastChatPos) * TINYCHAR_HEIGHT; + + w = 0; + + for (i = cgs.teamLastChatPos; i < cgs.teamChatPos; i++) { + len = CG_DrawStrlen(cgs.teamChatMsgs[i % chatHeight]); + if (len > w) + w = len; + } + w *= TINYCHAR_WIDTH; + w += TINYCHAR_WIDTH * 2; + + if ( cg.snap->ps.persistant[PERS_TEAM] == TEAM_HUMANS ) { + hcolor[0] = 1; + hcolor[1] = 0; + hcolor[2] = 0; + hcolor[3] = 0.33f; + } else if ( cg.snap->ps.persistant[PERS_TEAM] == TEAM_DROIDS ) { + hcolor[0] = 0; + hcolor[1] = 0; + hcolor[2] = 1; + hcolor[3] = 0.33f; + } else { + hcolor[0] = 0; + hcolor[1] = 1; + hcolor[2] = 0; + hcolor[3] = 0.33f; + } + + trap_R_SetColor( hcolor ); + CG_DrawPic( CHATLOC_X, CHATLOC_Y - h, 640, h, cgs.media.teamStatusBar ); + trap_R_SetColor( NULL ); + + hcolor[0] = hcolor[1] = hcolor[2] = 1.0; + hcolor[3] = 1.0; + + for (i = cgs.teamChatPos - 1; i >= cgs.teamLastChatPos; i--) { + CG_DrawStringExt( CHATLOC_X + TINYCHAR_WIDTH, + CHATLOC_Y - (cgs.teamChatPos - i)*TINYCHAR_HEIGHT, + cgs.teamChatMsgs[i % chatHeight], hcolor, qfalse, qfalse, + TINYCHAR_WIDTH, TINYCHAR_HEIGHT, 0 ); + } + } +} + +/* +=================== +CG_DrawHoldableItem +=================== +*/ +static void CG_DrawHoldableItem( void ) { + int value; + + //TA: not using q3 holdable item code + /*value = cg.snap->ps.stats[STAT_HOLDABLE_ITEM]; + if ( value ) { + CG_RegisterItemVisuals( value ); + CG_DrawPic( 640-ICON_SIZE, (SCREEN_HEIGHT-ICON_SIZE)/2, ICON_SIZE, ICON_SIZE, cg_items[ value ].icon ); + }*/ + +} + + +/* +=================== +CG_DrawReward +=================== +*/ +static void CG_DrawReward( void ) { + float *color; + int i, count; + float x, y; + char buf[32]; + + if ( !cg_drawRewards.integer ) { + return; + } + color = CG_FadeColor( cg.rewardTime, REWARD_TIME ); + if ( !color ) { + if (cg.rewardStack > 0) { + for(i = 0; i < cg.rewardStack; i++) { + cg.rewardSound[i] = cg.rewardSound[i+1]; + cg.rewardShader[i] = cg.rewardShader[i+1]; + cg.rewardCount[i] = cg.rewardCount[i+1]; + } + cg.rewardTime = cg.time; + cg.rewardStack--; + color = CG_FadeColor( cg.rewardTime, REWARD_TIME ); + trap_S_StartLocalSound(cg.rewardSound[0], CHAN_ANNOUNCER); + } else { + return; + } + } + + trap_R_SetColor( color ); + + if ( cg.rewardCount[0] >= 10 ) { + y = 56; + x = 320 - ICON_SIZE/2; + CG_DrawPic( x, y, ICON_SIZE-4, ICON_SIZE-4, cg.rewardShader[0] ); + Com_sprintf(buf, sizeof(buf), "%d", cg.rewardCount[0]); + x = ( SCREEN_WIDTH - SMALLCHAR_WIDTH * CG_DrawStrlen( buf ) ) / 2; + CG_DrawStringExt( x, y+ICON_SIZE, buf, color, qfalse, qtrue, + SMALLCHAR_WIDTH, SMALLCHAR_HEIGHT, 0 ); + } + else { + + count = cg.rewardCount[0]; + + y = 56; + x = 320 - count * ICON_SIZE/2; + for ( i = 0 ; i < count ; i++ ) { + CG_DrawPic( x, y, ICON_SIZE-4, ICON_SIZE-4, cg.rewardShader[0] ); + x += ICON_SIZE; + } + } + trap_R_SetColor( NULL ); +} + + +/* +=============================================================================== + +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. +============== +*/ +void CG_AddLagometerSnapshotInfo( snapshot_t *snap ) { + // 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_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; + + // draw the phone jack if we are completely past our buffers + cmdNum = trap_GetCurrentCmdNumber() - CMD_BACKUP + 1; + trap_GetUserCmd( cmdNum, &cmd ); + if ( cmd.serverTime <= cg.snap->ps.commandTime + || cmd.serverTime > cg.time ) { // special check for map_restart + return; + } + + // also add text in center of screen + s = "Connection Interrupted"; + w = CG_DrawStrlen( s ) * BIGCHAR_WIDTH; + CG_DrawBigString( 320 - w/2, 100, s, 1.0F); + + // 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( void ) { + int a, x, y, i; + float v; + float ax, ay, aw, ah, mid, range; + int color; + float vscale; + + if ( !cg_lagometer.integer || cgs.localServer ) { + CG_DrawDisconnect(); + return; + } + + // + // draw the graph + // + x = 640 - 48; + y = 480 - 48; + + trap_R_SetColor( NULL ); + CG_DrawPic( x, y, 48, 48, cgs.media.lagometerShader ); + + ax = x; + ay = y; + aw = 48; + ah = 48; + 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_DrawBigString( ax, ay, "snc", 1.0 ); + } + + CG_DrawDisconnect(); +} + + + +/* +=============================================================================== + +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; + 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.centerPrintCharWidth * CG_DrawStrlen( linebuffer ); + + x = ( SCREEN_WIDTH - w ) / 2; + + CG_DrawStringExt( x, y, linebuffer, color, qfalse, qtrue, + cg.centerPrintCharWidth, (int)(cg.centerPrintCharWidth * 1.5), 0 ); + + y += cg.centerPrintCharWidth * 1.5; + + while ( *start && ( *start != '\n' ) ) { + start++; + } + if ( !*start ) { + break; + } + start++; + } + + trap_R_SetColor( NULL ); +} + + + +/* +================================================================================ + +CROSSHAIR + +================================================================================ +*/ + + +/* +================= +CG_DrawCrosshair +================= +*/ +static void CG_DrawCrosshair(void) { + float w, h; + qhandle_t hShader; + float f; + float x, y; + int ca; + + if ( !cg_drawCrosshair.integer ) { + return; + } + + if ( cg.snap->ps.persistant[PERS_TEAM] == TEAM_SPECTATOR) { + return; + } + + if ( cg.renderingThirdPerson ) { + return; + } + + // set color based on health + if ( cg_crosshairHealth.integer ) { + vec4_t hcolor; + + CG_ColorForHealth( hcolor ); + trap_R_SetColor( hcolor ); + } else { + trap_R_SetColor( NULL ); + } + + w = h = cg_crosshairSize.value; + + // pulse the size of the crosshair when picking up items + f = cg.time - cg.itemPickupBlendTime; + if ( f > 0 && f < ITEM_BLOB_TIME ) { + f /= ITEM_BLOB_TIME; + w *= ( 1 + f ); + h *= ( 1 + f ); + } + + x = cg_crosshairX.integer; + y = cg_crosshairY.integer; + CG_AdjustFrom640( &x, &y, &w, &h ); + + ca = cg_drawCrosshair.integer; + if (ca < 0) { + ca = 0; + } + hShader = cgs.media.crosshairShader[ ca % NUM_CROSSHAIRS ]; + + 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; + + 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; + } + + // if the player is invisible, don't show it + /*if ( cg_entities[ trace.entityNum ].currentState.powerups & ( 1 << PW_INVIS ) ) { + return; + }*/ + + // update the fade timer + cg.crosshairClientNum = trace.entityNum; + cg.crosshairClientTime = cg.time; +} + + +/* +===================== +CG_DrawCrosshairNames +===================== +*/ +static void CG_DrawCrosshairNames( void ) { + float *color; + char *name; + float w; + + if ( !cg_drawCrosshair.integer ) { + return; + } + 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_DrawStrlen( name ) * BIGCHAR_WIDTH; + CG_DrawBigString( 320 - w / 2, 170, name, color[3] * 0.5f ); + + trap_R_SetColor( NULL ); +} + + + +//============================================================================== + +/* +================= +CG_DrawSpectator +================= +*/ +static void CG_DrawSpectator(void) { + CG_DrawBigString(320 - 9 * 8, 440, "SPECTATOR", 1.0F); + if ( cgs.gametype == GT_TOURNAMENT ) { + CG_DrawBigString(320 - 15 * 8, 460, "waiting to play", 1.0F); + } + if ( cgs.gametype >= GT_TEAM ) { + CG_DrawBigString(320 - 39 * 8, 460, "press ESC and use the JOIN menu to play", 1.0F); + } +} + +/* +================= +CG_DrawVote +================= +*/ +static void CG_DrawVote(void) { + char *s; + int sec; + + 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; + } + s = va("VOTE(%i):%s yes:%i no:%i", sec, cgs.voteString, cgs.voteYes, cgs.voteNo ); + CG_DrawSmallString( 0, 58, s, 1.0F ); +} + +/* +================= +CG_DrawTeamVote +================= +*/ +static void CG_DrawTeamVote(void) { + char *s; + int sec, cs_offset; + + if ( cgs.clientinfo->team == TEAM_HUMANS ) + cs_offset = 0; + else if ( cgs.clientinfo->team == TEAM_DROIDS ) + 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; + } + s = va("TEAMVOTE(%i):%s yes:%i no:%i", sec, cgs.teamVoteString[cs_offset], + cgs.teamVoteYes[cs_offset], cgs.teamVoteNo[cs_offset] ); + CG_DrawSmallString( 0, 90, s, 1.0F ); +} + + +static qboolean CG_DrawScoreboard() { + return CG_DrawOldScoreboard(); +} + +/* +================= +CG_DrawIntermission +================= +*/ +static void CG_DrawIntermission( void ) { + if ( cgs.gametype == GT_SINGLE_PLAYER ) { + CG_DrawCenterString(); + return; + } + + cg.scoreFadeTime = cg.time; + cg.scoreBoardShowing = CG_DrawScoreboard(); +} + +/* +================= +CG_DrawFollow +================= +*/ +static qboolean CG_DrawFollow( void ) { + float x; + vec4_t color; + const char *name; + + if ( !(cg.snap->ps.pm_flags & PMF_FOLLOW) ) { + return qfalse; + } + color[0] = 1; + color[1] = 1; + color[2] = 1; + color[3] = 1; + + + CG_DrawBigString( 320 - 9 * 8, 24, "following", 1.0F ); + + name = cgs.clientinfo[ cg.snap->ps.clientNum ].name; + + x = 0.5 * ( 640 - GIANT_WIDTH * CG_DrawStrlen( name ) ); + + CG_DrawStringExt( x, 40, name, color, qtrue, qtrue, GIANT_WIDTH, GIANT_HEIGHT, 0 ); + + return qtrue; +} + + + +/* +================= +CG_DrawAmmoWarning +================= +*/ +static void CG_DrawAmmoWarning( void ) { + const char *s; + int w; + + if ( cg_drawAmmoWarning.integer == 0 ) { + return; + } + + if ( !cg.lowAmmoWarning ) { + return; + } + + if ( cg.lowAmmoWarning == 2 ) { + s = "OUT OF AMMO"; + } else { + s = "LOW AMMO WARNING"; + } + w = CG_DrawStrlen( s ) * BIGCHAR_WIDTH; + CG_DrawBigString(320 - w / 2, 64, s, 1.0F); +} + +/* +================= +CG_DrawWarmup +================= +*/ +static void CG_DrawWarmup( void ) { + int w; + int sec; + int i; + float scale; + clientInfo_t *ci1, *ci2; + int cw; + const char *s; + + sec = cg.warmup; + if ( !sec ) { + return; + } + + if ( sec < 0 ) { + s = "Waiting for players"; + w = CG_DrawStrlen( s ) * BIGCHAR_WIDTH; + CG_DrawBigString(320 - w / 2, 24, s, 1.0F); + cg.warmupCount = 0; + return; + } + + if (cgs.gametype == GT_TOURNAMENT) { + // find the two active players + ci1 = NULL; + ci2 = NULL; + for ( i = 0 ; i < cgs.maxclients ; i++ ) { + if ( cgs.clientinfo[i].infoValid && cgs.clientinfo[i].team == TEAM_FREE ) { + if ( !ci1 ) { + ci1 = &cgs.clientinfo[i]; + } else { + ci2 = &cgs.clientinfo[i]; + } + } + } + + if ( ci1 && ci2 ) { + s = va( "%s vs %s", ci1->name, ci2->name ); + w = CG_DrawStrlen( s ); + if ( w > 640 / GIANT_WIDTH ) { + cw = 640 / w; + } else { + cw = GIANT_WIDTH; + } + CG_DrawStringExt( 320 - w * cw/2, 20,s, colorWhite, + qfalse, qtrue, cw, (int)(cw * 1.5f), 0 ); + } + } else { + if ( cgs.gametype == GT_FFA ) { + s = "Free For All"; + } else if ( cgs.gametype == GT_TEAM ) { + s = "Team Deathmatch"; + } else if ( cgs.gametype == GT_CTF ) { + s = "Capture the Flag"; + } else { + s = ""; + } + w = CG_DrawStrlen( s ); + if ( w > 640 / GIANT_WIDTH ) { + cw = 640 / w; + } else { + cw = GIANT_WIDTH; + } + CG_DrawStringExt( 320 - w * cw/2, 25,s, colorWhite, + qfalse, qtrue, cw, (int)(cw * 1.1f), 0 ); + } + + sec = ( sec - cg.time ) / 1000; + if ( sec < 0 ) { + cg.warmup = 0; + sec = 0; + } + s = va( "Starts in: %i", sec + 1 ); + if ( sec != cg.warmupCount ) { + cg.warmupCount = sec; + switch ( sec ) { + case 0: + trap_S_StartLocalSound( cgs.media.count1Sound, CHAN_ANNOUNCER ); + break; + case 1: + trap_S_StartLocalSound( cgs.media.count2Sound, CHAN_ANNOUNCER ); + break; + case 2: + trap_S_StartLocalSound( cgs.media.count3Sound, CHAN_ANNOUNCER ); + break; + default: + break; + } + } + scale = 0.45f; + switch ( cg.warmupCount ) { + case 0: + cw = 28; + scale = 0.54f; + break; + case 1: + cw = 24; + scale = 0.51f; + break; + case 2: + cw = 20; + scale = 0.48f; + break; + default: + cw = 16; + scale = 0.45f; + break; + } + + w = CG_DrawStrlen( s ); + CG_DrawStringExt( 320 - w * cw/2, 70, s, colorWhite, + qfalse, qtrue, cw, (int)(cw * 1.5), 0 ); +} + +//================================================================================== + +/* +================= +CG_Draw2D +================= +*/ +static void CG_Draw2D( void ) { + + // 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; + } + + if ( cg.snap->ps.persistant[PERS_TEAM] == TEAM_SPECTATOR ) { + CG_DrawSpectator(); + CG_DrawCrosshair(); + CG_DrawCrosshairNames(); + } else { + // don't draw any status if dead or the scoreboard is being explicitly shown + if ( !cg.showScores && cg.snap->ps.stats[STAT_HEALTH] > 0 ) { + CG_DrawLighting(); + CG_DrawStatusBar(); + CG_DrawAmmoWarning(); + CG_DrawCrosshair(); + CG_DrawCrosshairNames(); + CG_DrawWeaponSelect(); + CG_DrawHoldableItem(); + CG_DrawReward(); + } + if ( cgs.gametype >= GT_TEAM ) { + CG_DrawTeamInfo(); + } + if( cg.snap->ps.weapon == WP_SCANNER ) + CG_Scanner(); + } + + CG_DrawVote(); + CG_DrawTeamVote(); + + CG_DrawLagometer(); + + CG_DrawUpperRight(); + + CG_DrawLowerRight(); + + CG_DrawLowerLeft(); + + if ( !CG_DrawFollow() ) { + CG_DrawWarmup(); + } + + // don't draw center string if scoreboard is up + cg.scoreBoardShowing = CG_DrawScoreboard(); + if ( !cg.scoreBoardShowing) { + CG_DrawCenterString(); + } +} + + +static void CG_DrawTourneyScoreboard() { + CG_DrawOldTourneyScoreboard(); +} + + +/* +===================== +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 ) { + CG_DrawInformation(); + return; + } + + // optionally draw the tournement scoreboard instead + if ( cg.snap->ps.persistant[PERS_TEAM] == TEAM_SPECTATOR && + ( cg.snap->ps.pm_flags & PMF_SCOREBOARD ) ) { + CG_DrawTourneyScoreboard(); + 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 ); + } + + // 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 00000000..e968fd8d --- /dev/null +++ b/src/cgame/cg_drawtools.c @@ -0,0 +1,854 @@ +// Copyright (C) 1999-2000 Id Software, Inc. +// +// cg_drawtools.c -- helper functions called by cg_draw, cg_scoreboard, cg_info, etc + +/* + * Portions Copyright (C) 2000-2001 Tim Angus + * + * This program 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, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ + +/* To assertain which portions are licensed under the GPL and which are + * licensed by Id Software, Inc. please run a diff between the equivalent + * versions of the "Tremulous" modification and the unmodified "Quake3" + * game source code. + */ + +#include "cg_local.h" + +/* +================ +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_DrawChar + +Coordinates and size in 640*480 virtual screen size +=============== +*/ +void CG_DrawChar( int x, int y, int width, int height, int ch ) { + int row, col; + float frow, fcol; + float size; + float ax, ay, aw, ah; + + ch &= 255; + + if ( ch == ' ' ) { + return; + } + + ax = x; + ay = y; + aw = width; + ah = height; + CG_AdjustFrom640( &ax, &ay, &aw, &ah ); + + row = ch>>4; + col = ch&15; + + frow = row*0.0625; + fcol = col*0.0625; + size = 0.0625; + + trap_R_DrawStretchPic( ax, ay, aw, ah, + fcol, frow, + fcol + size, frow + size, + cgs.media.charsetShader ); +} + + +/* +================== +CG_DrawStringExt + +Draws a multi-colored string with a drop shadow, optionally forcing +to a fixed color. + +Coordinates are at 640 by 480 virtual resolution +================== +*/ +void CG_DrawStringExt( int x, int y, const char *string, const float *setColor, + qboolean forceColor, qboolean shadow, int charWidth, int charHeight, int maxChars ) { + vec4_t color; + const char *s; + int xx; + int cnt; + + if (maxChars <= 0) + maxChars = 32767; // do them all! + + // draw the drop shadow + if (shadow) { + color[0] = color[1] = color[2] = 0; + color[3] = setColor[3]; + trap_R_SetColor( color ); + s = string; + xx = x; + cnt = 0; + while ( *s && cnt < maxChars) { + if ( Q_IsColorString( s ) ) { + s += 2; + continue; + } + CG_DrawChar( xx + 2, y + 2, charWidth, charHeight, *s ); + cnt++; + xx += charWidth; + s++; + } + } + + // draw the colored text + s = string; + xx = x; + cnt = 0; + trap_R_SetColor( setColor ); + while ( *s && cnt < maxChars) { + if ( Q_IsColorString( s ) ) { + if ( !forceColor ) { + memcpy( color, g_color_table[ColorIndex(*(s+1))], sizeof( color ) ); + color[3] = setColor[3]; + trap_R_SetColor( color ); + } + s += 2; + continue; + } + CG_DrawChar( xx, y, charWidth, charHeight, *s ); + xx += charWidth; + cnt++; + s++; + } + trap_R_SetColor( NULL ); +} + +void CG_DrawBigString( int x, int y, const char *s, float alpha ) { + float color[4]; + + color[0] = color[1] = color[2] = 1.0; + color[3] = alpha; + CG_DrawStringExt( x, y, s, color, qfalse, qtrue, BIGCHAR_WIDTH, BIGCHAR_HEIGHT, 0 ); +} + +void CG_DrawBigStringColor( int x, int y, const char *s, vec4_t color ) { + CG_DrawStringExt( x, y, s, color, qtrue, qtrue, BIGCHAR_WIDTH, BIGCHAR_HEIGHT, 0 ); +} + +void CG_DrawSmallString( int x, int y, const char *s, float alpha ) { + float color[4]; + + color[0] = color[1] = color[2] = 1.0; + color[3] = alpha; + CG_DrawStringExt( x, y, s, color, qfalse, qfalse, SMALLCHAR_WIDTH, SMALLCHAR_HEIGHT, 0 ); +} + +void CG_DrawSmallStringColor( int x, int y, const char *s, vec4_t color ) { + CG_DrawStringExt( x, y, s, color, qtrue, qfalse, SMALLCHAR_WIDTH, SMALLCHAR_HEIGHT, 0 ); +} + +/* +================= +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_TeamColor +================ +*/ +float *CG_TeamColor( int team ) { + static vec4_t red = {1, 0.2f, 0.2f, 1}; + static vec4_t blue = {0.2f, 0.2f, 1, 1}; + static vec4_t other = {1, 1, 1, 1}; + static vec4_t spectator = {0.7f, 0.7f, 0.7f, 1}; + + switch ( team ) { + case TEAM_HUMANS: + return red; + case TEAM_DROIDS: + return blue; + case TEAM_SPECTATOR: + return spectator; + default: + return other; + } +} + + + +/* +================= +CG_GetColorForHealth +================= +*/ +void CG_GetColorForHealth( int health, int armor, vec4_t hcolor ) { + int count; + int max; + + // calculate the total points of damage that can + // be sustained at the current health / armor level + if ( health <= 0 ) { + VectorClear( hcolor ); // black + hcolor[3] = 1; + return; + } + count = armor; + max = health * ARMOR_PROTECTION / ( 1.0 - ARMOR_PROTECTION ); + if ( max < count ) { + count = max; + } + health += count; + + // set the color based on health + hcolor[0] = 1.0; + hcolor[3] = 1.0; + if ( health >= 100 ) { + hcolor[2] = 1.0; + } else if ( health < 66 ) { + hcolor[2] = 0; + } else { + hcolor[2] = ( health - 66 ) / 33.0; + } + + if ( health > 60 ) { + hcolor[1] = 1.0; + } else if ( health < 30 ) { + hcolor[1] = 0; + } else { + hcolor[1] = ( health - 30 ) / 30.0; + } +} + +/* +================= +CG_ColorForHealth +================= +*/ +void CG_ColorForHealth( vec4_t hcolor ) { + + CG_GetColorForHealth( cg.snap->ps.stats[STAT_HEALTH], + cg.snap->ps.stats[STAT_ARMOR], hcolor ); +} + + + + +/* +================= +UI_DrawProportionalString2 +================= +*/ +static int propMap[128][3] = { +{0, 0, -1}, {0, 0, -1}, {0, 0, -1}, {0, 0, -1}, {0, 0, -1}, {0, 0, -1}, {0, 0, -1}, {0, 0, -1}, +{0, 0, -1}, {0, 0, -1}, {0, 0, -1}, {0, 0, -1}, {0, 0, -1}, {0, 0, -1}, {0, 0, -1}, {0, 0, -1}, + +{0, 0, -1}, {0, 0, -1}, {0, 0, -1}, {0, 0, -1}, {0, 0, -1}, {0, 0, -1}, {0, 0, -1}, {0, 0, -1}, +{0, 0, -1}, {0, 0, -1}, {0, 0, -1}, {0, 0, -1}, {0, 0, -1}, {0, 0, -1}, {0, 0, -1}, {0, 0, -1}, + +{0, 0, PROP_SPACE_WIDTH}, // SPACE +{11, 122, 7}, // ! +{154, 181, 14}, // " +{55, 122, 17}, // # +{79, 122, 18}, // $ +{101, 122, 23}, // % +{153, 122, 18}, // & +{9, 93, 7}, // ' +{207, 122, 8}, // ( +{230, 122, 9}, // ) +{177, 122, 18}, // * +{30, 152, 18}, // + +{85, 181, 7}, // , +{34, 93, 11}, // - +{110, 181, 6}, // . +{130, 152, 14}, // / + +{22, 64, 17}, // 0 +{41, 64, 12}, // 1 +{58, 64, 17}, // 2 +{78, 64, 18}, // 3 +{98, 64, 19}, // 4 +{120, 64, 18}, // 5 +{141, 64, 18}, // 6 +{204, 64, 16}, // 7 +{162, 64, 17}, // 8 +{182, 64, 18}, // 9 +{59, 181, 7}, // : +{35,181, 7}, // ; +{203, 152, 14}, // < +{56, 93, 14}, // = +{228, 152, 14}, // > +{177, 181, 18}, // ? + +{28, 122, 22}, // @ +{5, 4, 18}, // A +{27, 4, 18}, // B +{48, 4, 18}, // C +{69, 4, 17}, // D +{90, 4, 13}, // E +{106, 4, 13}, // F +{121, 4, 18}, // G +{143, 4, 17}, // H +{164, 4, 8}, // I +{175, 4, 16}, // J +{195, 4, 18}, // K +{216, 4, 12}, // L +{230, 4, 23}, // M +{6, 34, 18}, // N +{27, 34, 18}, // O + +{48, 34, 18}, // P +{68, 34, 18}, // Q +{90, 34, 17}, // R +{110, 34, 18}, // S +{130, 34, 14}, // T +{146, 34, 18}, // U +{166, 34, 19}, // V +{185, 34, 29}, // W +{215, 34, 18}, // X +{234, 34, 18}, // Y +{5, 64, 14}, // Z +{60, 152, 7}, // [ +{106, 151, 13}, // '\' +{83, 152, 7}, // ] +{128, 122, 17}, // ^ +{4, 152, 21}, // _ + +{134, 181, 5}, // ' +{5, 4, 18}, // A +{27, 4, 18}, // B +{48, 4, 18}, // C +{69, 4, 17}, // D +{90, 4, 13}, // E +{106, 4, 13}, // F +{121, 4, 18}, // G +{143, 4, 17}, // H +{164, 4, 8}, // I +{175, 4, 16}, // J +{195, 4, 18}, // K +{216, 4, 12}, // L +{230, 4, 23}, // M +{6, 34, 18}, // N +{27, 34, 18}, // O + +{48, 34, 18}, // P +{68, 34, 18}, // Q +{90, 34, 17}, // R +{110, 34, 18}, // S +{130, 34, 14}, // T +{146, 34, 18}, // U +{166, 34, 19}, // V +{185, 34, 29}, // W +{215, 34, 18}, // X +{234, 34, 18}, // Y +{5, 64, 14}, // Z +{153, 152, 13}, // { +{11, 181, 5}, // | +{180, 152, 13}, // } +{79, 93, 17}, // ~ +{0, 0, -1} // DEL +}; + +static int propMapB[26][3] = { +{11, 12, 33}, +{49, 12, 31}, +{85, 12, 31}, +{120, 12, 30}, +{156, 12, 21}, +{183, 12, 21}, +{207, 12, 32}, + +{13, 55, 30}, +{49, 55, 13}, +{66, 55, 29}, +{101, 55, 31}, +{135, 55, 21}, +{158, 55, 40}, +{204, 55, 32}, + +{12, 97, 31}, +{48, 97, 31}, +{82, 97, 30}, +{118, 97, 30}, +{153, 97, 30}, +{185, 97, 25}, +{213, 97, 30}, + +{11, 139, 32}, +{42, 139, 51}, +{93, 139, 32}, +{126, 139, 31}, +{158, 139, 25}, +}; + +#define PROPB_GAP_WIDTH 4 +#define PROPB_SPACE_WIDTH 12 +#define PROPB_HEIGHT 36 + +/* +================= +UI_DrawBannerString +================= +*/ +static void UI_DrawBannerString2( int x, int y, const char* str, vec4_t color ) +{ + const char* s; + char ch; + float ax; + float ay; + float aw; + float ah; + float frow; + float fcol; + float fwidth; + float fheight; + + // draw the colored text + trap_R_SetColor( color ); + + ax = x * cgs.screenXScale + cgs.screenXBias; + ay = y * cgs.screenXScale; + + s = str; + while ( *s ) + { + ch = *s & 127; + if ( ch == ' ' ) { + ax += ((float)PROPB_SPACE_WIDTH + (float)PROPB_GAP_WIDTH)* cgs.screenXScale; + } + else if ( ch >= 'A' && ch <= 'Z' ) { + ch -= 'A'; + fcol = (float)propMapB[ch][0] / 256.0f; + frow = (float)propMapB[ch][1] / 256.0f; + fwidth = (float)propMapB[ch][2] / 256.0f; + fheight = (float)PROPB_HEIGHT / 256.0f; + aw = (float)propMapB[ch][2] * cgs.screenXScale; + ah = (float)PROPB_HEIGHT * cgs.screenXScale; + trap_R_DrawStretchPic( ax, ay, aw, ah, fcol, frow, fcol+fwidth, frow+fheight, cgs.media.charsetPropB ); + ax += (aw + (float)PROPB_GAP_WIDTH * cgs.screenXScale); + } + s++; + } + + trap_R_SetColor( NULL ); +} + +void UI_DrawBannerString( int x, int y, const char* str, int style, vec4_t color ) { + const char * s; + int ch; + int width; + vec4_t drawcolor; + + // find the width of the drawn text + s = str; + width = 0; + while ( *s ) { + ch = *s; + if ( ch == ' ' ) { + width += PROPB_SPACE_WIDTH; + } + else if ( ch >= 'A' && ch <= 'Z' ) { + width += propMapB[ch - 'A'][2] + PROPB_GAP_WIDTH; + } + s++; + } + width -= PROPB_GAP_WIDTH; + + switch( style & UI_FORMATMASK ) { + case UI_CENTER: + x -= width / 2; + break; + + case UI_RIGHT: + x -= width; + break; + + case UI_LEFT: + default: + break; + } + + if ( style & UI_DROPSHADOW ) { + drawcolor[0] = drawcolor[1] = drawcolor[2] = 0; + drawcolor[3] = color[3]; + UI_DrawBannerString2( x+2, y+2, str, drawcolor ); + } + + UI_DrawBannerString2( x, y, str, color ); +} + + +int UI_ProportionalStringWidth( const char* str ) { + const char * s; + int ch; + int charWidth; + int width; + + s = str; + width = 0; + while ( *s ) { + ch = *s & 127; + charWidth = propMap[ch][2]; + if ( charWidth != -1 ) { + width += charWidth; + width += PROP_GAP_WIDTH; + } + s++; + } + + width -= PROP_GAP_WIDTH; + return width; +} + +static void UI_DrawProportionalString2( int x, int y, const char* str, vec4_t color, float sizeScale, qhandle_t charset ) +{ + const char* s; + char ch; + float ax; + float ay; + float aw; + float ah; + float frow; + float fcol; + float fwidth; + float fheight; + + // draw the colored text + trap_R_SetColor( color ); + + ax = x * cgs.screenXScale + cgs.screenXBias; + ay = y * cgs.screenXScale; + + s = str; + while ( *s ) + { + ch = *s & 127; + if ( ch == ' ' ) { + aw = (float)PROP_SPACE_WIDTH * cgs.screenXScale * sizeScale; + } else if ( propMap[ch][2] != -1 ) { + fcol = (float)propMap[ch][0] / 256.0f; + frow = (float)propMap[ch][1] / 256.0f; + fwidth = (float)propMap[ch][2] / 256.0f; + fheight = (float)PROP_HEIGHT / 256.0f; + aw = (float)propMap[ch][2] * cgs.screenXScale * sizeScale; + ah = (float)PROP_HEIGHT * cgs.screenXScale * sizeScale; + trap_R_DrawStretchPic( ax, ay, aw, ah, fcol, frow, fcol+fwidth, frow+fheight, charset ); + } else { + aw = 0; + } + + ax += (aw + (float)PROP_GAP_WIDTH * cgs.screenXScale * sizeScale); + s++; + } + + trap_R_SetColor( NULL ); +} + +/* +================= +UI_ProportionalSizeScale +================= +*/ +float UI_ProportionalSizeScale( int style ) { + if( style & UI_SMALLFONT ) { + return 0.75; + } + + return 1.00; +} + + +/* +================= +UI_DrawProportionalString +================= +*/ +void UI_DrawProportionalString( int x, int y, const char* str, int style, vec4_t color ) { + vec4_t drawcolor; + int width; + float sizeScale; + + sizeScale = UI_ProportionalSizeScale( style ); + + switch( style & UI_FORMATMASK ) { + case UI_CENTER: + width = UI_ProportionalStringWidth( str ) * sizeScale; + x -= width / 2; + break; + + case UI_RIGHT: + width = UI_ProportionalStringWidth( str ) * sizeScale; + x -= width; + break; + + case UI_LEFT: + default: + break; + } + + if ( style & UI_DROPSHADOW ) { + drawcolor[0] = drawcolor[1] = drawcolor[2] = 0; + drawcolor[3] = color[3]; + UI_DrawProportionalString2( x+2, y+2, str, drawcolor, sizeScale, cgs.media.charsetProp ); + } + + if ( style & UI_INVERSE ) { + drawcolor[0] = color[0] * 0.8; + drawcolor[1] = color[1] * 0.8; + drawcolor[2] = color[2] * 0.8; + drawcolor[3] = color[3]; + UI_DrawProportionalString2( x, y, str, drawcolor, sizeScale, cgs.media.charsetProp ); + return; + } + + if ( style & UI_PULSE ) { + drawcolor[0] = color[0] * 0.8; + drawcolor[1] = color[1] * 0.8; + drawcolor[2] = color[2] * 0.8; + drawcolor[3] = color[3]; + UI_DrawProportionalString2( x, y, str, color, sizeScale, cgs.media.charsetProp ); + + drawcolor[0] = color[0]; + drawcolor[1] = color[1]; + drawcolor[2] = color[2]; + drawcolor[3] = 0.5 + 0.5 * sin( cg.time / PULSE_DIVISOR ); + UI_DrawProportionalString2( x, y, str, drawcolor, sizeScale, cgs.media.charsetPropGlow ); + return; + } + + UI_DrawProportionalString2( x, y, str, color, sizeScale, cgs.media.charsetProp ); +} + diff --git a/src/cgame/cg_ents.c b/src/cgame/cg_ents.c new file mode 100644 index 00000000..fc0783f7 --- /dev/null +++ b/src/cgame/cg_ents.c @@ -0,0 +1,1016 @@ +// Copyright (C) 1999-2000 Id Software, Inc. +// +// cg_ents.c -- present snapshot entities, happens every single frame + +/* + * Portions Copyright (C) 2000-2001 Tim Angus + * + * This program 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, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ + +/* To assertain which portions are licensed under the GPL and which are + * licensed by Id Software, Inc. please run a diff between the equivalent + * versions of the "Tremulous" modification and the unmodified "Quake3" + * game source code. + */ + +#include "cg_local.h" + + +/* +====================== +CG_PositionEntityOnTag + +Modifies the entities position and axis by the given +tag location +====================== +*/ +void CG_PositionEntityOnTag( refEntity_t *entity, const refEntity_t *parent, + qhandle_t parentModel, char *tagName ) { + int i; + orientation_t lerped; + + // lerp the tag + trap_R_LerpTag( &lerped, parentModel, parent->oldframe, parent->frame, + 1.0 - parent->backlerp, tagName ); + + // FIXME: allow origin offsets along tag? + VectorCopy( parent->origin, entity->origin ); + for ( i = 0 ; i < 3 ; i++ ) { + VectorMA( entity->origin, lerped.origin[i], parent->axis[i], entity->origin ); + } + + // had to cast away the const to avoid compiler problems... + MatrixMultiply( lerped.axis, ((refEntity_t *)parent)->axis, entity->axis ); + entity->backlerp = parent->backlerp; +} + + +/* +====================== +CG_PositionRotatedEntityOnTag + +Modifies the entities position and axis by the given +tag location +====================== +*/ +void CG_PositionRotatedEntityOnTag( refEntity_t *entity, const refEntity_t *parent, + qhandle_t parentModel, char *tagName ) { + int i; + orientation_t lerped; + vec3_t tempAxis[3]; + +//AxisClear( entity->axis ); + // lerp the tag + trap_R_LerpTag( &lerped, parentModel, parent->oldframe, parent->frame, + 1.0 - parent->backlerp, tagName ); + + // FIXME: allow origin offsets along tag? + VectorCopy( parent->origin, entity->origin ); + for ( i = 0 ; i < 3 ; i++ ) { + VectorMA( entity->origin, lerped.origin[i], parent->axis[i], entity->origin ); + } + + // had to cast away the const to avoid compiler problems... + MatrixMultiply( entity->axis, lerped.axis, tempAxis ); + MatrixMultiply( tempAxis, ((refEntity_t *)parent)->axis, entity->axis ); +} + + + +/* +========================================================================== + +FUNCTIONS CALLED EACH FRAME + +========================================================================== +*/ + +/* +====================== +CG_SetEntitySoundPosition + +Also called by event processing code +====================== +*/ +void CG_SetEntitySoundPosition( centity_t *cent ) { + if ( cent->currentState.solid == SOLID_BMODEL ) { + vec3_t origin; + float *v; + + v = cgs.inlineModelMidpoints[ cent->currentState.modelindex ]; + VectorAdd( cent->lerpOrigin, v, origin ); + trap_S_UpdateEntityPosition( cent->currentState.number, origin ); + } else { + trap_S_UpdateEntityPosition( cent->currentState.number, cent->lerpOrigin ); + } +} + +/* +================== +CG_EntityEffects + +Add continuous entity effects, like local entity emission and lighting +================== +*/ +static void CG_EntityEffects( centity_t *cent ) { + + // update sound origins + CG_SetEntitySoundPosition( cent ); + + // add loop sound + if ( cent->currentState.loopSound ) { + if (cent->currentState.eType != ET_SPEAKER) { + trap_S_AddLoopingSound( cent->currentState.number, cent->lerpOrigin, vec3_origin, + cgs.gameSounds[ cent->currentState.loopSound ] ); + } else { + trap_S_AddRealLoopingSound( cent->currentState.number, cent->lerpOrigin, vec3_origin, + cgs.gameSounds[ cent->currentState.loopSound ] ); + } + } + + + // constant light glow + if ( cent->currentState.constantLight && cent->currentState.eType != ET_TORCH ) + { + 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 ); + } + +} + + +/* +================== +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_Item +================== +*/ +static void CG_Item( centity_t *cent ) { + refEntity_t ent; + entityState_t *es; + gitem_t *item; + int msec; + float frac; + float scale; + weaponInfo_t *wi; + + + es = ¢->currentState; + if ( es->modelindex >= bg_numItems ) { + CG_Error( "Bad item index %i on entity", es->modelindex ); + } + + // if set to invisible, skip + if ( !es->modelindex || ( es->eFlags & EF_NODRAW ) ) { + return; + } + + item = &bg_itemlist[ es->modelindex ]; + if ( cg_simpleItems.integer && item->giType != IT_TEAM ) { + memset( &ent, 0, sizeof( ent ) ); + ent.reType = RT_SPRITE; + VectorCopy( cent->lerpOrigin, ent.origin ); + ent.radius = 14; + ent.customShader = cg_items[es->modelindex].icon; + ent.shaderRGBA[0] = 255; + ent.shaderRGBA[1] = 255; + ent.shaderRGBA[2] = 255; + ent.shaderRGBA[3] = 255; + trap_R_AddRefEntityToScene(&ent); + return; + } + + // items bob up and down continuously + scale = 0.005 + cent->currentState.number * 0.00001; + cent->lerpOrigin[2] += 4 + cos( ( cg.time + 1000 ) * scale ) * 4; + + memset (&ent, 0, sizeof(ent)); + + // autorotate at one of two speeds + if ( item->giType == IT_HEALTH ) { + VectorCopy( cg.autoAnglesFast, cent->lerpAngles ); + AxisCopy( cg.autoAxisFast, ent.axis ); + } else { + VectorCopy( cg.autoAngles, cent->lerpAngles ); + AxisCopy( cg.autoAxis, ent.axis ); + } + + // the weapons have their origin where they attatch to player + // models, so we need to offset them or they will rotate + // eccentricly + wi = NULL; + if ( item->giType == IT_WEAPON ) { + + wi = &cg_weapons[item->giTag]; + cent->lerpOrigin[0] -= + wi->weaponMidpoint[0] * ent.axis[0][0] + + wi->weaponMidpoint[1] * ent.axis[1][0] + + wi->weaponMidpoint[2] * ent.axis[2][0]; + cent->lerpOrigin[1] -= + wi->weaponMidpoint[0] * ent.axis[0][1] + + wi->weaponMidpoint[1] * ent.axis[1][1] + + wi->weaponMidpoint[2] * ent.axis[2][1]; + cent->lerpOrigin[2] -= + wi->weaponMidpoint[0] * ent.axis[0][2] + + wi->weaponMidpoint[1] * ent.axis[1][2] + + wi->weaponMidpoint[2] * ent.axis[2][2]; + + cent->lerpOrigin[2] += 8; // an extra height boost + } + + ent.hModel = cg_items[es->modelindex].models[0]; + + VectorCopy( cent->lerpOrigin, ent.origin); + VectorCopy( cent->lerpOrigin, ent.oldorigin); + + ent.nonNormalizedAxes = qfalse; + + // if just respawned, slowly scale up + msec = cg.time - cent->miscTime; + if ( msec >= 0 && msec < ITEM_SCALEUP_TIME ) { + frac = (float)msec / ITEM_SCALEUP_TIME; + VectorScale( ent.axis[0], frac, ent.axis[0] ); + VectorScale( ent.axis[1], frac, ent.axis[1] ); + VectorScale( ent.axis[2], frac, ent.axis[2] ); + ent.nonNormalizedAxes = qtrue; + } else { + frac = 1.0; + } + + // items without glow textures need to keep a minimum light value + // so they are always visible + if ( ( item->giType == IT_WEAPON ) || + ( item->giType == IT_ARMOR ) ) { + ent.renderfx |= RF_MINLIGHT; + } + + // increase the size of the weapons when they are presented as items + if ( item->giType == IT_WEAPON ) { + VectorScale( ent.axis[0], 1.5, ent.axis[0] ); + VectorScale( ent.axis[1], 1.5, ent.axis[1] ); + VectorScale( ent.axis[2], 1.5, ent.axis[2] ); + ent.nonNormalizedAxes = qtrue; + } + + // add to refresh list + trap_R_AddRefEntityToScene(&ent); + + // accompanying rings / spheres for powerups + if ( !cg_simpleItems.integer ) + { + vec3_t spinAngles; + + VectorClear( spinAngles ); + + if ( item->giType == IT_HEALTH || item->giType == IT_POWERUP ) + { + if ( ( ent.hModel = cg_items[es->modelindex].models[1] ) != 0 ) + { + if ( item->giType == IT_POWERUP ) + { + ent.origin[2] += 12; + spinAngles[1] = ( cg.time & 1023 ) * 360 / -1024.0f; + } + AnglesToAxis( spinAngles, ent.axis ); + + // scale up if respawning + if ( frac != 1.0 ) { + VectorScale( ent.axis[0], frac, ent.axis[0] ); + VectorScale( ent.axis[1], frac, ent.axis[1] ); + VectorScale( ent.axis[2], frac, ent.axis[2] ); + ent.nonNormalizedAxes = qtrue; + } + trap_R_AddRefEntityToScene( &ent ); + } + } + } +} + + +/* +================== +CG_Buildable +================== +*/ +static void CG_Buildable( centity_t *cent ) { + refEntity_t ent; + refEntity_t ent2; + entityState_t *es; + gitem_t *item; + int msec; + float frac; + float scale; + + es = ¢->currentState; + if ( es->modelindex >= bg_numItems ) { + CG_Error( "Bad item index %i on entity", es->modelindex ); + } + + // if set to invisible, skip + if ( !es->modelindex || ( es->eFlags & EF_NODRAW ) ) { + return; + } + + item = &bg_itemlist[ es->modelindex ]; + + memset (&ent, 0, sizeof(ent)); + + VectorCopy( es->angles, cent->lerpAngles ); + AnglesToAxis( cent->lerpAngles, ent.axis ); + + ent.hModel = cg_items[es->modelindex].models[0]; + + VectorCopy( cent->lerpOrigin, ent.origin); + VectorCopy( cent->lerpOrigin, ent.oldorigin); + + ent.nonNormalizedAxes = qfalse; + + // if just respawned, slowly scale up + msec = cg.time - cent->miscTime; + if ( msec >= 0 && msec < ITEM_SCALEUP_TIME ) { + frac = (float)msec / ITEM_SCALEUP_TIME; + VectorScale( ent.axis[0], frac, ent.axis[0] ); + VectorScale( ent.axis[1], frac, ent.axis[1] ); + VectorScale( ent.axis[2], frac, ent.axis[2] ); + ent.nonNormalizedAxes = qtrue; + } else { + frac = 1.0; + } + + + //TA: might be useful later: + // items without glow textures need to keep a minimum light value + // so they are always visible + /*if ( ( item->giType == IT_WEAPON ) || + ( item->giType == IT_ARMOR ) ) { + ent.renderfx |= RF_MINLIGHT; + }*/ + + //turret barrel bit + if( cg_items[ es->modelindex ].models[ 1 ] != 0 ) + { + vec3_t turretOrigin; + + memset( &ent2, 0, sizeof( ent2 ) ); + + AnglesToAxis( es->angles2, ent2.axis ); + + ent2.hModel = cg_items[ es->modelindex ].models[ 1 ]; + + VectorCopy( cent->lerpOrigin, turretOrigin ); + turretOrigin[ 2 ] += 5; + + VectorCopy( turretOrigin, ent2.origin ); + VectorCopy( turretOrigin, ent2.oldorigin ); + + ent2.nonNormalizedAxes = qfalse; + + trap_R_AddRefEntityToScene( &ent2 ); + } + + // add to refresh list + trap_R_AddRefEntityToScene(&ent); +} + + +//============================================================================ + +/* +=============== +CG_Missile +=============== +*/ +static void CG_Missile( centity_t *cent ) { + refEntity_t ent; + entityState_t *s1; + const weaponInfo_t *weapon; + + s1 = ¢->currentState; + if ( s1->weapon > WP_NUM_WEAPONS ) { + s1->weapon = 0; + } + weapon = &cg_weapons[s1->weapon]; + + // calculate the axis + VectorCopy( s1->angles, cent->lerpAngles); + + // add trails + if ( weapon->missileTrailFunc ) + { + weapon->missileTrailFunc( cent, weapon ); + } + + // add dynamic light + if ( weapon->missileDlight ) { + trap_R_AddLightToScene(cent->lerpOrigin, weapon->missileDlight, + weapon->missileDlightColor[0], weapon->missileDlightColor[1], weapon->missileDlightColor[2] ); + } + + // add missile sound + if ( weapon->missileSound ) { + vec3_t velocity; + + BG_EvaluateTrajectoryDelta( ¢->currentState.pos, cg.time, velocity ); + + trap_S_AddLoopingSound( cent->currentState.number, cent->lerpOrigin, velocity, weapon->missileSound ); + } + + // create the render entity + memset (&ent, 0, sizeof(ent)); + VectorCopy( cent->lerpOrigin, ent.origin); + VectorCopy( cent->lerpOrigin, ent.oldorigin); + + if( cent->currentState.weapon == WP_PLASMAGUN ) + { + ent.reType = RT_SPRITE; + ent.radius = 16; + ent.rotation = 0; + ent.customShader = cgs.media.plasmaBallShader; + trap_R_AddRefEntityToScene( &ent ); + return; + } + + if( cent->currentState.weapon == WP_FLAMER ) + { + ent.reType = RT_SPRITE; + ent.radius = ( ( cg.time - s1->pos.trTime ) * ( cg.time - s1->pos.trTime ) ) / 6000; + ent.rotation = 0; + ent.customShader = cgs.media.flameShader; + trap_R_AddRefEntityToScene( &ent ); + return; + } + + // flicker between two skins + ent.skinNum = cg.clientFrame & 1; + ent.hModel = weapon->missileModel; + ent.renderfx = weapon->missileRenderfx | RF_NOSHADOW; + + // convert direction of travel into axis + if ( VectorNormalize2( s1->pos.trDelta, ent.axis[0] ) == 0 ) { + ent.axis[0][2] = 1; + } + + // spin as it moves + if ( s1->pos.trType != TR_STATIONARY ) { + RotateAroundDirection( ent.axis, cg.time / 4 ); + } else { + RotateAroundDirection( ent.axis, s1->time ); + } + + // add to refresh list, possibly with quad glow + CG_AddRefEntityWithPowerups( &ent, s1->powerups, TEAM_FREE ); +} + +/* +=============== +CG_Grapple + +This is called when the grapple is sitting up against the wall +=============== +*/ +static void CG_Grapple( centity_t *cent ) { + refEntity_t ent; + entityState_t *s1; + const weaponInfo_t *weapon; + + s1 = ¢->currentState; + if ( s1->weapon > WP_NUM_WEAPONS ) { + s1->weapon = 0; + } + weapon = &cg_weapons[s1->weapon]; + + // calculate the axis + VectorCopy( s1->angles, cent->lerpAngles); + +#if 0 // FIXME add grapple pull sound here..? + // add missile sound + if ( weapon->missileSound ) { + trap_S_AddLoopingSound( cent->currentState.number, cent->lerpOrigin, vec3_origin, weapon->missileSound ); + } +#endif + + // Will draw cable if needed + CG_GrappleTrail ( cent, weapon ); + + // create the render entity + memset (&ent, 0, sizeof(ent)); + VectorCopy( cent->lerpOrigin, ent.origin); + VectorCopy( cent->lerpOrigin, ent.oldorigin); + + // flicker between two skins + ent.skinNum = cg.clientFrame & 1; + ent.hModel = weapon->missileModel; + ent.renderfx = weapon->missileRenderfx | RF_NOSHADOW; + + // convert direction of travel into axis + if ( VectorNormalize2( s1->pos.trDelta, ent.axis[0] ) == 0 ) { + ent.axis[0][2] = 1; + } + + 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); +} + +//============================================================================ + +/* +=============== +CG_TorchLight +=============== +*/ +static void CG_TorchLight( centity_t *cent ) +{ + float r, g, b; + int i, j, k; + byte lum; + + r = ( (float)( cent->currentState.constantLight & 0xFF ) ) / 255.0; + g = ( (float)( ( cent->currentState.constantLight >> 8 ) & 0xFF ) ) / 255.0; + b = ( (float)( ( cent->currentState.constantLight >> 16 ) & 0xFF ) ) / 255.0; + i = ( cent->currentState.constantLight >> 24 ) & 0xFF; + + lum = CG_AmbientLight( cent->lerpOrigin ); + + if( lum > 32 ) + { + k = 1; + } + else if( lum <= 32 ) + { + k = 2; + r *= 1.0f - ( (float)lum / 64.0f ); + g *= 1.0f - ( (float)lum / 64.0f ); + b *= 1.0f - ( (float)lum / 64.0f ); + } + + for( j = 0; j <= k; j++ ) + trap_R_AddLightToScene(cent->lerpOrigin, i*2, r, g, b ); +} + +/* +========================= +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 ) { + // 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; + } + + // just use the current frame and evaluate as best we can + BG_EvaluateTrajectory( ¢->currentState.pos, cg.time, cent->lerpOrigin ); + BG_EvaluateTrajectory( ¢->currentState.apos, cg.time, cent->lerpAngles ); + + // 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_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_ITEM: + CG_Item( cent ); + break; + case ET_BUILDABLE: + CG_Buildable( cent ); + break; + case ET_CREEP: + CG_Creep( cent ); + break; + case ET_MISSILE: + CG_Missile( cent ); + break; + case ET_TORCH: + CG_TorchLight( 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_GRAPPLE: + CG_Grapple( 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_AddCEntity( &cg.predictedPlayerEntity ); + + // lerp the non-predicted value for lightning gun origins + CG_CalcEntityLerpPositions( &cg_entities[ cg.snap->ps.clientNum ] ); + + //TA: "empty" item position arrays + cgIP.numDroidItems = 0; + cgIP.numHumanItems = 0; + cgIP.numDroidClients = 0; + cgIP.numHumanClients = 0; + + for ( num = 0 ; num < cg.snap->numEntities ; num++ ) + { + cent = &cg_entities[ cg.snap->entities[ num ].number ]; + + if( cent->currentState.eType == ET_BUILDABLE ) + { + //TA: add to list of item positions (for creep) + if( cent->currentState.modelindex2 == BIT_DROIDS ) + { + VectorCopy( cent->lerpOrigin, cgIP.droidItemPositions[ cgIP.numDroidItems ] ); + cgIP.droidItemTimes[ cgIP.numDroidItems ] = cent->miscTime; + cgIP.numDroidItems++; + } + else if( cent->currentState.modelindex2 == BIT_HUMANS ) + { + VectorCopy( cent->lerpOrigin, cgIP.humanItemPositions[ cgIP.numHumanItems ] ); + cgIP.numHumanItems++; + } + } + + if( cent->currentState.eType == ET_PLAYER ) + { + int team = cent->currentState.powerups & 0x00FF; + int class = ( cent->currentState.powerups & 0xFF00 ) >> 8; + + if( team == PTE_DROIDS ) + { + VectorCopy( cent->lerpOrigin, cgIP.droidClientPositions[ cgIP.numDroidClients ] ); + cgIP.droidClientClass = class; + cgIP.numDroidClients++; + } + else if( team == PTE_HUMANS ) + { + VectorCopy( cent->lerpOrigin, cgIP.humanClientPositions[ cgIP.numHumanClients ] ); + cgIP.humanClientClass = class; + cgIP.numHumanClients++; + } + } + } + + //Com_Printf( "%d %d\n", cgIP.numDroidClients, cgIP.numHumanClients ); + + // 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 ); + } +} + diff --git a/src/cgame/cg_event.c b/src/cgame/cg_event.c new file mode 100644 index 00000000..d1372885 --- /dev/null +++ b/src/cgame/cg_event.c @@ -0,0 +1,1018 @@ +// Copyright (C) 1999-2000 Id Software, Inc. +// +// cg_event.c -- handle entity events at snapshot or playerstate transitions + +/* + * Portions Copyright (C) 2000-2001 Tim Angus + * + * This program 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, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ + +/* To assertain which portions are licensed under the GPL and which are + * licensed by Id Software, Inc. please run a diff between the equivalent + * versions of the "Tremulous" modification and the unmodified "Quake3" + * game source code. + */ + +#include "cg_local.h" + +// for the voice chats +#include "../ta_ui/menudef.h" + +//========================================================================== + +/* +=================== +CG_PlaceString + +Also called by scoreboard drawing +=================== +*/ +const char *CG_PlaceString( int rank ) { + static char str[64]; + char *s, *t; + + if ( rank & RANK_TIED_FLAG ) { + rank &= ~RANK_TIED_FLAG; + t = "Tied for "; + } else { + t = ""; + } + + if ( rank == 1 ) { + s = S_COLOR_BLUE "1st" S_COLOR_WHITE; // draw in blue + } else if ( rank == 2 ) { + s = S_COLOR_RED "2nd" S_COLOR_WHITE; // draw in red + } else if ( rank == 3 ) { + s = S_COLOR_YELLOW "3rd" S_COLOR_WHITE; // draw in yellow + } else if ( rank == 11 ) { + s = "11th"; + } else if ( rank == 12 ) { + s = "12th"; + } else if ( rank == 13 ) { + s = "13th"; + } else if ( rank % 10 == 1 ) { + s = va("%ist", rank); + } else if ( rank % 10 == 2 ) { + s = va("%ind", rank); + } else if ( rank % 10 == 3 ) { + s = va("%ird", rank); + } else { + s = va("%ith", rank); + } + + Com_sprintf( str, sizeof( str ), "%s%s", t, s ); + return str; +} + +/* +============= +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]; + gender_t gender; + clientInfo_t *ci; + + 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 ); + } + + 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 = "cratered"; + break; + case MOD_CRUSH: + message = "was squished"; + break; + case MOD_WATER: + message = "sank like a rock"; + 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 = "was melted by the acid blood"; + break; + default: + message = NULL; + break; + } + + if (attacker == target) { + gender = ci->gender; + switch (mod) { + case MOD_GRENADE_SPLASH: + if ( gender == GENDER_FEMALE ) + message = "tripped on her own grenade"; + else if ( gender == GENDER_NEUTER ) + message = "tripped on its own grenade"; + else + message = "tripped on his own grenade"; + break; + case MOD_ROCKET_SPLASH: + if ( gender == GENDER_FEMALE ) + message = "blew herself up"; + else if ( gender == GENDER_NEUTER ) + message = "blew itself up"; + else + message = "blew himself up"; + break; + 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_BFG_SPLASH: + message = "should have used a smaller gun"; + 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 kill messages from the current clientNum + if ( attacker == cg.snap->ps.clientNum ) { + char *s; + + if ( cgs.gametype < GT_TEAM ) { + s = va("You fragged %s\n%s place with %i", targetName, + CG_PlaceString( cg.snap->ps.persistant[PERS_RANK] + 1 ), + cg.snap->ps.persistant[PERS_SCORE] ); + } else { + s = va("You fragged %s", targetName ); + } + CG_CenterPrint( s, SCREEN_HEIGHT * 0.30, BIGCHAR_WIDTH ); + + // print the text message as well + } + + // 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_GRAPPLE: + message = "was caught by"; + break; + case MOD_GAUNTLET: + message = "was pummeled 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_GRENADE: + message = "ate"; + message2 = "'s grenade"; + break; + case MOD_GRENADE_SPLASH: + message = "was shredded by"; + message2 = "'s shrapnel"; + break; + case MOD_ROCKET: + message = "ate"; + message2 = "'s rocket"; + break; + case MOD_ROCKET_SPLASH: + message = "almost dodged"; + message2 = "'s rocket"; + break; + case MOD_FLAMER: + message = "was toasted by"; + message2 = "'s flamer"; + break; + case MOD_FLAMER_SPLASH: + message = "was toasted by"; + message2 = "'s flamer"; + break; + case MOD_RAILGUN: + message = "was railed by"; + break; + case MOD_LIGHTNING: + message = "was electrocuted by"; + break; + case MOD_VENOM: + message = "was biten by"; + break; + case MOD_BFG: + case MOD_BFG_SPLASH: + message = "was blasted by"; + message2 = "'s BFG"; + 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\n", + targetName, message, attackerName, message2); + return; + } + } + + // we don't know what it was + CG_Printf( "%s died.\n", targetName ); +} + +//========================================================================== + +/* +=============== +CG_UseItem +=============== +*/ +static void CG_UseItem( centity_t *cent ) { + clientInfo_t *ci; + int itemNum, clientNum; + gitem_t *item; + entityState_t *es; + + es = ¢->currentState; + + itemNum = (es->event & ~EV_EVENT_BITS) - EV_USE_ITEM0; + if ( itemNum < 0 || itemNum > HI_NUM_HOLDABLE ) { + itemNum = 0; + } + + // print a message if the local player + if ( es->number == cg.snap->ps.clientNum ) { + if ( !itemNum ) { + CG_CenterPrint( "No item to use", SCREEN_HEIGHT * 0.30, BIGCHAR_WIDTH ); + } else { + item = BG_FindItemForHoldable( itemNum ); + CG_CenterPrint( va("Use %s", item->pickup_name), SCREEN_HEIGHT * 0.30, BIGCHAR_WIDTH ); + } + } + + switch ( itemNum ) { + default: + case HI_NONE: + trap_S_StartSound (NULL, es->number, CHAN_BODY, cgs.media.useNothingSound ); + break; + + case HI_TELEPORTER: + break; + + case HI_MEDKIT: + clientNum = cent->currentState.clientNum; + if ( clientNum >= 0 && clientNum < MAX_CLIENTS ) { + ci = &cgs.clientinfo[ clientNum ]; + ci->medkitUsageTime = cg.time; + } + trap_S_StartSound (NULL, es->number, CHAN_BODY, cgs.media.medkitSound ); + break; + } + +} + +/* +================ +CG_ItemPickup + +A new item was picked up this frame +================ +*/ +static void CG_ItemPickup( int itemNum ) { + cg.itemPickup = itemNum; + cg.itemPickupTime = cg.time; + cg.itemPickupBlendTime = cg.time; + // see if it should be the grabbed weapon + if ( bg_itemlist[itemNum].giType == IT_WEAPON ) { + // select it immediately + if ( cg_autoswitch.integer && bg_itemlist[itemNum].giTag != WP_MACHINEGUN ) { + cg.weaponSelectTime = cg.time; + cg.weaponSelect = bg_itemlist[itemNum].giTag; + } + } + +} + + +/* +================ +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_Menu +============== +*/ +void CG_Menu( centity_t *cent, int eventParm ) +{ + switch( eventParm ) + { + case MN_TEAM: + trap_SendConsoleCommand( "menu teammenu\n" ); + break; + + case MN_DROID: + trap_SendConsoleCommand( "menu aclassmenu\n" ); + break; + + case MN_HUMAN: + trap_SendConsoleCommand( "menu hsitemmenu\n" ); + break; + + case MN_ABUILD: + trap_SendConsoleCommand( "menu abuildmenu\n" ); + break; + + case MN_HBUILD: + trap_SendConsoleCommand( "menu hbuildmenu\n" ); + break; + + default: + Com_Printf( "cgame: debug: no such menu no %d\n", eventParm ); + + } +} + +/* +============== +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; + + BG_unpackAttributes( NULL, NULL, &steptime, cg.predictedPlayerState.stats ); + + 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) { + 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) { + 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) { + 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) { + 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) { + 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) { + 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_STEP_4: + case EV_STEP_8: + case EV_STEP_12: + case EV_STEP_16: // smooth out step up 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 + step = 4 * (event - EV_STEP_4 + 1 ); + cg.stepChange = oldStep + step; + if ( cg.stepChange > MAX_STEP_CHANGE ) { + cg.stepChange = MAX_STEP_CHANGE; + } + cg.stepTime = cg.time; + break; + } + + case EV_JUMP_PAD: + DEBUGNAME("EV_JUMP_PAD"); +// CG_Printf( "EV_JUMP_PAD w/effect #%i\n", es->eventParm ); + { + localEntity_t *smoke; + vec3_t up = {0, 0, 1}; + + + smoke = CG_SmokePuff( cent->lerpOrigin, up, + 32, + 1, 1, 1, 0.33f, + 1000, + cg.time, 0, + LEF_PUFF_DONT_SCALE, + cgs.media.smokePuffShader ); + } + // boing sound at origin, jump sound on player + trap_S_StartSound ( cent->lerpOrigin, -1, CHAN_VOICE, cgs.media.jumpPadSound ); + trap_S_StartSound (NULL, es->number, CHAN_VOICE, CG_CustomSound( es->number, "*jump1.wav" ) ); + break; + + case EV_JUMP: + DEBUGNAME("EV_JUMP"); + trap_S_StartSound (NULL, es->number, CHAN_VOICE, CG_CustomSound( es->number, "*jump1.wav" ) ); + break; + case EV_TAUNT: + DEBUGNAME("EV_TAUNT"); + 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; + + case EV_ITEM_PICKUP: + DEBUGNAME("EV_ITEM_PICKUP"); + { + gitem_t *item; + int index; + + index = es->eventParm; // player predicted + + if ( index < 1 || index >= bg_numItems ) { + break; + } + item = &bg_itemlist[ index ]; + + // powerups and team items will have a separate global sound, this one + // will be played at prediction time + if ( item->giType == IT_POWERUP || item->giType == IT_TEAM) { + trap_S_StartSound (NULL, es->number, CHAN_AUTO, cgs.media.n_healthSound ); + } else { + trap_S_StartSound (NULL, es->number, CHAN_AUTO, trap_S_RegisterSound( item->pickup_sound, qfalse ) ); + } + + // show icon and name on status bar + if ( es->number == cg.snap->ps.clientNum ) { + CG_ItemPickup( index ); + } + } + break; + + case EV_GLOBAL_ITEM_PICKUP: + DEBUGNAME("EV_GLOBAL_ITEM_PICKUP"); + { + gitem_t *item; + int index; + + index = es->eventParm; // player predicted + + if ( index < 1 || index >= bg_numItems ) { + break; + } + item = &bg_itemlist[ index ]; + // powerup pickups are global + if( item->pickup_sound ) { + trap_S_StartSound (NULL, cg.snap->ps.clientNum, CHAN_AUTO, trap_S_RegisterSound( item->pickup_sound, qfalse ) ); + } + + // show icon and name on status bar + if ( es->number == cg.snap->ps.clientNum ) { + CG_ItemPickup( index ); + } + } + break; + + // + // weapon events + // + case EV_NOAMMO: + DEBUGNAME("EV_NOAMMO"); +// trap_S_StartSound (NULL, es->number, CHAN_AUTO, cgs.media.noAmmoSound ); + if ( es->number == cg.snap->ps.clientNum ) { + CG_OutOfAmmoChange(); + } + 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 ); + break; + + case EV_USE_ITEM0: + DEBUGNAME("EV_USE_ITEM0"); + CG_UseItem( cent ); + break; + case EV_USE_ITEM1: + DEBUGNAME("EV_USE_ITEM1"); + CG_UseItem( cent ); + break; + case EV_USE_ITEM2: + DEBUGNAME("EV_USE_ITEM2"); + CG_UseItem( cent ); + break; + case EV_USE_ITEM3: + DEBUGNAME("EV_USE_ITEM3"); + CG_UseItem( cent ); + break; + case EV_USE_ITEM4: + DEBUGNAME("EV_USE_ITEM4"); + CG_UseItem( cent ); + break; + case EV_USE_ITEM5: + DEBUGNAME("EV_USE_ITEM5"); + CG_UseItem( cent ); + break; + case EV_USE_ITEM6: + DEBUGNAME("EV_USE_ITEM6"); + CG_UseItem( cent ); + break; + case EV_USE_ITEM7: + DEBUGNAME("EV_USE_ITEM7"); + CG_UseItem( cent ); + break; + case EV_USE_ITEM8: + DEBUGNAME("EV_USE_ITEM8"); + CG_UseItem( cent ); + break; + case EV_USE_ITEM9: + DEBUGNAME("EV_USE_ITEM9"); + CG_UseItem( cent ); + break; + case EV_USE_ITEM10: + DEBUGNAME("EV_USE_ITEM10"); + CG_UseItem( cent ); + break; + case EV_USE_ITEM11: + DEBUGNAME("EV_USE_ITEM11"); + CG_UseItem( cent ); + break; + case EV_USE_ITEM12: + DEBUGNAME("EV_USE_ITEM12"); + CG_UseItem( cent ); + break; + case EV_USE_ITEM13: + DEBUGNAME("EV_USE_ITEM13"); + CG_UseItem( cent ); + break; + case EV_USE_ITEM14: + DEBUGNAME("EV_USE_ITEM14"); + CG_UseItem( cent ); + break; + + //================================================================= + + // + // other events + // + case EV_PLAYER_TELEPORT_IN: + DEBUGNAME("EV_PLAYER_TELEPORT_IN"); + trap_S_StartSound (NULL, es->number, CHAN_AUTO, cgs.media.teleInSound ); + CG_SpawnEffect( position); + break; + + case EV_PLAYER_TELEPORT_OUT: + DEBUGNAME("EV_PLAYER_TELEPORT_OUT"); + trap_S_StartSound (NULL, es->number, CHAN_AUTO, cgs.media.teleOutSound ); + CG_SpawnEffect( position); + break; + + case EV_ITEM_POP: + DEBUGNAME("EV_ITEM_POP"); + trap_S_StartSound (NULL, es->number, CHAN_AUTO, cgs.media.respawnSound ); + break; + case EV_ITEM_RESPAWN: + DEBUGNAME("EV_ITEM_RESPAWN"); + cent->miscTime = cg.time; // scale up from this + trap_S_StartSound (NULL, es->number, CHAN_AUTO, cgs.media.respawnSound ); + break; + + //TA: make droid items "grow" + case EV_ITEM_GROW: + DEBUGNAME("EV_ITEM_GROW"); + cent->miscTime = cg.time; // scale up from this + break; + + case EV_GRENADE_BOUNCE: + DEBUGNAME("EV_GRENADE_BOUNCE"); + if ( rand() & 1 ) { + trap_S_StartSound (NULL, es->number, CHAN_AUTO, cgs.media.hgrenb1aSound ); + } else { + trap_S_StartSound (NULL, es->number, CHAN_AUTO, cgs.media.hgrenb2aSound ); + } + break; + + // + // missile impacts + // + case EV_MISSILE_HIT: + DEBUGNAME("EV_MISSILE_HIT"); + ByteToDir( es->eventParm, dir ); + CG_MissileHitPlayer( es->weapon, position, dir, es->otherEntityNum ); + break; + + case EV_MISSILE_MISS: + DEBUGNAME("EV_MISSILE_MISS"); + ByteToDir( es->eventParm, dir ); + CG_MissileHitWall( es->weapon, 0, position, dir, IMPACTSOUND_DEFAULT ); + break; + + case EV_MISSILE_MISS_METAL: + DEBUGNAME("EV_MISSILE_MISS_METAL"); + ByteToDir( es->eventParm, dir ); + CG_MissileHitWall( es->weapon, 0, position, dir, IMPACTSOUND_METAL ); + break; + + case EV_ITEM_EXPLOSION: + DEBUGNAME("EV_ITEM_EXPLOSION"); + ByteToDir( es->eventParm, dir ); + CG_Explosion( 0, position, dir ); + break; + + case EV_RAILTRAIL: + DEBUGNAME("EV_RAILTRAIL"); + cent->currentState.weapon = WP_RAILGUN; + // if the end was on a nomark surface, don't make an explosion + if ( es->eventParm != 255 ) { + ByteToDir( es->eventParm, dir ); + CG_MissileHitWall( es->weapon, es->clientNum, position, dir, IMPACTSOUND_DEFAULT ); + } + CG_RailTrail( ci, es->origin2, es->pos.trBase ); + 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; + + // + // powerup events + // + case EV_POWERUP_QUAD: + DEBUGNAME("EV_POWERUP_QUAD"); + if ( es->number == cg.snap->ps.clientNum ) { + cg.powerupActive = PW_QUAD; + cg.powerupTime = cg.time; + } + trap_S_StartSound (NULL, es->number, CHAN_ITEM, cgs.media.quadSound ); + break; + case EV_POWERUP_BATTLESUIT: + DEBUGNAME("EV_POWERUP_BATTLESUIT"); + if ( es->number == cg.snap->ps.clientNum ) { + cg.powerupActive = PW_BATTLESUIT; + cg.powerupTime = cg.time; + } + trap_S_StartSound (NULL, es->number, CHAN_ITEM, cgs.media.protectSound ); + break; + case EV_POWERUP_REGEN: + DEBUGNAME("EV_POWERUP_REGEN"); + if ( es->number == cg.snap->ps.clientNum ) { + cg.powerupActive = PW_REGEN; + cg.powerupTime = cg.time; + } + trap_S_StartSound (NULL, es->number, CHAN_ITEM, cgs.media.regenSound ); + break; + + case EV_GIB_PLAYER: + DEBUGNAME("EV_GIB_PLAYER"); + trap_S_StartSound( NULL, es->number, CHAN_BODY, cgs.media.gibSound ); + CG_GibPlayer( cent->lerpOrigin ); + break; + + case EV_GIB_GENERIC: + DEBUGNAME("EV_GIB_GENERIC"); + trap_S_StartSound( NULL, es->number, CHAN_BODY, cgs.media.gibSound ); + CG_GenericGib( cent->lerpOrigin ); + 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_MENU: + DEBUGNAME("EV_MENU"); + CG_Menu( cent, es->eventParm ); + break; + + default: + DEBUGNAME("UNKNOWN"); + CG_Error( "Unknown event: %i", event ); + break; + } + +} + + +/* +============== +CG_CheckEvents + +============== +*/ +void CG_CheckEvents( centity_t *cent ) { + // check for event-only entities + if ( cent->currentState.eType > ET_EVENTS ) { + if ( cent->previousEvent ) { + return; // already fired + } + cent->previousEvent = 1; + + cent->currentState.event = cent->currentState.eType - ET_EVENTS; + } 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 ); +} + diff --git a/src/cgame/cg_local.h b/src/cgame/cg_local.h new file mode 100644 index 00000000..bbf15533 --- /dev/null +++ b/src/cgame/cg_local.h @@ -0,0 +1,1630 @@ +// Copyright (C) 1999-2000 Id Software, Inc. +// + +/* + * Portions Copyright (C) 2000-2001 Tim Angus + * + * This program 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, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ + +/* To assertain which portions are licensed under the GPL and which are + * licensed by Id Software, Inc. please run a diff between the equivalent + * versions of the "Tremulous" modification and the unmodified "Quake3" + * game source code. + */ + +#include "../game/q_shared.h" +#include "tr_types.h" +#include "../game/bg_public.h" +#include "cg_public.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 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 STEP_TIME 200 +#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 + +#define TEAMCHAT_WIDTH 80 +#define TEAMCHAT_HEIGHT 8 + +// 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" + +#define DEFAULT_REDTEAM_NAME "Stroggs" +#define DEFAULT_BLUETEAM_NAME "Pagans" + +typedef enum { + FOOTSTEP_NORMAL, + FOOTSTEP_BOOT, + FOOTSTEP_FLESH, + FOOTSTEP_MECH, + FOOTSTEP_ENERGY, + FOOTSTEP_METAL, + FOOTSTEP_SPLASH, + + FOOTSTEP_TOTAL +} footstep_t; + +typedef enum { + IMPACTSOUND_DEFAULT, + IMPACTSOUND_METAL, + IMPACTSOUND_FLESH +} impactSound_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 + +// 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; + + +typedef struct { + lerpFrame_t legs, torso, flag; + int painTime; + int painDirection; // flip from 0 to 1 + int lightningFiring; + + // railgun trail spawning + vec3_t railgunImpact; + qboolean railgunFlash; + + // machinegun spinning + float barrelAngle; + int barrelTime; + qboolean barrelSpinning; +} playerEntity_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 previousEvent; + int teleportFlag; + + int trailTime; // so missile trails can handle dropped initial packets + int dustTrailTime; + int miscTime; + + 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; + + //TA: value to store corpse number + int corpseNum; +} centity_t; + + +//====================================================================== + +// local entities are created as a result of events or predicted actions, +// and live independantly from all server transmitted entities + +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 enum { + LE_MARK, + LE_EXPLOSION, + LE_SPRITE_EXPLOSION, + LE_FRAGMENT, + LE_MOVE_SCALE_FADE, + LE_FALL_SCALE_FADE, + LE_FADE_RGB, + LE_SCALE_FADE +} leType_t; + +typedef enum { + LEF_PUFF_DONT_SCALE = 0x0001, // do not scale size over time + LEF_TUMBLE = 0x0002 // tumble over time, used for ejecting shells +} leFlag_t; + +typedef enum { + LEMT_NONE, + LEMT_BURN, + LEMT_BLOOD, + LEMT_GREENBLOOD, //TA: when droids are injured + LEMT_BANG //TA: human item explosions +} leMarkType_t; // fragment local entities can leave marks on walls + +typedef enum { + LEBS_NONE, + LEBS_BLOOD, + LEBS_BANG, //TA: human item explosions + LEBS_BRASS +} leBounceSoundType_t; // fragment local entities can make sounds on impacts + +typedef struct localEntity_s { + struct localEntity_s *prev, *next; + leType_t leType; + int leFlags; + + int startTime; + int endTime; + int fadeInTime; + + float lifeRate; // 1.0 / (endTime - startTime) + + trajectory_t pos; + trajectory_t angles; + + float bounceFactor; // 0.0 = no bounce, 1.0 = perfect + + float color[4]; + + float radius; + + float light; + vec3_t lightColor; + + leMarkType_t leMarkType; // mark to leave on fragment impact + leBounceSoundType_t leBounceSoundType; + + refEntity_t refEntity; +} localEntity_t; + +//====================================================================== + + +typedef struct { + int client; + int score; + int ping; + int time; + int scoreFlags; + int powerUps; + int accuracy; + int impressiveCount; + int excellentCount; + int guantletCount; + int defendCount; + int assistCount; + int captures; + qboolean perfect; + int team; +} score_t; + +// each client has an associated clientInfo_t +// that contains media references necessary to present the +// client model and other color coded effects +// this is regenerated each time a client's configstring changes, +// usually as a result of a userinfo (name, model, etc) change +#define MAX_CUSTOM_SOUNDS 32 +typedef struct { + qboolean infoValid; + + char name[MAX_QPATH]; + team_t team; + + int botSkill; // 0 = not bot, 1-5 = bot + + vec3_t color; + + 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]; + char redTeam[MAX_TEAMNAME]; + char blueTeam[MAX_TEAMNAME]; + + qboolean deferred; + + qboolean newAnims; // true if using the new mission pack animations + + 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 modelIcon; + + animation_t animations[MAX_TOTALANIMATIONS]; + + sfxHandle_t sounds[MAX_CUSTOM_SOUNDS]; +} clientInfo_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; + gitem_t *item; + + 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 + + float flashDlight; + vec3_t flashDlightColor; + sfxHandle_t flashSound[4]; // fast firing weapons randomly choose + + qhandle_t weaponIcon; + qhandle_t ammoIcon; + + qhandle_t ammoModel; + + qhandle_t missileModel; + sfxHandle_t missileSound; + void (*missileTrailFunc)( centity_t *, const struct weaponInfo_s *wi ); + float missileDlight; + vec3_t missileDlightColor; + int missileRenderfx; + + void (*ejectBrassFunc)( centity_t * ); + + float trailRadius; + float wiTrailTime; + + sfxHandle_t readySound; + sfxHandle_t firingSound; + qboolean loopFireSound; +} weaponInfo_t; + +typedef struct upgradeInfo_s { + qboolean registered; + gitem_t *item; + + qhandle_t upgradeIcon; +} upgradeInfo_t; + + +// each IT_* item has an associated itemInfo_t +// that constains media references necessary to present the +// item and its effects +typedef struct { + qboolean registered; + qhandle_t models[MAX_ITEM_MODELS]; + qhandle_t icon; +} itemInfo_t; + + +typedef struct { + int itemNum; +} powerupInfo_t; + +#define MAX_SKULLTRAIL 10 + +typedef struct { + vec3_t positions[MAX_SKULLTRAIL]; + int numpositions; +} skulltrail_t; + + +#define MAX_REWARDSTACK 10 +#define MAX_SOUNDBUFFER 20 + +//====================================================================== + +// 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 + +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; + 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 + + // skull trails + skulltrail_t skulltrails[MAX_CLIENTS]; + + // 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]; + + // 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; + char testModelName[MAX_QPATH]; + qboolean testGun; + +} 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 charsetProp; + qhandle_t charsetPropGlow; + qhandle_t charsetPropB; + qhandle_t whiteShader; + + qhandle_t redCubeModel; + qhandle_t blueCubeModel; + qhandle_t redCubeIcon; + qhandle_t blueCubeIcon; + + qhandle_t redFlagModel; + qhandle_t blueFlagModel; + qhandle_t neutralFlagModel; + qhandle_t redFlagShader[3]; + qhandle_t blueFlagShader[3]; + qhandle_t flagShader[4]; +#ifdef NEW_ANIMS + qhandle_t flagPoleModel; + qhandle_t flagFlapModel; + + qhandle_t redFlagFlapSkin; + qhandle_t blueFlagFlapSkin; + qhandle_t neutralFlagFlapSkin; + + qhandle_t redFlagBaseModel; + qhandle_t blueFlagBaseModel; + qhandle_t neutralFlagBaseModel; + + qhandle_t overloadBaseModel; + qhandle_t overloadTargetModel; + qhandle_t overloadLightsModel; + qhandle_t overloadEnergyModel; + + qhandle_t harvesterModel; + qhandle_t harvesterRedSkin; + qhandle_t harvesterBlueSkin; + qhandle_t harvesterNeutralModel; +#endif + + qhandle_t armorModel; + qhandle_t armorIcon; + + qhandle_t teamStatusBar; + + qhandle_t deferShader; + + // gib explosions + qhandle_t gibAbdomen; + qhandle_t gibArm; + qhandle_t gibChest; + qhandle_t gibFist; + qhandle_t gibFoot; + qhandle_t gibForearm; + qhandle_t gibIntestine; + qhandle_t gibLeg; + qhandle_t gibSkull; + qhandle_t gibBrain; + + qhandle_t smoke2; + + qhandle_t machinegunBrassModel; + qhandle_t shotgunBrassModel; + + qhandle_t railRingsShader; + qhandle_t railCoreShader; + + qhandle_t lightningShader; + + qhandle_t friendShader; + + qhandle_t balloonShader; + qhandle_t connectionShader; + + qhandle_t selectShader; + qhandle_t viewBloodShader; + qhandle_t tracerShader; + qhandle_t crosshairShader[NUM_CROSSHAIRS]; + qhandle_t lagometerShader; + qhandle_t backTileShader; + qhandle_t noammoShader; + + qhandle_t smokePuffShader; + qhandle_t smokePuffRageProShader; + qhandle_t shotgunSmokePuffShader; + qhandle_t plasmaBallShader; + qhandle_t waterBubbleShader; + qhandle_t bloodTrailShader; + + //TA: extra stuff + qhandle_t explosionShader; + qhandle_t greenBloodTrailShader; + qhandle_t greenBloodMarkShader; + qhandle_t greenBloodExplosionShader; + qhandle_t explosionTrailShader; + + qhandle_t humanNV; + qhandle_t droidNav10; + qhandle_t droidNav15; + qhandle_t droidNav20; + qhandle_t droidNav25; + qhandle_t droidNav30; + qhandle_t droidNav35; + qhandle_t droidNav40; + qhandle_t droidNav45; + qhandle_t droidNav50; + qhandle_t droidNav55; + qhandle_t droidNav60; + qhandle_t droidNav65; + qhandle_t droidNav70; + qhandle_t droidNav75; + qhandle_t droidNav80; + qhandle_t droidHealth; + + qhandle_t flameShader; + qhandle_t flameExplShader; + qhandle_t creepShader; + + qhandle_t scannerShader; + qhandle_t scannerBlipShader; + qhandle_t scannerLineShader; + + + qhandle_t numberShaders[11]; + + qhandle_t shadowMarkShader; + + qhandle_t botSkillShaders[5]; + + // wall mark shaders + qhandle_t wakeMarkShader; + qhandle_t bloodMarkShader; + qhandle_t bulletMarkShader; + qhandle_t burnMarkShader; + qhandle_t holeMarkShader; + qhandle_t energyMarkShader; + + // powerup shaders + qhandle_t quadShader; + qhandle_t redQuadShader; + qhandle_t quadWeaponShader; + qhandle_t invisShader; + qhandle_t regenShader; + qhandle_t battleSuitShader; + qhandle_t battleWeaponShader; + qhandle_t hastePuffShader; + qhandle_t redKamikazeShader; + qhandle_t blueKamikazeShader; + + // weapon effect models + qhandle_t bulletFlashModel; + qhandle_t ringFlashModel; + qhandle_t dishFlashModel; + qhandle_t lightningExplosionModel; + + // weapon effect shaders + qhandle_t railExplosionShader; + qhandle_t plasmaExplosionShader; + qhandle_t bulletExplosionShader; + qhandle_t rocketExplosionShader; + qhandle_t grenadeExplosionShader; + qhandle_t bfgExplosionShader; + qhandle_t bloodExplosionShader; + + // special effects models + qhandle_t teleportEffectModel; + qhandle_t teleportEffectShader; + + // scoreboard headers + qhandle_t scoreboardName; + qhandle_t scoreboardPing; + qhandle_t scoreboardScore; + qhandle_t scoreboardTime; + + // medals shown during gameplay + qhandle_t medalImpressive; + qhandle_t medalExcellent; + qhandle_t medalGauntlet; + qhandle_t medalDefend; + qhandle_t medalAssist; + qhandle_t medalCapture; + + // sounds + sfxHandle_t quadSound; + sfxHandle_t tracerSound; + sfxHandle_t selectSound; + sfxHandle_t useNothingSound; + sfxHandle_t wearOffSound; + sfxHandle_t footsteps[FOOTSTEP_TOTAL][4]; + sfxHandle_t sfx_lghit1; + sfxHandle_t sfx_lghit2; + sfxHandle_t sfx_lghit3; + sfxHandle_t sfx_ric1; + sfxHandle_t sfx_ric2; + sfxHandle_t sfx_ric3; + sfxHandle_t sfx_railg; + sfxHandle_t sfx_rockexp; + sfxHandle_t sfx_plasmaexp; + sfxHandle_t gibSound; + sfxHandle_t gibBounce1Sound; + sfxHandle_t gibBounce2Sound; + sfxHandle_t gibBounce3Sound; + sfxHandle_t teleInSound; + sfxHandle_t teleOutSound; + sfxHandle_t noAmmoSound; + sfxHandle_t respawnSound; + sfxHandle_t talkSound; + sfxHandle_t landSound; + sfxHandle_t fallSound; + sfxHandle_t jumpPadSound; + + sfxHandle_t oneMinuteSound; + sfxHandle_t fiveMinuteSound; + sfxHandle_t suddenDeathSound; + + sfxHandle_t threeFragSound; + sfxHandle_t twoFragSound; + sfxHandle_t oneFragSound; + + sfxHandle_t hitSound; + sfxHandle_t hitSoundHighArmor; + sfxHandle_t hitSoundLowArmor; + sfxHandle_t hitTeamSound; + sfxHandle_t impressiveSound; + sfxHandle_t excellentSound; + sfxHandle_t deniedSound; + sfxHandle_t humiliationSound; + sfxHandle_t assistSound; + sfxHandle_t defendSound; + sfxHandle_t firstImpressiveSound; + sfxHandle_t firstExcellentSound; + sfxHandle_t firstHumiliationSound; + + sfxHandle_t takenLeadSound; + sfxHandle_t tiedLeadSound; + sfxHandle_t lostLeadSound; + + sfxHandle_t voteNow; + sfxHandle_t votePassed; + sfxHandle_t voteFailed; + + sfxHandle_t watrInSound; + sfxHandle_t watrOutSound; + sfxHandle_t watrUnSound; + + sfxHandle_t flightSound; + sfxHandle_t medkitSound; + + sfxHandle_t weaponHoverSound; + + // teamplay sounds + sfxHandle_t captureAwardSound; + sfxHandle_t redScoredSound; + sfxHandle_t blueScoredSound; + sfxHandle_t redLeadsSound; + sfxHandle_t blueLeadsSound; + sfxHandle_t teamsTiedSound; + + sfxHandle_t captureYourTeamSound; + sfxHandle_t captureOpponentSound; + sfxHandle_t returnYourTeamSound; + sfxHandle_t returnOpponentSound; + sfxHandle_t takenYourTeamSound; + sfxHandle_t takenOpponentSound; + + sfxHandle_t redFlagReturnedSound; + sfxHandle_t blueFlagReturnedSound; + sfxHandle_t neutralFlagReturnedSound; + sfxHandle_t enemyTookYourFlagSound; + sfxHandle_t enemyTookTheFlagSound; + sfxHandle_t yourTeamTookEnemyFlagSound; + sfxHandle_t yourTeamTookTheFlagSound; + sfxHandle_t youHaveFlagSound; + sfxHandle_t yourBaseIsUnderAttackSound; + sfxHandle_t holyShitSound; + + // tournament sounds + sfxHandle_t count3Sound; + sfxHandle_t count2Sound; + sfxHandle_t count1Sound; + sfxHandle_t countFightSound; + sfxHandle_t countPrepareSound; + + qhandle_t cursor; + qhandle_t selectCursor; + qhandle_t sizeCursor; + + sfxHandle_t regenSound; + sfxHandle_t protectSound; + sfxHandle_t n_healthSound; + sfxHandle_t hgrenb1aSound; + sfxHandle_t hgrenb2aSound; + sfxHandle_t wstbimplSound; + sfxHandle_t wstbimpmSound; + sfxHandle_t wstbimpdSound; + sfxHandle_t wstbactvSound; + +} cgMedia_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 + gametype_t gametype; + int dmflags; + int teamflags; + int fraglimit; + int capturelimit; + int timelimit; + int maxclients; + char mapname[MAX_QPATH]; + char redTeam[MAX_QPATH]; + char blueTeam[MAX_QPATH]; + + 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 + int redflag, blueflag; // flag status from configstrings + int flagStatus; + + qboolean newHud; + + int hBuildPoints, aBuildPoints; + + // + // locally derived information from gamestate + // + qhandle_t gameModels[MAX_MODELS]; + 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]; + + // teamchat width is *3 because of embedded color codes + char teamChatMsgs[TEAMCHAT_HEIGHT][TEAMCHAT_WIDTH*3+1]; + int teamChatMsgTimes[TEAMCHAT_HEIGHT]; + int teamChatPos; + int teamLastChatPos; + + int cursorX; + int cursorY; + qboolean eventHandling; + qboolean mouseCaptured; + qboolean sizingHud; + void *capturedItem; + qhandle_t activeCursor; + + // orders + int currentOrder; + qboolean orderPending; + int orderTime; + int currentVoiceClient; + int acceptOrderTime; + int acceptTask; + int acceptLeader; + char acceptVoice[MAX_NAME_LENGTH]; + + // 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]; + +extern itemInfo_t cg_items[MAX_ITEMS]; +extern markPoly_t cg_markPolys[MAX_MARK_POLYS]; + +//TA: +typedef struct +{ + vec3_t droidItemPositions[ MAX_ITEMS ]; + int droidItemTimes[ MAX_ITEMS ]; + int numDroidItems; + + vec3_t humanItemPositions[ MAX_ITEMS ]; + int numHumanItems; + + vec3_t droidClientPositions[ MAX_CLIENTS ]; + int droidClientClass; + int numDroidClients; + + vec3_t humanClientPositions[ MAX_CLIENTS ]; + int humanClientClass; + int numHumanClients; + +} cgItemPos_t; + +extern cgItemPos_t cgIP; + + +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_drawFPS; +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_crosshairSize; +extern vmCvar_t cg_crosshairHealth; +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_railTrailTime; +extern vmCvar_t cg_errorDecay; +extern vmCvar_t cg_nopredict; +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_teamChatTime; +extern vmCvar_t cg_teamChatHeight; +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_creepRes; +extern vmCvar_t cg_drawSurfNormal; + +// +// 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 ); + +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_RankRunFrame( void ); +void CG_SetScoreSelection(void *menu); +score_t *CG_GetSelectedScore(); +void CG_BuildSpectatorString(); + + +// +// cg_view.c +// +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_ZoomDown_f( void ); +void CG_ZoomUp_f( void ); +void CG_AddBufferedSound( sfxHandle_t sfx); +void CG_DrawActiveFrame( int serverTime, stereoFrame_t stereoView, qboolean demoPlayback ); + + +// +// cg_drawtools.c +// +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 ); +//TA: draw a pic weighted between two colors. <- hmm me is turning into a yank. +void CG_DrawFadePic( float x, float y, float width, float height, vec4_t fcolor, vec4_t tcolor, float amount, qhandle_t hShader ); +void CG_DrawString( float x, float y, const char *string, + float charWidth, float charHeight, const float *modulate ); + + +void CG_DrawStringExt( int x, int y, const char *string, const float *setColor, + qboolean forceColor, qboolean shadow, int charWidth, int charHeight, int maxChars ); +void CG_DrawBigString( int x, int y, const char *s, float alpha ); +void CG_DrawBigStringColor( int x, int y, const char *s, vec4_t color ); +void CG_DrawSmallString( int x, int y, const char *s, float alpha ); +void CG_DrawSmallStringColor( int x, int y, const char *s, vec4_t color ); + +int CG_DrawStrlen( const char *str ); + +float *CG_FadeColor( int startMsec, int totalMsec ); +float *CG_TeamColor( int team ); +void CG_TileClear( void ); +void CG_ColorForHealth( vec4_t hcolor ); +void CG_GetColorForHealth( int health, int armor, vec4_t hcolor ); + +void UI_DrawProportionalString( int x, int y, const char* str, int style, vec4_t color ); +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); + + +// +// cg_draw.c +// +extern int sortedTeamPlayers[TEAM_MAXOVERLAY]; +extern int numSortedTeamPlayers; +extern int drawTeamOverlayModificationCount; +extern char systemChat[256]; +extern char teamChat1[256]; +extern char teamChat2[256]; + +void CG_AddLagometerFrameInfo( void ); +void CG_AddLagometerSnapshotInfo( snapshot_t *snap ); +void CG_CenterPrint( const char *str, int y, int charWidth ); +void CG_DrawHead( float x, float y, float w, float h, int clientNum, vec3_t headAngles ); +void CG_DrawActive( stereoFrame_t stereoView ); +void CG_DrawFlagModel( float x, float y, float w, float h, int team, qboolean force2D ); +void CG_DrawTeamBackground( int x, int y, int w, int h, float alpha, int team ); +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); +void CG_SelectPrevPlayer(); +void CG_SelectNextPlayer(); +float CG_GetValue(int ownerDraw); +qboolean CG_OwnerDrawVisible(int flags); +void CG_RunMenuScript(char **args); +void CG_ShowResponseHead(); +void CG_SetPrintString(int type, const char *p); +void CG_InitTeamChat(); +void CG_GetTeamColor(vec4_t *color); +const char *CG_GetGameStatusText(); +const char *CG_GetKillerText(); +void CG_Draw3DModel( float x, float y, float w, float h, qhandle_t model, qhandle_t skin, vec3_t origin, vec3_t angles ); +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_CheckOrderPending(); +const char *CG_GameTypeString(); +qboolean CG_YourTeamHasFlag(); +qboolean CG_OtherTeamHasFlag(); +qhandle_t CG_StatusHandle(int task); + +// +// cg_player.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( int clientNum ); +sfxHandle_t CG_CustomSound( int clientNum, const char *soundName ); + +// +// cg_predict.c +// +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_PredictPlayerState( void ); +void CG_LoadDeferredPlayers( void ); + + +// +// cg_events.c +// +void CG_CheckEvents( centity_t *cent ); +const char *CG_PlaceString( int rank ); +void CG_EntityEvent( centity_t *cent, vec3_t position ); +void CG_PainEvent( centity_t *cent, int health ); + + +// +// cg_ents.c +// +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_RegisterWeapon( int weaponNum ); +void CG_RegisterItemVisuals( int itemNum ); + +void CG_FireWeapon( centity_t *cent ); +void CG_MissileHitWall( int weapon, int clientNum, vec3_t origin, vec3_t dir, impactSound_t soundType ); +void CG_Explosion( int clientNum, vec3_t origin, vec3_t dir ); +void CG_MissileHitPlayer( int weapon, vec3_t origin, vec3_t dir, int entityNum ); +void CG_ShotgunFire( entityState_t *es ); +void CG_Bullet( vec3_t origin, int sourceEntityNum, vec3_t normal, qboolean flesh, int fleshEntityNum ); + +void CG_RailTrail( clientInfo_t *ci, vec3_t start, vec3_t end ); +void CG_GrappleTrail( centity_t *ent, const weaponInfo_t *wi ); +void CG_AddViewWeapon (playerState_t *ps); +void CG_AddPlayerWeapon( refEntity_t *parent, playerState_t *ps, centity_t *cent ); +void CG_DrawWeaponSelect( void ); + +void CG_OutOfAmmoChange( void ); // should this be in pmove? + + +// +// cg_creep.c +// +void CG_Creep( centity_t *cent ); + +// +// cg_scanner.c +// +void CG_Scanner( void ); + +// +// 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_localents.c +// +void CG_InitLocalEntities( void ); +localEntity_t *CG_AllocLocalEntity( void ); +void CG_AddLocalEntities( void ); + +// +// cg_effects.c +// +localEntity_t *CG_SmokePuff( const vec3_t p, + const vec3_t vel, + float radius, + float r, float g, float b, float a, + float duration, + int startTime, + int fadeInTime, + int leFlags, + qhandle_t hShader ); +void CG_BubbleTrail( vec3_t start, vec3_t end, float spacing ); +void CG_SpawnEffect( vec3_t org ); +void CG_GibPlayer( vec3_t playerOrigin ); +void CG_BigExplode( vec3_t playerOrigin ); + +void CG_Bleed( vec3_t origin, int entityNum ); + +localEntity_t *CG_MakeExplosion( vec3_t origin, vec3_t dir, + qhandle_t hModel, qhandle_t shader, int msec, + qboolean isSprite ); + +// +// cg_snapshot.c +// +void CG_ProcessSnapshots( void ); + +// +// cg_info.c +// +void CG_LoadingString( const char *s ); +void CG_LoadingItem( int itemNum ); +void CG_LoadingClient( int clientNum ); +void CG_DrawInformation( void ); + +// +// cg_scoreboard.c +// +qboolean CG_DrawOldScoreboard( void ); +void CG_DrawOldTourneyScoreboard( void ); + +// +// cg_consolecmds.c +// +qboolean CG_ConsoleCommand( void ); +void CG_InitConsoleCommands( void ); + +// +// cg_servercmds.c +// +void CG_ExecuteNewServerCommands( int latestSequence ); +void CG_ParseServerinfo( void ); +void CG_SetConfigValues( void ); +void CG_LoadVoiceChats( void ); +void CG_ShaderStateChanged(void); +void CG_VoiceChatLocal( int mode, qboolean voiceOnly, int clientNum, int color, const char *cmd ); +void CG_PlayBufferedVoiceChats( 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_Alloc( int size ); + +//=============================================== + +// +// 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 ); + +// 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 ); + +// 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 ); + +// 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 ); + +// repatialize 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_AddLightToScene( 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 ); + +//TA: um... +//typedef enum { +// SYSTEM_PRINT, +// CHAT_PRINT, +// TEAMCHAT_PRINT +//}; + + +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 ); + diff --git a/src/cgame/cg_main.c b/src/cgame/cg_main.c new file mode 100644 index 00000000..da803ac0 --- /dev/null +++ b/src/cgame/cg_main.c @@ -0,0 +1,1132 @@ +// Copyright (C) 1999-2000 Id Software, Inc. +// +// cg_main.c -- initialization and primary entry point for cgame + +/* + * Portions Copyright (C) 2000-2001 Tim Angus + * + * This program 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, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ + +/* To assertain which portions are licensed under the GPL and which are + * licensed by Id Software, Inc. please run a diff between the equivalent + * versions of the "Tremulous" modification and the unmodified "Quake3" + * game source code. + */ + +#include "cg_local.h" + +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 +================ +*/ +int 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_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: + 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]; + +itemInfo_t cg_items[MAX_ITEMS]; + +//TA: +cgItemPos_t cgIP; + + +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_drawFPS; +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_crosshairSize; +vmCvar_t cg_crosshairX; +vmCvar_t cg_crosshairY; +vmCvar_t cg_crosshairHealth; +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_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_teamChatTime; +vmCvar_t cg_teamChatHeight; +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_creepRes; +vmCvar_t cg_drawSurfNormal; + + +typedef struct { + vmCvar_t *vmCvar; + char *cvarName; + char *defaultString; + int cvarFlags; +} cvarTable_t; + +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", "0", CVAR_ARCHIVE }, + { &cg_drawFPS, "cg_drawFPS", "0", 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", "4", CVAR_ARCHIVE }, + { &cg_drawCrosshairNames, "cg_drawCrosshairNames", "1", CVAR_ARCHIVE }, + { &cg_drawRewards, "cg_drawRewards", "1", CVAR_ARCHIVE }, + { &cg_crosshairSize, "cg_crosshairSize", "24", CVAR_ARCHIVE }, + { &cg_crosshairHealth, "cg_crosshairHealth", "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", "1", 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_ARCHIVE }, + { &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_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_teamChatTime, "cg_teamChatTime", "3000", CVAR_ARCHIVE }, + { &cg_teamChatHeight, "cg_teamChatHeight", "0", CVAR_ARCHIVE }, + { &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 }, + + // 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_smallFont, "ui_smallFont", "0.25", CVAR_ARCHIVE}, + { &cg_bigFont, "ui_bigFont", "0.4", CVAR_ARCHIVE}, + +// { &cg_pmove_fixed, "cg_pmove_fixed", "0", CVAR_USERINFO | CVAR_ARCHIVE } +}; + +int cvarTableSize = sizeof( cvarTable ) / sizeof( cvarTable[0] ); + +/* +================= +CG_RegisterCvars +================= +*/ +void CG_RegisterCvars( void ) { + int i; + cvarTable_t *cv; + char var[MAX_TOKEN_CHARS]; + + for ( i = 0, cv = cvarTable ; i < cvarTableSize ; i++, cv++ ) { + trap_Cvar_Register( cv->vmCvar, cv->cvarName, + cv->defaultString, cv->cvarFlags ); + } + + // see if we are also running the server on this machine + trap_Cvar_VariableStringBuffer( "sv_running", var, sizeof( var ) ); + cgs.localServer = atoi( var ); + 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 team overlay is on, ask for updates from the server. If its off, + // let the server know so we don't receive it + if ( drawTeamOverlayModificationCount != cg_drawTeamOverlay.modificationCount ) { + drawTeamOverlayModificationCount = cg_drawTeamOverlay.modificationCount; + + if ( cg_drawTeamOverlay.integer > 0 ) { + trap_Cvar_Set( "teamoverlay", "1" ); + } else { + trap_Cvar_Set( "teamoverlay", "0" ); + } + // FIXME E3 HACK + trap_Cvar_Set( "teamoverlay", "1" ); + } + + // 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]; +} + + +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 ); +} + +#ifndef CGAME_HARD_LINKED +// this is only here so the functions in q_shared.c and bg_*.c can link (FIXME) + +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); +} + +#endif + + + +/* +================ +CG_Argv +================ +*/ +const char *CG_Argv( int arg ) { + static char buffer[MAX_STRING_CHARS]; + + trap_Argv( arg, buffer, sizeof( buffer ) ); + + return buffer; +} + + +//======================================================================== + +/* +================= +CG_RegisterItemSounds + +The server says this item is used on this level +================= +*/ +static void CG_RegisterItemSounds( int itemNum ) { + gitem_t *item; + char data[MAX_QPATH]; + char *s, *start; + int len; + + item = &bg_itemlist[ itemNum ]; + + if( item->pickup_sound ) { + trap_S_RegisterSound( item->pickup_sound, qfalse ); + } + + // parse the space seperated precache string for other media + s = item->sounds; + if (!s || !s[0]) + return; + + while (*s) { + start = s; + while (*s && *s != ' ') { + s++; + } + + len = s-start; + if (len >= MAX_QPATH || len < 5) { + CG_Error( "PrecacheItem: %s has bad precache string", + item->classname); + return; + } + memcpy (data, start, len); + data[len] = 0; + if ( *s ) { + s++; + } + + if ( !strcmp(data+len-3, "wav" )) { + trap_S_RegisterSound( data, qfalse ); + } + } +} + + +/* +================= +CG_RegisterSounds + +called during a precache command +================= +*/ +static void CG_RegisterSounds( void ) { + int i; + char items[MAX_ITEMS+1]; + char name[MAX_QPATH]; + const char *soundName; + + // voice commands + + cgs.media.oneMinuteSound = trap_S_RegisterSound( "sound/feedback/1_minute.wav", qfalse ); + cgs.media.fiveMinuteSound = trap_S_RegisterSound( "sound/feedback/5_minute.wav", qfalse ); + cgs.media.suddenDeathSound = trap_S_RegisterSound( "sound/feedback/sudden_death.wav", qfalse ); + cgs.media.oneFragSound = trap_S_RegisterSound( "sound/feedback/1_frag.wav", qfalse ); + cgs.media.twoFragSound = trap_S_RegisterSound( "sound/feedback/2_frags.wav", qfalse ); + cgs.media.threeFragSound = trap_S_RegisterSound( "sound/feedback/3_frags.wav", qfalse ); + cgs.media.count3Sound = trap_S_RegisterSound( "sound/feedback/three.wav", qfalse ); + cgs.media.count2Sound = trap_S_RegisterSound( "sound/feedback/two.wav", qfalse ); + cgs.media.count1Sound = trap_S_RegisterSound( "sound/feedback/one.wav", qfalse ); + cgs.media.countFightSound = trap_S_RegisterSound( "sound/feedback/fight.wav", qfalse ); + cgs.media.countPrepareSound = trap_S_RegisterSound( "sound/feedback/prepare.wav", qfalse ); + + if ( cgs.gametype >= GT_TEAM || cg_buildScript.integer ) { + cgs.media.captureAwardSound = trap_S_RegisterSound( "sound/teamplay/flagcapture_yourteam.wav", qfalse ); + cgs.media.redLeadsSound = trap_S_RegisterSound( "sound/feedback/redleads.wav", qfalse ); + cgs.media.blueLeadsSound = trap_S_RegisterSound( "sound/feedback/blueleads.wav", qfalse ); + cgs.media.teamsTiedSound = trap_S_RegisterSound( "sound/feedback/teamstied.wav", qfalse ); + cgs.media.hitTeamSound = trap_S_RegisterSound( "sound/feedback/hit_teammate.wav", qfalse ); + + cgs.media.redScoredSound = trap_S_RegisterSound( "sound/teamplay/voc_red_scores.wav", qfalse ); + cgs.media.blueScoredSound = trap_S_RegisterSound( "sound/teamplay/voc_blue_scores.wav", qfalse ); + + cgs.media.captureYourTeamSound = trap_S_RegisterSound( "sound/teamplay/flagcapture_yourteam.wav", qfalse ); + cgs.media.captureOpponentSound = trap_S_RegisterSound( "sound/teamplay/flagcapture_opponent.wav", qfalse ); + + cgs.media.returnYourTeamSound = trap_S_RegisterSound( "sound/teamplay/flagreturn_yourteam.wav", qfalse ); + cgs.media.returnOpponentSound = trap_S_RegisterSound( "sound/teamplay/flagreturn_opponent.wav", qfalse ); + + cgs.media.takenYourTeamSound = trap_S_RegisterSound( "sound/teamplay/flagtaken_yourteam.wav", qfalse ); + cgs.media.takenOpponentSound = trap_S_RegisterSound( "sound/teamplay/flagtaken_opponent.wav", qfalse ); + + if ( cgs.gametype == GT_CTF || cg_buildScript.integer ) { + cgs.media.redFlagReturnedSound = trap_S_RegisterSound( "sound/teamplay/voc_red_returned.wav", qfalse ); + cgs.media.blueFlagReturnedSound = trap_S_RegisterSound( "sound/teamplay/voc_blue_returned.wav", qfalse ); + cgs.media.enemyTookYourFlagSound = trap_S_RegisterSound( "sound/teamplay/voc_enemy_flag.wav", qfalse ); + cgs.media.yourTeamTookEnemyFlagSound = trap_S_RegisterSound( "sound/teamplay/voc_team_flag.wav", qfalse ); + } + + cgs.media.youHaveFlagSound = trap_S_RegisterSound( "sound/teamplay/voc_you_flag.wav", qfalse ); + cgs.media.holyShitSound = trap_S_RegisterSound("sound/feedback/voc_holyshit.wav", qfalse); + cgs.media.neutralFlagReturnedSound = trap_S_RegisterSound( "sound/teamplay/flagreturn_opponent.wav", qfalse ); + cgs.media.yourTeamTookTheFlagSound = trap_S_RegisterSound( "sound/teamplay/voc_team_1flag.wav", qfalse ); + cgs.media.enemyTookTheFlagSound = trap_S_RegisterSound( "sound/teamplay/voc_enemy_1flag.wav", qfalse ); + } + + cgs.media.tracerSound = trap_S_RegisterSound( "sound/weapons/machinegun/buletby1.wav", qfalse ); + cgs.media.selectSound = trap_S_RegisterSound( "sound/weapons/change.wav", qfalse ); + cgs.media.wearOffSound = trap_S_RegisterSound( "sound/items/wearoff.wav", qfalse ); + cgs.media.useNothingSound = trap_S_RegisterSound( "sound/items/use_nothing.wav", qfalse ); + cgs.media.gibSound = trap_S_RegisterSound( "sound/player/gibsplt1.wav", qfalse ); + cgs.media.gibBounce1Sound = trap_S_RegisterSound( "sound/player/gibimp1.wav", qfalse ); + cgs.media.gibBounce2Sound = trap_S_RegisterSound( "sound/player/gibimp2.wav", qfalse ); + cgs.media.gibBounce3Sound = trap_S_RegisterSound( "sound/player/gibimp3.wav", qfalse ); + + cgs.media.teleInSound = trap_S_RegisterSound( "sound/world/telein.wav", qfalse ); + cgs.media.teleOutSound = trap_S_RegisterSound( "sound/world/teleout.wav", qfalse ); + cgs.media.respawnSound = trap_S_RegisterSound( "sound/items/respawn1.wav", qfalse ); + + cgs.media.noAmmoSound = trap_S_RegisterSound( "sound/weapons/noammo.wav", qfalse ); + + cgs.media.talkSound = trap_S_RegisterSound( "sound/player/talk.wav", qfalse ); + cgs.media.landSound = trap_S_RegisterSound( "sound/player/land1.wav", qfalse); + + cgs.media.hitSound = trap_S_RegisterSound( "sound/feedback/hit.wav", qfalse ); + + cgs.media.impressiveSound = trap_S_RegisterSound( "sound/feedback/impressive.wav", qfalse ); + cgs.media.excellentSound = trap_S_RegisterSound( "sound/feedback/excellent.wav", qfalse ); + cgs.media.deniedSound = trap_S_RegisterSound( "sound/feedback/denied.wav", qfalse ); + cgs.media.humiliationSound = trap_S_RegisterSound( "sound/feedback/humiliation.wav", qfalse ); + cgs.media.assistSound = trap_S_RegisterSound( "sound/feedback/assist.wav", qfalse ); + cgs.media.defendSound = trap_S_RegisterSound( "sound/feedback/defense.wav", qfalse ); + + cgs.media.takenLeadSound = trap_S_RegisterSound( "sound/feedback/takenlead.wav", qfalse); + cgs.media.tiedLeadSound = trap_S_RegisterSound( "sound/feedback/tiedlead.wav", qfalse); + cgs.media.lostLeadSound = trap_S_RegisterSound( "sound/feedback/lostlead.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.jumpPadSound = trap_S_RegisterSound ("sound/world/jumppad.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/boot%i.wav", i+1); + cgs.media.footsteps[FOOTSTEP_BOOT][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/mech%i.wav", i+1); + cgs.media.footsteps[FOOTSTEP_MECH][i] = trap_S_RegisterSound (name, qfalse); + + Com_sprintf (name, sizeof(name), "sound/player/footsteps/energy%i.wav", i+1); + cgs.media.footsteps[FOOTSTEP_ENERGY][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); + } + + // only register the items that the server says we need + strcpy( items, CG_ConfigString( CS_ITEMS ) ); + + for ( i = 1 ; i < bg_numItems ; i++ ) { + //if ( items[ i ] == '1' || cg_buildScript.integer ) { + CG_RegisterItemSounds( i ); + //} + } + + 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 ); + } + + // FIXME: only needed with item + cgs.media.flightSound = trap_S_RegisterSound( "sound/items/flight.wav", qfalse ); + cgs.media.medkitSound = trap_S_RegisterSound ("sound/items/use_medkit.wav", qfalse); + cgs.media.quadSound = trap_S_RegisterSound("sound/items/damage3.wav", qfalse); + cgs.media.sfx_ric1 = trap_S_RegisterSound ("sound/weapons/machinegun/ric1.wav", qfalse); + cgs.media.sfx_ric2 = trap_S_RegisterSound ("sound/weapons/machinegun/ric2.wav", qfalse); + cgs.media.sfx_ric3 = trap_S_RegisterSound ("sound/weapons/machinegun/ric3.wav", qfalse); + cgs.media.sfx_railg = trap_S_RegisterSound ("sound/weapons/railgun/railgf1a.wav", qfalse); + cgs.media.sfx_rockexp = trap_S_RegisterSound ("sound/weapons/rocket/rocklx1a.wav", qfalse); + cgs.media.sfx_plasmaexp = trap_S_RegisterSound ("sound/weapons/plasma/plasmx1a.wav", qfalse); + + cgs.media.regenSound = trap_S_RegisterSound("sound/items/regen.wav", qfalse); + cgs.media.protectSound = trap_S_RegisterSound("sound/items/protect3.wav", qfalse); + cgs.media.n_healthSound = trap_S_RegisterSound("sound/items/n_health.wav", qfalse ); + cgs.media.hgrenb1aSound = trap_S_RegisterSound("sound/weapons/grenade/hgrenb1a.wav", qfalse); + cgs.media.hgrenb2aSound = trap_S_RegisterSound("sound/weapons/grenade/hgrenb2a.wav", qfalse); + cgs.media.wstbimplSound = trap_S_RegisterSound("sound/weapons/proxmine/wstbimpl.wav", qfalse); + cgs.media.wstbimpmSound = trap_S_RegisterSound("sound/weapons/proxmine/wstbimpm.wav", qfalse); + cgs.media.wstbimpdSound = trap_S_RegisterSound("sound/weapons/proxmine/wstbimpd.wav", qfalse); + cgs.media.wstbactvSound = trap_S_RegisterSound("sound/weapons/proxmine/wstbactv.wav", qfalse); +} + + +//=================================================================================== + + +/* +================= +CG_RegisterGraphics + +This function may execute for a couple of minutes with a slow disk. +================= +*/ +static void CG_RegisterGraphics( void ) { + int i; + char items[MAX_ITEMS+1]; + 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", + }; + + // clear any references to old media + memset( &cg.refdef, 0, sizeof( cg.refdef ) ); + trap_R_ClearScene(); + + CG_LoadingString( cgs.mapname ); + + trap_R_LoadWorldMap( cgs.mapname ); + + // precache status bar pics + CG_LoadingString( "game media" ); + + for ( i=0 ; i<11 ; i++) { + cgs.media.numberShaders[i] = trap_R_RegisterShader( sb_nums[i] ); + } + + cgs.media.botSkillShaders[0] = trap_R_RegisterShader( "menu/art/skill1.tga" ); + cgs.media.botSkillShaders[1] = trap_R_RegisterShader( "menu/art/skill2.tga" ); + cgs.media.botSkillShaders[2] = trap_R_RegisterShader( "menu/art/skill3.tga" ); + cgs.media.botSkillShaders[3] = trap_R_RegisterShader( "menu/art/skill4.tga" ); + cgs.media.botSkillShaders[4] = trap_R_RegisterShader( "menu/art/skill5.tga" ); + + cgs.media.viewBloodShader = trap_R_RegisterShader( "viewBloodBlend" ); + + cgs.media.deferShader = trap_R_RegisterShaderNoMip( "gfx/2d/defer.tga" ); + + cgs.media.scoreboardName = trap_R_RegisterShaderNoMip( "menu/tab/name.tga" ); + cgs.media.scoreboardPing = trap_R_RegisterShaderNoMip( "menu/tab/ping.tga" ); + cgs.media.scoreboardScore = trap_R_RegisterShaderNoMip( "menu/tab/score.tga" ); + cgs.media.scoreboardTime = trap_R_RegisterShaderNoMip( "menu/tab/time.tga" ); + + cgs.media.smokePuffShader = trap_R_RegisterShader( "smokePuff" ); + cgs.media.smokePuffRageProShader = trap_R_RegisterShader( "smokePuffRagePro" ); + cgs.media.shotgunSmokePuffShader = trap_R_RegisterShader( "shotgunSmokePuff" ); + cgs.media.plasmaBallShader = trap_R_RegisterShader( "sprites/plasma1" ); + cgs.media.bloodTrailShader = trap_R_RegisterShader( "bloodTrail" ); + cgs.media.lagometerShader = trap_R_RegisterShader("lagometer" ); + cgs.media.connectionShader = trap_R_RegisterShader( "disconnected" ); + + + //TA: extra stuff + cgs.media.explosionShader = trap_R_RegisterShader( "grenadeExplosion" ); + cgs.media.greenBloodTrailShader = trap_R_RegisterShader( "greenBloodTrail" ); + cgs.media.greenBloodExplosionShader = trap_R_RegisterShader( "greenBloodExplosion" ); + cgs.media.greenBloodMarkShader = trap_R_RegisterShader( "greenBloodMark" ); + cgs.media.explosionTrailShader = trap_R_RegisterShader( "explosionTrail" ); + + cgs.media.flameShader = trap_R_RegisterShader( "sprites/flameball" ); + cgs.media.creepShader = trap_R_RegisterShader( "creep" ); + + cgs.media.scannerBlipShader = trap_R_RegisterShader( "gfx/2d/droidhealth" ); + cgs.media.scannerLineShader = trap_R_RegisterShader( "gfx/2d/func/mult2" ); + cgs.media.scannerShader = trap_R_RegisterShader( "gfx/2d/scanner" ); + + cgs.media.waterBubbleShader = trap_R_RegisterShader( "waterBubble" ); + + cgs.media.tracerShader = trap_R_RegisterShader( "gfx/misc/tracer" ); + cgs.media.selectShader = trap_R_RegisterShader( "gfx/2d/select" ); + + for ( i = 0 ; i < NUM_CROSSHAIRS ; i++ ) { + cgs.media.crosshairShader[i] = trap_R_RegisterShader( va("gfx/2d/crosshair%c", 'a'+i) ); + } + + cgs.media.backTileShader = trap_R_RegisterShader( "gfx/2d/backtile" ); + cgs.media.noammoShader = trap_R_RegisterShader( "icons/noammo" ); + + // powerup shaders + cgs.media.quadShader = trap_R_RegisterShader("powerups/quad" ); + cgs.media.quadWeaponShader = trap_R_RegisterShader("powerups/quadWeapon" ); + cgs.media.battleSuitShader = trap_R_RegisterShader("powerups/battleSuit" ); + cgs.media.battleWeaponShader = trap_R_RegisterShader("powerups/battleWeapon" ); + cgs.media.invisShader = trap_R_RegisterShader("powerups/invisibility" ); + cgs.media.regenShader = trap_R_RegisterShader("powerups/regen" ); + cgs.media.hastePuffShader = trap_R_RegisterShader("hasteSmokePuff" ); + + if ( cgs.gametype == GT_CTF || cg_buildScript.integer ) { + cgs.media.redCubeModel = trap_R_RegisterModel( "models/powerups/orb/r_orb.md3" ); + cgs.media.blueCubeModel = trap_R_RegisterModel( "models/powerups/orb/b_orb.md3" ); + cgs.media.redCubeIcon = trap_R_RegisterShader( "icons/skull_red" ); + cgs.media.blueCubeIcon = trap_R_RegisterShader( "icons/skull_blue" ); + } + + if ( cgs.gametype == GT_CTF || cg_buildScript.integer ) { + cgs.media.redFlagModel = trap_R_RegisterModel( "models/flags/r_flag.md3" ); + cgs.media.blueFlagModel = trap_R_RegisterModel( "models/flags/b_flag.md3" ); + cgs.media.redFlagShader[0] = trap_R_RegisterShaderNoMip( "icons/iconf_red1" ); + cgs.media.redFlagShader[1] = trap_R_RegisterShaderNoMip( "icons/iconf_red2" ); + cgs.media.redFlagShader[2] = trap_R_RegisterShaderNoMip( "icons/iconf_red3" ); + cgs.media.blueFlagShader[0] = trap_R_RegisterShaderNoMip( "icons/iconf_blu1" ); + cgs.media.blueFlagShader[1] = trap_R_RegisterShaderNoMip( "icons/iconf_blu2" ); + cgs.media.blueFlagShader[2] = trap_R_RegisterShaderNoMip( "icons/iconf_blu3" ); + } + + if ( cgs.gametype >= GT_TEAM || cg_buildScript.integer ) { + cgs.media.friendShader = trap_R_RegisterShader( "sprites/foe" ); + cgs.media.redQuadShader = trap_R_RegisterShader("powerups/blueflag" ); + cgs.media.teamStatusBar = trap_R_RegisterShader( "gfx/2d/colorbar.tga" ); + } + + //TA: screenfades + cgs.media.humanNV = trap_R_RegisterShader( "humanNV" ); + cgs.media.droidNav10 = trap_R_RegisterShader( "droidNav10" ); + cgs.media.droidNav15 = trap_R_RegisterShader( "droidNav15" ); + cgs.media.droidNav20 = trap_R_RegisterShader( "droidNav20" ); + cgs.media.droidNav25 = trap_R_RegisterShader( "droidNav25" ); + cgs.media.droidNav30 = trap_R_RegisterShader( "droidNav30" ); + cgs.media.droidNav35 = trap_R_RegisterShader( "droidNav35" ); + cgs.media.droidNav40 = trap_R_RegisterShader( "droidNav40" ); + cgs.media.droidNav45 = trap_R_RegisterShader( "droidNav45" ); + cgs.media.droidNav50 = trap_R_RegisterShader( "droidNav50" ); + cgs.media.droidNav55 = trap_R_RegisterShader( "droidNav55" ); + cgs.media.droidNav60 = trap_R_RegisterShader( "droidNav60" ); + cgs.media.droidNav65 = trap_R_RegisterShader( "droidNav65" ); + cgs.media.droidNav70 = trap_R_RegisterShader( "droidNav70" ); + cgs.media.droidNav75 = trap_R_RegisterShader( "droidNav75" ); + cgs.media.droidNav80 = trap_R_RegisterShader( "droidNav80" ); + cgs.media.droidHealth = trap_R_RegisterShader( "gfx/2d/droidhealth.tga" ); + + cgs.media.armorModel = trap_R_RegisterModel( "models/powerups/armor/armor_yel.md3" ); + cgs.media.armorIcon = trap_R_RegisterShaderNoMip( "icons/iconr_yellow" ); + + cgs.media.machinegunBrassModel = trap_R_RegisterModel( "models/weapons2/shells/m_shell.md3" ); + cgs.media.shotgunBrassModel = trap_R_RegisterModel( "models/weapons2/shells/s_shell.md3" ); + + cgs.media.gibAbdomen = trap_R_RegisterModel( "models/gibs/abdomen.md3" ); + cgs.media.gibArm = trap_R_RegisterModel( "models/gibs/arm.md3" ); + cgs.media.gibChest = trap_R_RegisterModel( "models/gibs/chest.md3" ); + cgs.media.gibFist = trap_R_RegisterModel( "models/gibs/fist.md3" ); + cgs.media.gibFoot = trap_R_RegisterModel( "models/gibs/foot.md3" ); + cgs.media.gibForearm = trap_R_RegisterModel( "models/gibs/forearm.md3" ); + cgs.media.gibIntestine = trap_R_RegisterModel( "models/gibs/intestine.md3" ); + cgs.media.gibLeg = trap_R_RegisterModel( "models/gibs/leg.md3" ); + cgs.media.gibSkull = trap_R_RegisterModel( "models/gibs/skull.md3" ); + cgs.media.gibBrain = trap_R_RegisterModel( "models/gibs/brain.md3" ); + + cgs.media.smoke2 = trap_R_RegisterModel( "models/weapons2/shells/s_shell.md3" ); + + cgs.media.balloonShader = trap_R_RegisterShader( "sprites/balloon3" ); + + cgs.media.bloodExplosionShader = trap_R_RegisterShader( "bloodExplosion" ); + + cgs.media.bulletFlashModel = trap_R_RegisterModel("models/weaphits/bullet.md3"); + cgs.media.ringFlashModel = trap_R_RegisterModel("models/weaphits/ring02.md3"); + cgs.media.dishFlashModel = trap_R_RegisterModel("models/weaphits/boom01.md3"); + cgs.media.teleportEffectModel = trap_R_RegisterModel( "models/misc/telep.md3" ); + cgs.media.teleportEffectShader = trap_R_RegisterShader( "teleportEffect" ); + + cgs.media.medalImpressive = trap_R_RegisterShaderNoMip( "medal_impressive" ); + cgs.media.medalExcellent = trap_R_RegisterShaderNoMip( "medal_excellent" ); + cgs.media.medalGauntlet = trap_R_RegisterShaderNoMip( "medal_gauntlet" ); + cgs.media.medalDefend = trap_R_RegisterShaderNoMip( "medal_defend" ); + cgs.media.medalAssist = trap_R_RegisterShaderNoMip( "medal_assist" ); + cgs.media.medalCapture = trap_R_RegisterShaderNoMip( "medal_capture" ); + + + memset( cg_items, 0, sizeof( cg_items ) ); + memset( cg_weapons, 0, sizeof( cg_weapons ) ); + memset( cg_upgrades, 0, sizeof( cg_upgrades ) ); + + // only register the items that the server says we need + strcpy( items, CG_ConfigString( CS_ITEMS) ); + + for ( i = 1 ; i < bg_numItems ; i++ ) { + if ( items[ i ] == '1' || cg_buildScript.integer ) { + CG_LoadingItem( i ); + CG_RegisterItemVisuals( i ); + } + } + + // wall marks + cgs.media.bulletMarkShader = trap_R_RegisterShader( "gfx/damage/bullet_mrk" ); + cgs.media.burnMarkShader = trap_R_RegisterShader( "gfx/damage/burn_med_mrk" ); + cgs.media.holeMarkShader = trap_R_RegisterShader( "gfx/damage/hole_lg_mrk" ); + cgs.media.energyMarkShader = trap_R_RegisterShader( "gfx/damage/plasma_mrk" ); + cgs.media.shadowMarkShader = trap_R_RegisterShader( "markShadow" ); + cgs.media.wakeMarkShader = trap_R_RegisterShader( "wake" ); + cgs.media.bloodMarkShader = trap_R_RegisterShader( "bloodMark" ); + + // 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_BuildSpectatorString + +======================= +*/ +void CG_BuildSpectatorString() { + int i; + cg.spectatorList[0] = 0; + for (i = 0; i < MAX_CLIENTS; i++) { + if (cgs.clientinfo[i].infoValid && cgs.clientinfo[i].team == TEAM_SPECTATOR ) { + Q_strcat(cg.spectatorList, sizeof(cg.spectatorList), va("%s ", 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_LoadingClient(cg.clientNum); + CG_NewClientInfo(cg.clientNum); + + for (i=0 ; i<MAX_CLIENTS+MAX_PRECACHES; i++) { + const char *clientInfo; + + if (cg.clientNum == i) { + continue; + } + + clientInfo = CG_ConfigString( CS_PLAYERS+i ); + if ( !clientInfo[0] ) { + continue; + } + CG_LoadingClient( i ); + + if( i < MAX_CLIENTS ) + { + CG_NewClientInfo( i ); + } + else + { + CG_PrecacheClientInfo( 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_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_entities, 0, sizeof(cg_entities) ); + memset( cg_upgrades, 0, sizeof(cg_upgrades) ); + memset( cg_items, 0, sizeof(cg_items) ); + + cg.clientNum = clientNum; + + cgs.processedSnapshotNum = serverMessageNum; + cgs.serverCommandSequence = serverCommandSequence; + + // load a few needed things before we do any screen updates + cgs.media.charsetShader = trap_R_RegisterShader( "gfx/2d/bigchars" ); + cgs.media.whiteShader = trap_R_RegisterShader( "white" ); + cgs.media.charsetProp = trap_R_RegisterShaderNoMip( "menu/art/font1_prop.tga" ); + cgs.media.charsetPropGlow = trap_R_RegisterShaderNoMip( "menu/art/font1_prop_glo.tga" ); + cgs.media.charsetPropB = trap_R_RegisterShaderNoMip( "menu/art/font2_prop.tga" ); + + //TA: dyn memory + CG_InitMemory( ); + + CG_RegisterCvars(); + + CG_InitConsoleCommands(); + + //cg.weaponSelect = WP_MACHINEGUN; + //TA: if it does weird things, this is why: + cg.weaponSelect = WP_NONE; + + cgs.redflag = cgs.blueflag = -1; // For compatibily, default to unset for + cgs.flagStatus = -1; + // 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 + CG_LoadingString( "collision map" ); + + trap_CM_LoadMap( cgs.mapname ); + + cg.loading = qtrue; // force players to load instead of defer + + CG_LoadingString( "sounds" ); + + CG_RegisterSounds(); + + CG_LoadingString( "graphics" ); + + CG_RegisterGraphics(); + + CG_LoadingString( "clients" ); + + CG_RegisterClients(); // if low on memory, some clients will be deferred + + cg.loading = qfalse; // future players will be deferred + + CG_InitLocalEntities(); + + CG_InitMarkPolys(); + + // remove the last loading update + cg.infoScreenText[0] = 0; + + // Make sure we have update values (scores) + CG_SetConfigValues(); + + CG_StartMusic(); + + CG_LoadingString( "" ); + + CG_ShaderStateChanged(); + + trap_S_ClearLoopingSounds( qtrue ); +} + +/* +================= +CG_Shutdown + +Called before every level change or subsystem restart +================= +*/ +void CG_Shutdown( void ) { + // some mods may need to do cleanup work here, + // like closing files or archiving session data +} + +/* +================== +CG_EventHandling +================== + type 0 - no event handling + 1 - team menu + 2 - hud editor + +*/ +void CG_EventHandling(int type) { +} + + + +void CG_KeyEvent(int key, qboolean down) { +} + +void CG_MouseEvent(int x, int y) { +} + diff --git a/src/cgame/cg_marks.c b/src/cgame/cg_marks.c new file mode 100644 index 00000000..00fc89c2 --- /dev/null +++ b/src/cgame/cg_marks.c @@ -0,0 +1,298 @@ +// Copyright (C) 1999-2000 Id Software, Inc. +// +// cg_marks.c -- wall marks + +/* + * Portions Copyright (C) 2000-2001 Tim Angus + * + * This program 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, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ + +/* To assertain which portions are licensed under the GPL and which are + * licensed by Id Software, Inc. please run a diff between the equivalent + * versions of the "Tremulous" modification and the unmodified "Quake3" + * game source code. + */ + +#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 out the energy bursts + if ( mp->markShader == cgs.media.energyMarkShader ) { + + fade = 450 - 450 * ( (cg.time - mp->time ) / 3000.0 ); + if ( fade < 255 ) { + if ( fade < 0 ) { + fade = 0; + } + if ( mp->verts[0].modulate[0] != 0 ) { + 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; + } + } + } + } + + // 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 00000000..1b0b2093 --- /dev/null +++ b/src/cgame/cg_mem.c @@ -0,0 +1,62 @@ +// Copyright (C) 1999-2000 Id Software, Inc. +// +// +// g_mem.c +// +//TA: hack to provide dynanmic allocation clientside + +/* + * Portions Copyright (C) 2000-2001 Tim Angus + * + * This program 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, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ + +/* To assertain which portions are licensed under the GPL and which are + * licensed by Id Software, Inc. please run a diff between the equivalent + * versions of the "Tremulous" modification and the unmodified "Quake3" + * game source code. + */ + +#include "cg_local.h" + + +#define POOLSIZE (256 * 1024) + +static char memoryPool[POOLSIZE]; +static int allocPoint; + +void *CG_Alloc( int size ) { + char *p; + + /*if ( g_debugAlloc.integer ) { + G_Printf( "CG_Alloc of %i bytes (%i left)\n", size, POOLSIZE - allocPoint - ( ( size + 31 ) & ~31 ) ); + }*/ + + if ( allocPoint + size > POOLSIZE ) { + CG_Error( "CG_Alloc: failed on allocation of %u bytes\n", size ); + return NULL; + } + + p = &memoryPool[allocPoint]; + + allocPoint += ( size + 31 ) & ~31; + + return p; +} + +void CG_InitMemory( void ) { + allocPoint = 0; +} + diff --git a/src/cgame/cg_players.c b/src/cgame/cg_players.c new file mode 100644 index 00000000..2b1b42bf --- /dev/null +++ b/src/cgame/cg_players.c @@ -0,0 +1,2092 @@ +// Copyright (C) 1999-2000 Id Software, Inc. +// +// cg_players.c -- handle the media and animation for player entities + +/* + * Portions Copyright (C) 2000-2001 Tim Angus + * + * This program 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, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ + +/* To assertain which portions are licensed under the GPL and which are + * licensed by Id Software, Inc. please run a diff between the equivalent + * versions of the "Tremulous" modification and the unmodified "Quake3" + * game source code. + */ + +#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 ); + 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; + + // 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, "boot" ) ) { + ci->footsteps = FOOTSTEP_BOOT; + } else if ( !Q_stricmp( token, "flesh" ) ) { + ci->footsteps = FOOTSTEP_FLESH; + } else if ( !Q_stricmp( token, "mech" ) ) { + ci->footsteps = FOOTSTEP_MECH; + } else if ( !Q_stricmp( token, "energy" ) ) { + ci->footsteps = FOOTSTEP_ENERGY; + } 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; + } + + // 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 ); + } + + // read information for each frame + for ( i = 0 ; i < MAX_ANIMATIONS ; i++ ) { + + token = COM_Parse( &text_p ); + if ( !*token ) { +#ifdef NEW_ANIMS + 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; + } +#endif + 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_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; + // + // new anims changes + // +// animations[TORSO_GETFLAG].flipflop = qtrue; +// animations[TORSO_GUARDBASE].flipflop = qtrue; +// animations[TORSO_PATROL].flipflop = qtrue; +// animations[TORSO_AFFIRMATIVE].flipflop = qtrue; +// animations[TORSO_NEGATIVE].flipflop = qtrue; + // + return qtrue; +} + +/* +========================== +CG_RegisterClientSkin +========================== +*/ +static qboolean CG_RegisterClientSkin( clientInfo_t *ci, const char *modelName, const char *skinName ) { + char filename[MAX_QPATH]; + + 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; + } + + return qtrue; +} + +/* +========================== +CG_RegisterClientModelname +========================== +*/ +static qboolean CG_RegisterClientModelname( clientInfo_t *ci, const char *modelName, const char *skinName ) { + char filename[MAX_QPATH]; + + // load cmodels before models so filecache works + + 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; + } + + // 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; + } + + // 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; + } + + 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. +This will usually be deferred to a safe time +=================== +*/ +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 ( cgs.gametype >= GT_TEAM ) { + // keep skin name + if ( !CG_RegisterClientModelname( ci, DEFAULT_MODEL, ci->skinName ) ) { + CG_Error( "DEFAULT_MODEL / skin (%s/%s) failed to register", + DEFAULT_MODEL, ci->skinName ); + } + } else { + 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; + } + 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 ); + } + } + + ci->deferred = qfalse; + + // reset any existing players and bodies, because they might be in bad + // frames for this new model + if( clientNum <= MAX_CLIENTS ) + { + 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->modelIcon = from->modelIcon; + + memcpy( to->animations, from->animations, sizeof( to->animations ) ); + memcpy( to->sounds, from->sounds, sizeof( to->sounds ) ); +} + + +/* +====================== +CG_GetCorpseNum +====================== +*/ +static int CG_GetCorpseNum( clientInfo_t *ci ) { + int i; + clientInfo_t *match; + + for ( i = 0 ; i < cgs.maxclients ; i++ ) { + match = &cgs.corpseinfo[ i ]; + if ( !match->infoValid ) { + continue; + } + if ( match->deferred ) { + continue; + } + if ( !Q_stricmp( ci->modelName, match->modelName ) + && !Q_stricmp( ci->skinName, match->skinName ) ) { + // this clientinfo is identical, so use it's handles + + return i; + } + } + + //something has gone badly wrong + return -1; +} + + +/* +====================== +CG_ScanForExistingClientInfo +====================== +*/ +static qboolean CG_ScanForExistingClientInfo( clientInfo_t *ci ) { + int i; + clientInfo_t *match; + + for ( i = 0 ; i < cgs.maxclients ; i++ ) { + match = &cgs.clientinfo[ i ]; + if ( !match->infoValid ) { + continue; + } + if ( match->deferred ) { + continue; + } + if ( !Q_stricmp( ci->modelName, match->modelName ) + && !Q_stricmp( ci->skinName, match->skinName ) ) { + // this clientinfo is identical, so use it's handles + + ci->deferred = qfalse; + + CG_CopyClientInfoModel( match, ci ); + + return qtrue; + } + } + + // nothing matches, so defer the load + return qfalse; +} + +/* +====================== +CG_SetDeferredClientInfo + +We aren't going to load it now, so grab some other +client's info to use until we have some spare time. +====================== +*/ +static void CG_SetDeferredClientInfo( clientInfo_t *ci ) { + int i; + clientInfo_t *match; + + // if we are in teamplay, only grab a model if the skin is correct + if ( cgs.gametype >= GT_TEAM ) { + for ( i = 0 ; i < cgs.maxclients ; i++ ) { + match = &cgs.clientinfo[ i ]; + if ( !match->infoValid ) { + continue; + } + if ( Q_stricmp( ci->skinName, match->skinName ) ) { + continue; + } + ci->deferred = qtrue; + CG_CopyClientInfoModel( match, ci ); + return; + } + + // load the full model, because we don't ever want to show + // an improper team skin. This will cause a hitch for the first + // player, when the second enters. Combat shouldn't be going on + // yet, so it shouldn't matter + CG_LoadClientInfo( ci ); + return; + } + + // find the first valid clientinfo and grab its stuff + for ( i = 0 ; i < cgs.maxclients ; i++ ) { + match = &cgs.clientinfo[ i ]; + if ( !match->infoValid ) { + continue; + } + + ci->deferred = qtrue; + CG_CopyClientInfoModel( match, ci ); + return; + } + + // we should never get here... + CG_Printf( "CG_SetDeferredClientInfo: no valid clients!\n" ); + + CG_LoadClientInfo( ci ); +} + + +/* +====================== +CG_PrecacheClientInfo +====================== +*/ +void CG_PrecacheClientInfo( int clientNum ) { + clientInfo_t *ci; + clientInfo_t newInfo; + const char *configstring; + const char *v; + char *slash; + + ci = &cgs.corpseinfo[ clientNum - MAX_CLIENTS ]; + + //CG_Printf( "%d %d\n", clientNum, (clientNum - MAX_CLIENTS ) ); + + configstring = CG_ConfigString( clientNum + CS_PLAYERS ); + if ( !configstring[0] ) { + return; // player just left + } + + // build into a temp buffer so the defer checks can use + // 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.color ); + + // 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); + + v = Info_ValueForKey( configstring, "g_redteam" ); + Q_strncpyz(newInfo.redTeam, v, MAX_TEAMNAME); + + v = Info_ValueForKey( configstring, "g_blueteam" ); + Q_strncpyz(newInfo.blueTeam, v, MAX_TEAMNAME); + + // 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( "PCI: %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; + } + + newInfo.deferred = qfalse; + newInfo.infoValid = qtrue; + CG_LoadClientInfo( &newInfo ); + *ci = newInfo; +} + + +/* +====================== +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 + } + + // build into a temp buffer so the defer checks can use + // 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.color ); + + // 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); + + v = Info_ValueForKey( configstring, "g_redteam" ); + Q_strncpyz(newInfo.redTeam, v, MAX_TEAMNAME); + + v = Info_ValueForKey( configstring, "g_blueteam" ); + Q_strncpyz(newInfo.blueTeam, v, MAX_TEAMNAME); + + // 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; + } + + // scan for an existing clientinfo that matches this modelname + // so we can avoid loading checks if possible + if ( !CG_ScanForExistingClientInfo( &newInfo ) ) { + qboolean forceDefer; + + forceDefer = trap_MemoryRemaining() < 4000000; + + // if we are defering loads, just have it pick the first valid + //TA: we should only defer models when ABSOLUTELY TOTALLY necessary since models are precached + if ( forceDefer ) //|| ( cg_deferPlayers.integer && !cg_buildScript.integer && !cg.loading ) ) + { + // keep whatever they had if it won't violate team skins + if ( ci->infoValid && + ( cgs.gametype < GT_TEAM || !Q_stricmp( newInfo.skinName, ci->skinName ) ) ) + { + CG_CopyClientInfoModel( ci, &newInfo ); + newInfo.deferred = qtrue; + } + else + { + // use whatever is available + CG_SetDeferredClientInfo( &newInfo ); + } + + // if we are low on memory, leave them with this model + if ( forceDefer ) + { + CG_Printf( "Memory is low. Using deferred model.\n" ); + newInfo.deferred = qfalse; + } + } + else + { + CG_LoadClientInfo( &newInfo ); + } + } + + // replace whatever was there with the new one + newInfo.infoValid = qtrue; + *ci = newInfo; +} + + + +/* +====================== +CG_LoadDeferredPlayers + +Called each frame when a player is dead +and the scoreboard is up +so deferred players can be loaded +====================== +*/ +void CG_LoadDeferredPlayers( void ) { + int i; + clientInfo_t *ci; + + // scan for a deferred player to load + for ( i = 0, ci = cgs.clientinfo ; i < cgs.maxclients ; i++, ci++ ) { + if ( ci->infoValid && ci->deferred ) { + // if we are low on memory, leave it deferred + if ( trap_MemoryRemaining() < 4000000 ) { + CG_Printf( "Memory is low. Using deferred model.\n" ); + ci->deferred = qfalse; + continue; + } + CG_LoadClientInfo( ci ); +// break; + } + } +} + +/* +============================================================================= + +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 | ANIM_WALLCLIMBING ); + + if ( newAnimation < 0 || newAnimation >= MAX_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_RunLerpFrame + +Sets cg.snap, cg.oldFrame, and cg.backlerp +cg.time should be between oldFrameTime and frameTime after exit +=============== +*/ +static void CG_RunLerpFrame( 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; + + clientNum = cent->currentState.clientNum; + + if ( cg_noPlayerAnims.integer ) { + *legsOld = *legs = *torsoOld = *torso = 0; + return; + } + + /*if ( cent->currentState.powerups & ( 1 << PW_HASTE ) ) { + speedScale = 1.5; + } else*/ { + speedScale = 1; + } + + ci = &cgs.clientinfo[ clientNum ]; + + // do the shuffle turn frames locally + if ( cent->pe.legs.yawing && ( cent->currentState.legsAnim & ~( ANIM_TOGGLEBIT | ANIM_WALLCLIMBING ) ) == LEGS_IDLE ) { + CG_RunLerpFrame( ci, ¢->pe.legs, LEGS_TURN, speedScale ); + } else { + CG_RunLerpFrame( ci, ¢->pe.legs, cent->currentState.legsAnim, speedScale ); + } + + *legsOld = cent->pe.legs.oldFrame; + *legs = cent->pe.legs.frame; + *legsBackLerp = cent->pe.legs.backlerp; + + CG_RunLerpFrame( ci, ¢->pe.torso, cent->currentState.torsoAnim, speedScale ); + + *torsoOld = cent->pe.torso.oldFrame; + *torso = cent->pe.torso.frame; + *torsoBackLerp = cent->pe.torso.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 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; + + VectorCopy( cent->lerpAngles, headAngles ); + headAngles[YAW] = AngleMod( headAngles[YAW] ); + VectorClear( legsAngles ); + VectorClear( torsoAngles ); + + // --------- yaw ------------- + + // allow yaw to drift a bit + if ( ( cent->currentState.legsAnim & ~( ANIM_TOGGLEBIT | ANIM_WALLCLIMBING ) ) != LEGS_IDLE + || ( cent->currentState.torsoAnim & ~(ANIM_TOGGLEBIT | ANIM_WALLCLIMBING ) ) != 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 { + //TA: 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 + 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; + + // --------- 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; + } + + // 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 ); +} + + +//========================================================================== + +/* +=============== +CG_HasteTrail +=============== +*/ +static void CG_HasteTrail( centity_t *cent ) { + localEntity_t *smoke; + vec3_t origin; + int anim; + + if ( cent->trailTime > cg.time ) { + return; + } + anim = cent->pe.legs.animationNumber & ~( ANIM_TOGGLEBIT | ANIM_WALLCLIMBING ); + if ( anim != LEGS_RUN && anim != LEGS_BACK ) { + return; + } + + cent->trailTime += 100; + if ( cent->trailTime < cg.time ) { + cent->trailTime = cg.time; + } + + VectorCopy( cent->lerpOrigin, origin ); + origin[2] -= 16; + + smoke = CG_SmokePuff( origin, vec3_origin, + 8, + 1, 1, 1, 1, + 500, + cg.time, + 0, + 0, + cgs.media.hastePuffShader ); + + // use the optimized local entity add + smoke->leType = LE_SCALE_FADE; +} + +/* +=============== +CG_TrailItem +=============== +*/ +static void CG_TrailItem( centity_t *cent, qhandle_t hModel ) { + refEntity_t ent; + vec3_t angles; + vec3_t axis[3]; + + VectorCopy( cent->lerpAngles, angles ); + angles[PITCH] = 0; + angles[ROLL] = 0; + AnglesToAxis( angles, axis ); + + memset( &ent, 0, sizeof( ent ) ); + VectorMA( cent->lerpOrigin, -16, axis[0], ent.origin ); + ent.origin[2] += 16; + angles[YAW] += 90; + AnglesToAxis( angles, ent.axis ); + + ent.hModel = hModel; + trap_R_AddRefEntityToScene( &ent ); +} + + +/* +=============== +CG_PlayerPowerups +=============== +*/ +#ifdef NEW_ANIMS +static void CG_PlayerPowerups( centity_t *cent, refEntity_t *torso ) { +#else +static void CG_PlayerPowerups( centity_t *cent ) { +#endif + int powerups; + clientInfo_t *ci; + + /*powerups = cent->currentState.powerups; + if ( !powerups ) { + return; + } + + // quad gives a dlight + if ( powerups & ( 1 << PW_QUAD ) ) { + trap_R_AddLightToScene( cent->lerpOrigin, 200 + (rand()&31), 0.2, 0.2, 1 ); + } + + // flight plays a looped sound + if ( powerups & ( 1 << PW_FLIGHT ) ) { + trap_S_AddLoopingSound( cent->currentState.number, cent->lerpOrigin, vec3_origin, cgs.media.flightSound ); + } + + // redflag + if ( powerups & ( 1 << PW_REDFLAG ) ) { + CG_TrailItem( cent, cgs.media.redFlagModel ); + trap_R_AddLightToScene( cent->lerpOrigin, 200 + (rand()&31), 1, 0.2, 0.2 ); + } + + // blueflag + if ( powerups & ( 1 << PW_BLUEFLAG ) ) { + CG_TrailItem( cent, cgs.media.blueFlagModel ); + trap_R_AddLightToScene( cent->lerpOrigin, 200 + (rand()&31), 0.2, 0.2, 1 ); + } + + // haste leaves smoke trails + if ( powerups & ( 1 << PW_HASTE ) ) { + CG_HasteTrail( cent ); + }*/ +} + + +/* +=============== +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 ) { + int team; + + if ( cent->currentState.eFlags & EF_CONNECTION ) { + CG_PlayerFloatSprite( cent, cgs.media.connectionShader ); + return; + } + + if ( cent->currentState.eFlags & EF_TALK ) { + CG_PlayerFloatSprite( cent, cgs.media.balloonShader ); + return; + } + + if ( cent->currentState.eFlags & EF_AWARD_IMPRESSIVE ) { + CG_PlayerFloatSprite( cent, cgs.media.medalImpressive ); + return; + } + + if ( cent->currentState.eFlags & EF_AWARD_EXCELLENT ) { + CG_PlayerFloatSprite( cent, cgs.media.medalExcellent ); + return; + } + + if ( cent->currentState.eFlags & EF_AWARD_GAUNTLET ) { + CG_PlayerFloatSprite( cent, cgs.media.medalGauntlet ); + return; + } + + team = cgs.clientinfo[ cent->currentState.clientNum ].team; + if ( !(cent->currentState.eFlags & EF_DEAD) && + cg.snap->ps.persistant[PERS_TEAM] == team && + cgs.gametype >= GT_TEAM) { + if (cg_drawFriend.integer) { + CG_PlayerFloatSprite( cent, cgs.media.friendShader ); + } + 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 ) { + vec3_t end, mins = {-15, -15, 0}, maxs = {15, 15, 2}; + trace_t trace; + float alpha; + + *shadowPlane = 0; + + if ( cg_shadows.integer == 0 ) { + return qfalse; + } + + // no shadows when invisible + /*if ( cent->currentState.powerups & ( 1 << PW_INVIS ) ) { + return qfalse; + }*/ + + // send a trace down from the player to the ground + VectorCopy( cent->lerpOrigin, end ); + end[2] -= SHADOW_DISTANCE; + + trap_CM_BoxTrace( &trace, cent->lerpOrigin, end, mins, maxs, 0, MASK_PLAYERSOLID ); + + // no shadow if too high + if ( trace.fraction == 1.0 ) { + return qfalse; + } + + *shadowPlane = trace.endpos[2] + 1; + + 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, alpha,alpha,alpha,1, qfalse, 24, qtrue ); + + return qtrue; +} + + +/* +=============== +CG_PlayerSplash + +Draw a mark at the water surface +=============== +*/ +static void CG_PlayerSplash( centity_t *cent ) { + vec3_t start, end; + trace_t trace; + int contents; + polyVert_t verts[4]; + + if ( !cg_shadows.integer ) { + return; + } + + VectorCopy( cent->lerpOrigin, end ); + end[2] -= 24; + + // 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.0 ) { + return; + } + + // create a mark polygon + VectorCopy( trace.endpos, verts[0].xyz ); + verts[0].xyz[0] -= 32; + verts[0].xyz[1] -= 32; + 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; + + VectorCopy( trace.endpos, verts[1].xyz ); + verts[1].xyz[0] -= 32; + verts[1].xyz[1] += 32; + verts[1].st[0] = 0; + verts[1].st[1] = 1; + verts[1].modulate[0] = 255; + verts[1].modulate[1] = 255; + verts[1].modulate[2] = 255; + verts[1].modulate[3] = 255; + + VectorCopy( trace.endpos, verts[2].xyz ); + verts[2].xyz[0] += 32; + verts[2].xyz[1] += 32; + 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; + + VectorCopy( trace.endpos, verts[3].xyz ); + verts[3].xyz[0] += 32; + verts[3].xyz[1] -= 32; + verts[3].st[0] = 1; + 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.wakeMarkShader, 4, verts ); +} + + + +/* +=============== +CG_AddRefEntityWithPowerups + +Adds a piece with modifications or duplications for powerups +Also called by CG_Missile for quad rockets, but nobody can tell... +=============== +*/ +void CG_AddRefEntityWithPowerups( refEntity_t *ent, int powerups, int team ) { + + /*if ( powerups & ( 1 << PW_INVIS ) ) { + ent->customShader = cgs.media.invisShader; + trap_R_AddRefEntityToScene( ent ); + } else {*/ + trap_R_AddRefEntityToScene( ent ); + + /*if ( powerups & ( 1 << PW_QUAD ) ) + { + if (team == TEAM_HUMANS) + ent->customShader = cgs.media.redQuadShader; + else + ent->customShader = cgs.media.quadShader; + trap_R_AddRefEntityToScene( ent ); + } + if ( powerups & ( 1 << PW_REGEN ) ) { + if ( ( ( cg.time / 100 ) % 10 ) == 1 ) { + ent->customShader = cgs.media.regenShader; + trap_R_AddRefEntityToScene( ent ); + } + } + if ( powerups & ( 1 << PW_BATTLESUIT ) ) { + ent->customShader = cgs.media.battleSuitShader; + trap_R_AddRefEntityToScene( ent ); + } + }*/ +} + + +/* +================= +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 i, 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 ); +} + + +/* +=============== +CG_Player +=============== +*/ +void CG_Player( centity_t *cent ) { + clientInfo_t *ci; + refEntity_t legs; + refEntity_t torso; + refEntity_t head; + int clientNum; + int renderfx; + qboolean shadow; + float shadowPlane; + + // 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 = cent->currentState.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; + } + + // get the player model information + renderfx = 0; + if ( cent->currentState.number == cg.snap->ps.clientNum) { + if (!cg.renderingThirdPerson) { + renderfx = RF_THIRD_PERSON; // only draw in mirrors + } else { + if (cg_cameraMode.integer) { + return; + } + } + } + + memset( &legs, 0, sizeof(legs) ); + memset( &torso, 0, sizeof(torso) ); + memset( &head, 0, sizeof(head) ); + + // get the rotation information + CG_PlayerAngles( cent, legs.axis, torso.axis, head.axis ); + + // get the animation state (after rotation, to allow feet shuffle) + CG_PlayerAnimation( cent, &legs.oldframe, &legs.frame, &legs.backlerp, + &torso.oldframe, &torso.frame, &torso.backlerp ); + +#ifndef NEW_ANIMS + // add powerups floating behind the player + //CG_PlayerPowerups( cent ); +#endif + + // add the talk baloon or disconnect icon + CG_PlayerSprites( cent ); + + // add the shadow + //TA: but only for humans + if( ( cent->currentState.powerups & 0xFF ) == PTE_HUMANS ) + shadow = CG_PlayerShadow( cent, &shadowPlane ); + + // add a water splash if partially in and out of water + CG_PlayerSplash( cent ); + + if ( cg_shadows.integer == 3 && shadow ) { + renderfx |= RF_SHADOW_PLANE; + } + renderfx |= RF_LIGHTING_ORIGIN; // use the same origin for all + + // + // add the legs + // + legs.hModel = ci->legsModel; + legs.customSkin = ci->legsSkin; + + 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 + + //TA: rotate the model so it sits on a wall + if( cent->currentState.legsAnim & ANIM_WALLCLIMBING && + !( cent->currentState.eFlags & EF_DEAD ) && + !( cg.intermissionStarted ) ) + { + vec3_t forward, surfNormal; + trace_t tr; + + VectorCopy( cent->currentState.angles2, surfNormal ); + + /*CG_Printf( "%d: ", cent->currentState.number ); + CG_Printf( "%f ", surfNormal[ 0 ] ); + CG_Printf( "%f ", surfNormal[ 1 ] ); + CG_Printf( "%f ", surfNormal[ 2 ] ); + CG_Printf( "\n" );*/ + + AngleVectors( cent->lerpAngles, forward, NULL, NULL ); + VectorCopy( surfNormal, legs.axis[2] ); + ProjectPointOnPlane( legs.axis[0], forward, legs.axis[2] ); + if( !VectorNormalize( legs.axis[0] ) ) + { + AngleVectors( cent->lerpAngles, NULL, NULL, forward ); + ProjectPointOnPlane( legs.axis[0], forward, legs.axis[2] ); + VectorNormalize( legs.axis[0] ); + } + CrossProduct( legs.axis[0], legs.axis[2], legs.axis[1] ); + legs.axis[1][0] = -legs.axis[1][0]; + legs.axis[1][1] = -legs.axis[1][1]; + legs.axis[1][2] = -legs.axis[1][2]; + + VectorCopy( legs.origin, legs.lightingOrigin ); + VectorCopy( legs.origin, legs.oldorigin ); // don't positionally lerp at all + } + + + //CG_AddRefEntityWithPowerups( &legs, cent->currentState.powerups, ci->team ); + trap_R_AddRefEntityToScene( &legs ); + + // if the model failed, allow the default nullmodel to be displayed + if (!legs.hModel) { + return; + } + + // + // add the torso + // + torso.hModel = ci->torsoModel; + if (!torso.hModel) { + return; + } + + torso.customSkin = ci->torsoSkin; + + VectorCopy( cent->lerpOrigin, torso.lightingOrigin ); + + CG_PositionRotatedEntityOnTag( &torso, &legs, ci->legsModel, "tag_torso"); + + torso.shadowPlane = shadowPlane; + torso.renderfx = renderfx; + + //CG_AddRefEntityWithPowerups( &torso, cent->currentState.powerups, ci->team ); + trap_R_AddRefEntityToScene( &torso ); + + // + // add the head + // + head.hModel = ci->headModel; + if (!head.hModel) { + return; + } + head.customSkin = ci->headSkin; + + VectorCopy( cent->lerpOrigin, head.lightingOrigin ); + + CG_PositionRotatedEntityOnTag( &head, &torso, ci->torsoModel, "tag_head"); + + head.shadowPlane = shadowPlane; + head.renderfx = renderfx; + + //CG_AddRefEntityWithPowerups( &head, cent->currentState.powerups, ci->team ); + trap_R_AddRefEntityToScene( &head ); + + // + // add the gun / barrel / flash + // + CG_AddPlayerWeapon( &torso, NULL, cent ); +} + +/* +=============== +CG_Corpse +=============== +*/ +void CG_Corpse( centity_t *cent ) +{ + clientInfo_t *ci; + refEntity_t legs; + refEntity_t torso; + refEntity_t head; + int clientNum; + int renderfx; + qboolean shadow; + float shadowPlane; + + //if this is the first time the function has been run set cent->corpseNum + if( cent->corpseNum < 1 ) + { + ci = &cgs.clientinfo[ cent->currentState.clientNum ]; + cent->corpseNum = CG_GetCorpseNum( ci ) + 1; + if ( cent->corpseNum < 1 || cent->corpseNum >= MAX_CLIENTS + 1 ) + { + CG_Error( "Bad corpseNum on corpse entity: %d", cent->corpseNum ); + } + } + + ci = &cgs.corpseinfo[ cent->corpseNum - 1 ]; + + // 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) ); + + // get the rotation information + CG_PlayerAngles( cent, legs.axis, torso.axis, head.axis ); + + //set the correct frame (should always be dead) + if ( cg_noPlayerAnims.integer ) + legs.oldframe = legs.frame = torso.oldframe = torso.frame = 0; + else + { + CG_RunLerpFrame( ci, ¢->pe.legs, cent->currentState.legsAnim, 1 ); + legs.oldframe = cent->pe.legs.oldFrame; + legs.frame = cent->pe.legs.frame; + legs.backlerp = cent->pe.legs.backlerp; + + CG_RunLerpFrame( ci, ¢->pe.torso, cent->currentState.torsoAnim, 1 ); + torso.oldframe = cent->pe.torso.oldFrame; + torso.frame = cent->pe.torso.frame; + torso.backlerp = cent->pe.torso.backlerp; + } + + + // add the shadow + shadow = CG_PlayerShadow( cent, &shadowPlane ); + + // 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 + // + legs.hModel = ci->legsModel; + legs.customSkin = ci->legsSkin; + + 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 + + //CG_AddRefEntityWithPowerups( &legs, cent->currentState.powerups, ci->team ); + trap_R_AddRefEntityToScene( &legs ); + + // if the model failed, allow the default nullmodel to be displayed + if (!legs.hModel) { + return; + } + + // + // add the torso + // + torso.hModel = ci->torsoModel; + if (!torso.hModel) { + return; + } + + torso.customSkin = ci->torsoSkin; + + VectorCopy( cent->lerpOrigin, torso.lightingOrigin ); + + CG_PositionRotatedEntityOnTag( &torso, &legs, ci->legsModel, "tag_torso"); + + torso.shadowPlane = shadowPlane; + torso.renderfx = renderfx; + + //CG_AddRefEntityWithPowerups( &torso, cent->currentState.powerups, ci->team ); + trap_R_AddRefEntityToScene( &torso ); + + // + // add the head + // + head.hModel = ci->headModel; + if (!head.hModel) { + return; + } + head.customSkin = ci->headSkin; + + VectorCopy( cent->lerpOrigin, head.lightingOrigin ); + + CG_PositionRotatedEntityOnTag( &head, &torso, ci->torsoModel, "tag_head"); + + head.shadowPlane = shadowPlane; + head.renderfx = renderfx; + + //CG_AddRefEntityWithPowerups( &head, cent->currentState.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 ); + + 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; + + if ( cg_debugPosition.integer ) { + CG_Printf("%i ResetPlayerEntity yaw=%i\n", cent->currentState.number, cent->pe.torso.yawAngle ); + } +} + diff --git a/src/cgame/cg_playerstate.c b/src/cgame/cg_playerstate.c new file mode 100644 index 00000000..3477f108 --- /dev/null +++ b/src/cgame/cg_playerstate.c @@ -0,0 +1,502 @@ +// Copyright (C) 1999-2000 Id Software, Inc. +// +// 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 + +/* + * Portions Copyright (C) 2000-2001 Tim Angus + * + * This program 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, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ + +/* To assertain which portions are licensed under the GPL and which are + * licensed by Id Software, Inc. please run a diff between the equivalent + * versions of the "Tremulous" modification and the unmodified "Quake3" + * game source code. + */ + +#include "cg_local.h" + +/* +============== +CG_CheckAmmo + +If the ammo has gone low enough to generate the warning, play a sound +============== +*/ +void CG_CheckAmmo( void ) { + int i; + int total; + int previous; + int ammo, clips, maxclips; + + // see about how many seconds of ammo we have remaining + total = 0; + for ( i = WP_MACHINEGUN ; i < WP_NUM_WEAPONS ; i++ ) { + if ( !BG_gotWeapon( i, cg.snap->ps.stats ) ) { + continue; + } + + BG_unpackAmmoArray( i, cg.snap->ps.ammo, cg.snap->ps.powerups, &ammo, &clips, &maxclips ); + + switch ( i ) { + case WP_ROCKET_LAUNCHER: + case WP_GRENADE_LAUNCHER: + case WP_RAILGUN: + case WP_SHOTGUN: + total += ammo * 1000; + break; + default: + if( clips ) + total += 5000; + total += ammo * 200; + break; + } + if ( total >= 5000 || BG_infiniteAmmo( i ) ) { + cg.lowAmmoWarning = 0; + return; + } + } + + previous = cg.lowAmmoWarning; + + if ( total == 0 ) { + cg.lowAmmoWarning = 2; + } else { + cg.lowAmmoWarning = 1; + } + + // play a sound on transitions + if ( cg.lowAmmoWarning != previous ) { + trap_S_StartLocalSound( cgs.media.noAmmoSound, CHAN_LOCAL_SOUND ); + } +} + +/* +============== +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.1 ) { + 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; +} + +extern char *eventnames[]; + +/* +============== +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"); + } + } + } + } +} + +/* +================== +pushReward +================== +*/ +static void pushReward(sfxHandle_t sfx, qhandle_t shader, int rewardCount) { + if (cg.rewardStack < (MAX_REWARDSTACK-1)) { + cg.rewardStack++; + cg.rewardSound[cg.rewardStack] = sfx; + cg.rewardShader[cg.rewardStack] = shader; + cg.rewardCount[cg.rewardStack] = rewardCount; + } +} + + +/* +================== +CG_CheckLocalSounds +================== +*/ +void CG_CheckLocalSounds( playerState_t *ps, playerState_t *ops ) { + int highScore, health, armor, reward; + sfxHandle_t sfx; + + // don't play the sounds if the player just changed teams + if ( ps->persistant[PERS_TEAM] != ops->persistant[PERS_TEAM] ) { + return; + } + + // hit changes + if ( ps->persistant[PERS_HITS] > ops->persistant[PERS_HITS] ) { + /*armor = ps->persistant[PERS_ATTACKEE_ARMOR] & 0xff; + health = ps->persistant[PERS_ATTACKEE_ARMOR] >> 8;*/ + trap_S_StartLocalSound( cgs.media.hitSound, CHAN_LOCAL_SOUND ); + } else if ( ps->persistant[PERS_HITS] < ops->persistant[PERS_HITS] ) { + trap_S_StartLocalSound( cgs.media.hitTeamSound, CHAN_LOCAL_SOUND ); + } + + // 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; + /*if (ps->persistant[PERS_CAPTURES] != ops->persistant[PERS_CAPTURES]) { + pushReward(cgs.media.captureAwardSound, cgs.media.medalCapture, ps->persistant[PERS_CAPTURES]); + reward = qtrue; + //Com_Printf("capture\n"); + } + if (ps->persistant[PERS_IMPRESSIVE_COUNT] != ops->persistant[PERS_IMPRESSIVE_COUNT]) { + sfx = cgs.media.impressiveSound; + pushReward(sfx, cgs.media.medalImpressive, ps->persistant[PERS_IMPRESSIVE_COUNT]); + reward = qtrue; + //Com_Printf("impressive\n"); + } + if (ps->persistant[PERS_EXCELLENT_COUNT] != ops->persistant[PERS_EXCELLENT_COUNT]) { + sfx = cgs.media.excellentSound; + pushReward(sfx, cgs.media.medalExcellent, ps->persistant[PERS_EXCELLENT_COUNT]); + reward = qtrue; + //Com_Printf("excellent\n"); + } + if (ps->persistant[PERS_GAUNTLET_FRAG_COUNT] != ops->persistant[PERS_GAUNTLET_FRAG_COUNT]) { + sfx = cgs.media.humiliationSound; + pushReward(sfx, cgs.media.medalGauntlet, ps->persistant[PERS_GAUNTLET_FRAG_COUNT]); + reward = qtrue; + //Com_Printf("guantlet frag\n"); + } + if (ps->persistant[PERS_DEFEND_COUNT] != ops->persistant[PERS_DEFEND_COUNT]) { + pushReward(cgs.media.defendSound, cgs.media.medalDefend, ps->persistant[PERS_DEFEND_COUNT]); + reward = qtrue; + //Com_Printf("defend\n"); + } + if (ps->persistant[PERS_ASSIST_COUNT] != ops->persistant[PERS_ASSIST_COUNT]) { + pushReward(cgs.media.assistSound, cgs.media.medalAssist, ps->persistant[PERS_ASSIST_COUNT]); + reward = qtrue; + //Com_Printf("assist\n"); + }*/ + // if any of the player event bits changed + /*if (ps->persistant[PERS_PLAYEREVENTS] != ops->persistant[PERS_PLAYEREVENTS]) { + if ((ps->persistant[PERS_PLAYEREVENTS] & PLAYEREVENT_DENIEDREWARD) != + (ops->persistant[PERS_PLAYEREVENTS] & PLAYEREVENT_DENIEDREWARD)) { + trap_S_StartLocalSound( cgs.media.deniedSound, CHAN_ANNOUNCER ); + } + else if ((ps->persistant[PERS_PLAYEREVENTS] & PLAYEREVENT_GAUNTLETREWARD) != + (ops->persistant[PERS_PLAYEREVENTS] & PLAYEREVENT_GAUNTLETREWARD)) { + trap_S_StartLocalSound( cgs.media.humiliationSound, CHAN_ANNOUNCER ); + } + else if ((ps->persistant[PERS_PLAYEREVENTS] & PLAYEREVENT_HOLYSHIT) != + (ops->persistant[PERS_PLAYEREVENTS] & PLAYEREVENT_HOLYSHIT)) { + trap_S_StartLocalSound( cgs.media.holyShitSound, CHAN_ANNOUNCER ); + } + reward = qtrue; + }*/ + + // check for flag pickup + /*if ( cgs.gametype >= GT_TEAM ) { + if ((ps->powerups[PW_REDFLAG] != ops->powerups[PW_REDFLAG] && ps->powerups[PW_REDFLAG]) || + (ps->powerups[PW_BLUEFLAG] != ops->powerups[PW_BLUEFLAG] && ps->powerups[PW_BLUEFLAG]) || + (ps->powerups[PW_NEUTRALFLAG] != ops->powerups[PW_NEUTRALFLAG] && ps->powerups[PW_NEUTRALFLAG]) ) + { + trap_S_StartLocalSound( cgs.media.youHaveFlagSound, CHAN_ANNOUNCER ); + } + }*/ + + // lead changes + /*if (!reward) { + // + if ( !cg.warmup ) { + // never play lead changes during warmup + if ( ps->persistant[PERS_RANK] != ops->persistant[PERS_RANK] ) { + if ( cgs.gametype < GT_TEAM) { + if ( ps->persistant[PERS_RANK] == 0 ) { + CG_AddBufferedSound(cgs.media.takenLeadSound); + } else if ( ps->persistant[PERS_RANK] == RANK_TIED_FLAG ) { + CG_AddBufferedSound(cgs.media.tiedLeadSound); + } else if ( ( ops->persistant[PERS_RANK] & ~RANK_TIED_FLAG ) == 0 ) { + CG_AddBufferedSound(cgs.media.lostLeadSound); + } + } + } + } + }*/ + + // timelimit warnings + if ( cgs.timelimit > 0 ) { + int msec; + + msec = cg.time - cgs.levelStartTime; + if ( !( cg.timelimitWarnings & 4 ) && msec > ( cgs.timelimit * 60 + 2 ) * 1000 ) { + cg.timelimitWarnings |= 1 | 2 | 4; + trap_S_StartLocalSound( cgs.media.suddenDeathSound, CHAN_ANNOUNCER ); + } + else if ( !( cg.timelimitWarnings & 2 ) && msec > (cgs.timelimit - 1) * 60 * 1000 ) { + cg.timelimitWarnings |= 1 | 2; + trap_S_StartLocalSound( cgs.media.oneMinuteSound, CHAN_ANNOUNCER ); + } + else if ( cgs.timelimit > 5 && !( cg.timelimitWarnings & 1 ) && msec > (cgs.timelimit - 5) * 60 * 1000 ) { + cg.timelimitWarnings |= 1; + trap_S_StartLocalSound( cgs.media.fiveMinuteSound, CHAN_ANNOUNCER ); + } + } + + // fraglimit warnings + if ( cgs.fraglimit > 0 && cgs.gametype < GT_CTF) { + highScore = cgs.scores1; + if ( !( cg.fraglimitWarnings & 4 ) && highScore == (cgs.fraglimit - 1) ) { + cg.fraglimitWarnings |= 1 | 2 | 4; + CG_AddBufferedSound(cgs.media.oneFragSound); + } + else if ( cgs.fraglimit > 2 && !( cg.fraglimitWarnings & 2 ) && highScore == (cgs.fraglimit - 2) ) { + cg.fraglimitWarnings |= 1 | 2; + CG_AddBufferedSound(cgs.media.twoFragSound); + } + else if ( cgs.fraglimit > 3 && !( cg.fraglimitWarnings & 1 ) && highScore == (cgs.fraglimit - 3) ) { + cg.fraglimitWarnings |= 1; + CG_AddBufferedSound(cgs.media.threeFragSound); + } + } +} + + +/* +=============== +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; + } + + // 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 ); + } + + // check for going low on ammo + CG_CheckAmmo(); + + // 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 00000000..0b9505ba --- /dev/null +++ b/src/cgame/cg_predict.c @@ -0,0 +1,615 @@ +// Copyright (C) 1999-2000 Id Software, Inc. +// +// 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 + +/* + * Portions Copyright (C) 2000-2001 Tim Angus + * + * This program 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, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ + +/* To assertain which portions are licensed under the GPL and which are + * licensed by Id Software, Inc. please run a diff between the equivalent + * versions of the "Tremulous" modification and the unmodified "Quake3" + * game source code. + */ + +#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 ) { + 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 ) { + int i, x, zd, zu; + trace_t trace; + entityState_t *ent; + clipHandle_t cmodel; + vec3_t bmins, bmaxs; + vec3_t origin, angles; + centity_t *cent; + + for ( i = 0 ; i < cg_numSolidEntities ; i++ ) { + cent = cg_solidEntities[ i ]; + 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; + + cmodel = trap_CM_TempBoxModel( bmins, bmaxs ); + VectorCopy( vec3_origin, angles ); + VectorCopy( cent->lerpOrigin, origin ); + } + + + trap_CM_TransformedBoxTrace ( &trace, start, end, + mins, maxs, cmodel, mask, origin, angles); + + if (trace.allsolid || trace.fraction < tr->fraction) { + trace.entityNum = ent->number; + *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); + + *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_TouchItem +=================== +*/ +static void CG_TouchItem( centity_t *cent ) { + gitem_t *item; + int ammo, clips, maxclips; + + BG_unpackAmmoArray( item->giTag, cg.predictedPlayerState.ammo, cg.predictedPlayerState.powerups, &ammo, &clips, &maxclips ); + + if ( !cg_predictItems.integer ) { + return; + } + if ( !BG_PlayerTouchesItem( &cg.predictedPlayerState, ¢->currentState, cg.time ) ) { + return; + } + + // never pick an item up twice in a prediction + if ( cent->miscTime == cg.time ) { + return; + } + + if ( !BG_CanItemBeGrabbed( cgs.gametype, ¢->currentState, &cg.predictedPlayerState ) ) { + return; // can't hold it + } + + item = &bg_itemlist[ cent->currentState.modelindex ]; + + // grab it + BG_AddPredictableEventToPlayerstate( EV_ITEM_PICKUP, cent->currentState.modelindex , &cg.predictedPlayerState); + + // remove it from the frame so it won't be drawn + cent->currentState.eFlags |= EF_NODRAW; + + // don't touch it again this prediction + cent->miscTime = cg.time; + + // if its a weapon, give them some predicted ammo so the autoswitch will work + if ( item->giType == IT_WEAPON ) { + BG_packWeapon( item->giTag, cg.predictedPlayerState.stats ); + if ( ammo == 0 && clips == 0 ) { + BG_packAmmoArray( item->giTag, cg.predictedPlayerState.ammo, cg.predictedPlayerState.powerups, 1, 0, 0 ); + } + } +} + + +/* +========================= +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->eType == ET_ITEM && !spectator ) { + CG_TouchItem( cent ); + continue; + } + + 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; + } else if ( ent->eType == ET_PUSH_TRIGGER ) { + BG_TouchJumpPad( &cg.predictedPlayerState, ent ); + } + } + + // if we didn't touch a jump pad this pmove frame + if ( cg.predictedPlayerState.jumppad_frame != cg.predictedPlayerState.pmove_framecount ) { + cg.predictedPlayerState.jumppad_frame = 0; + cg.predictedPlayerState.jumppad_ent = 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; + playerState_t oldPlayerState; + qboolean moved; + usercmd_t oldestCmd; + usercmd_t latestCmd; + + 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.trace = CG_Trace; + cg_pmove.pointcontents = CG_PointContents; + 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 = ( cgs.dmflags & DF_NO_FOOTSTEPS ) > 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; + + // 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 + cg_pmove.gauntletHit = qfalse; + + if ( cg_pmove.pmove_fixed ) { + cg_pmove.cmd.serverTime = ((cg_pmove.cmd.serverTime + pmove_msec.integer-1) / pmove_msec.integer) * pmove_msec.integer; + } + + Pmove (&cg_pmove); + + moved = qtrue; + + // add push trigger movement effects + CG_TouchTriggerPrediction(); + + // check for predictable events that changed from previous predictions + //CG_CheckChangedPredictableEvents(&cg.predictedPlayerState); + } + + if ( cg_showmiss.integer > 1 ) { + CG_Printf( "[%i : %i] ", cg_pmove.cmd.serverTime, cg.time ); + } + + if ( !moved ) { + if ( cg_showmiss.integer ) { + CG_Printf( "not moved\n" ); + } + return; + } + + // adjust for the movement of the groundentity + CG_AdjustPositionForMover( cg.predictedPlayerState.origin, + cg.predictedPlayerState.groundEntityNum, + cg.physicsTime, cg.time, cg.predictedPlayerState.origin ); + + if ( cg_showmiss.integer ) { + if (cg.predictedPlayerState.eventSequence > oldPlayerState.eventSequence + MAX_PS_EVENTS) { + CG_Printf("WARNING: dropped event\n"); + } + } + + // fire events and other transition triggered things + CG_TransitionPlayerState( &cg.predictedPlayerState, &oldPlayerState ); + + if ( cg_showmiss.integer ) { + if (cg.eventSequence > cg.predictedPlayerState.eventSequence) { + CG_Printf("WARNING: double event\n"); + cg.eventSequence = cg.predictedPlayerState.eventSequence; + } + } +} + + diff --git a/src/cgame/cg_public.h b/src/cgame/cg_public.h new file mode 100644 index 00000000..6da3b293 --- /dev/null +++ b/src/cgame/cg_public.h @@ -0,0 +1,225 @@ +// Copyright (C) 1999-2000 Id Software, Inc. +// + +/* + * Portions Copyright (C) 2000-2001 Tim Angus + * + * This program 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, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ + +/* To assertain which portions are licensed under the GPL and which are + * licensed by Id Software, Inc. please run a diff between the equivalent + * versions of the "Tremulous" modification and the unmodified "Quake3" + * game source code. + */ + +#define CMD_BACKUP 64 +#define CMD_MASK (CMD_BACKUP - 1) +// allow a lot of command backups for very fast systems +// multiple commands may be combined into a single packet, so this +// needs to be larger than PACKET_BACKUP + + +#define MAX_ENTITIES_IN_SNAPSHOT 256 + +// snapshots are a view of the server at a given time + +// Snapshots are generated at regular time intervals by the server, +// but they may not be sent if a client's rate level is exceeded, or +// they may be dropped by the network. +typedef struct { + int snapFlags; // SNAPFLAG_RATE_DELAYED, etc + int ping; + + int serverTime; // server time the message is valid for (in msec) + + byte areamask[MAX_MAP_AREA_BYTES]; // portalarea visibility bits + + playerState_t ps; // complete information about the current player at this time + + int numEntities; // all of the entities that need to be presented + entityState_t entities[MAX_ENTITIES_IN_SNAPSHOT]; // at the time of this snapshot + + int numServerCommands; // text based server commands to execute when this + int serverCommandSequence; // snapshot becomes current +} snapshot_t; + +enum { + CGAME_EVENT_NONE, + CGAME_EVENT_TEAMMENU, + CGAME_EVENT_SCOREBOARD, + CGAME_EVENT_EDITHUD +}; + +/* +================================================================== + +functions imported from the main executable + +================================================================== +*/ + +#define CGAME_IMPORT_API_VERSION 4 + +typedef enum { + CG_PRINT, + CG_ERROR, + CG_MILLISECONDS, + CG_CVAR_REGISTER, + CG_CVAR_UPDATE, + CG_CVAR_SET, + CG_CVAR_VARIABLESTRINGBUFFER, + CG_ARGC, + CG_ARGV, + CG_ARGS, + CG_FS_FOPENFILE, + CG_FS_READ, + CG_FS_WRITE, + CG_FS_FCLOSEFILE, + CG_SENDCONSOLECOMMAND, + CG_ADDCOMMAND, + CG_SENDCLIENTCOMMAND, + CG_UPDATESCREEN, + CG_CM_LOADMAP, + CG_CM_NUMINLINEMODELS, + CG_CM_INLINEMODEL, + CG_CM_LOADMODEL, + CG_CM_TEMPBOXMODEL, + CG_CM_POINTCONTENTS, + CG_CM_TRANSFORMEDPOINTCONTENTS, + CG_CM_BOXTRACE, + CG_CM_TRANSFORMEDBOXTRACE, + CG_CM_MARKFRAGMENTS, + CG_S_STARTSOUND, + CG_S_STARTLOCALSOUND, + CG_S_CLEARLOOPINGSOUNDS, + CG_S_ADDLOOPINGSOUND, + CG_S_UPDATEENTITYPOSITION, + CG_S_RESPATIALIZE, + CG_S_REGISTERSOUND, + CG_S_STARTBACKGROUNDTRACK, + CG_R_LOADWORLDMAP, + CG_R_REGISTERMODEL, + CG_R_REGISTERSKIN, + CG_R_REGISTERSHADER, + CG_R_CLEARSCENE, + CG_R_ADDREFENTITYTOSCENE, + CG_R_ADDPOLYTOSCENE, + CG_R_ADDLIGHTTOSCENE, + CG_R_RENDERSCENE, + CG_R_SETCOLOR, + CG_R_DRAWSTRETCHPIC, + CG_R_MODELBOUNDS, + CG_R_LERPTAG, + CG_GETGLCONFIG, + CG_GETGAMESTATE, + CG_GETCURRENTSNAPSHOTNUMBER, + CG_GETSNAPSHOT, + CG_GETSERVERCOMMAND, + CG_GETCURRENTCMDNUMBER, + CG_GETUSERCMD, + CG_SETUSERCMDVALUE, + CG_R_REGISTERSHADERNOMIP, + CG_MEMORY_REMAINING, + CG_R_REGISTERFONT, + CG_KEY_ISDOWN, + CG_KEY_GETCATCHER, + CG_KEY_SETCATCHER, + CG_KEY_GETKEY, + CG_PC_ADD_GLOBAL_DEFINE, + CG_PC_LOAD_SOURCE, + CG_PC_FREE_SOURCE, + CG_PC_READ_TOKEN, + CG_PC_SOURCE_FILE_AND_LINE, + CG_S_STOPBACKGROUNDTRACK, + CG_REAL_TIME, + CG_SNAPVECTOR, + CG_REMOVECOMMAND, + CG_R_LIGHTFORPOINT, + CG_CIN_PLAYCINEMATIC, + CG_CIN_STOPCINEMATIC, + CG_CIN_RUNCINEMATIC, + CG_CIN_DRAWCINEMATIC, + CG_CIN_SETEXTENTS, + CG_R_REMAP_SHADER, + CG_S_ADDREALLOOPINGSOUND, + CG_S_STOPLOOPINGSOUND, + + CG_MEMSET = 100, + CG_MEMCPY, + CG_STRNCPY, + CG_SIN, + CG_COS, + CG_ATAN2, + CG_SQRT, + CG_FLOOR, + CG_CEIL, + + CG_TESTPRINTINT, + CG_TESTPRINTFLOAT, + CG_ACOS +} cgameImport_t; + + +/* +================================================================== + +functions exported to the main executable + +================================================================== +*/ + +typedef enum { + CG_INIT, +// void CG_Init( int serverMessageNum, int serverCommandSequence, int clientNum ) + // called when the level loads or when the renderer is restarted + // all media should be registered at this time + // cgame will display loading status by calling SCR_Update, which + // will call CG_DrawInformation during the loading process + // reliableCommandSequence will be 0 on fresh loads, but higher for + // demos, tourney restarts, or vid_restarts + + CG_SHUTDOWN, +// void (*CG_Shutdown)( void ); + // oportunity to flush and close any open files + + CG_CONSOLE_COMMAND, +// qboolean (*CG_ConsoleCommand)( void ); + // a console command has been issued locally that is not recognized by the + // main game system. + // use Cmd_Argc() / Cmd_Argv() to read the command, return qfalse if the + // command is not known to the game + + CG_DRAW_ACTIVE_FRAME, +// void (*CG_DrawActiveFrame)( int serverTime, stereoFrame_t stereoView, qboolean demoPlayback ); + // Generates and draws a game scene and status information at the given time. + // If demoPlayback is set, local movement prediction will not be enabled + + CG_CROSSHAIR_PLAYER, +// int (*CG_CrosshairPlayer)( void ); + + CG_LAST_ATTACKER, +// int (*CG_LastAttacker)( void ); + + CG_KEY_EVENT, +// void (*CG_KeyEvent)( int key, qboolean down ); + + CG_MOUSE_EVENT, +// void (*CG_MouseEvent)( int dx, int dy ); + CG_EVENT_HANDLING +// void (*CG_EventHandling)(int type); +} cgameExport_t; + +//---------------------------------------------- diff --git a/src/cgame/cg_scanner.c b/src/cgame/cg_scanner.c new file mode 100644 index 00000000..336b9864 --- /dev/null +++ b/src/cgame/cg_scanner.c @@ -0,0 +1,117 @@ +/* + * Portions Copyright (C) 2000-2001 Tim Angus + * + * This program 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, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ + +/* To assertain which portions are licensed under the GPL and which are + * licensed by Id Software, Inc. please run a diff between the equivalent + * versions of the "Tremulous" modification and the unmodified "Quake3" + * game source code. + */ + +#include "cg_local.h" + +void CG_Scanner( ) +{ + int i; + vec3_t origin; + vec3_t relOrigin; + vec3_t drawOrigin; + vec3_t up = { 0, 0, 1 }; + vec4_t hIabove = { 0, 1, 0, 1 }; + vec4_t hIbelow = { 0, 0.5, 0, 1 }; + vec4_t aIabove = { 1, 0, 0, 1 }; + vec4_t aIbelow = { 0.5, 0, 0, 1 }; + + VectorCopy( cg.refdef.vieworg, origin ); + + for( i = 0; i < cgIP.numHumanItems; i++ ) + { + VectorClear( relOrigin ); + VectorSubtract( cgIP.humanItemPositions[ i ], origin, relOrigin ); + + if( VectorLength( relOrigin ) < 1000 && ( relOrigin[ 2 ] < 0 ) ) + { + RotatePointAroundVector( drawOrigin, up, relOrigin, -cg.refdefViewAngles[ 1 ]-90 ); + drawOrigin[ 0 ] /= ( 1000 / 180 ); + drawOrigin[ 1 ] /= ( 1000 / 40 ); + drawOrigin[ 2 ] /= ( 1000 / 180 ); + + trap_R_SetColor( hIbelow ); + CG_DrawPic( 319 - drawOrigin[ 0 ], 360 + drawOrigin[ 1 ], 2, -drawOrigin[ 2 ], cgs.media.scannerLineShader ); + CG_DrawPic( 312 - drawOrigin[ 0 ], 356 + drawOrigin[ 1 ] - drawOrigin[ 2 ], 16, 8, cgs.media.scannerBlipShader ); + trap_R_SetColor( NULL ); + } + } + + for( i = 0; i < cgIP.numDroidItems; i++ ) + { + VectorClear( relOrigin ); + VectorSubtract( cgIP.droidItemPositions[ i ], origin, relOrigin ); + + if( VectorLength( relOrigin ) < 1000 && ( relOrigin[ 2 ] < 0 ) ) + { + RotatePointAroundVector( drawOrigin, up, relOrigin, -cg.refdefViewAngles[ 1 ]-90 ); + drawOrigin[ 0 ] /= ( 1000 / 180 ); + drawOrigin[ 1 ] /= ( 1000 / 40 ); + drawOrigin[ 2 ] /= ( 1000 / 180 ); + + trap_R_SetColor( aIbelow ); + CG_DrawPic( 319 - drawOrigin[ 0 ], 360 + drawOrigin[ 1 ], 2, -drawOrigin[ 2 ], cgs.media.scannerLineShader ); + CG_DrawPic( 312 - drawOrigin[ 0 ], 356 + drawOrigin[ 1 ] - drawOrigin[ 2 ], 16, 8, cgs.media.scannerBlipShader ); + trap_R_SetColor( NULL ); + } + } + + CG_DrawPic( 140, 320, 360, 80, cgs.media.scannerShader ); + + for( i = 0; i < cgIP.numHumanItems; i++ ) + { + VectorClear( relOrigin ); + VectorSubtract( cgIP.humanItemPositions[ i ], origin, relOrigin ); + + if( VectorLength( relOrigin ) < 1000 && ( relOrigin[ 2 ] > 0 ) ) + { + RotatePointAroundVector( drawOrigin, up, relOrigin, -cg.refdefViewAngles[ 1 ]-90 ); + drawOrigin[ 0 ] /= ( 1000 / 180 ); + drawOrigin[ 1 ] /= ( 1000 / 40 ); + drawOrigin[ 2 ] /= ( 1000 / 180 ); + + trap_R_SetColor( hIabove ); + CG_DrawPic( 319 - drawOrigin[ 0 ], 360 + drawOrigin[ 1 ], 2, -drawOrigin[ 2 ], cgs.media.scannerLineShader ); + CG_DrawPic( 312 - drawOrigin[ 0 ], 356 + drawOrigin[ 1 ] - drawOrigin[ 2 ], 16, 8, cgs.media.scannerBlipShader ); + trap_R_SetColor( NULL ); + } + } + for( i = 0; i < cgIP.numDroidItems; i++ ) + { + VectorClear( relOrigin ); + VectorSubtract( cgIP.droidItemPositions[ i ], origin, relOrigin ); + + if( VectorLength( relOrigin ) < 1000 && ( relOrigin[ 2 ] > 0 ) ) + { + RotatePointAroundVector( drawOrigin, up, relOrigin, -cg.refdefViewAngles[ 1 ]-90 ); + drawOrigin[ 0 ] /= ( 1000 / 180 ); + drawOrigin[ 1 ] /= ( 1000 / 40 ); + drawOrigin[ 2 ] /= ( 1000 / 180 ); + + trap_R_SetColor( aIabove ); + CG_DrawPic( 319 - drawOrigin[ 0 ], 360 + drawOrigin[ 1 ], 2, -drawOrigin[ 2 ], cgs.media.scannerLineShader ); + CG_DrawPic( 312 - drawOrigin[ 0 ], 356 + drawOrigin[ 1 ] - drawOrigin[ 2 ], 16, 8, cgs.media.scannerBlipShader ); + trap_R_SetColor( NULL ); + } + } +} diff --git a/src/cgame/cg_servercmds.c b/src/cgame/cg_servercmds.c new file mode 100644 index 00000000..31264077 --- /dev/null +++ b/src/cgame/cg_servercmds.c @@ -0,0 +1,1021 @@ +// Copyright (C) 1999-2000 Id Software, Inc. +// +// 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 + +/* + * Portions Copyright (C) 2000-2001 Tim Angus + * + * This program 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, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ + +/* To assertain which portions are licensed under the GPL and which are + * licensed by Id Software, Inc. please run a diff between the equivalent + * versions of the "Tremulous" modification and the unmodified "Quake3" + * game source code. + */ + +#include "cg_local.h" + +#include "../ta_ui/menudef.h" + +typedef struct { + const char *order; + int taskNum; +} orderTask_t; + +static const orderTask_t validOrders[] = { + /*{ VOICECHAT_GETFLAG, TEAMTASK_OFFENSE }, + { VOICECHAT_OFFENSE, TEAMTASK_OFFENSE }, + { VOICECHAT_DEFEND, TEAMTASK_DEFENSE }, + { VOICECHAT_DEFENDFLAG, TEAMTASK_DEFENSE }, + { VOICECHAT_PATROL, TEAMTASK_PATROL }, + { VOICECHAT_CAMP, TEAMTASK_CAMP }, + { VOICECHAT_FOLLOWME, TEAMTASK_FOLLOW }, + { VOICECHAT_RETURNFLAG, TEAMTASK_RETRIEVE }, + { VOICECHAT_FOLLOWFLAGCARRIER, TEAMTASK_ESCORT }*/ + { NULL, 0 } +}; + +static const int numValidOrders = sizeof(validOrders) / sizeof(orderTask_t); + +/* +================= +CG_ParseScores + +================= +*/ +static void CG_ParseScores( void ) { + int i, powerups; + + 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 ) ); + for ( i = 0 ; i < cg.numScores ; i++ ) { + // + cg.scores[i].client = atoi( CG_Argv( i * 14 + 4 ) ); + cg.scores[i].score = atoi( CG_Argv( i * 14 + 5 ) ); + cg.scores[i].ping = atoi( CG_Argv( i * 14 + 6 ) ); + cg.scores[i].time = atoi( CG_Argv( i * 14 + 7 ) ); + cg.scores[i].scoreFlags = atoi( CG_Argv( i * 14 + 8 ) ); + powerups = atoi( CG_Argv( i * 14 + 9 ) ); + cg.scores[i].accuracy = atoi(CG_Argv(i * 14 + 10)); + cg.scores[i].impressiveCount = atoi(CG_Argv(i * 14 + 11)); + cg.scores[i].excellentCount = atoi(CG_Argv(i * 14 + 12)); + cg.scores[i].guantletCount = atoi(CG_Argv(i * 14 + 13)); + cg.scores[i].defendCount = atoi(CG_Argv(i * 14 + 14)); + cg.scores[i].assistCount = atoi(CG_Argv(i * 14 + 15)); + cg.scores[i].perfect = atoi(CG_Argv(i * 14 + 16)); + cg.scores[i].captures = atoi(CG_Argv(i * 14 + 17)); + + 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 = powerups; + + 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.gametype = atoi( Info_ValueForKey( info, "g_gametype" ) ); + trap_Cvar_Set("g_gametype", va("%i", cgs.gametype)); + cgs.dmflags = atoi( Info_ValueForKey( info, "dmflags" ) ); + cgs.teamflags = atoi( Info_ValueForKey( info, "teamflags" ) ); + cgs.fraglimit = atoi( Info_ValueForKey( info, "fraglimit" ) ); + cgs.capturelimit = atoi( Info_ValueForKey( info, "capturelimit" ) ); + cgs.timelimit = atoi( Info_ValueForKey( info, "timelimit" ) ); + cgs.maxclients = atoi( Info_ValueForKey( info, "sv_maxclients" ) ); + mapname = Info_ValueForKey( info, "mapname" ); + Com_sprintf( cgs.mapname, sizeof( cgs.mapname ), "maps/%s.bsp", mapname ); + Q_strncpyz( cgs.redTeam, Info_ValueForKey( info, "g_redTeam" ), sizeof(cgs.redTeam) ); + trap_Cvar_Set("g_redTeam", cgs.redTeam); + Q_strncpyz( cgs.blueTeam, Info_ValueForKey( info, "g_blueTeam" ), sizeof(cgs.blueTeam) ); + trap_Cvar_Set("g_blueTeam", cgs.blueTeam); +} + +/* +================== +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 ) { + + } else if ( warmup > 0 && cg.warmup <= 0 ) { + trap_S_StartLocalSound( cgs.media.countPrepareSound, CHAN_ANNOUNCER ); + } + + cg.warmup = warmup; +} + +/* +================ +CG_SetConfigValues + +Called on load to set the initial values from configure strings +================ +*/ +void CG_SetConfigValues( void ) { + const char *s; + + cgs.scores1 = atoi( CG_ConfigString( CS_SCORES1 ) ); + cgs.scores2 = atoi( CG_ConfigString( CS_SCORES2 ) ); + + cgs.aBuildPoints = atoi( CG_ConfigString( CS_ABPOINTS ) ); + cgs.hBuildPoints = atoi( CG_ConfigString( CS_HBPOINTS ) ); + + cgs.levelStartTime = atoi( CG_ConfigString( CS_LEVEL_START_TIME ) ); + if( cgs.gametype == GT_CTF ) { + s = CG_ConfigString( CS_FLAGSTATUS ); + cgs.redflag = s[0] - '0'; + cgs.blueflag = s[1] - '0'; + } + 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_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_SCORES1 ) { + cgs.scores1 = atoi( str ); + } else if ( num == CS_SCORES2 ) { + cgs.scores2 = atoi( str ); + } else if ( num == CS_ABPOINTS ) { + cgs.aBuildPoints = atoi( str ); + } else if ( num == CS_HBPOINTS ) { + cgs.hBuildPoints = atoi( str ); + } else if ( num == CS_LEVEL_START_TIME ) { + cgs.levelStartTime = atoi( str ); + } else if ( num == CS_VOTE_TIME ) { + cgs.voteTime = atoi( str ); + cgs.voteModified = qtrue; + } 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) { + cgs.teamVoteTime[num-CS_TEAMVOTE_TIME] = atoi( str ); + cgs.teamVoteModified[num-CS_TEAMVOTE_TIME] = qtrue; + } 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 ) ); + } 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_SOUNDS && num < CS_SOUNDS+MAX_MODELS ) { + 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_FLAGSTATUS ) { + if( cgs.gametype == GT_CTF ) { + // format is rb where its red/blue, 0 is at base, 1 is taken, 2 is dropped + cgs.redflag = str[0] - '0'; + cgs.blueflag = str[1] - '0'; + } + } + else if ( num == CS_SHADERSTATE ) { + CG_ShaderStateChanged(); + } + +} + + +/* +======================= +CG_AddToTeamChat + +======================= +*/ +static void CG_AddToTeamChat( const char *str ) { + int len; + char *p, *ls; + int lastcolor; + int chatHeight; + + if (cg_teamChatHeight.integer < TEAMCHAT_HEIGHT) { + chatHeight = cg_teamChatHeight.integer; + } else { + chatHeight = TEAMCHAT_HEIGHT; + } + + if (chatHeight <= 0 || cg_teamChatTime.integer <= 0) { + // team chat disabled, dump into normal chat + cgs.teamChatPos = cgs.teamLastChatPos = 0; + return; + } + + len = 0; + + p = cgs.teamChatMsgs[cgs.teamChatPos % chatHeight]; + *p = 0; + + lastcolor = '7'; + + ls = NULL; + while (*str) { + if (len > TEAMCHAT_WIDTH - 1) { + if (ls) { + str -= (p - ls); + str++; + p -= (p - ls); + } + *p = 0; + + cgs.teamChatMsgTimes[cgs.teamChatPos % chatHeight] = cg.time; + + cgs.teamChatPos++; + p = cgs.teamChatMsgs[cgs.teamChatPos % chatHeight]; + *p = 0; + *p++ = Q_COLOR_ESCAPE; + *p++ = lastcolor; + len = 0; + ls = NULL; + } + + if ( Q_IsColorString( str ) ) { + *p++ = *str++; + lastcolor = *str; + *p++ = *str++; + continue; + } + if (*str == ' ') { + ls = p; + } + *p++ = *str++; + len++; + } + *p = 0; + + cgs.teamChatMsgTimes[cgs.teamChatPos % chatHeight] = cg.time; + cgs.teamChatPos++; + + if (cgs.teamChatPos - cgs.teamLastChatPos > chatHeight) + cgs.teamLastChatPos = cgs.teamChatPos - chatHeight; +} + + + +/* +=============== +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_InitLocalEntities(); + 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 /* && cgs.gametype == GT_TOURNAMENT */) { + trap_S_StartLocalSound( cgs.media.countFightSound, CHAN_ANNOUNCER ); + CG_CenterPrint( "FIGHT!", 120, GIANTCHAR_WIDTH*2 ); + } +} + +#define MAX_VOICEFILESIZE 16384 +#define MAX_VOICEFILES 8 +#define MAX_VOICECHATS 64 +#define MAX_VOICESOUNDS 64 +#define MAX_CHATSIZE 64 +#define MAX_HEADMODELS 64 + +typedef struct voiceChat_s +{ + char id[64]; + int numSounds; + sfxHandle_t sounds[MAX_VOICESOUNDS]; + char chats[MAX_VOICESOUNDS][MAX_CHATSIZE]; +} voiceChat_t; + +typedef struct voiceChatList_s +{ + char name[64]; + int gender; + int numVoiceChats; + voiceChat_t voiceChats[MAX_VOICECHATS]; +} voiceChatList_t; + +typedef struct headModelVoiceChat_s +{ + char headmodel[64]; + int voiceChatNum; +} headModelVoiceChat_t; + +voiceChatList_t voiceChatLists[MAX_VOICEFILES]; +headModelVoiceChat_t headModelVoiceChat[MAX_HEADMODELS]; + +/* +================= +CG_ParseVoiceChats +================= +*/ +int CG_ParseVoiceChats( const char *filename, voiceChatList_t *voiceChatList, int maxVoiceChats ) { + int len, i; + fileHandle_t f; + char buf[MAX_VOICEFILESIZE]; + char **p, *ptr; + char *token; + voiceChat_t *voiceChats; + qboolean compress; + + compress = qtrue; + if (cg_buildScript.integer) { + compress = qfalse; + } + + len = trap_FS_FOpenFile( filename, &f, FS_READ ); + if ( !f ) { + trap_Print( va( S_COLOR_RED "voice chat file not found: %s\n", filename ) ); + return qfalse; + } + if ( len >= MAX_VOICEFILESIZE ) { + trap_Print( va( S_COLOR_RED "voice chat file too large: %s is %i, max allowed is %i", filename, len, MAX_VOICEFILESIZE ) ); + trap_FS_FCloseFile( f ); + return qfalse; + } + + trap_FS_Read( buf, len, f ); + buf[len] = 0; + trap_FS_FCloseFile( f ); + + ptr = buf; + p = &ptr; + + Com_sprintf(voiceChatList->name, sizeof(voiceChatList->name), "%s", filename); + voiceChats = voiceChatList->voiceChats; + for ( i = 0; i < maxVoiceChats; i++ ) { + voiceChats[i].id[0] = 0; + } + token = COM_ParseExt(p, qtrue); + if (!token || token[0] == 0) { + return qtrue; + } + if (!Q_stricmp(token, "female")) { + voiceChatList->gender = GENDER_FEMALE; + } + else if (!Q_stricmp(token, "male")) { + voiceChatList->gender = GENDER_MALE; + } + else if (!Q_stricmp(token, "neuter")) { + voiceChatList->gender = GENDER_NEUTER; + } + else { + trap_Print( va( S_COLOR_RED "expected gender not found in voice chat file: %s\n", filename ) ); + return qfalse; + } + + voiceChatList->numVoiceChats = 0; + while ( 1 ) { + token = COM_ParseExt(p, qtrue); + if (!token || token[0] == 0) { + return qtrue; + } + Com_sprintf(voiceChats[voiceChatList->numVoiceChats].id, sizeof( voiceChats[voiceChatList->numVoiceChats].id ), "%s", token); + token = COM_ParseExt(p, qtrue); + if (Q_stricmp(token, "{")) { + trap_Print( va( S_COLOR_RED "expected { found %s in voice chat file: %s\n", token, filename ) ); + return qfalse; + } + voiceChats[voiceChatList->numVoiceChats].numSounds = 0; + while(1) { + token = COM_ParseExt(p, qtrue); + if (!token || token[0] == 0) { + return qtrue; + } + if (!Q_stricmp(token, "}")) + break; + voiceChats[voiceChatList->numVoiceChats].sounds[voiceChats[voiceChatList->numVoiceChats].numSounds] = + trap_S_RegisterSound( token , compress ); + token = COM_ParseExt(p, qtrue); + if (!token || token[0] == 0) { + return qtrue; + } + Com_sprintf(voiceChats[voiceChatList->numVoiceChats].chats[ + voiceChats[voiceChatList->numVoiceChats].numSounds], MAX_CHATSIZE, "%s", token); + voiceChats[voiceChatList->numVoiceChats].numSounds++; + if (voiceChats[voiceChatList->numVoiceChats].numSounds >= MAX_VOICESOUNDS) + break; + } + voiceChatList->numVoiceChats++; + if (voiceChatList->numVoiceChats >= maxVoiceChats) + return qtrue; + } + return qtrue; +} + +/* +================= +CG_LoadVoiceChats +================= +*/ +void CG_LoadVoiceChats( void ) { + int size; + + size = trap_MemoryRemaining(); + CG_ParseVoiceChats( "scripts/female1.voice", &voiceChatLists[0], MAX_VOICECHATS ); + CG_ParseVoiceChats( "scripts/female2.voice", &voiceChatLists[1], MAX_VOICECHATS ); + CG_ParseVoiceChats( "scripts/female3.voice", &voiceChatLists[2], MAX_VOICECHATS ); + CG_ParseVoiceChats( "scripts/male1.voice", &voiceChatLists[3], MAX_VOICECHATS ); + CG_ParseVoiceChats( "scripts/male2.voice", &voiceChatLists[4], MAX_VOICECHATS ); + CG_ParseVoiceChats( "scripts/male3.voice", &voiceChatLists[5], MAX_VOICECHATS ); + CG_ParseVoiceChats( "scripts/male4.voice", &voiceChatLists[6], MAX_VOICECHATS ); + CG_ParseVoiceChats( "scripts/male5.voice", &voiceChatLists[7], MAX_VOICECHATS ); + CG_Printf("voice chat memory size = %d\n", size - trap_MemoryRemaining()); +} + +/* +================= +CG_HeadModelVoiceChats +================= +*/ +int CG_HeadModelVoiceChats( char *filename ) { + int len, i; + fileHandle_t f; + char buf[MAX_VOICEFILESIZE]; + char **p, *ptr; + char *token; + + len = trap_FS_FOpenFile( filename, &f, FS_READ ); + if ( !f ) { + trap_Print( va( "voice chat file not found: %s\n", filename ) ); + return -1; + } + if ( len >= MAX_VOICEFILESIZE ) { + trap_Print( va( S_COLOR_RED "voice chat file too large: %s is %i, max allowed is %i", filename, len, MAX_VOICEFILESIZE ) ); + trap_FS_FCloseFile( f ); + return -1; + } + + trap_FS_Read( buf, len, f ); + buf[len] = 0; + trap_FS_FCloseFile( f ); + + ptr = buf; + p = &ptr; + + token = COM_ParseExt(p, qtrue); + if (!token || token[0] == 0) { + return -1; + } + + for ( i = 0; i < MAX_VOICEFILES; i++ ) { + if ( !Q_stricmp(token, voiceChatLists[i].name) ) { + return i; + } + } + + //FIXME: maybe try to load the .voice file which name is stored in token? + + return -1; +} + +/* +================= +CG_GetVoiceChat +================= +*/ +int CG_GetVoiceChat( voiceChatList_t *voiceChatList, const char *id, sfxHandle_t *snd, char **chat) { + int i, rnd; + + for ( i = 0; i < voiceChatList->numVoiceChats; i++ ) { + if ( !Q_stricmp( id, voiceChatList->voiceChats[i].id ) ) { + rnd = random() * voiceChatList->voiceChats[i].numSounds; + *snd = voiceChatList->voiceChats[i].sounds[rnd]; + *chat = voiceChatList->voiceChats[i].chats[rnd]; + return qtrue; + } + } + return qfalse; +} + +/* +================= +CG_VoiceChatListForClient +================= +*/ +voiceChatList_t *CG_VoiceChatListForClient( int clientNum ) { + clientInfo_t *ci; + int voiceChatNum, i, j, k, gender; + char filename[128], *headModelName; + + if ( clientNum < 0 || clientNum >= MAX_CLIENTS ) { + clientNum = 0; + } + ci = &cgs.clientinfo[ clientNum ]; + + headModelName = ci->headModelName; + if (headModelName[0] == '*') + headModelName++; + // find the voice file for the head model the client uses + for ( i = 0; i < MAX_HEADMODELS; i++ ) { + if (!Q_stricmp(headModelVoiceChat[i].headmodel, headModelName)) { + break; + } + } + if (i < MAX_HEADMODELS) { + return &voiceChatLists[headModelVoiceChat[i].voiceChatNum]; + } + // find a <headmodelname>.vc file + for ( i = 0; i < MAX_HEADMODELS; i++ ) { + if (!strlen(headModelVoiceChat[i].headmodel)) { + Com_sprintf(filename, sizeof(filename), "scripts/%s.vc", headModelName); + voiceChatNum = CG_HeadModelVoiceChats(filename); + if (voiceChatNum == -1) + break; + Com_sprintf(headModelVoiceChat[i].headmodel, sizeof ( headModelVoiceChat[i].headmodel ), + "%s", headModelName); + headModelVoiceChat[i].voiceChatNum = voiceChatNum; + return &voiceChatLists[headModelVoiceChat[i].voiceChatNum]; + } + } + gender = ci->gender; + for (k = 0; k < 2; k++) { + // just pick the first with the right gender + for ( i = 0; i < MAX_VOICEFILES; i++ ) { + if (strlen(voiceChatLists[i].name)) { + if (voiceChatLists[i].gender == gender) { + // store this head model with voice chat for future reference + for ( j = 0; j < MAX_HEADMODELS; j++ ) { + if (!strlen(headModelVoiceChat[j].headmodel)) { + Com_sprintf(headModelVoiceChat[j].headmodel, sizeof ( headModelVoiceChat[j].headmodel ), + "%s", headModelName); + headModelVoiceChat[j].voiceChatNum = i; + break; + } + } + return &voiceChatLists[i]; + } + } + } + // fall back to male gender because we don't have neuter in the mission pack + if (gender == GENDER_MALE) + break; + gender = GENDER_MALE; + } + // store this head model with voice chat for future reference + for ( j = 0; j < MAX_HEADMODELS; j++ ) { + if (!strlen(headModelVoiceChat[j].headmodel)) { + Com_sprintf(headModelVoiceChat[j].headmodel, sizeof ( headModelVoiceChat[j].headmodel ), + "%s", headModelName); + headModelVoiceChat[j].voiceChatNum = 0; + break; + } + } + // just return the first voice chat list + return &voiceChatLists[0]; +} + +#define MAX_VOICECHATBUFFER 32 + +typedef struct bufferedVoiceChat_s +{ + int clientNum; + sfxHandle_t snd; + int voiceOnly; + char cmd[MAX_SAY_TEXT]; + char message[MAX_SAY_TEXT]; +} bufferedVoiceChat_t; + +bufferedVoiceChat_t voiceChatBuffer[MAX_VOICECHATBUFFER]; +int voiceChatBufferIn, voiceChatBufferOut; +int voiceChatTime; + +/* +================= +CG_PlayVoiceChat +================= +*/ +void CG_PlayVoiceChat( bufferedVoiceChat_t *vchat ) { + // if we are going into the intermission, don't start any voices + if ( cg.intermissionStarted ) { + return; + } + + if ( !cg_noVoiceChats.integer ) { + trap_S_StartLocalSound( vchat->snd, CHAN_VOICE); + } + if (!vchat->voiceOnly && !cg_noVoiceText.integer) { + CG_AddToTeamChat( vchat->message ); + CG_Printf( "%s\n", vchat->message ); + } + voiceChatBuffer[voiceChatBufferOut].snd = 0; +} + +/* +===================== +CG_PlayBufferedVoieChats +===================== +*/ +void CG_PlayBufferedVoiceChats( void ) { + if ( voiceChatTime < cg.time ) { + if (voiceChatBufferOut != voiceChatBufferIn && voiceChatBuffer[voiceChatBufferOut].snd) { + // + CG_PlayVoiceChat(&voiceChatBuffer[voiceChatBufferOut]); + // + voiceChatBufferOut = (voiceChatBufferOut + 1) % MAX_VOICECHATBUFFER; + voiceChatTime = cg.time + 1000; + } + } +} + +/* +===================== +CG_AddBufferedVoiceChat +===================== +*/ +void CG_AddBufferedVoiceChat( bufferedVoiceChat_t *vchat ) { + // if we are going into the intermission, don't start any voices + if ( cg.intermissionStarted ) { + return; + } + + memcpy(&voiceChatBuffer[voiceChatBufferIn], vchat, sizeof(bufferedVoiceChat_t)); + voiceChatBufferIn = (voiceChatBufferIn + 1) % MAX_VOICECHATBUFFER; + if (voiceChatBufferIn == voiceChatBufferOut) { + CG_PlayVoiceChat( &voiceChatBuffer[voiceChatBufferOut] ); + voiceChatBufferOut++; + } +} + +/* +================= +CG_VoiceChatLocal +================= +*/ +void CG_VoiceChatLocal( int mode, qboolean voiceOnly, int clientNum, int color, const char *cmd ) { + char *chat; + voiceChatList_t *voiceChatList; + clientInfo_t *ci; + sfxHandle_t snd; + bufferedVoiceChat_t vchat; + + // if we are going into the intermission, don't start any voices + if ( cg.intermissionStarted ) { + return; + } + + if ( clientNum < 0 || clientNum >= MAX_CLIENTS ) { + clientNum = 0; + } + ci = &cgs.clientinfo[ clientNum ]; + + cgs.currentVoiceClient = clientNum; + + voiceChatList = CG_VoiceChatListForClient( clientNum ); + + if ( CG_GetVoiceChat( voiceChatList, cmd, &snd, &chat ) ) { + // + if ( mode == SAY_TEAM || !cg_teamChatsOnly.integer ) { + vchat.clientNum = clientNum; + vchat.snd = snd; + vchat.voiceOnly = voiceOnly; + Q_strncpyz(vchat.cmd, cmd, sizeof(vchat.cmd)); + if ( mode == SAY_TELL ) { + Com_sprintf(vchat.message, sizeof(vchat.message), "[%s]: %c%c%s", ci->name, Q_COLOR_ESCAPE, color, chat); + } + else if ( mode == SAY_TEAM ) { + Com_sprintf(vchat.message, sizeof(vchat.message), "(%s): %c%c%s", ci->name, Q_COLOR_ESCAPE, color, chat); + } + else { + Com_sprintf(vchat.message, sizeof(vchat.message), "%s: %c%c%s", ci->name, Q_COLOR_ESCAPE, color, chat); + } + CG_AddBufferedVoiceChat(&vchat); + } + } +} + +/* +================= +CG_VoiceChat +================= +*/ +void CG_VoiceChat( int mode ) { + const char *cmd; + int clientNum, color; + qboolean voiceOnly; + + voiceOnly = atoi(CG_Argv(1)); + clientNum = atoi(CG_Argv(2)); + color = atoi(CG_Argv(3)); + cmd = CG_Argv(4); + + if (cg_noTaunt.integer != 0) { + /*if (!strcmp(cmd, VOICECHAT_KILLINSULT) || !strcmp(cmd, VOICECHAT_TAUNT) || \ + !strcmp(cmd, VOICECHAT_DEATHINSULT) || !strcmp(cmd, VOICECHAT_KILLGAUNTLET) || \ + !strcmp(cmd, VOICECHAT_PRAISE)) { + return; + }*/ + } + + CG_VoiceChatLocal( mode, voiceOnly, clientNum, color, cmd ); +} + +/* +================= +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_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 ) { + trap_S_StartLocalSound( cgs.media.talkSound, CHAN_LOCAL_SOUND ); + Q_strncpyz( text, CG_Argv(1), MAX_SAY_TEXT ); + CG_RemoveChatEscapeChar( text ); + CG_Printf( "%s\n", text ); + } + return; + } + + if ( !strcmp( cmd, "tchat" ) ) { + trap_S_StartLocalSound( cgs.media.talkSound, CHAN_LOCAL_SOUND ); + Q_strncpyz( text, CG_Argv(1), MAX_SAY_TEXT ); + CG_RemoveChatEscapeChar( text ); + CG_AddToTeamChat( text ); + CG_Printf( "%s\n", text ); + return; + } + if ( !strcmp( cmd, "vchat" ) ) { + CG_VoiceChat( SAY_ALL ); + return; + } + + if ( !strcmp( cmd, "vtchat" ) ) { + CG_VoiceChat( SAY_TEAM ); + return; + } + + if ( !strcmp( cmd, "vtell" ) ) { + CG_VoiceChat( SAY_TELL ); + 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)); + } + } + + // loaddeferred can be both a servercmd and a consolecmd + if ( !strcmp( cmd, "loaddefered" ) ) { // FIXME: spelled wrong, but not changing for demo + CG_LoadDeferredPlayers(); + return; + } + + // clientLevelShot is sent before taking a special screenshot for + // the menu system during development + if ( !strcmp( cmd, "clientLevelShot" ) ) { + cg.levelShot = qtrue; + 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 00000000..be64bc89 --- /dev/null +++ b/src/cgame/cg_snapshot.c @@ -0,0 +1,402 @@ +// Copyright (C) 1999-2000 Id Software, Inc. +// +// cg_snapshot.c -- things that happen on snapshot transition, +// not necessarily every single rendered frame + +/* + * Portions Copyright (C) 2000-2001 Tim Angus + * + * This program 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, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ + +/* To assertain which portions are licensed under the GPL and which are + * licensed by Id Software, Inc. please run a diff between the equivalent + * versions of the "Tremulous" modification and the unmodified "Quake3" + * game source code. + */ + +#include "cg_local.h" + + + +/* +================== +CG_ResetEntity +================== +*/ +static void CG_ResetEntity( centity_t *cent ) { + // if an event is set, assume it is new enough to use + // if the event had timed out, it would have been cleared + 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 ) { + } + + // 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 ); + } + + 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 00000000..3298ef73 --- /dev/null +++ b/src/cgame/cg_syscalls.asm @@ -0,0 +1,99 @@ +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_PC_AddGlobalDefine -65 +equ trap_PC_LoadSource -66 +equ trap_PC_FreeSource -67 +equ trap_PC_ReadToken -68 +equ trap_PC_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 memset -101 +equ memcpy -102 +equ strncpy -103 +equ sin -104 +equ cos -105 +equ atan2 -106 +equ sqrt -107 +equ floor -108 +equ ceil -109 +equ testPrintInt -110 +equ testPrintFloat -111 +equ acos -112 + diff --git a/src/cgame/cg_syscalls.c b/src/cgame/cg_syscalls.c new file mode 100644 index 00000000..3e2e05fa --- /dev/null +++ b/src/cgame/cg_syscalls.c @@ -0,0 +1,396 @@ +// Copyright (C) 1999-2000 Id Software, Inc. +// +// cg_syscalls.c -- this file is only included when building a dll +// cg_syscalls.asm is included instead when building a qvm + +/* + * Portions Copyright (C) 2000-2001 Tim Angus + * + * This program 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, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ + +/* To assertain which portions are licensed under the GPL and which are + * licensed by Id Software, Inc. please run a diff between the equivalent + * versions of the "Tremulous" modification and the unmodified "Quake3" + * game source code. + */ + +#include "cg_local.h" + +static int (QDECL *syscall)( int arg, ... ) = (int (QDECL *)( int, ...))-1; + + +void dllEntry( int (QDECL *syscallptr)( int 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 ); +} + +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_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 ); +} + +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_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 ); +} + +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 ); +} + +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_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_PC_AddGlobalDefine( char *define ) { + return syscall( CG_PC_ADD_GLOBAL_DEFINE, define ); +} + +int trap_PC_LoadSource( const char *filename ) { + return syscall( CG_PC_LOAD_SOURCE, filename ); +} + +int trap_PC_FreeSource( int handle ) { + return syscall( CG_PC_FREE_SOURCE, handle ); +} + +int trap_PC_ReadToken( int handle, pc_token_t *pc_token ) { + return syscall( CG_PC_READ_TOKEN, handle, pc_token ); +} + +int trap_PC_SourceFileAndLine( int handle, char *filename, int *line ) { + return syscall( CG_PC_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); +} + diff --git a/src/cgame/cg_view.c b/src/cgame/cg_view.c new file mode 100644 index 00000000..e4550a71 --- /dev/null +++ b/src/cgame/cg_view.c @@ -0,0 +1,976 @@ +// Copyright (C) 1999-2000 Id Software, Inc. +// +// cg_view.c -- setup all the parameters (position, angle, etc) +// for a 3D rendering + +/* + * Portions Copyright (C) 2000-2001 Tim Angus + * + * This program 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, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ + +/* To assertain which portions are licensed under the GPL and which are + * licensed by Id Software, Inc. please run a diff between the equivalent + * versions of the "Tremulous" modification and the unmodified "Quake3" + * game source code. + */ + +#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) ); + if ( trap_Argc() < 2 ) { + return; + } + + Q_strncpyz (cg.testModelName, CG_Argv( 1 ), MAX_QPATH ); + cg.testModelEntity.hModel = trap_R_RegisterModel( cg.testModelName ); + + 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; +} + +/* +================= +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 ); + 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 ); +} + + + +//============================================================================ + + +/* +================= +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 = { -4, -4, -4 }; + static vec3_t maxs = { 4, 4, 4 }; + vec3_t focusPoint; + float focusDist; + float forwardScale, sideScale; + + //TA: when wall climbing the viewheight is not straight up + if( cg.predictedPlayerState.stats[ STAT_STATE ] & SS_WALLCLIMBING ) + VectorMA( cg.refdef.vieworg, cg.predictedPlayerState.viewheight, cg.predictedPlayerState.grapplePoint, cg.refdef.vieworg ); + else + cg.refdef.vieworg[2] += cg.predictedPlayerState.viewheight; + + VectorCopy( cg.refdefViewAngles, focusAngles ); + + // if dead, look at killer + if ( cg.predictedPlayerState.stats[STAT_HEALTH] <= 0 ) { + focusAngles[YAW] = cg.predictedPlayerState.stats[STAT_DEAD_YAW]; + cg.refdefViewAngles[YAW] = cg.predictedPlayerState.stats[STAT_DEAD_YAW]; + } + + //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 ); + + //TA: when wall climbing the viewheight is not straight up + if( cg.predictedPlayerState.stats[ STAT_STATE ] & SS_WALLCLIMBING ) + VectorMA( view, 8, cg.predictedPlayerState.grapplePoint, view ); + else + view[2] += 8; + + //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 ) { + int timeDelta; + int steptime; + + BG_unpackAttributes( NULL, NULL, &steptime, cg.predictedPlayerState.stats ); + + // smooth out stair climbing + timeDelta = cg.time - cg.stepTime; + if ( timeDelta < steptime ) { + cg.refdef.vieworg[2] -= cg.stepChange + * (steptime - timeDelta) / steptime; + } +} + +/* +=============== +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; + int bob2; + + if ( cg.snap->ps.pm_type == PM_INTERMISSION ) { + return; + } + + origin = cg.refdef.vieworg; + angles = cg.refdefViewAngles; + + // 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_DEAD_YAW]; + 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 + //TA: bob amount is class dependant + BG_unpackAttributes( NULL, &bob2, NULL, cg.predictedPlayerState.stats ); + if( bob2 != 0 ) + { + // make sure the bob is visible even at low speeds + speed = cg.xyspeed > 200 ? cg.xyspeed : 200; + + delta = cg.bobfracsin * ( bob2 / 1000.0 ) * speed; + if (cg.predictedPlayerState.pm_flags & PMF_DUCKED) + delta *= 3; // crouching + angles[PITCH] += delta; + delta = cg.bobfracsin * ( bob2 / 1000.0 ) * speed; + if (cg.predictedPlayerState.pm_flags & PMF_DUCKED) + delta *= 3; // crouching accentuates roll + if (cg.bobcycle & 1) + delta = -delta; + angles[ROLL] += delta; + } + + //TA: this *feels* more realisitic for humans + if( cg.predictedPlayerState.stats[ STAT_PTEAM ] == PTE_HUMANS ) + { + angles[PITCH] += cg.bobfracsin * bob2 * 0.5; + + //TA: heavy breathing effects + 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 + //TA: when wall climbing the viewheight is not straight up + if( cg.predictedPlayerState.stats[ STAT_STATE ] & SS_WALLCLIMBING ) + VectorMA( origin, cg.predictedPlayerState.viewheight, cg.predictedPlayerState.grapplePoint, 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 * cg_bobup.value; + if (bob > 6) { + bob = 6; + } + + //TA: likewise for bob + if( cg.predictedPlayerState.stats[ STAT_STATE ] & SS_WALLCLIMBING ) + VectorMA( origin, bob, cg.predictedPlayerState.grapplePoint, 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); + + // pivot the eye based on a neck length +#if 0 + { +#define NECK_LENGTH 8 + vec3_t forward, up; + + cg.refdef.vieworg[2] -= NECK_LENGTH; + AngleVectors( cg.refdefViewAngles, forward, NULL, up ); + VectorMA( cg.refdef.vieworg, 3, forward, cg.refdef.vieworg ); + VectorMA( cg.refdef.vieworg, NECK_LENGTH, up, cg.refdef.vieworg ); + } +#endif +} + +//====================================================================== + +void CG_ZoomDown_f( void ) { + if ( cg.zoomed ) { + return; + } + cg.zoomed = qtrue; + cg.zoomTime = cg.time; +} + +void CG_ZoomUp_f( void ) { + if ( !cg.zoomed ) { + return; + } + cg.zoomed = qfalse; + cg.zoomTime = cg.time; +} + + +/* +==================== +CG_CalcFov + +Fixed fov at intermissions, otherwise account for fov variable and zooms. +==================== +*/ +#define WAVE_AMPLITUDE 1 +#define WAVE_FREQUENCY 0.4 + +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; + int a; + float b; + + BG_unpackAttributes( &attribFov, NULL, NULL, cg.predictedPlayerState.stats ); + + if ( cg.predictedPlayerState.pm_type == PM_INTERMISSION ) { + // if in intermission, use a fixed value + fov_x = 90; + } + else + { + //TA: don't lock the fov globally - we need to be able to change it + fov_x = attribFov; + + if ( fov_x < 1 ) + fov_x = 1; + else if ( fov_x > 160 ) + fov_x = 160; + } + + // account for zooms + zoomFov = cg_zoomFov.value; + if ( zoomFov < 1 ) + zoomFov = 1; + else if ( zoomFov > attribFov ) + zoomFov = attribFov; + + //TA: only do all the zoom stuff if the client CAN zoom + if( cg.predictedPlayerState.stats[ STAT_ABILITIES ] & SCA_CANZOOM ) + { + 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 ); + } + 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 ); + } + } + + 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; + } + + + // 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; +} + + + +/* +=============== +CG_DamageBlendBlob + +=============== +*/ +static void CG_DamageBlendBlob( void ) { + int t; + int maxTime; + refEntity_t ent; + + if ( !cg.damageValue ) { + return; + } + + // ragePro systems can't fade blends, so don't obscure the screen + if ( cgs.glconfig.hardwareType == GLHW_RAGEPRO ) { + return; + } + + maxTime = DAMAGE_TIME; + t = cg.time - cg.damageTime; + if ( t <= 0 || t >= maxTime ) { + return; + } + + + memset( &ent, 0, sizeof( ent ) ); + ent.reType = RT_SPRITE; + ent.renderfx = RF_FIRST_PERSON; + + VectorMA( cg.refdef.vieworg, 8, cg.refdef.viewaxis[0], ent.origin ); + VectorMA( ent.origin, cg.damageX * -8, cg.refdef.viewaxis[1], ent.origin ); + VectorMA( ent.origin, cg.damageY * 8, cg.refdef.viewaxis[2], ent.origin ); + + ent.radius = cg.damageValue * 3; + ent.customShader = cgs.media.viewBloodShader; + ent.shaderRGBA[0] = 255; + ent.shaderRGBA[1] = 255; + ent.shaderRGBA[2] = 255; + ent.shaderRGBA[3] = 200 * ( 1.0 - ((float)t / maxTime) ); + trap_R_AddRefEntityToScene( &ent ); +} + + +/* +=============== +CG_DrawSurfNormal + +Draws a vector against +the surface player is looking at +=============== +*/ +static void CG_DrawSurfNormal( void ) +{ + trace_t tr; + vec3_t end, temp; + vec3_t up = { 0, 5, 0 }; + polyVert_t normal[4]; + + VectorMA( cg.refdef.vieworg, 8192, cg.refdef.viewaxis[ 0 ], end ); + + CG_Trace( &tr, cg.refdef.vieworg, NULL, NULL, end, cg.predictedPlayerState.clientNum, MASK_SOLID ); + //CG_Printf( "%f %f %f\n", tr.plane.normal[ 0 ], tr.plane.normal[ 1 ], tr.plane.normal[ 2 ] ); + + VectorCopy( tr.endpos, normal[0].xyz ); + normal[0].st[0] = 0; + normal[0].st[1] = 0; + normal[0].modulate[0] = 255; + normal[0].modulate[1] = 255; + normal[0].modulate[2] = 255; + normal[0].modulate[3] = 255; + + VectorAdd( up, tr.endpos, temp ); + VectorCopy( temp, normal[1].xyz); + normal[1].st[0] = 0; + normal[1].st[1] = 1; + normal[1].modulate[0] = 255; + normal[1].modulate[1] = 255; + normal[1].modulate[2] = 255; + normal[1].modulate[3] = 255; + + VectorMA( tr.endpos, 64, tr.plane.normal, temp ); + VectorAdd( temp, up, temp ); + VectorCopy( temp, normal[2].xyz ); + normal[2].st[0] = 1; + normal[2].st[1] = 1; + normal[2].modulate[0] = 255; + normal[2].modulate[1] = 255; + normal[2].modulate[2] = 255; + normal[2].modulate[3] = 255; + + VectorMA( tr.endpos, 64, tr.plane.normal, temp ); + VectorCopy( temp, normal[3].xyz ); + normal[3].st[0] = 1; + normal[3].st[1] = 0; + normal[3].modulate[0] = 255; + normal[3].modulate[1] = 255; + normal[3].modulate[2] = 255; + normal[3].modulate[3] = 255; + + trap_R_AddPolyToScene( cgs.media.whiteShader, 4, normal ); +} + + +/* +=============== +CG_CalcViewValues + +Sets cg.refdef view values +=============== +*/ +static int CG_CalcViewValues( void ) { + playerState_t *ps; + + memset( &cg.refdef, 0, sizeof( cg.refdef ) ); + + // strings for in game rendering + // Q_strncpyz( cg.refdef.text[0], "Park Ranger", sizeof(cg.refdef.text[0]) ); + // Q_strncpyz( cg.refdef.text[1], "19", sizeof(cg.refdef.text[1]) ); + + // 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 ); + VectorCopy( ps->viewangles, cg.refdefViewAngles ); + + if (cg_cameraOrbit.integer) { + if (cg.time > cg.nextOrbitTime) { + cg.nextOrbitTime = cg.time + cg_cameraOrbitDelay.integer; + cg_thirdPersonAngle.value += cg_cameraOrbit.value; + } + } + // 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; + } + } + + 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_PowerupTimerSounds +===================== +*/ +static void CG_PowerupTimerSounds( void ) { + int i; + int t; + + // powerup timers going away + for ( i = 0 ; i < MAX_POWERUPS ; i++ ) { + t = cg.snap->ps.powerups[i]; + if ( t <= cg.time ) { + continue; + } + if ( t - cg.time >= POWERUP_BLINKS * POWERUP_BLINK_TIME ) { + continue; + } + if ( ( t - cg.time ) / POWERUP_BLINK_TIME != ( t - cg.oldTime ) / POWERUP_BLINK_TIME ) { + trap_S_StartSound( NULL, cg.snap->ps.clientNum, CHAN_ITEM, cgs.media.wearOffSound ); + } + } +} + +/* +===================== +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_DrawInformation(); + 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_DrawInformation(); + 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(); + + // first person blend blobs, done after AnglesToAxis + if ( !cg.renderingThirdPerson ) { + CG_DamageBlendBlob(); + } + + // build the render lists + if ( !cg.hyperspace ) { + CG_AddPacketEntities(); // adter calcViewValues, so predicted player state is correct + CG_AddMarks(); + CG_AddLocalEntities(); + } + CG_AddViewWeapon( &cg.predictedPlayerState ); + + // add buffered sounds + CG_PlayBufferedSounds(); + + // play buffered voice chats + CG_PlayBufferedVoiceChats(); + + // 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 ) ); + + // warning sounds when powerup is wearing off + CG_PowerupTimerSounds(); + + // 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 00000000..a5535586 --- /dev/null +++ b/src/cgame/cg_weapons.c @@ -0,0 +1,1994 @@ +// Copyright (C) 1999-2000 Id Software, Inc. +// +// cg_weapons.c -- events and effects dealing with weapons + +/* + * Portions Copyright (C) 2000-2001 Tim Angus + * + * This program 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, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ + +/* To assertain which portions are licensed under the GPL and which are + * licensed by Id Software, Inc. please run a diff between the equivalent + * versions of the "Tremulous" modification and the unmodified "Quake3" + * game source code. + */ + +#include "cg_local.h" + +/* +========================== +CG_MachineGunEjectBrass +========================== +*/ +static void CG_MachineGunEjectBrass( centity_t *cent ) { + localEntity_t *le; + refEntity_t *re; + vec3_t velocity, xvelocity; + vec3_t offset, xoffset; + float waterScale = 1.0f; + vec3_t v[3]; + + if ( cg_brassTime.integer <= 0 ) { + return; + } + + le = CG_AllocLocalEntity(); + re = &le->refEntity; + + velocity[0] = 0; + velocity[1] = -50 + 40 * crandom(); + velocity[2] = 100 + 50 * crandom(); + + le->leType = LE_FRAGMENT; + le->startTime = cg.time; + le->endTime = le->startTime + cg_brassTime.integer + ( cg_brassTime.integer / 4 ) * random(); + + le->pos.trType = TR_GRAVITY; + le->pos.trTime = cg.time - (rand()&15); + + AnglesToAxis( cent->lerpAngles, v ); + + offset[0] = 8; + offset[1] = -4; + offset[2] = 24; + + xoffset[0] = offset[0] * v[0][0] + offset[1] * v[1][0] + offset[2] * v[2][0]; + xoffset[1] = offset[0] * v[0][1] + offset[1] * v[1][1] + offset[2] * v[2][1]; + xoffset[2] = offset[0] * v[0][2] + offset[1] * v[1][2] + offset[2] * v[2][2]; + VectorAdd( cent->lerpOrigin, xoffset, re->origin ); + + VectorCopy( re->origin, le->pos.trBase ); + + if ( CG_PointContents( re->origin, -1 ) & CONTENTS_WATER ) { + waterScale = 0.10f; + } + + xvelocity[0] = velocity[0] * v[0][0] + velocity[1] * v[1][0] + velocity[2] * v[2][0]; + xvelocity[1] = velocity[0] * v[0][1] + velocity[1] * v[1][1] + velocity[2] * v[2][1]; + xvelocity[2] = velocity[0] * v[0][2] + velocity[1] * v[1][2] + velocity[2] * v[2][2]; + VectorScale( xvelocity, waterScale, le->pos.trDelta ); + + AxisCopy( axisDefault, re->axis ); + re->hModel = cgs.media.machinegunBrassModel; + + le->bounceFactor = 0.4 * waterScale; + + le->angles.trType = TR_LINEAR; + le->angles.trTime = cg.time; + le->angles.trBase[0] = rand()&31; + le->angles.trBase[1] = rand()&31; + le->angles.trBase[2] = rand()&31; + le->angles.trDelta[0] = 2; + le->angles.trDelta[1] = 1; + le->angles.trDelta[2] = 0; + + le->leFlags = LEF_TUMBLE; + le->leBounceSoundType = LEBS_BRASS; + le->leMarkType = LEMT_NONE; +} + +/* +========================== +CG_ShotgunEjectBrass +========================== +*/ +static void CG_ShotgunEjectBrass( centity_t *cent ) { + localEntity_t *le; + refEntity_t *re; + vec3_t velocity, xvelocity; + vec3_t offset, xoffset; + vec3_t v[3]; + int i; + + if ( cg_brassTime.integer <= 0 ) { + return; + } + + for ( i = 0; i < 2; i++ ) { + float waterScale = 1.0f; + + le = CG_AllocLocalEntity(); + re = &le->refEntity; + + velocity[0] = 60 + 60 * crandom(); + if ( i == 0 ) { + velocity[1] = 40 + 10 * crandom(); + } else { + velocity[1] = -40 + 10 * crandom(); + } + velocity[2] = 100 + 50 * crandom(); + + le->leType = LE_FRAGMENT; + le->startTime = cg.time; + le->endTime = le->startTime + cg_brassTime.integer*3 + cg_brassTime.integer * random(); + + le->pos.trType = TR_GRAVITY; + le->pos.trTime = cg.time; + + AnglesToAxis( cent->lerpAngles, v ); + + offset[0] = 8; + offset[1] = 0; + offset[2] = 24; + + xoffset[0] = offset[0] * v[0][0] + offset[1] * v[1][0] + offset[2] * v[2][0]; + xoffset[1] = offset[0] * v[0][1] + offset[1] * v[1][1] + offset[2] * v[2][1]; + xoffset[2] = offset[0] * v[0][2] + offset[1] * v[1][2] + offset[2] * v[2][2]; + VectorAdd( cent->lerpOrigin, xoffset, re->origin ); + VectorCopy( re->origin, le->pos.trBase ); + if ( CG_PointContents( re->origin, -1 ) & CONTENTS_WATER ) { + waterScale = 0.10f; + } + + xvelocity[0] = velocity[0] * v[0][0] + velocity[1] * v[1][0] + velocity[2] * v[2][0]; + xvelocity[1] = velocity[0] * v[0][1] + velocity[1] * v[1][1] + velocity[2] * v[2][1]; + xvelocity[2] = velocity[0] * v[0][2] + velocity[1] * v[1][2] + velocity[2] * v[2][2]; + VectorScale( xvelocity, waterScale, le->pos.trDelta ); + + AxisCopy( axisDefault, re->axis ); + re->hModel = cgs.media.shotgunBrassModel; + le->bounceFactor = 0.3f; + + le->angles.trType = TR_LINEAR; + le->angles.trTime = cg.time; + le->angles.trBase[0] = rand()&31; + le->angles.trBase[1] = rand()&31; + le->angles.trBase[2] = rand()&31; + le->angles.trDelta[0] = 1; + le->angles.trDelta[1] = 0.5; + le->angles.trDelta[2] = 0; + + le->leFlags = LEF_TUMBLE; + le->leBounceSoundType = LEBS_BRASS; + le->leMarkType = LEMT_NONE; + } +} + + +/* +========================== +CG_RailTrail +========================== +*/ +void CG_RailTrail( clientInfo_t *ci, vec3_t start, vec3_t end ) { + localEntity_t *le; + refEntity_t *re; + + // + // rings + // + le = CG_AllocLocalEntity(); + re = &le->refEntity; + + le->leType = LE_FADE_RGB; + le->startTime = cg.time; + le->endTime = cg.time + cg_railTrailTime.value; + le->lifeRate = 1.0 / ( le->endTime - le->startTime ); + + re->shaderTime = cg.time / 1000.0f; + re->reType = RT_RAIL_RINGS; + re->customShader = cgs.media.railRingsShader; + + VectorCopy( start, re->origin ); + VectorCopy( end, re->oldorigin ); + + // nudge down a bit so it isn't exactly in center + re->origin[2] -= 8; + re->oldorigin[2] -= 8; + + le->color[0] = ci->color[0] * 0.75; + le->color[1] = ci->color[1] * 0.75; + le->color[2] = ci->color[2] * 0.75; + le->color[3] = 1.0f; + + AxisClear( re->axis ); + + // + // core + // + le = CG_AllocLocalEntity(); + re = &le->refEntity; + + le->leType = LE_FADE_RGB; + le->startTime = cg.time; + le->endTime = cg.time + cg_railTrailTime.value; + le->lifeRate = 1.0 / ( le->endTime - le->startTime ); + + re->shaderTime = cg.time / 1000.0f; + re->reType = RT_RAIL_CORE; + re->customShader = cgs.media.railCoreShader; + + VectorCopy( start, re->origin ); + VectorCopy( end, re->oldorigin ); + + // nudge down a bit so it isn't exactly in center + re->origin[2] -= 8; + re->oldorigin[2] -= 8; + + le->color[0] = ci->color[0] * 0.75; + le->color[1] = ci->color[1] * 0.75; + le->color[2] = ci->color[2] * 0.75; + le->color[3] = 1.0f; + + AxisClear( re->axis ); +} + +/* +========================== +CG_RocketTrail +========================== +*/ +static void CG_RocketTrail( centity_t *ent, const weaponInfo_t *wi ) { + int step; + vec3_t origin, lastPos; + int t; + int startTime, contents; + int lastContents; + entityState_t *es; + vec3_t up; + localEntity_t *smoke; + + up[0] = 0; + up[1] = 0; + up[2] = 0; + + step = 50; + + es = &ent->currentState; + startTime = ent->trailTime; + t = step * ( (startTime + step) / step ); + + BG_EvaluateTrajectory( &es->pos, cg.time, origin ); + contents = CG_PointContents( origin, -1 ); + + // if object (e.g. grenade) is stationary, don't toss up smoke + if ( es->pos.trType == TR_STATIONARY ) { + ent->trailTime = cg.time; + return; + } + + BG_EvaluateTrajectory( &es->pos, ent->trailTime, lastPos ); + lastContents = CG_PointContents( lastPos, -1 ); + + ent->trailTime = cg.time; + + if ( contents & ( CONTENTS_WATER | CONTENTS_SLIME | CONTENTS_LAVA ) ) { + if ( contents & lastContents & CONTENTS_WATER ) { + CG_BubbleTrail( lastPos, origin, 8 ); + } + return; + } + + for ( ; t <= ent->trailTime ; t += step ) { + BG_EvaluateTrajectory( &es->pos, t, lastPos ); + + smoke = CG_SmokePuff( lastPos, up, + wi->trailRadius, + 1, 1, 1, 0.33f, + wi->wiTrailTime, + t, + 0, + 0, + cgs.media.smokePuffShader ); + // use the optimized local entity add + smoke->leType = LE_SCALE_FADE; + } + +} + +/* +========================== +CG_GrappleTrail +========================== +*/ +void CG_GrappleTrail( centity_t *ent, const weaponInfo_t *wi ) { + vec3_t origin; + entityState_t *es; + vec3_t forward, up; + refEntity_t beam; + + es = &ent->currentState; + + BG_EvaluateTrajectory( &es->pos, cg.time, origin ); + ent->trailTime = cg.time; + + memset( &beam, 0, sizeof( beam ) ); + //FIXME adjust for muzzle position + VectorCopy ( cg_entities[ ent->currentState.otherEntityNum ].lerpOrigin, beam.origin ); + beam.origin[2] += 26; + AngleVectors( cg_entities[ ent->currentState.otherEntityNum ].lerpAngles, forward, NULL, up ); + VectorMA( beam.origin, -6, up, beam.origin ); + VectorCopy( origin, beam.oldorigin ); + + if (Distance( beam.origin, beam.oldorigin ) < 64 ) + return; // Don't draw if close + + beam.reType = RT_LIGHTNING; + beam.customShader = cgs.media.lightningShader; + + AxisClear( beam.axis ); + beam.shaderRGBA[0] = 0xff; + beam.shaderRGBA[1] = 0xff; + beam.shaderRGBA[2] = 0xff; + beam.shaderRGBA[3] = 0xff; + trap_R_AddRefEntityToScene( &beam ); +} + +/* +========================== +CG_GrenadeTrail +========================== +*/ +static void CG_GrenadeTrail( centity_t *ent, const weaponInfo_t *wi ) { + CG_RocketTrail( ent, wi ); +} + +/* +================= +CG_RegisterUpgrade + +The server says this item is used on this level +================= +*/ +void CG_RegisterUpgrade( int upgradeNum ) { + upgradeInfo_t *upgradeInfo; + gitem_t *item; + char path[MAX_QPATH]; + int i; + + upgradeInfo = &cg_upgrades[ upgradeNum ]; + + if ( upgradeNum == 0 ) { + return; + } + + if ( upgradeInfo->registered ) { + return; + } + + memset( upgradeInfo, 0, sizeof( *upgradeInfo ) ); + upgradeInfo->registered = qtrue; + + for ( item = bg_itemlist + 1 ; item->classname ; item++ ) { + if ( item->giType == IT_UPGRADE && item->giTag == upgradeNum ) { + upgradeInfo->item = item; + break; + } + } + if ( !item->classname ) { + CG_Error( "Couldn't find upgrade %i", upgradeNum ); + } + CG_RegisterItemVisuals( item - bg_itemlist ); + + upgradeInfo->upgradeIcon = trap_R_RegisterShader( item->icon ); +} + +/* +================= +CG_RegisterWeapon + +The server says this item is used on this level +================= +*/ +void CG_RegisterWeapon( int weaponNum ) { + weaponInfo_t *weaponInfo; + gitem_t *item, *ammo; + 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; + + for ( item = bg_itemlist + 1 ; item->classname ; item++ ) { + if ( item->giType == IT_WEAPON && item->giTag == weaponNum ) { + weaponInfo->item = item; + break; + } + } + if ( !item->classname ) { + CG_Error( "Couldn't find weapon %i", weaponNum ); + } + CG_RegisterItemVisuals( item - bg_itemlist ); + + // load cmodel before model so filecache works + weaponInfo->weaponModel = trap_R_RegisterModel( item->world_model[0] ); + + // 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] ); + } + + weaponInfo->weaponIcon = trap_R_RegisterShader( item->icon ); + weaponInfo->ammoIcon = trap_R_RegisterShader( item->icon ); + + for ( ammo = bg_itemlist + 1 ; ammo->classname ; ammo++ ) { + if ( ammo->giType == IT_AMMO && ammo->giTag == weaponNum ) { + break; + } + } + if ( ammo->classname && ammo->world_model[0] ) { + weaponInfo->ammoModel = trap_R_RegisterModel( ammo->world_model[0] ); + } + + strcpy( path, item->world_model[0] ); + COM_StripExtension( path, path ); + strcat( path, "_flash.md3" ); + weaponInfo->flashModel = trap_R_RegisterModel( path ); + + strcpy( path, item->world_model[0] ); + COM_StripExtension( path, path ); + strcat( path, "_barrel.md3" ); + weaponInfo->barrelModel = trap_R_RegisterModel( path ); + + strcpy( path, item->world_model[0] ); + COM_StripExtension( path, path ); + strcat( path, "_hand.md3" ); + weaponInfo->handsModel = trap_R_RegisterModel( path ); + + if ( !weaponInfo->handsModel ) { + weaponInfo->handsModel = trap_R_RegisterModel( "models/weapons2/shotgun/shotgun_hand.md3" ); + } + + weaponInfo->loopFireSound = qfalse; + + switch ( weaponNum ) { + case WP_GAUNTLET: + MAKERGB( weaponInfo->flashDlightColor, 0.6f, 0.6f, 1.0f ); + weaponInfo->firingSound = trap_S_RegisterSound( "sound/weapons/melee/fstrun.wav", qfalse ); + weaponInfo->flashSound[0] = trap_S_RegisterSound( "sound/weapons/melee/fstatck.wav", qfalse ); + break; + + case WP_LIGHTNING: + MAKERGB( weaponInfo->flashDlightColor, 0.6f, 0.6f, 1.0f ); + weaponInfo->readySound = trap_S_RegisterSound( "sound/weapons/melee/fsthum.wav", qfalse ); + weaponInfo->firingSound = trap_S_RegisterSound( "sound/weapons/lightning/lg_hum.wav", qfalse ); + + weaponInfo->flashSound[0] = trap_S_RegisterSound( "sound/weapons/lightning/lg_fire.wav", qfalse ); + cgs.media.lightningShader = trap_R_RegisterShader( "lightningBolt"); + cgs.media.lightningExplosionModel = trap_R_RegisterModel( "models/weaphits/crackle.md3" ); + cgs.media.sfx_lghit1 = trap_S_RegisterSound( "sound/weapons/lightning/lg_hit.wav", qfalse ); + cgs.media.sfx_lghit2 = trap_S_RegisterSound( "sound/weapons/lightning/lg_hit2.wav", qfalse ); + cgs.media.sfx_lghit3 = trap_S_RegisterSound( "sound/weapons/lightning/lg_hit3.wav", qfalse ); + + break; + + case WP_GRAPPLING_HOOK: + MAKERGB( weaponInfo->flashDlightColor, 0.6f, 0.6f, 1.0f ); + weaponInfo->missileModel = trap_R_RegisterModel( "models/ammo/rocket/rocket.md3" ); + weaponInfo->missileTrailFunc = CG_GrappleTrail; + weaponInfo->missileDlight = 200; + weaponInfo->wiTrailTime = 2000; + weaponInfo->trailRadius = 64; + MAKERGB( weaponInfo->missileDlightColor, 1, 0.75f, 0 ); + weaponInfo->readySound = trap_S_RegisterSound( "sound/weapons/melee/fsthum.wav", qfalse ); + weaponInfo->firingSound = trap_S_RegisterSound( "sound/weapons/melee/fstrun.wav", qfalse ); + break; + + case WP_MACHINEGUN: + MAKERGB( weaponInfo->flashDlightColor, 1, 1, 0 ); + weaponInfo->flashSound[0] = trap_S_RegisterSound( "sound/weapons/machinegun/machgf1b.wav", qfalse ); + weaponInfo->flashSound[1] = trap_S_RegisterSound( "sound/weapons/machinegun/machgf2b.wav", qfalse ); + weaponInfo->flashSound[2] = trap_S_RegisterSound( "sound/weapons/machinegun/machgf3b.wav", qfalse ); + weaponInfo->flashSound[3] = trap_S_RegisterSound( "sound/weapons/machinegun/machgf4b.wav", qfalse ); + weaponInfo->ejectBrassFunc = CG_MachineGunEjectBrass; + cgs.media.bulletExplosionShader = trap_R_RegisterShader( "bulletExplosion" ); + break; + + case WP_CHAINGUN: + MAKERGB( weaponInfo->flashDlightColor, 1, 1, 0 ); + weaponInfo->flashSound[0] = trap_S_RegisterSound( "sound/weapons/machinegun/machgf1b.wav", qfalse ); + weaponInfo->flashSound[1] = trap_S_RegisterSound( "sound/weapons/machinegun/machgf2b.wav", qfalse ); + weaponInfo->flashSound[2] = trap_S_RegisterSound( "sound/weapons/machinegun/machgf3b.wav", qfalse ); + weaponInfo->flashSound[3] = trap_S_RegisterSound( "sound/weapons/machinegun/machgf4b.wav", qfalse ); + weaponInfo->ejectBrassFunc = CG_MachineGunEjectBrass; + cgs.media.bulletExplosionShader = trap_R_RegisterShader( "bulletExplosion" ); + break; + + case WP_SHOTGUN: + MAKERGB( weaponInfo->flashDlightColor, 1, 1, 0 ); + weaponInfo->flashSound[0] = trap_S_RegisterSound( "sound/weapons/shotgun/sshotf1b.wav", qfalse ); + weaponInfo->ejectBrassFunc = CG_ShotgunEjectBrass; + break; + + case WP_ROCKET_LAUNCHER: + weaponInfo->missileModel = trap_R_RegisterModel( "models/ammo/rocket/rocket.md3" ); + weaponInfo->missileSound = trap_S_RegisterSound( "sound/weapons/rocket/rockfly.wav", qfalse ); + weaponInfo->missileTrailFunc = CG_RocketTrail; + weaponInfo->missileDlight = 200; + weaponInfo->wiTrailTime = 2000; + weaponInfo->trailRadius = 64; + MAKERGB( weaponInfo->missileDlightColor, 1, 0.75f, 0 ); + MAKERGB( weaponInfo->flashDlightColor, 1, 0.75f, 0 ); + weaponInfo->flashSound[0] = trap_S_RegisterSound( "sound/weapons/rocket/rocklf1a.wav", qfalse ); + cgs.media.rocketExplosionShader = trap_R_RegisterShader( "rocketExplosion" ); + break; + + case WP_GRENADE_LAUNCHER: + weaponInfo->missileModel = trap_R_RegisterModel( "models/ammo/grenade1.md3" ); + weaponInfo->missileTrailFunc = CG_GrenadeTrail; + weaponInfo->wiTrailTime = 700; + weaponInfo->trailRadius = 32; + MAKERGB( weaponInfo->flashDlightColor, 1, 0.70f, 0 ); + weaponInfo->flashSound[0] = trap_S_RegisterSound( "sound/weapons/grenade/grenlf1a.wav", qfalse ); + cgs.media.grenadeExplosionShader = trap_R_RegisterShader( "grenadeExplosion" ); + break; + + case WP_FLAMER: + weaponInfo->missileSound = trap_S_RegisterSound( "sound/weapons/plasma/lasfly.wav", qfalse ); + MAKERGB( weaponInfo->flashDlightColor, 0.25, 0.1, 0 ); + weaponInfo->flashSound[0] = trap_S_RegisterSound( "sound/weapons/plasma/rg_hum.wav", qfalse ); + cgs.media.flameExplShader = trap_R_RegisterShader( "rocketExplosion" ); + break; + + case WP_PLASMAGUN: + weaponInfo->missileSound = trap_S_RegisterSound( "sound/weapons/plasma/lasfly.wav", qfalse ); + MAKERGB( weaponInfo->flashDlightColor, 0.6f, 0.6f, 1.0f ); + weaponInfo->flashSound[0] = trap_S_RegisterSound( "sound/weapons/plasma/hyprbf1a.wav", qfalse ); + cgs.media.plasmaExplosionShader = trap_R_RegisterShader( "plasmaExplosion" ); + break; + + case WP_RAILGUN: + weaponInfo->readySound = trap_S_RegisterSound( "sound/weapons/railgun/rg_hum.wav", qfalse ); + MAKERGB( weaponInfo->flashDlightColor, 1, 0.5f, 0 ); + weaponInfo->flashSound[0] = trap_S_RegisterSound( "sound/weapons/railgun/railgf1a.wav", qfalse ); + cgs.media.railExplosionShader = trap_R_RegisterShader( "railExplosion" ); + cgs.media.railRingsShader = trap_R_RegisterShader( "railDisc" ); + cgs.media.railCoreShader = trap_R_RegisterShader( "railCore" ); + break; + + case WP_BFG: + weaponInfo->readySound = trap_S_RegisterSound( "sound/weapons/bfg/bfg_hum.wav", qfalse ); + MAKERGB( weaponInfo->flashDlightColor, 1, 0.7f, 1 ); + weaponInfo->flashSound[0] = trap_S_RegisterSound( "sound/weapons/bfg/bfg_fire.wav", qfalse ); + cgs.media.bfgExplosionShader = trap_R_RegisterShader( "bfgExplosion" ); + weaponInfo->missileModel = trap_R_RegisterModel( "models/weaphits/bfg.md3" ); + weaponInfo->missileSound = trap_S_RegisterSound( "sound/weapons/rocket/rockfly.wav", qfalse ); + break; + + case WP_VENOM: + MAKERGB( weaponInfo->flashDlightColor, 0, 0, 0 ); + weaponInfo->flashSound[0] = trap_S_RegisterSound( "sound/weapons/melee/fstatck.wav", qfalse ); + break; + + case WP_ABUILD: + case WP_HBUILD: + case WP_SCANNER: + //nowt + break; + + default: + MAKERGB( weaponInfo->flashDlightColor, 1, 1, 1 ); + weaponInfo->flashSound[0] = trap_S_RegisterSound( "sound/weapons/rocket/rocklf1a.wav", qfalse ); + break; + } +} + +/* +================= +CG_RegisterItemVisuals + +The server says this item is used on this level +================= +*/ +void CG_RegisterItemVisuals( int itemNum ) { + itemInfo_t *itemInfo; + gitem_t *item; + int i; + + itemInfo = &cg_items[ itemNum ]; + if ( itemInfo->registered ) { + return; + } + + item = &bg_itemlist[ itemNum ]; + + memset( itemInfo, 0, sizeof( &itemInfo ) ); + itemInfo->registered = qtrue; + + itemInfo->icon = trap_R_RegisterShader( item->icon ); + + if ( item->giType == IT_WEAPON ) { + CG_RegisterWeapon( item->giTag ); + } + + for( i = 0; i < 4; i++ ) + { + if( item->world_model[ i ] ) + itemInfo->models[i] = trap_R_RegisterModel( item->world_model[i] ); + } + +} + + +/* +======================================================================================== + +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_ATTACK].firstFrame; + } + + return 0; +} + + +/* +============== +CG_CalculateWeaponPosition +============== +*/ +static void CG_CalculateWeaponPosition( vec3_t origin, vec3_t angles ) { + float scale; + int delta; + float fracsin; + int 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 + //TA: bob amount is class dependant + BG_unpackAttributes( NULL, &bob, NULL, cg.predictedPlayerState.stats ); + 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( !( cg.predictedPlayerState.stats[ STAT_ABILITIES ] & 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; + } + + //TA: huh? why drop the weapon when stair climbing... just as well this isn't actually used :) +#if 0 + // drop the weapon when stair climbing + delta = cg.time - cg.stepTime; + if ( delta < STEP_TIME/2 ) { + origin[2] -= cg.stepChange*0.25 * delta / (STEP_TIME/2); + } else if ( delta < STEP_TIME ) { + origin[2] -= cg.stepChange*0.25 * (STEP_TIME - delta) / (STEP_TIME/2); + } +#endif + + // 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_LightningBolt + +Origin will be the exact tag point, which is slightly +different than the muzzle point used for determining hits. +The cent should be the non-predicted cent if it is from the player, +so the endpoint will reflect the simulated strike (lagging the predicted +angle) +=============== +*/ +static void CG_LightningBolt( centity_t *cent, vec3_t origin ) { + trace_t trace; + refEntity_t beam; + vec3_t forward; + vec3_t muzzlePoint, endPoint; + + if ( cent->currentState.weapon != WP_LIGHTNING ) { + return; + } + + memset( &beam, 0, sizeof( beam ) ); + + // find muzzle point for this frame + VectorCopy( cent->lerpOrigin, muzzlePoint ); + AngleVectors( cent->lerpAngles, forward, NULL, NULL ); + + // FIXME: crouch + muzzlePoint[2] += DEFAULT_VIEWHEIGHT; + + VectorMA( muzzlePoint, 14, forward, muzzlePoint ); + + // project forward by the lightning range + VectorMA( muzzlePoint, LIGHTNING_RANGE, forward, endPoint ); + + // see if it hit a wall + CG_Trace( &trace, muzzlePoint, vec3_origin, vec3_origin, endPoint, + cent->currentState.number, MASK_SHOT ); + + // this is the endpoint + VectorCopy( trace.endpos, beam.oldorigin ); + + // use the provided origin, even though it may be slightly + // different than the muzzle origin + VectorCopy( origin, beam.origin ); + + beam.reType = RT_LIGHTNING; + beam.customShader = cgs.media.lightningShader; + trap_R_AddRefEntityToScene( &beam ); + + // add the impact flare if it hit something + if ( trace.fraction < 1.0 ) { + vec3_t angles; + vec3_t dir; + + VectorSubtract( beam.oldorigin, beam.origin, dir ); + VectorNormalize( dir ); + + memset( &beam, 0, sizeof( beam ) ); + beam.hModel = cgs.media.lightningExplosionModel; + + VectorMA( trace.endpos, -16, dir, beam.origin ); + + // make a random orientation + angles[0] = rand() % 360; + angles[1] = rand() % 360; + angles[2] = rand() % 360; + AnglesToAxis( angles, beam.axis ); + trap_R_AddRefEntityToScene( &beam ); + } +} + + +/* +=============== +CG_SpawnRailTrail + +Origin will be the exact tag point, which is slightly +different than the muzzle point used for determining hits. +=============== +*/ +static void CG_SpawnRailTrail( centity_t *cent, vec3_t origin ) { + clientInfo_t *ci; + + if ( cent->currentState.weapon != WP_RAILGUN ) { + return; + } + if ( !cent->pe.railgunFlash ) { + return; + } + cent->pe.railgunFlash = qtrue; + ci = &cgs.clientinfo[ cent->currentState.clientNum ]; + CG_RailTrail( ci, origin, cent->pe.railgunImpact ); +} + + +/* +====================== +CG_MachinegunSpinAngle +====================== +*/ +#define SPIN_SPEED 0.9 +#define COAST_TIME 1000 +static float CG_MachinegunSpinAngle( centity_t *cent ) { + 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 == !(cent->currentState.eFlags & EF_FIRING) ) { + cent->pe.barrelTime = cg.time; + cent->pe.barrelAngle = AngleMod( angle ); + cent->pe.barrelSpinning = !!(cent->currentState.eFlags & EF_FIRING); + } + + return angle; +} + + +/* +======================== +CG_AddWeaponWithPowerups +======================== +*/ +static void CG_AddWeaponWithPowerups( refEntity_t *gun, int powerups ) { + // add powerup effects + /*if ( powerups & ( 1 << PW_INVIS ) ) { + gun->customShader = cgs.media.invisShader; + trap_R_AddRefEntityToScene( gun ); + } else*/ { + trap_R_AddRefEntityToScene( gun ); + + /*if ( powerups & ( 1 << PW_BATTLESUIT ) ) { + gun->customShader = cgs.media.battleWeaponShader; + trap_R_AddRefEntityToScene( gun ); + } + if ( powerups & ( 1 << PW_QUAD ) ) { + gun->customShader = cgs.media.quadWeaponShader; + trap_R_AddRefEntityToScene( gun ); + }*/ + } +} + + +/* +============= +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; + weaponInfo_t *weapon; + centity_t *nonPredictedCent; + + weaponNum = cent->currentState.weapon; + + 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 ) { + if ( cg.predictedPlayerState.weapon == WP_RAILGUN + && cg.predictedPlayerState.weaponstate == WEAPON_FIRING ) { + float f; + + f = (float)cg.predictedPlayerState.weaponTime / 1500; + gun.shaderRGBA[1] = 0; + gun.shaderRGBA[0] = + gun.shaderRGBA[2] = 255 * ( 1.0 - f ); + } else { + gun.shaderRGBA[0] = 255; + gun.shaderRGBA[1] = 255; + gun.shaderRGBA[2] = 255; + gun.shaderRGBA[3] = 255; + } + } + + gun.hModel = weapon->weaponModel; + if (!gun.hModel) { + return; + } + + if ( !ps ) { + // add weapon ready sound + cent->pe.lightningFiring = qfalse; + if ( ( cent->currentState.eFlags & EF_FIRING ) && weapon->firingSound ) { + // lightning gun and guantlet make a different sound when fire is held down + trap_S_AddLoopingSound( cent->currentState.number, cent->lerpOrigin, vec3_origin, weapon->firingSound ); + cent->pe.lightningFiring = qtrue; + } else if ( weapon->readySound ) { + trap_S_AddLoopingSound( cent->currentState.number, cent->lerpOrigin, vec3_origin, weapon->readySound ); + } + } + + CG_PositionEntityOnTag( &gun, parent, parent->hModel, "tag_weapon"); + + CG_AddWeaponWithPowerups( &gun, cent->currentState.powerups ); + + // 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 ); + AnglesToAxis( angles, barrel.axis ); + + CG_PositionRotatedEntityOnTag( &barrel, &gun, weapon->weaponModel, "tag_barrel" ); + + CG_AddWeaponWithPowerups( &barrel, cent->currentState.powerups ); + } + + // make sure we aren't looking at cg.predictedPlayerEntity for LG + nonPredictedCent = &cg_entities[cent->currentState.clientNum]; + + // if the index of the nonPredictedCent is not the same as the clientNum + // then this is a fake player (like on teh single player podiums), so + // go ahead and use the cent + if( ( nonPredictedCent - cg_entities ) != cent->currentState.clientNum ) { + nonPredictedCent = cent; + } + + // add the flash + if ( ( weaponNum == WP_LIGHTNING || weaponNum == WP_GAUNTLET || weaponNum == WP_GRAPPLING_HOOK ) + && ( nonPredictedCent->currentState.eFlags & EF_FIRING ) ) + { + // continuous flash + } else { + // impulse flash + if ( cg.time - cent->muzzleFlashTime > MUZZLE_FLASH_TIME && !cent->pe.railgunFlash ) { + 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) { + return; + } + angles[YAW] = 0; + angles[PITCH] = 0; + angles[ROLL] = crandom() * 10; + AnglesToAxis( angles, flash.axis ); + + // colorize the railgun blast + if ( weaponNum == WP_RAILGUN ) { + clientInfo_t *ci; + + ci = &cgs.clientinfo[ cent->currentState.clientNum ]; + flash.shaderRGBA[0] = 255 * ci->color[0]; + flash.shaderRGBA[1] = 255 * ci->color[1]; + flash.shaderRGBA[2] = 255 * ci->color[2]; + } + + CG_PositionRotatedEntityOnTag( &flash, &gun, weapon->weaponModel, "tag_flash"); + trap_R_AddRefEntityToScene( &flash ); + + if ( ps || cg.renderingThirdPerson || + cent->currentState.number != cg.predictedPlayerState.clientNum ) { + // add lightning bolt + CG_LightningBolt( nonPredictedCent, flash.origin ); + + // add rail trail + CG_SpawnRailTrail( cent, flash.origin ); + + // make a dlight for the flash + if ( weapon->flashDlightColor[0] || weapon->flashDlightColor[1] || weapon->flashDlightColor[2] ) { + trap_R_AddLightToScene( flash.origin, 300 + (rand()&31), weapon->flashDlightColor[0], + weapon->flashDlightColor[1], weapon->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 *weapon; + + if ( ps->persistant[PERS_TEAM] == TEAM_SPECTATOR ) { + return; + } + + if ( ps->pm_type == PM_INTERMISSION ) { + return; + } + + // 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; + + if ( cg.predictedPlayerState.eFlags & EF_FIRING ) { + // special hack for lightning gun... + VectorCopy( cg.refdef.vieworg, origin ); + VectorMA( origin, -8, cg.refdef.viewaxis[2], origin ); + CG_LightningBolt( &cg_entities[ps->clientNum], origin ); + } + return; + } + + // don't draw if testing a gun model + if ( cg.testGun ) { + return; + } + + // drop gun lower at higher fov + //if ( cg_fov.integer > 90 ) { + //TA: the client side variable isn't used ( shouldn't iD have done this anyway? ) + if( cg.refdef.fov_y > 90 ) + fovOffset = -0.2 * ( cg.refdef.fov_y - 90 ); + else + fovOffset = 0; + + cent = &cg.predictedPlayerEntity; // &cg_entities[cg.snap->ps.clientNum]; + CG_RegisterWeapon( ps->weapon ); + weapon = &cg_weapons[ ps->weapon ]; + + 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 ); + + 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 = weapon->handsModel; + hand.renderfx = RF_DEPTHHACK | RF_FIRST_PERSON | RF_MINLIGHT; + + // add everything onto the hand + CG_AddPlayerWeapon( &hand, ps, &cg.predictedPlayerEntity ); +} + +/* +============================================================================== + +WEAPON SELECTION + +============================================================================== +*/ + +/* +=================== +CG_DrawWeaponSelect + +=================== +*/ +void CG_DrawWeaponSelect( void ) { + int i; + //int count; + int x, y, w; + char *name; + float *color; + int ammo, clips, maxclips; + + // don't display if dead + if ( cg.predictedPlayerState.stats[STAT_HEALTH] <= 0 ) { + return; + } + + color = CG_FadeColor( cg.weaponSelectTime, WEAPON_SELECT_TIME ); + if ( !color ) { + return; + } + trap_R_SetColor( color ); + + // showing weapon select clears pickup item display, but not the blend blob + cg.itemPickupTime = 0; + + /*// count the number of weapons owned + count = 0; + for ( i = WP_GAUNTLET; i < WP_NUM_WEAPONS; i++ ) { + if ( BG_gotWeapon( i, cg.snap->ps.stats ) ) { + count++; + } + }*/ + + //x = 320 - count * 20; + //y = 380; + + x = 10; + y = 10; + + for ( i = WP_GAUNTLET; i < WP_NUM_WEAPONS; i++ ) { + if( !BG_gotWeapon( i, cg.snap->ps.stats ) ) + continue; + + CG_RegisterWeapon( i ); + + // draw weapon icon + CG_DrawPic( x, y, 16, 16, cg_weapons[i].weaponIcon ); + + // draw selection marker + if ( i == cg.weaponSelect ) { + CG_DrawPic( x-2, y-2, 20, 20, cgs.media.selectShader ); + } + + BG_unpackAmmoArray( i, cg.snap->ps.ammo, cg.snap->ps.powerups, &ammo, &clips, &maxclips ); + + // no ammo cross on top + if ( !ammo && !clips && !BG_infiniteAmmo( i ) ) { + CG_DrawPic( x, y, 16, 16, cgs.media.noammoShader ); + } + + y += 20; + } + + for ( i = UP_TORCH; i < UP_NUM_UPGRADES; i++ ) { + if( !BG_gotItem( i, cg.snap->ps.stats ) ) + continue; + + CG_RegisterUpgrade( i ); + + // draw weapon icon + CG_DrawPic( x, y, 16, 16, cg_upgrades[i].upgradeIcon ); + + // draw selection marker + if ( i == ( cg.weaponSelect - 32 ) ) { + CG_DrawPic( x-2, y-2, 20, 20, cgs.media.selectShader ); + } + + y += 20; + } + + // draw the selected name + if( cg.weaponSelect <= 32 ) + { + if ( cg_weapons[ cg.weaponSelect ].item ) { + name = cg_weapons[ cg.weaponSelect ].item->pickup_name; + if ( name ) { + w = CG_DrawStrlen( name ) * BIGCHAR_WIDTH; + x = ( SCREEN_WIDTH - w ) / 2; + CG_DrawBigStringColor(x, y - 22, name, color); + } + } + } + else if( cg.weaponSelect > 32 ) + { + if ( cg_upgrades[ cg.weaponSelect - 32 ].item ) { + name = cg_upgrades[ cg.weaponSelect - 32 ].item->pickup_name; + if ( name ) { + w = CG_DrawStrlen( name ) * BIGCHAR_WIDTH; + x = ( SCREEN_WIDTH - w ) / 2; + CG_DrawBigStringColor(x, y - 22, name, color); + } + } + } + + trap_R_SetColor( NULL ); +} + + +/* +=============== +CG_WeaponSelectable +=============== +*/ +static qboolean CG_WeaponSelectable( int i ) +{ + int ammo, clips, maxclips; + + BG_unpackAmmoArray( i, cg.snap->ps.ammo, cg.snap->ps.powerups, &ammo, &clips, &maxclips ); + + if ( !ammo && !clips && !BG_infiniteAmmo( i ) ) { + return qfalse; + } + if ( !BG_gotWeapon( i, cg.snap->ps.stats ) ) { + return qfalse; + } + + return qtrue; +} + + +/* +=============== +CG_ItemSelectable +=============== +*/ +static qboolean CG_ItemSelectable( int i ) +{ + if( !BG_gotItem( i, cg.snap->ps.stats ) ) { + return qfalse; + } + + return qtrue; +} + + +/* +=============== +CG_NextWeapon_f +=============== +*/ +void CG_NextWeapon_f( void ) { + int i; + int original; + + if ( !cg.snap ) { + return; + } + if ( cg.snap->ps.pm_flags & PMF_FOLLOW ) { + 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 == WP_GAUNTLET ) { + continue; // never cycle to gauntlet + }*/ + + if( cg.weaponSelect <= 32 ) + { + if ( CG_WeaponSelectable( cg.weaponSelect ) ) { + break; + } + } + else if( cg.weaponSelect > 32 ) + { + if ( CG_ItemSelectable( 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 ) { + 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 == WP_GAUNTLET ) { + continue; // never cycle to gauntlet + }*/ + if( cg.weaponSelect <= 32 ) + { + if ( CG_WeaponSelectable( cg.weaponSelect ) ) { + break; + } + } + else if( cg.weaponSelect > 32 ) + { + if ( CG_ItemSelectable( 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_gotWeapon( num, cg.snap->ps.stats ) ) { + return; // don't have the weapon + } + + cg.weaponSelect = num; +} + +/* +=================== +CG_OutOfAmmoChange + +The current weapon has just run out of ammo +=================== +*/ +void CG_OutOfAmmoChange( void ) { + int i; + + //TA: mwhaha, must manually change weapons + /*cg.weaponSelectTime = cg.time; + + for ( i = 31 ; i > 0 ; i-- ) { + if ( CG_WeaponSelectable( i ) ) { + cg.weaponSelect = i; + break; + } + }*/ +} + + + +/* +=================================================================================================== + +WEAPON EVENTS + +=================================================================================================== +*/ + +/* +================ +CG_FireWeapon + +Caused by an EV_FIRE_WEAPON event +================ +*/ +void CG_FireWeapon( centity_t *cent ) { + entityState_t *ent; + int c; + weaponInfo_t *weap; + + ent = ¢->currentState; + if ( ent->weapon == WP_NONE ) { + return; + } + if ( ent->weapon >= WP_NUM_WEAPONS ) { + CG_Error( "CG_FireWeapon: ent->weapon >= WP_NUM_WEAPONS" ); + return; + } + weap = &cg_weapons[ ent->weapon ]; + + // mark the entity as muzzle flashing, so when it is added it will + // append the flash to the weapon model + cent->muzzleFlashTime = cg.time; + + // lightning gun only does this this on initial press + if ( ent->weapon == WP_LIGHTNING ) { + if ( cent->pe.lightningFiring ) { + return; + } + } + + // play quad sound if needed + /*if ( cent->currentState.powerups & ( 1 << PW_QUAD ) ) { + trap_S_StartSound (NULL, cent->currentState.number, CHAN_ITEM, cgs.media.quadSound ); + }*/ + + // play a sound + for ( c = 0 ; c < 4 ; c++ ) { + if ( !weap->flashSound[c] ) { + break; + } + } + if ( c > 0 ) { + c = rand() % c; + if ( weap->flashSound[c] ) + { + trap_S_StartSound( NULL, ent->number, CHAN_WEAPON, weap->flashSound[c] ); + } + } + + // do brass ejection + if ( weap->ejectBrassFunc && cg_brassTime.integer > 0 ) { + weap->ejectBrassFunc( cent ); + } +} + + +/* +================= +CG_MissileHitWall + +Caused by an EV_MISSILE_MISS event, or directly by local bullet tracing +================= +*/ +void CG_MissileHitWall( int weapon, int clientNum, vec3_t origin, vec3_t dir, impactSound_t soundType ) { + qhandle_t mod; + qhandle_t mark; + qhandle_t shader; + sfxHandle_t sfx; + float radius; + float light; + vec3_t lightColor; + localEntity_t *le; + int r; + qboolean alphaFade; + qboolean isSprite; + int duration; + + mark = 0; + radius = 32; + sfx = 0; + mod = 0; + shader = 0; + light = 0; + lightColor[0] = 1; + lightColor[1] = 1; + lightColor[2] = 0; + + // set defaults + isSprite = qfalse; + duration = 600; + + switch ( weapon ) { + default: + case WP_LIGHTNING: + // no explosion at LG impact, it is added with the beam + r = rand() & 3; + if ( r < 2 ) { + sfx = cgs.media.sfx_lghit2; + } else if ( r == 2 ) { + sfx = cgs.media.sfx_lghit1; + } else { + sfx = cgs.media.sfx_lghit3; + } + mark = cgs.media.holeMarkShader; + radius = 12; + break; + case WP_GRENADE_LAUNCHER: + mod = cgs.media.dishFlashModel; + shader = cgs.media.grenadeExplosionShader; + sfx = cgs.media.sfx_rockexp; + mark = cgs.media.burnMarkShader; + radius = 64; + light = 300; + isSprite = qtrue; + break; + case WP_ROCKET_LAUNCHER: + mod = cgs.media.dishFlashModel; + shader = cgs.media.rocketExplosionShader; + sfx = cgs.media.sfx_rockexp; + mark = cgs.media.burnMarkShader; + radius = 64; + light = 300; + isSprite = qtrue; + duration = 1000; + lightColor[0] = 1; + lightColor[1] = 0.75; + lightColor[2] = 0.0; + break; + case WP_RAILGUN: + mod = cgs.media.ringFlashModel; + shader = cgs.media.railExplosionShader; + sfx = cgs.media.sfx_plasmaexp; + mark = cgs.media.energyMarkShader; + radius = 24; + break; + case WP_FLAMER: + mod = cgs.media.dishFlashModel; + shader = cgs.media.flameExplShader; + sfx = cgs.media.sfx_lghit2; + mark = cgs.media.burnMarkShader; + radius = 48; + isSprite = qtrue; + break; + case WP_PLASMAGUN: + mod = cgs.media.ringFlashModel; + shader = cgs.media.plasmaExplosionShader; + sfx = cgs.media.sfx_plasmaexp; + mark = cgs.media.energyMarkShader; + radius = 16; + break; + case WP_BFG: + mod = cgs.media.dishFlashModel; + shader = cgs.media.bfgExplosionShader; + sfx = cgs.media.sfx_rockexp; + mark = cgs.media.burnMarkShader; + radius = 32; + isSprite = qtrue; + break; + case WP_SHOTGUN: + mod = cgs.media.bulletFlashModel; + shader = cgs.media.bulletExplosionShader; + mark = cgs.media.bulletMarkShader; + sfx = 0; + radius = 4; + break; + case WP_MACHINEGUN: + mod = cgs.media.bulletFlashModel; + shader = cgs.media.bulletExplosionShader; + mark = cgs.media.bulletMarkShader; + case WP_CHAINGUN: + mod = cgs.media.bulletFlashModel; + shader = cgs.media.bulletExplosionShader; + mark = cgs.media.bulletMarkShader; + + r = rand() & 3; + if ( r < 2 ) { + sfx = cgs.media.sfx_ric1; + } else if ( r == 2 ) { + sfx = cgs.media.sfx_ric2; + } else { + sfx = cgs.media.sfx_ric3; + } + + radius = 8; + break; + } + + if ( sfx ) { + trap_S_StartSound( origin, ENTITYNUM_WORLD, CHAN_AUTO, sfx ); + } + + // + // create the explosion + // + if ( mod ) { + le = CG_MakeExplosion( origin, dir, + mod, shader, + duration, isSprite ); + le->light = light; + VectorCopy( lightColor, le->lightColor ); + if ( weapon == WP_RAILGUN ) { + // colorize with client color + VectorCopy( cgs.clientinfo[clientNum].color, le->color ); + } + } + + // + // impact mark + // + alphaFade = (mark == cgs.media.energyMarkShader); // plasma fades alpha, all others fade color + if ( weapon == WP_RAILGUN ) { + float *color; + + // colorize with client color + color = cgs.clientinfo[clientNum].color; + CG_ImpactMark( mark, origin, dir, random()*360, color[0],color[1], color[2],1, alphaFade, radius, qfalse ); + } else { + CG_ImpactMark( mark, origin, dir, random()*360, 1,1,1,1, alphaFade, radius, qfalse ); + } +} + + +/* +================= +CG_MissileHitPlayer +================= +*/ +void CG_MissileHitPlayer( int weapon, vec3_t origin, vec3_t dir, int entityNum ) { + CG_Bleed( origin, entityNum ); + + // some weapons will make an explosion with the blood, while + // others will just make the blood + switch ( weapon ) { + case WP_GRENADE_LAUNCHER: + case WP_ROCKET_LAUNCHER: + CG_MissileHitWall( weapon, 0, origin, dir, IMPACTSOUND_FLESH ); + break; + default: + break; + } +} + + + +/* +============================================================================ + +SHOTGUN TRACING + +============================================================================ +*/ + +/* +================ +CG_ShotgunPellet +================ +*/ +static void CG_ShotgunPellet( vec3_t start, vec3_t end, int skipNum ) { + trace_t tr; + int sourceContentType, destContentType; + + CG_Trace( &tr, start, NULL, NULL, end, skipNum, MASK_SHOT ); + + sourceContentType = trap_CM_PointContents( start, 0 ); + destContentType = trap_CM_PointContents( tr.endpos, 0 ); + + // FIXME: should probably move this cruft into CG_BubbleTrail + if ( sourceContentType == destContentType ) { + if ( sourceContentType & CONTENTS_WATER ) { + CG_BubbleTrail( start, tr.endpos, 32 ); + } + } else if ( sourceContentType & CONTENTS_WATER ) { + trace_t trace; + + trap_CM_BoxTrace( &trace, end, start, NULL, NULL, 0, CONTENTS_WATER ); + CG_BubbleTrail( start, trace.endpos, 32 ); + } else if ( destContentType & CONTENTS_WATER ) { + trace_t trace; + + trap_CM_BoxTrace( &trace, start, end, NULL, NULL, 0, CONTENTS_WATER ); + CG_BubbleTrail( tr.endpos, trace.endpos, 32 ); + } + + if ( tr.surfaceFlags & SURF_NOIMPACT ) { + return; + } + + if ( cg_entities[tr.entityNum].currentState.eType == ET_PLAYER ) { + CG_MissileHitPlayer( WP_SHOTGUN, tr.endpos, tr.plane.normal, tr.entityNum ); + } else { + if ( tr.surfaceFlags & SURF_NOIMPACT ) { + // SURF_NOIMPACT will not make a flame puff or a mark + return; + } + if ( tr.surfaceFlags & SURF_METALSTEPS ) { + CG_MissileHitWall( WP_SHOTGUN, 0, tr.endpos, tr.plane.normal, IMPACTSOUND_METAL ); + } else { + CG_MissileHitWall( WP_SHOTGUN, 0, tr.endpos, tr.plane.normal, IMPACTSOUND_DEFAULT ); + } + } +} + +/* +================ +CG_ShotgunPattern + +Perform the same traces the server did to locate the +hit splashes (FIXME: ranom seed isn't synce anymore) +================ +*/ +static void CG_ShotgunPattern( vec3_t origin, vec3_t origin2, int otherEntNum ) { + int i; + float r, u; + vec3_t end; + vec3_t forward, right, up; + + // 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 < DEFAULT_SHOTGUN_COUNT ; i++ ) { + r = crandom() * DEFAULT_SHOTGUN_SPREAD * 16; + u = crandom() * DEFAULT_SHOTGUN_SPREAD * 16; + VectorMA( origin, 8192 * 16, forward, end); + VectorMA (end, r, right, end); + VectorMA (end, u, up, end); + + CG_ShotgunPellet( origin, end, otherEntNum ); + } +} + +/* +============== +CG_ShotgunFire +============== +*/ +void CG_ShotgunFire( entityState_t *es ) { + vec3_t v; + int contents; + + VectorSubtract( es->origin2, es->pos.trBase, v ); + VectorNormalize( v ); + VectorScale( v, 32, v ); + VectorAdd( es->pos.trBase, v, v ); + if ( cgs.glconfig.hardwareType != GLHW_RAGEPRO ) { + // ragepro can't alpha fade, so don't even bother with smoke + vec3_t up; + + contents = trap_CM_PointContents( es->pos.trBase, 0 ); + if ( !( contents & CONTENTS_WATER ) ) { + VectorSet( up, 0, 0, 8 ); + CG_SmokePuff( v, up, 32, 1, 1, 1, 0.33f, 900, cg.time, 0, LEF_PUFF_DONT_SCALE, cgs.media.shotgunSmokePuffShader ); + } + } + CG_ShotgunPattern( es->pos.trBase, es->origin2, es->otherEntityNum ); +} + +/* +============================================================================ + +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 ) { + trace_t trace; + int sourceContentType, destContentType; + 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 ) ) { + sourceContentType = trap_CM_PointContents( start, 0 ); + destContentType = trap_CM_PointContents( end, 0 ); + + // do a complete bubble trail if necessary + if ( ( sourceContentType == destContentType ) && ( sourceContentType & CONTENTS_WATER ) ) { + CG_BubbleTrail( start, end, 32 ); + } + // bubble trail from water into air + else if ( ( sourceContentType & CONTENTS_WATER ) ) { + trap_CM_BoxTrace( &trace, end, start, NULL, NULL, 0, CONTENTS_WATER ); + CG_BubbleTrail( start, trace.endpos, 32 ); + } + // bubble trail from air into water + else if ( ( destContentType & CONTENTS_WATER ) ) { + trap_CM_BoxTrace( &trace, start, end, NULL, NULL, 0, CONTENTS_WATER ); + CG_BubbleTrail( trace.endpos, end, 32 ); + } + + // draw a tracer + if ( random() < cg_tracerChance.value ) { + CG_Tracer( start, end ); + } + } + } + + // impact splash and mark + if ( flesh ) { + CG_Bleed( end, fleshEntityNum ); + } else { + CG_MissileHitWall( WP_MACHINEGUN, 0, end, normal, IMPACTSOUND_DEFAULT ); + } + +} diff --git a/src/cgame/tr_types.h b/src/cgame/tr_types.h new file mode 100644 index 00000000..c1cd2264 --- /dev/null +++ b/src/cgame/tr_types.h @@ -0,0 +1,224 @@ +// Copyright (C) 1999-2000 Id Software, Inc. +// + +/* + * Portions Copyright (C) 2000-2001 Tim Angus + * + * This program 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, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ + +/* To assertain which portions are licensed under the GPL and which are + * licensed by Id Software, Inc. please run a diff between the equivalent + * versions of the "Tremulous" modification and the unmodified "Quake3" + * game source code. + */ + +#ifndef __TR_TYPES_H +#define __TR_TYPES_H + + +#define MAX_DLIGHTS 32 // can't be increased, because bit flags are used on surfaces +#define MAX_ENTITIES 1023 // can't be increased without changing drawsurf bit packing + +// renderfx flags +#define RF_MINLIGHT 1 // allways have some light (viewmodel, some items) +#define RF_THIRD_PERSON 2 // don't draw through eyes, only mirrors (player bodies, chat sprites) +#define RF_FIRST_PERSON 4 // only draw through eyes (view weapon, damage blood blob) +#define RF_DEPTHHACK 8 // for view weapon Z crunching +#define RF_NOSHADOW 64 // don't add stencil shadows + +#define RF_LIGHTING_ORIGIN 128 // use refEntity->lightingOrigin instead of refEntity->origin + // for lighting. This allows entities to sink into the floor + // with their origin going solid, and allows all parts of a + // player to get the same lighting +#define RF_SHADOW_PLANE 256 // use refEntity->shadowPlane +#define RF_WRAP_FRAMES 512 // mod the model frames by the maxframes to allow continuous + // animation without needing to know the frame count + +// refdef flags +#define RDF_NOWORLDMODEL 1 // used for player configuration screen +#define RDF_HYPERSPACE 4 // teleportation effect + +typedef struct { + vec3_t xyz; + float st[2]; + byte modulate[4]; +} polyVert_t; + +typedef struct poly_s { + qhandle_t hShader; + int numVerts; + polyVert_t *verts; +} poly_t; + +typedef enum { + RT_MODEL, + RT_POLY, + RT_SPRITE, + RT_BEAM, + RT_RAIL_CORE, + RT_RAIL_RINGS, + RT_LIGHTNING, + RT_PORTALSURFACE, // doesn't draw anything, just info for portals + + RT_MAX_REF_ENTITY_TYPE +} refEntityType_t; + +typedef struct { + refEntityType_t reType; + int renderfx; + + qhandle_t hModel; // opaque type outside refresh + + // most recent data + vec3_t lightingOrigin; // so multi-part models can be lit identically (RF_LIGHTING_ORIGIN) + float shadowPlane; // projection shadows go here, stencils go slightly lower + + vec3_t axis[3]; // rotation vectors + qboolean nonNormalizedAxes; // axis are not normalized, i.e. they have scale + float origin[3]; // also used as MODEL_BEAM's "from" + int frame; // also used as MODEL_BEAM's diameter + + // previous data for frame interpolation + float oldorigin[3]; // also used as MODEL_BEAM's "to" + int oldframe; + float backlerp; // 0.0 = current, 1.0 = old + + // texturing + int skinNum; // inline skin index + qhandle_t customSkin; // NULL for default skin + qhandle_t customShader; // use one image for the entire thing + + // misc + byte shaderRGBA[4]; // colors used by rgbgen entity shaders + float shaderTexCoord[2]; // texture coordinates used by tcMod entity modifiers + float shaderTime; // subtracted from refdef time to control effect start times + + // extra sprite information + float radius; + float rotation; +} refEntity_t; + + +#define MAX_RENDER_STRINGS 8 +#define MAX_RENDER_STRING_LENGTH 32 + +typedef struct { + int x, y, width, height; + float fov_x, fov_y; + vec3_t vieworg; + vec3_t viewaxis[3]; // transformation matrix + + // time in milliseconds for shader effects and other time dependent rendering issues + int time; + + int rdflags; // RDF_NOWORLDMODEL, etc + + // 1 bits will prevent the associated area from rendering at all + byte areamask[MAX_MAP_AREA_BYTES]; + + // text messages for deform text shaders + char text[MAX_RENDER_STRINGS][MAX_RENDER_STRING_LENGTH]; +} refdef_t; + + +typedef enum { + STEREO_CENTER, + STEREO_LEFT, + STEREO_RIGHT +} stereoFrame_t; + + +/* +** glconfig_t +** +** Contains variables specific to the OpenGL configuration +** being run right now. These are constant once the OpenGL +** subsystem is initialized. +*/ +typedef enum { + TC_NONE, + TC_S3TC +} textureCompression_t; + +typedef enum { + GLDRV_ICD, // driver is integrated with window system + // WARNING: there are tests that check for + // > GLDRV_ICD for minidriverness, so this + // should always be the lowest value in this + // enum set + GLDRV_STANDALONE, // driver is a non-3Dfx standalone driver + GLDRV_VOODOO // driver is a 3Dfx standalone driver +} glDriverType_t; + +typedef enum { + GLHW_GENERIC, // where everthing works the way it should + GLHW_3DFX_2D3D, // Voodoo Banshee or Voodoo3, relevant since if this is + // the hardware type then there can NOT exist a secondary + // display adapter + GLHW_RIVA128, // where you can't interpolate alpha + GLHW_RAGEPRO, // where you can't modulate alpha on alpha textures + GLHW_PERMEDIA2 // where you don't have src*dst +} glHardwareType_t; + +typedef struct { + char renderer_string[MAX_STRING_CHARS]; + char vendor_string[MAX_STRING_CHARS]; + char version_string[MAX_STRING_CHARS]; + char extensions_string[BIG_INFO_STRING]; + + int maxTextureSize; // queried from GL + int maxActiveTextures; // multitexture ability + + int colorBits, depthBits, stencilBits; + + glDriverType_t driverType; + glHardwareType_t hardwareType; + + qboolean deviceSupportsGamma; + textureCompression_t textureCompression; + qboolean textureEnvAddAvailable; + + int vidWidth, vidHeight; + // aspect is the screen's physical width / height, which may be different + // than scrWidth / scrHeight if the pixels are non-square + // normal screens should be 4/3, but wide aspect monitors may be 16/9 + float windowAspect; + + int displayFrequency; + + // synonymous with "does rendering consume the entire screen?", therefore + // a Voodoo or Voodoo2 will have this set to TRUE, as will a Win32 ICD that + // used CDS. + qboolean isFullscreen; + qboolean stereoEnabled; + qboolean smpActive; // dual processor +} glconfig_t; + + +#if !defined _WIN32 + +#define _3DFX_DRIVER_NAME "libMesaVoodooGL.so" +#define OPENGL_DRIVER_NAME "libGL.so" + +#else + +#define _3DFX_DRIVER_NAME "3dfxvgl" +#define OPENGL_DRIVER_NAME "opengl32" + +#endif // !defined _WIN32 + + +#endif // __TR_TYPES_H diff --git a/src/game/bg_lib.c b/src/game/bg_lib.c new file mode 100644 index 00000000..e55b47bd --- /dev/null +++ b/src/game/bg_lib.c @@ -0,0 +1,1337 @@ +// Copyright (C) 1999-2000 Id Software, Inc. +// +// Copyright (C) 1999-2000 Id Software, Inc. +// +// bg_lib,c -- standard C library replacement routines used by code +// compiled for the virtual machine + +/* + * Portions Copyright (C) 2000-2001 Tim Angus + * + * This program 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, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ + +/* To assertain which portions are licensed under the GPL and which are + * licensed by Id Software, Inc. please run a diff between the equivalent + * versions of the "Tremulous" modification and the unmodified "Quake3" + * game source code. + */ + +#include "q_shared.h" + +/*- + * Copyright (c) 1992, 1993 + * The Regents of the University of California. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * 3. All advertising materials mentioning features or use of this software + * must display the following acknowledgement: + * This product includes software developed by the University of + * California, Berkeley and its contributors. + * 4. Neither the name of the University nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS + * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY + * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. + */ + +#if defined(LIBC_SCCS) && !defined(lint) +#if 0 +static char sccsid[] = "@(#)qsort.c 8.1 (Berkeley) 6/4/93"; +#endif +static const char rcsid[] = + "$Id$"; +#endif /* LIBC_SCCS and not lint */ + +//typedef int cmp_t(const void *, const void *); +static char* med3(char *, char *, char *, cmp_t *); +static void swapfunc(char *, char *, int, int); + +#ifndef min +#define min(a, b) (a) < (b) ? a : b +#endif + +/* + * Qsort routine from Bentley & McIlroy's "Engineering a Sort Function". + */ +#define swapcode(TYPE, parmi, parmj, n) { \ + long i = (n) / sizeof (TYPE); \ + register TYPE *pi = (TYPE *) (parmi); \ + register TYPE *pj = (TYPE *) (parmj); \ + do { \ + register TYPE t = *pi; \ + *pi++ = *pj; \ + *pj++ = t; \ + } while (--i > 0); \ +} + +#define SWAPINIT(a, es) swaptype = ((char *)a - (char *)0) % sizeof(long) || \ + es % sizeof(long) ? 2 : es == sizeof(long)? 0 : 1; + +static void +swapfunc(a, b, n, swaptype) + char *a, *b; + int n, swaptype; +{ + if(swaptype <= 1) + swapcode(long, a, b, n) + else + swapcode(char, a, b, n) +} + +#define swap(a, b) \ + if (swaptype == 0) { \ + long t = *(long *)(a); \ + *(long *)(a) = *(long *)(b); \ + *(long *)(b) = t; \ + } else \ + swapfunc(a, b, es, swaptype) + +#define vecswap(a, b, n) if ((n) > 0) swapfunc(a, b, n, swaptype) + +static char * +med3(a, b, c, cmp) + char *a, *b, *c; + cmp_t *cmp; +{ + return cmp(a, b) < 0 ? + (cmp(b, c) < 0 ? b : (cmp(a, c) < 0 ? c : a )) + :(cmp(b, c) > 0 ? b : (cmp(a, c) < 0 ? a : c )); +} + +void +qsort(a, n, es, cmp) + void *a; + size_t n, es; + cmp_t *cmp; +{ + char *pa, *pb, *pc, *pd, *pl, *pm, *pn; + int d, r, swaptype, swap_cnt; + +loop: SWAPINIT(a, es); + swap_cnt = 0; + if (n < 7) { + for (pm = (char *)a + es; pm < (char *)a + n * es; pm += es) + for (pl = pm; pl > (char *)a && cmp(pl - es, pl) > 0; + pl -= es) + swap(pl, pl - es); + return; + } + pm = (char *)a + (n / 2) * es; + if (n > 7) { + pl = a; + pn = (char *)a + (n - 1) * es; + if (n > 40) { + d = (n / 8) * es; + pl = med3(pl, pl + d, pl + 2 * d, cmp); + pm = med3(pm - d, pm, pm + d, cmp); + pn = med3(pn - 2 * d, pn - d, pn, cmp); + } + pm = med3(pl, pm, pn, cmp); + } + swap(a, pm); + pa = pb = (char *)a + es; + + pc = pd = (char *)a + (n - 1) * es; + for (;;) { + while (pb <= pc && (r = cmp(pb, a)) <= 0) { + if (r == 0) { + swap_cnt = 1; + swap(pa, pb); + pa += es; + } + pb += es; + } + while (pb <= pc && (r = cmp(pc, a)) >= 0) { + if (r == 0) { + swap_cnt = 1; + swap(pc, pd); + pd -= es; + } + pc -= es; + } + if (pb > pc) + break; + swap(pb, pc); + swap_cnt = 1; + pb += es; + pc -= es; + } + if (swap_cnt == 0) { /* Switch to insertion sort */ + for (pm = (char *)a + es; pm < (char *)a + n * es; pm += es) + for (pl = pm; pl > (char *)a && cmp(pl - es, pl) > 0; + pl -= es) + swap(pl, pl - es); + return; + } + + pn = (char *)a + n * es; + r = min(pa - (char *)a, pb - pa); + vecswap(a, pb - r, r); + r = min(pd - pc, pn - pd - es); + vecswap(pb, pn - r, r); + if ((r = pb - pa) > es) + qsort(a, r / es, es, cmp); + if ((r = pd - pc) > es) { + /* Iterate rather than recurse to save stack space */ + a = pn - r; + n = r / es; + goto loop; + } +/* qsort(pn - r, r / es, es, cmp);*/ +} + +//================================================================================== + + +// this file is excluded from release builds because of intrinsics + +size_t strlen( const char *string ) { + const char *s; + + s = string; + while ( *s ) { + s++; + } + return s - string; +} + + +char *strcat( char *strDestination, const char *strSource ) { + char *s; + + s = strDestination; + while ( *s ) { + s++; + } + while ( *strSource ) { + *s++ = *strSource++; + } + *s = 0; + return strDestination; +} + +char *strcpy( char *strDestination, const char *strSource ) { + char *s; + + s = strDestination; + while ( *strSource ) { + *s++ = *strSource++; + } + *s = 0; + return strDestination; +} + + +int strcmp( const char *string1, const char *string2 ) { + while ( *string1 == *string2 && *string1 && *string2 ) { + string1++; + string2++; + } + return *string1 - *string2; +} + + +char *strchr( const char *string, int c ) { + while ( *string ) { + if ( *string == c ) { + return ( char * )string; + } + string++; + } + return (char *)0; +} + +char *strstr( const char *string, const char *strCharSet ) { + while ( *string ) { + int i; + + for ( i = 0 ; strCharSet[i] ; i++ ) { + if ( string[i] != strCharSet[i] ) { + break; + } + } + if ( !strCharSet[i] ) { + return (char *)string; + } + string++; + } + return (char *)0; +} + +#if !defined ( _MSC_VER ) && !defined ( __linux__ ) + +int tolower( int c ) { + if ( c >= 'A' && c <= 'Z' ) { + c += 'a' - 'A'; + } + return c; +} + + +int toupper( int c ) { + if ( c >= 'a' && c <= 'z' ) { + c += 'A' - 'a'; + } + return c; +} + +#endif +//#ifndef _MSC_VER + +void *memmove( void *dest, const void *src, size_t count ) { + int i; + + if ( dest > src ) { + for ( i = count-1 ; i >= 0 ; i-- ) { + ((char *)dest)[i] = ((char *)src)[i]; + } + } else { + for ( i = 0 ; i < count ; i++ ) { + ((char *)dest)[i] = ((char *)src)[i]; + } + } + return dest; +} + + +#if 0 + +double floor( double x ) { + return (int)(x + 0x40000000) - 0x40000000; +} + +void *memset( void *dest, int c, size_t count ) { + while ( count-- ) { + ((char *)dest)[count] = c; + } + return dest; +} + +void *memcpy( void *dest, const void *src, size_t count ) { + while ( count-- ) { + ((char *)dest)[count] = ((char *)src)[count]; + } + return dest; +} + +char *strncpy( char *strDest, const char *strSource, size_t count ) { + char *s; + + s = strDest; + while ( *strSource && count ) { + *s++ = *strSource++; + count--; + } + while ( count-- ) { + *s++ = 0; + } + return strDest; +} + +double sqrt( double x ) { + float y; + float delta; + float maxError; + + if ( x <= 0 ) { + return 0; + } + + // initial guess + y = x / 2; + + // refine + maxError = x * 0.001; + + do { + delta = ( y * y ) - x; + y -= delta / ( 2 * y ); + } while ( delta > maxError || delta < -maxError ); + + return y; +} + + +float sintable[1024] = { +0.000000,0.001534,0.003068,0.004602,0.006136,0.007670,0.009204,0.010738, +0.012272,0.013805,0.015339,0.016873,0.018407,0.019940,0.021474,0.023008, +0.024541,0.026075,0.027608,0.029142,0.030675,0.032208,0.033741,0.035274, +0.036807,0.038340,0.039873,0.041406,0.042938,0.044471,0.046003,0.047535, +0.049068,0.050600,0.052132,0.053664,0.055195,0.056727,0.058258,0.059790, +0.061321,0.062852,0.064383,0.065913,0.067444,0.068974,0.070505,0.072035, +0.073565,0.075094,0.076624,0.078153,0.079682,0.081211,0.082740,0.084269, +0.085797,0.087326,0.088854,0.090381,0.091909,0.093436,0.094963,0.096490, +0.098017,0.099544,0.101070,0.102596,0.104122,0.105647,0.107172,0.108697, +0.110222,0.111747,0.113271,0.114795,0.116319,0.117842,0.119365,0.120888, +0.122411,0.123933,0.125455,0.126977,0.128498,0.130019,0.131540,0.133061, +0.134581,0.136101,0.137620,0.139139,0.140658,0.142177,0.143695,0.145213, +0.146730,0.148248,0.149765,0.151281,0.152797,0.154313,0.155828,0.157343, +0.158858,0.160372,0.161886,0.163400,0.164913,0.166426,0.167938,0.169450, +0.170962,0.172473,0.173984,0.175494,0.177004,0.178514,0.180023,0.181532, +0.183040,0.184548,0.186055,0.187562,0.189069,0.190575,0.192080,0.193586, +0.195090,0.196595,0.198098,0.199602,0.201105,0.202607,0.204109,0.205610, +0.207111,0.208612,0.210112,0.211611,0.213110,0.214609,0.216107,0.217604, +0.219101,0.220598,0.222094,0.223589,0.225084,0.226578,0.228072,0.229565, +0.231058,0.232550,0.234042,0.235533,0.237024,0.238514,0.240003,0.241492, +0.242980,0.244468,0.245955,0.247442,0.248928,0.250413,0.251898,0.253382, +0.254866,0.256349,0.257831,0.259313,0.260794,0.262275,0.263755,0.265234, +0.266713,0.268191,0.269668,0.271145,0.272621,0.274097,0.275572,0.277046, +0.278520,0.279993,0.281465,0.282937,0.284408,0.285878,0.287347,0.288816, +0.290285,0.291752,0.293219,0.294685,0.296151,0.297616,0.299080,0.300543, +0.302006,0.303468,0.304929,0.306390,0.307850,0.309309,0.310767,0.312225, +0.313682,0.315138,0.316593,0.318048,0.319502,0.320955,0.322408,0.323859, +0.325310,0.326760,0.328210,0.329658,0.331106,0.332553,0.334000,0.335445, +0.336890,0.338334,0.339777,0.341219,0.342661,0.344101,0.345541,0.346980, +0.348419,0.349856,0.351293,0.352729,0.354164,0.355598,0.357031,0.358463, +0.359895,0.361326,0.362756,0.364185,0.365613,0.367040,0.368467,0.369892, +0.371317,0.372741,0.374164,0.375586,0.377007,0.378428,0.379847,0.381266, +0.382683,0.384100,0.385516,0.386931,0.388345,0.389758,0.391170,0.392582, +0.393992,0.395401,0.396810,0.398218,0.399624,0.401030,0.402435,0.403838, +0.405241,0.406643,0.408044,0.409444,0.410843,0.412241,0.413638,0.415034, +0.416430,0.417824,0.419217,0.420609,0.422000,0.423390,0.424780,0.426168, +0.427555,0.428941,0.430326,0.431711,0.433094,0.434476,0.435857,0.437237, +0.438616,0.439994,0.441371,0.442747,0.444122,0.445496,0.446869,0.448241, +0.449611,0.450981,0.452350,0.453717,0.455084,0.456449,0.457813,0.459177, +0.460539,0.461900,0.463260,0.464619,0.465976,0.467333,0.468689,0.470043, +0.471397,0.472749,0.474100,0.475450,0.476799,0.478147,0.479494,0.480839, +0.482184,0.483527,0.484869,0.486210,0.487550,0.488889,0.490226,0.491563, +0.492898,0.494232,0.495565,0.496897,0.498228,0.499557,0.500885,0.502212, +0.503538,0.504863,0.506187,0.507509,0.508830,0.510150,0.511469,0.512786, +0.514103,0.515418,0.516732,0.518045,0.519356,0.520666,0.521975,0.523283, +0.524590,0.525895,0.527199,0.528502,0.529804,0.531104,0.532403,0.533701, +0.534998,0.536293,0.537587,0.538880,0.540171,0.541462,0.542751,0.544039, +0.545325,0.546610,0.547894,0.549177,0.550458,0.551738,0.553017,0.554294, +0.555570,0.556845,0.558119,0.559391,0.560662,0.561931,0.563199,0.564466, +0.565732,0.566996,0.568259,0.569521,0.570781,0.572040,0.573297,0.574553, +0.575808,0.577062,0.578314,0.579565,0.580814,0.582062,0.583309,0.584554, +0.585798,0.587040,0.588282,0.589521,0.590760,0.591997,0.593232,0.594466, +0.595699,0.596931,0.598161,0.599389,0.600616,0.601842,0.603067,0.604290, +0.605511,0.606731,0.607950,0.609167,0.610383,0.611597,0.612810,0.614022, +0.615232,0.616440,0.617647,0.618853,0.620057,0.621260,0.622461,0.623661, +0.624859,0.626056,0.627252,0.628446,0.629638,0.630829,0.632019,0.633207, +0.634393,0.635578,0.636762,0.637944,0.639124,0.640303,0.641481,0.642657, +0.643832,0.645005,0.646176,0.647346,0.648514,0.649681,0.650847,0.652011, +0.653173,0.654334,0.655493,0.656651,0.657807,0.658961,0.660114,0.661266, +0.662416,0.663564,0.664711,0.665856,0.667000,0.668142,0.669283,0.670422, +0.671559,0.672695,0.673829,0.674962,0.676093,0.677222,0.678350,0.679476, +0.680601,0.681724,0.682846,0.683965,0.685084,0.686200,0.687315,0.688429, +0.689541,0.690651,0.691759,0.692866,0.693971,0.695075,0.696177,0.697278, +0.698376,0.699473,0.700569,0.701663,0.702755,0.703845,0.704934,0.706021, +0.707107,0.708191,0.709273,0.710353,0.711432,0.712509,0.713585,0.714659, +0.715731,0.716801,0.717870,0.718937,0.720003,0.721066,0.722128,0.723188, +0.724247,0.725304,0.726359,0.727413,0.728464,0.729514,0.730563,0.731609, +0.732654,0.733697,0.734739,0.735779,0.736817,0.737853,0.738887,0.739920, +0.740951,0.741980,0.743008,0.744034,0.745058,0.746080,0.747101,0.748119, +0.749136,0.750152,0.751165,0.752177,0.753187,0.754195,0.755201,0.756206, +0.757209,0.758210,0.759209,0.760207,0.761202,0.762196,0.763188,0.764179, +0.765167,0.766154,0.767139,0.768122,0.769103,0.770083,0.771061,0.772036, +0.773010,0.773983,0.774953,0.775922,0.776888,0.777853,0.778817,0.779778, +0.780737,0.781695,0.782651,0.783605,0.784557,0.785507,0.786455,0.787402, +0.788346,0.789289,0.790230,0.791169,0.792107,0.793042,0.793975,0.794907, +0.795837,0.796765,0.797691,0.798615,0.799537,0.800458,0.801376,0.802293, +0.803208,0.804120,0.805031,0.805940,0.806848,0.807753,0.808656,0.809558, +0.810457,0.811355,0.812251,0.813144,0.814036,0.814926,0.815814,0.816701, +0.817585,0.818467,0.819348,0.820226,0.821103,0.821977,0.822850,0.823721, +0.824589,0.825456,0.826321,0.827184,0.828045,0.828904,0.829761,0.830616, +0.831470,0.832321,0.833170,0.834018,0.834863,0.835706,0.836548,0.837387, +0.838225,0.839060,0.839894,0.840725,0.841555,0.842383,0.843208,0.844032, +0.844854,0.845673,0.846491,0.847307,0.848120,0.848932,0.849742,0.850549, +0.851355,0.852159,0.852961,0.853760,0.854558,0.855354,0.856147,0.856939, +0.857729,0.858516,0.859302,0.860085,0.860867,0.861646,0.862424,0.863199, +0.863973,0.864744,0.865514,0.866281,0.867046,0.867809,0.868571,0.869330, +0.870087,0.870842,0.871595,0.872346,0.873095,0.873842,0.874587,0.875329, +0.876070,0.876809,0.877545,0.878280,0.879012,0.879743,0.880471,0.881197, +0.881921,0.882643,0.883363,0.884081,0.884797,0.885511,0.886223,0.886932, +0.887640,0.888345,0.889048,0.889750,0.890449,0.891146,0.891841,0.892534, +0.893224,0.893913,0.894599,0.895284,0.895966,0.896646,0.897325,0.898001, +0.898674,0.899346,0.900016,0.900683,0.901349,0.902012,0.902673,0.903332, +0.903989,0.904644,0.905297,0.905947,0.906596,0.907242,0.907886,0.908528, +0.909168,0.909806,0.910441,0.911075,0.911706,0.912335,0.912962,0.913587, +0.914210,0.914830,0.915449,0.916065,0.916679,0.917291,0.917901,0.918508, +0.919114,0.919717,0.920318,0.920917,0.921514,0.922109,0.922701,0.923291, +0.923880,0.924465,0.925049,0.925631,0.926210,0.926787,0.927363,0.927935, +0.928506,0.929075,0.929641,0.930205,0.930767,0.931327,0.931884,0.932440, +0.932993,0.933544,0.934093,0.934639,0.935184,0.935726,0.936266,0.936803, +0.937339,0.937872,0.938404,0.938932,0.939459,0.939984,0.940506,0.941026, +0.941544,0.942060,0.942573,0.943084,0.943593,0.944100,0.944605,0.945107, +0.945607,0.946105,0.946601,0.947094,0.947586,0.948075,0.948561,0.949046, +0.949528,0.950008,0.950486,0.950962,0.951435,0.951906,0.952375,0.952842, +0.953306,0.953768,0.954228,0.954686,0.955141,0.955594,0.956045,0.956494, +0.956940,0.957385,0.957826,0.958266,0.958703,0.959139,0.959572,0.960002, +0.960431,0.960857,0.961280,0.961702,0.962121,0.962538,0.962953,0.963366, +0.963776,0.964184,0.964590,0.964993,0.965394,0.965793,0.966190,0.966584, +0.966976,0.967366,0.967754,0.968139,0.968522,0.968903,0.969281,0.969657, +0.970031,0.970403,0.970772,0.971139,0.971504,0.971866,0.972226,0.972584, +0.972940,0.973293,0.973644,0.973993,0.974339,0.974684,0.975025,0.975365, +0.975702,0.976037,0.976370,0.976700,0.977028,0.977354,0.977677,0.977999, +0.978317,0.978634,0.978948,0.979260,0.979570,0.979877,0.980182,0.980485, +0.980785,0.981083,0.981379,0.981673,0.981964,0.982253,0.982539,0.982824, +0.983105,0.983385,0.983662,0.983937,0.984210,0.984480,0.984749,0.985014, +0.985278,0.985539,0.985798,0.986054,0.986308,0.986560,0.986809,0.987057, +0.987301,0.987544,0.987784,0.988022,0.988258,0.988491,0.988722,0.988950, +0.989177,0.989400,0.989622,0.989841,0.990058,0.990273,0.990485,0.990695, +0.990903,0.991108,0.991311,0.991511,0.991710,0.991906,0.992099,0.992291, +0.992480,0.992666,0.992850,0.993032,0.993212,0.993389,0.993564,0.993737, +0.993907,0.994075,0.994240,0.994404,0.994565,0.994723,0.994879,0.995033, +0.995185,0.995334,0.995481,0.995625,0.995767,0.995907,0.996045,0.996180, +0.996313,0.996443,0.996571,0.996697,0.996820,0.996941,0.997060,0.997176, +0.997290,0.997402,0.997511,0.997618,0.997723,0.997825,0.997925,0.998023, +0.998118,0.998211,0.998302,0.998390,0.998476,0.998559,0.998640,0.998719, +0.998795,0.998870,0.998941,0.999011,0.999078,0.999142,0.999205,0.999265, +0.999322,0.999378,0.999431,0.999481,0.999529,0.999575,0.999619,0.999660, +0.999699,0.999735,0.999769,0.999801,0.999831,0.999858,0.999882,0.999905, +0.999925,0.999942,0.999958,0.999971,0.999981,0.999989,0.999995,0.999999 +}; + +double sin( double x ) { + int index; + int quad; + + index = 1024 * x / (M_PI * 0.5); + quad = ( index >> 10 ) & 3; + index &= 1023; + switch ( quad ) { + case 0: + return sintable[index]; + case 1: + return sintable[1023-index]; + case 2: + return -sintable[index]; + case 3: + return -sintable[1023-index]; + } + return 0; +} + + +double cos( double x ) { + int index; + int quad; + + index = 1024 * x / (M_PI * 0.5); + quad = ( index >> 10 ) & 3; + index &= 1023; + switch ( quad ) { + case 3: + return sintable[index]; + case 0: + return sintable[1023-index]; + case 1: + return -sintable[index]; + case 2: + return -sintable[1023-index]; + } + return 0; +} + + +/* +void create_acostable( void ) { + int i; + FILE *fp; + float a; + + fp = fopen("c:\\acostable.txt", "w"); + fprintf(fp, "float acostable[] = {"); + for (i = 0; i < 1024; i++) { + if (!(i & 7)) + fprintf(fp, "\n"); + a = acos( (float) -1 + i / 512 ); + fprintf(fp, "%1.8f,", a); + } + fprintf(fp, "\n}\n"); + fclose(fp); +} +*/ + + +float acostable[] = { +3.14159265,3.07908248,3.05317551,3.03328655,3.01651113,3.00172442,2.98834964,2.97604422, +2.96458497,2.95381690,2.94362719,2.93393068,2.92466119,2.91576615,2.90720289,2.89893629, +2.89093699,2.88318015,2.87564455,2.86831188,2.86116621,2.85419358,2.84738169,2.84071962, +2.83419760,2.82780691,2.82153967,2.81538876,2.80934770,2.80341062,2.79757211,2.79182724, +2.78617145,2.78060056,2.77511069,2.76969824,2.76435988,2.75909250,2.75389319,2.74875926, +2.74368816,2.73867752,2.73372510,2.72882880,2.72398665,2.71919677,2.71445741,2.70976688, +2.70512362,2.70052613,2.69597298,2.69146283,2.68699438,2.68256642,2.67817778,2.67382735, +2.66951407,2.66523692,2.66099493,2.65678719,2.65261279,2.64847088,2.64436066,2.64028133, +2.63623214,2.63221238,2.62822133,2.62425835,2.62032277,2.61641398,2.61253138,2.60867440, +2.60484248,2.60103507,2.59725167,2.59349176,2.58975488,2.58604053,2.58234828,2.57867769, +2.57502832,2.57139977,2.56779164,2.56420354,2.56063509,2.55708594,2.55355572,2.55004409, +2.54655073,2.54307530,2.53961750,2.53617701,2.53275354,2.52934680,2.52595650,2.52258238, +2.51922417,2.51588159,2.51255441,2.50924238,2.50594525,2.50266278,2.49939476,2.49614096, +2.49290115,2.48967513,2.48646269,2.48326362,2.48007773,2.47690482,2.47374472,2.47059722, +2.46746215,2.46433933,2.46122860,2.45812977,2.45504269,2.45196720,2.44890314,2.44585034, +2.44280867,2.43977797,2.43675809,2.43374890,2.43075025,2.42776201,2.42478404,2.42181622, +2.41885841,2.41591048,2.41297232,2.41004380,2.40712480,2.40421521,2.40131491,2.39842379, +2.39554173,2.39266863,2.38980439,2.38694889,2.38410204,2.38126374,2.37843388,2.37561237, +2.37279910,2.36999400,2.36719697,2.36440790,2.36162673,2.35885335,2.35608768,2.35332964, +2.35057914,2.34783610,2.34510044,2.34237208,2.33965094,2.33693695,2.33423003,2.33153010, +2.32883709,2.32615093,2.32347155,2.32079888,2.31813284,2.31547337,2.31282041,2.31017388, +2.30753373,2.30489988,2.30227228,2.29965086,2.29703556,2.29442632,2.29182309,2.28922580, +2.28663439,2.28404881,2.28146900,2.27889490,2.27632647,2.27376364,2.27120637,2.26865460, +2.26610827,2.26356735,2.26103177,2.25850149,2.25597646,2.25345663,2.25094195,2.24843238, +2.24592786,2.24342836,2.24093382,2.23844420,2.23595946,2.23347956,2.23100444,2.22853408, +2.22606842,2.22360742,2.22115104,2.21869925,2.21625199,2.21380924,2.21137096,2.20893709, +2.20650761,2.20408248,2.20166166,2.19924511,2.19683280,2.19442469,2.19202074,2.18962092, +2.18722520,2.18483354,2.18244590,2.18006225,2.17768257,2.17530680,2.17293493,2.17056692, +2.16820274,2.16584236,2.16348574,2.16113285,2.15878367,2.15643816,2.15409630,2.15175805, +2.14942338,2.14709226,2.14476468,2.14244059,2.14011997,2.13780279,2.13548903,2.13317865, +2.13087163,2.12856795,2.12626757,2.12397047,2.12167662,2.11938600,2.11709859,2.11481435, +2.11253326,2.11025530,2.10798044,2.10570867,2.10343994,2.10117424,2.09891156,2.09665185, +2.09439510,2.09214129,2.08989040,2.08764239,2.08539725,2.08315496,2.08091550,2.07867884, +2.07644495,2.07421383,2.07198545,2.06975978,2.06753681,2.06531651,2.06309887,2.06088387, +2.05867147,2.05646168,2.05425445,2.05204979,2.04984765,2.04764804,2.04545092,2.04325628, +2.04106409,2.03887435,2.03668703,2.03450211,2.03231957,2.03013941,2.02796159,2.02578610, +2.02361292,2.02144204,2.01927344,2.01710710,2.01494300,2.01278113,2.01062146,2.00846399, +2.00630870,2.00415556,2.00200457,1.99985570,1.99770895,1.99556429,1.99342171,1.99128119, +1.98914271,1.98700627,1.98487185,1.98273942,1.98060898,1.97848051,1.97635399,1.97422942, +1.97210676,1.96998602,1.96786718,1.96575021,1.96363511,1.96152187,1.95941046,1.95730088, +1.95519310,1.95308712,1.95098292,1.94888050,1.94677982,1.94468089,1.94258368,1.94048818, +1.93839439,1.93630228,1.93421185,1.93212308,1.93003595,1.92795046,1.92586659,1.92378433, +1.92170367,1.91962459,1.91754708,1.91547113,1.91339673,1.91132385,1.90925250,1.90718266, +1.90511432,1.90304746,1.90098208,1.89891815,1.89685568,1.89479464,1.89273503,1.89067683, +1.88862003,1.88656463,1.88451060,1.88245794,1.88040664,1.87835668,1.87630806,1.87426076, +1.87221477,1.87017008,1.86812668,1.86608457,1.86404371,1.86200412,1.85996577,1.85792866, +1.85589277,1.85385809,1.85182462,1.84979234,1.84776125,1.84573132,1.84370256,1.84167495, +1.83964848,1.83762314,1.83559892,1.83357582,1.83155381,1.82953289,1.82751305,1.82549429, +1.82347658,1.82145993,1.81944431,1.81742973,1.81541617,1.81340362,1.81139207,1.80938151, +1.80737194,1.80536334,1.80335570,1.80134902,1.79934328,1.79733848,1.79533460,1.79333164, +1.79132959,1.78932843,1.78732817,1.78532878,1.78333027,1.78133261,1.77933581,1.77733985, +1.77534473,1.77335043,1.77135695,1.76936428,1.76737240,1.76538132,1.76339101,1.76140148, +1.75941271,1.75742470,1.75543743,1.75345090,1.75146510,1.74948002,1.74749565,1.74551198, +1.74352900,1.74154672,1.73956511,1.73758417,1.73560389,1.73362426,1.73164527,1.72966692, +1.72768920,1.72571209,1.72373560,1.72175971,1.71978441,1.71780969,1.71583556,1.71386199, +1.71188899,1.70991653,1.70794462,1.70597325,1.70400241,1.70203209,1.70006228,1.69809297, +1.69612416,1.69415584,1.69218799,1.69022062,1.68825372,1.68628727,1.68432127,1.68235571, +1.68039058,1.67842588,1.67646160,1.67449772,1.67253424,1.67057116,1.66860847,1.66664615, +1.66468420,1.66272262,1.66076139,1.65880050,1.65683996,1.65487975,1.65291986,1.65096028, +1.64900102,1.64704205,1.64508338,1.64312500,1.64116689,1.63920905,1.63725148,1.63529416, +1.63333709,1.63138026,1.62942366,1.62746728,1.62551112,1.62355517,1.62159943,1.61964388, +1.61768851,1.61573332,1.61377831,1.61182346,1.60986877,1.60791422,1.60595982,1.60400556, +1.60205142,1.60009739,1.59814349,1.59618968,1.59423597,1.59228235,1.59032882,1.58837536, +1.58642196,1.58446863,1.58251535,1.58056211,1.57860891,1.57665574,1.57470259,1.57274945, +1.57079633,1.56884320,1.56689007,1.56493692,1.56298375,1.56103055,1.55907731,1.55712403, +1.55517069,1.55321730,1.55126383,1.54931030,1.54735668,1.54540297,1.54344917,1.54149526, +1.53954124,1.53758710,1.53563283,1.53367843,1.53172389,1.52976919,1.52781434,1.52585933, +1.52390414,1.52194878,1.51999323,1.51803748,1.51608153,1.51412537,1.51216900,1.51021240, +1.50825556,1.50629849,1.50434117,1.50238360,1.50042576,1.49846765,1.49650927,1.49455060, +1.49259163,1.49063237,1.48867280,1.48671291,1.48475270,1.48279215,1.48083127,1.47887004, +1.47690845,1.47494650,1.47298419,1.47102149,1.46905841,1.46709493,1.46513106,1.46316677, +1.46120207,1.45923694,1.45727138,1.45530538,1.45333893,1.45137203,1.44940466,1.44743682, +1.44546850,1.44349969,1.44153038,1.43956057,1.43759024,1.43561940,1.43364803,1.43167612, +1.42970367,1.42773066,1.42575709,1.42378296,1.42180825,1.41983295,1.41785705,1.41588056, +1.41390346,1.41192573,1.40994738,1.40796840,1.40598877,1.40400849,1.40202755,1.40004594, +1.39806365,1.39608068,1.39409701,1.39211264,1.39012756,1.38814175,1.38615522,1.38416795, +1.38217994,1.38019117,1.37820164,1.37621134,1.37422025,1.37222837,1.37023570,1.36824222, +1.36624792,1.36425280,1.36225684,1.36026004,1.35826239,1.35626387,1.35426449,1.35226422, +1.35026307,1.34826101,1.34625805,1.34425418,1.34224937,1.34024364,1.33823695,1.33622932, +1.33422072,1.33221114,1.33020059,1.32818904,1.32617649,1.32416292,1.32214834,1.32013273, +1.31811607,1.31609837,1.31407960,1.31205976,1.31003885,1.30801684,1.30599373,1.30396951, +1.30194417,1.29991770,1.29789009,1.29586133,1.29383141,1.29180031,1.28976803,1.28773456, +1.28569989,1.28366400,1.28162688,1.27958854,1.27754894,1.27550809,1.27346597,1.27142257, +1.26937788,1.26733189,1.26528459,1.26323597,1.26118602,1.25913471,1.25708205,1.25502803, +1.25297262,1.25091583,1.24885763,1.24679802,1.24473698,1.24267450,1.24061058,1.23854519, +1.23647833,1.23440999,1.23234015,1.23026880,1.22819593,1.22612152,1.22404557,1.22196806, +1.21988898,1.21780832,1.21572606,1.21364219,1.21155670,1.20946958,1.20738080,1.20529037, +1.20319826,1.20110447,1.19900898,1.19691177,1.19481283,1.19271216,1.19060973,1.18850553, +1.18639955,1.18429178,1.18218219,1.18007079,1.17795754,1.17584244,1.17372548,1.17160663, +1.16948589,1.16736324,1.16523866,1.16311215,1.16098368,1.15885323,1.15672081,1.15458638, +1.15244994,1.15031147,1.14817095,1.14602836,1.14388370,1.14173695,1.13958808,1.13743709, +1.13528396,1.13312866,1.13097119,1.12881153,1.12664966,1.12448556,1.12231921,1.12015061, +1.11797973,1.11580656,1.11363107,1.11145325,1.10927308,1.10709055,1.10490563,1.10271831, +1.10052856,1.09833638,1.09614174,1.09394462,1.09174500,1.08954287,1.08733820,1.08513098, +1.08292118,1.08070879,1.07849378,1.07627614,1.07405585,1.07183287,1.06960721,1.06737882, +1.06514770,1.06291382,1.06067715,1.05843769,1.05619540,1.05395026,1.05170226,1.04945136, +1.04719755,1.04494080,1.04268110,1.04041841,1.03815271,1.03588399,1.03361221,1.03133735, +1.02905939,1.02677830,1.02449407,1.02220665,1.01991603,1.01762219,1.01532509,1.01302471, +1.01072102,1.00841400,1.00610363,1.00378986,1.00147268,0.99915206,0.99682798,0.99450039, +0.99216928,0.98983461,0.98749636,0.98515449,0.98280898,0.98045980,0.97810691,0.97575030, +0.97338991,0.97102573,0.96865772,0.96628585,0.96391009,0.96153040,0.95914675,0.95675912, +0.95436745,0.95197173,0.94957191,0.94716796,0.94475985,0.94234754,0.93993099,0.93751017, +0.93508504,0.93265556,0.93022170,0.92778341,0.92534066,0.92289341,0.92044161,0.91798524, +0.91552424,0.91305858,0.91058821,0.90811309,0.90563319,0.90314845,0.90065884,0.89816430, +0.89566479,0.89316028,0.89065070,0.88813602,0.88561619,0.88309116,0.88056088,0.87802531, +0.87548438,0.87293806,0.87038629,0.86782901,0.86526619,0.86269775,0.86012366,0.85754385, +0.85495827,0.85236686,0.84976956,0.84716633,0.84455709,0.84194179,0.83932037,0.83669277, +0.83405893,0.83141877,0.82877225,0.82611928,0.82345981,0.82079378,0.81812110,0.81544172, +0.81275556,0.81006255,0.80736262,0.80465570,0.80194171,0.79922057,0.79649221,0.79375655, +0.79101352,0.78826302,0.78550497,0.78273931,0.77996593,0.77718475,0.77439569,0.77159865, +0.76879355,0.76598029,0.76315878,0.76032891,0.75749061,0.75464376,0.75178826,0.74892402, +0.74605092,0.74316887,0.74027775,0.73737744,0.73446785,0.73154885,0.72862033,0.72568217, +0.72273425,0.71977644,0.71680861,0.71383064,0.71084240,0.70784376,0.70483456,0.70181469, +0.69878398,0.69574231,0.69268952,0.68962545,0.68654996,0.68346288,0.68036406,0.67725332, +0.67413051,0.67099544,0.66784794,0.66468783,0.66151492,0.65832903,0.65512997,0.65191753, +0.64869151,0.64545170,0.64219789,0.63892987,0.63564741,0.63235028,0.62903824,0.62571106, +0.62236849,0.61901027,0.61563615,0.61224585,0.60883911,0.60541564,0.60197515,0.59851735, +0.59504192,0.59154856,0.58803694,0.58450672,0.58095756,0.57738911,0.57380101,0.57019288, +0.56656433,0.56291496,0.55924437,0.55555212,0.55183778,0.54810089,0.54434099,0.54055758, +0.53675018,0.53291825,0.52906127,0.52517867,0.52126988,0.51733431,0.51337132,0.50938028, +0.50536051,0.50131132,0.49723200,0.49312177,0.48897987,0.48480547,0.48059772,0.47635573, +0.47207859,0.46776530,0.46341487,0.45902623,0.45459827,0.45012983,0.44561967,0.44106652, +0.43646903,0.43182577,0.42713525,0.42239588,0.41760600,0.41276385,0.40786755,0.40291513, +0.39790449,0.39283339,0.38769946,0.38250016,0.37723277,0.37189441,0.36648196,0.36099209, +0.35542120,0.34976542,0.34402054,0.33818204,0.33224495,0.32620390,0.32005298,0.31378574, +0.30739505,0.30087304,0.29421096,0.28739907,0.28042645,0.27328078,0.26594810,0.25841250, +0.25065566,0.24265636,0.23438976,0.22582651,0.21693146,0.20766198,0.19796546,0.18777575, +0.17700769,0.16554844,0.15324301,0.13986823,0.12508152,0.10830610,0.08841715,0.06251018, +}; + +double acos( double x ) { + int index; + + if (x < -1) + x = -1; + if (x > 1) + x = 1; + index = (float) (1.0 + x) * 511.9; + return acostable[index]; +} + + +double atan2( double y, double x ) { + float base; + float temp; + float dir; + float test; + int i; + + if ( x < 0 ) { + if ( y >= 0 ) { + // quad 1 + base = M_PI / 2; + temp = x; + x = y; + y = -temp; + } else { + // quad 2 + base = M_PI; + x = -x; + y = -y; + } + } else { + if ( y < 0 ) { + // quad 3 + base = 3 * M_PI / 2; + temp = x; + x = -y; + y = temp; + } + } + + if ( y > x ) { + base += M_PI/2; + temp = x; + x = y; + y = temp; + dir = -1; + } else { + dir = 1; + } + + // calcualte angle in octant 0 + if ( x == 0 ) { + return base; + } + y /= x; + + for ( i = 0 ; i < 512 ; i++ ) { + test = sintable[i] / sintable[1023-i]; + if ( test > y ) { + break; + } + } + + return base + dir * i * ( M_PI/2048); +} + + +#endif + +double tan( double x ) { + return sin(x) / cos(x); +} + + +static int randSeed = 0; + +void srand( unsigned seed ) { + randSeed = seed; +} + +int rand( void ) { + randSeed = (69069 * randSeed + 1); + return randSeed & 0x7fff; +} + +double atof( const char *string ) { + float sign; + float value; + int c; + + + // skip whitespace + while ( *string <= ' ' ) { + if ( !*string ) { + return 0; + } + string++; + } + + // check sign + switch ( *string ) { + case '+': + string++; + sign = 1; + break; + case '-': + string++; + sign = -1; + break; + default: + sign = 1; + break; + } + + // read digits + value = 0; + c = string[0]; + if ( c != '.' ) { + do { + c = *string++; + if ( c < '0' || c > '9' ) { + break; + } + c -= '0'; + value = value * 10 + c; + } while ( 1 ); + } else { + string++; + } + + // check for decimal point + if ( c == '.' ) { + double fraction; + + fraction = 0.1; + do { + c = *string++; + if ( c < '0' || c > '9' ) { + break; + } + c -= '0'; + value += c * fraction; + fraction *= 0.1; + } while ( 1 ); + + } + + // not handling 10e10 notation... + + return value * sign; +} + +double _atof( const char **stringPtr ) { + const char *string; + float sign; + float value; + int c; + + string = *stringPtr; + + // skip whitespace + while ( *string <= ' ' ) { + if ( !*string ) { + *stringPtr = string; + return 0; + } + string++; + } + + // check sign + switch ( *string ) { + case '+': + string++; + sign = 1; + break; + case '-': + string++; + sign = -1; + break; + default: + sign = 1; + break; + } + + // read digits + value = 0; + if ( string[0] != '.' ) { + do { + c = *string++; + if ( c < '0' || c > '9' ) { + break; + } + c -= '0'; + value = value * 10 + c; + } while ( 1 ); + } + + // check for decimal point + if ( c == '.' ) { + double fraction; + + fraction = 0.1; + do { + c = *string++; + if ( c < '0' || c > '9' ) { + break; + } + c -= '0'; + value += c * fraction; + fraction *= 0.1; + } while ( 1 ); + + } + + // not handling 10e10 notation... + *stringPtr = string; + + return value * sign; +} + + +#if !defined( _MSC_VER ) && !defined( __linux__ ) + +int atoi( const char *string ) { + int sign; + int value; + int c; + + + // skip whitespace + while ( *string <= ' ' ) { + if ( !*string ) { + return 0; + } + string++; + } + + // check sign + switch ( *string ) { + case '+': + string++; + sign = 1; + break; + case '-': + string++; + sign = -1; + break; + default: + sign = 1; + break; + } + + // read digits + value = 0; + do { + c = *string++; + if ( c < '0' || c > '9' ) { + break; + } + c -= '0'; + value = value * 10 + c; + } while ( 1 ); + + // not handling 10e10 notation... + + return value * sign; +} + + +int _atoi( const char **stringPtr ) { + int sign; + int value; + int c; + const char *string; + + string = *stringPtr; + + // skip whitespace + while ( *string <= ' ' ) { + if ( !*string ) { + return 0; + } + string++; + } + + // check sign + switch ( *string ) { + case '+': + string++; + sign = 1; + break; + case '-': + string++; + sign = -1; + break; + default: + sign = 1; + break; + } + + // read digits + value = 0; + do { + c = *string++; + if ( c < '0' || c > '9' ) { + break; + } + c -= '0'; + value = value * 10 + c; + } while ( 1 ); + + // not handling 10e10 notation... + + *stringPtr = string; + + return value * sign; +} + +int abs( int n ) { + return n < 0 ? -n : n; +} + +double fabs( double x ) { + return x < 0 ? -x : x; +} + + + +//========================================================= + + +#define ALT 0x00000001 /* alternate form */ +#define HEXPREFIX 0x00000002 /* add 0x or 0X prefix */ +#define LADJUST 0x00000004 /* left adjustment */ +#define LONGDBL 0x00000008 /* long double */ +#define LONGINT 0x00000010 /* long integer */ +#define QUADINT 0x00000020 /* quad integer */ +#define SHORTINT 0x00000040 /* short integer */ +#define ZEROPAD 0x00000080 /* zero (as opposed to blank) pad */ +#define FPT 0x00000100 /* floating point number */ + +#define to_digit(c) ((c) - '0') +#define is_digit(c) ((unsigned)to_digit(c) <= 9) +#define to_char(n) ((n) + '0') + +void AddInt( char **buf_p, int val, int width, int flags ) { + char text[32]; + int digits; + int signedVal; + char *buf; + + digits = 0; + signedVal = val; + if ( val < 0 ) { + val = -val; + } + do { + text[digits++] = '0' + val % 10; + val /= 10; + } while ( val ); + + if ( signedVal < 0 ) { + text[digits++] = '-'; + } + + buf = *buf_p; + + if( !( flags & LADJUST ) ) { + while ( digits < width ) { + *buf++ = ( flags & ZEROPAD ) ? '0' : ' '; + width--; + } + } + + while ( digits-- ) { + *buf++ = text[digits]; + width--; + } + + if( flags & LADJUST ) { + while ( width-- ) { + *buf++ = ( flags & ZEROPAD ) ? '0' : ' '; + } + } + + *buf_p = buf; +} + +void AddFloat( char **buf_p, float fval, int width, int prec ) { + char text[32]; + int digits; + float signedVal; + char *buf; + int val; + + // get the sign + signedVal = fval; + if ( fval < 0 ) { + fval = -fval; + } + + // write the float number + digits = 0; + val = (int)fval; + do { + text[digits++] = '0' + val % 10; + val /= 10; + } while ( val ); + + if ( signedVal < 0 ) { + text[digits++] = '-'; + } + + buf = *buf_p; + + while ( digits < width ) { + *buf++ = ' '; + width--; + } + + while ( digits-- ) { + *buf++ = text[digits]; + } + + *buf_p = buf; + + if (prec < 0) + prec = 6; + // write the fraction + digits = 0; + while (digits < prec) { + fval -= (int) fval; + fval *= 10.0; + val = (int) fval; + text[digits++] = '0' + val % 10; + } + + if (digits > 0) { + buf = *buf_p; + *buf++ = '.'; + for (prec = 0; prec < digits; prec++) { + *buf++ = text[prec]; + } + *buf_p = buf; + } +} + + +void AddString( char **buf_p, char *string, int width, int prec ) { + int size; + char *buf; + + buf = *buf_p; + + if ( string == NULL ) { + string = "(null)"; + prec = -1; + } + + if ( prec >= 0 ) { + for( size = 0; size < prec; size++ ) { + if( string[size] == '\0' ) { + break; + } + } + } + else { + size = strlen( string ); + } + + width -= size; + + while( size-- ) { + *buf++ = *string++; + } + + while( width-- > 0 ) { + *buf++ = ' '; + } + + *buf_p = buf; +} + +/* +vsprintf + +I'm not going to support a bunch of the more arcane stuff in here +just to keep it simpler. For example, the '*' and '$' are not +currently supported. I've tried to make it so that it will just +parse and ignore formats we don't support. +*/ +int vsprintf( char *buffer, const char *fmt, va_list argptr ) { + int *arg; + char *buf_p; + char ch; + int flags; + int width; + int prec; + int n; + char sign; + + buf_p = buffer; + arg = (int *)argptr; + + while( qtrue ) { + // run through the format string until we hit a '%' or '\0' + for ( ch = *fmt; (ch = *fmt) != '\0' && ch != '%'; fmt++ ) { + *buf_p++ = ch; + } + if ( ch == '\0' ) { + goto done; + } + + // skip over the '%' + fmt++; + + // reset formatting state + flags = 0; + width = 0; + prec = -1; + sign = '\0'; + +rflag: + ch = *fmt++; +reswitch: + switch( ch ) { + case '-': + flags |= LADJUST; + goto rflag; + case '.': + n = 0; + while( is_digit( ( ch = *fmt++ ) ) ) { + n = 10 * n + ( ch - '0' ); + } + prec = n < 0 ? -1 : n; + goto reswitch; + case '0': + flags |= ZEROPAD; + goto rflag; + case '1': + case '2': + case '3': + case '4': + case '5': + case '6': + case '7': + case '8': + case '9': + n = 0; + do { + n = 10 * n + ( ch - '0' ); + ch = *fmt++; + } while( is_digit( ch ) ); + width = n; + goto reswitch; + case 'c': + *buf_p++ = (char)*arg; + arg++; + break; + case 'd': + case 'i': + AddInt( &buf_p, *arg, width, flags ); + arg++; + break; + case 'f': + AddFloat( &buf_p, *(double *)arg, width, prec ); +#ifdef __LCC__ + arg += 1; // everything is 32 bit in my compiler +#else + arg += 2; +#endif + break; + case 's': + AddString( &buf_p, (char *)*arg, width, prec ); + arg++; + break; + case '%': + *buf_p++ = ch; + break; + default: + *buf_p++ = (char)*arg; + arg++; + break; + } + } + +done: + *buf_p = 0; + return buf_p - buffer; +} + + +/* this is really crappy */ +int sscanf( const char *buffer, const char *fmt, ... ) { + int cmd; + int **arg; + int count; + + arg = (int **)&fmt + 1; + count = 0; + + while ( *fmt ) { + if ( fmt[0] != '%' ) { + fmt++; + continue; + } + + cmd = fmt[1]; + fmt += 2; + + switch ( cmd ) { + case 'i': + case 'd': + case 'u': + **arg = _atoi( &buffer ); + break; + case 'f': + *(float *)*arg = _atof( &buffer ); + break; + } + arg++; + } + + return count; +} + +#endif diff --git a/src/game/bg_lib.h b/src/game/bg_lib.h new file mode 100644 index 00000000..306684b0 --- /dev/null +++ b/src/game/bg_lib.h @@ -0,0 +1,94 @@ +// bg_lib.h -- standard C library replacement routines used by code +// compiled for the virtual machine + +// This file is NOT included on native builds + +/* + * Portions Copyright (C) 2000-2001 Tim Angus + * + * This program 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, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ + +/* To assertain which portions are licensed under the GPL and which are + * licensed by Id Software, Inc. please run a diff between the equivalent + * versions of the "Tremulous" modification and the unmodified "Quake3" + * game source code. + */ + +typedef int size_t; + +typedef char * va_list; +#define _INTSIZEOF(n) ( (sizeof(n) + sizeof(int) - 1) & ~(sizeof(int) - 1) ) +#define va_start(ap,v) ( ap = (va_list)&v + _INTSIZEOF(v) ) +#define va_arg(ap,t) ( *(t *)((ap += _INTSIZEOF(t)) - _INTSIZEOF(t)) ) +#define va_end(ap) ( ap = (va_list)0 ) + +#define CHAR_BIT 8 /* number of bits in a char */ +#define SCHAR_MIN (-128) /* minimum signed char value */ +#define SCHAR_MAX 127 /* maximum signed char value */ +#define UCHAR_MAX 0xff /* maximum unsigned char value */ + +#define SHRT_MIN (-32768) /* minimum (signed) short value */ +#define SHRT_MAX 32767 /* maximum (signed) short value */ +#define USHRT_MAX 0xffff /* maximum unsigned short value */ +#define INT_MIN (-2147483647 - 1) /* minimum (signed) int value */ +#define INT_MAX 2147483647 /* maximum (signed) int value */ +#define UINT_MAX 0xffffffff /* maximum unsigned int value */ +#define LONG_MIN (-2147483647L - 1) /* minimum (signed) long value */ +#define LONG_MAX 2147483647L /* maximum (signed) long value */ +#define ULONG_MAX 0xffffffffUL /* maximum unsigned long value */ + +// Misc functions +typedef int cmp_t(const void *, const void *); +void qsort(void *a, size_t n, size_t es, cmp_t *cmp); +void srand( unsigned seed ); +int rand( void ); + +// String functions +size_t strlen( const char *string ); +char *strcat( char *strDestination, const char *strSource ); +char *strcpy( char *strDestination, const char *strSource ); +int strcmp( const char *string1, const char *string2 ); +char *strchr( const char *string, int c ); +char *strstr( const char *string, const char *strCharSet ); +char *strncpy( char *strDest, const char *strSource, size_t count ); +int tolower( int c ); +int toupper( int c ); + +double atof( const char *string ); +double _atof( const char **stringPtr ); +int atoi( const char *string ); +int _atoi( const char **stringPtr ); + +int vsprintf( char *buffer, const char *fmt, va_list argptr ); +int sscanf( const char *buffer, const char *fmt, ... ); + +// Memory functions +void *memmove( void *dest, const void *src, size_t count ); +void *memset( void *dest, int c, size_t count ); +void *memcpy( void *dest, const void *src, size_t count ); + +// Math functions +double ceil( double x ); +double floor( double x ); +double sqrt( double x ); +double sin( double x ); +double cos( double x ); +double atan2( double y, double x ); +double tan( double x ); +int abs( int n ); +double fabs( double x ); +double acos( double x ); + diff --git a/src/game/bg_local.h b/src/game/bg_local.h new file mode 100644 index 00000000..de2803ec --- /dev/null +++ b/src/game/bg_local.h @@ -0,0 +1,110 @@ +// Copyright (C) 1999-2000 Id Software, Inc. +// +// bg_local.h -- local definitions for the bg (both games) files + +/* + * Portions Copyright (C) 2000-2001 Tim Angus + * + * This program 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, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ + +/* To assertain which portions are licensed under the GPL and which are + * licensed by Id Software, Inc. please run a diff between the equivalent + * versions of the "Tremulous" modification and the unmodified "Quake3" + * game source code. + */ + +#define MIN_WALK_NORMAL 0.7f // can't walk on very steep slopes + +#define STEPSIZE 18 + +#define JUMP_VELOCITY 270 + +#define TIMER_LAND 130 +#define TIMER_GESTURE (34*66+50) + +#define OVERCLIP 1.001f + +// all of the locals will be zeroed before each +// pmove, just to make damn sure we don't have +// any differences when running on client or server +typedef struct { + vec3_t forward, right, up; + float frametime; + + int msec; + + qboolean walking; + qboolean groundPlane; + trace_t groundTrace; + + float impactSpeed; + + vec3_t previous_origin; + vec3_t previous_velocity; + int previous_waterlevel; +} pml_t; + +//TA: struct that stores smooth ops +// there is an array of these in bg_pmove.c +typedef struct +{ + float time; + + vec3_t rotAxis; + float rotAngle; +} smooth_t; + +//TA: make this into a cvar later.... +#define SMOOTHTIME 300 +#define MAXSMOOTHS 10 + +//TA: wall climbing local +typedef struct +{ + smooth_t sList[ MAXSMOOTHS ]; + + qboolean justFallen; + vec3_t lastNormal; + vec3_t nonSvangles; +} wcl_t; + +extern pmove_t *pm; +extern pml_t pml; + +// movement parameters +extern float pm_stopspeed; +extern float pm_duckScale; +extern float pm_swimScale; +extern float pm_wadeScale; + +extern float pm_accelerate; +extern float pm_airaccelerate; +extern float pm_wateraccelerate; +extern float pm_flyaccelerate; + +extern float pm_friction; +extern float pm_waterfriction; +extern float pm_flightfriction; + +extern int c_pmove; + +void PM_ClipVelocity( vec3_t in, vec3_t normal, vec3_t out, float overbounce ); +void PM_AddTouchEnt( int entityNum ); +void PM_AddEvent( int newEvent ); + +qboolean PM_SlideMove( qboolean gravity ); +void PM_StepSlideMove( qboolean gravity ); + diff --git a/src/game/bg_misc.c b/src/game/bg_misc.c new file mode 100644 index 00000000..130ff0d8 --- /dev/null +++ b/src/game/bg_misc.c @@ -0,0 +1,1709 @@ +// Copyright (C) 1999-2000 Id Software, Inc. +// +// bg_misc.c -- both games misc functions, all completely stateless + +/* + * Portions Copyright (C) 2000-2001 Tim Angus + * + * This program 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, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ + +/* To assertain which portions are licensed under the GPL and which are + * licensed by Id Software, Inc. please run a diff between the equivalent + * versions of the "Tremulous" modification and the unmodified "Quake3" + * game source code. + */ + +#include "q_shared.h" +#include "bg_public.h" + +/*QUAKED item_***** ( 0 0 0 ) (-16 -16 -16) (16 16 16) suspended +DO NOT USE THIS CLASS, IT JUST HOLDS GENERAL INFORMATION. +The suspended flag will allow items to hang in the air, otherwise they are dropped to the next surface. + +If an item is the target of another entity, it will not spawn in until fired. + +An item fires all of its targets when it is picked up. If the toucher can't carry it, the targets won't be fired. + +"notfree" if set to 1, don't spawn in free for all games +"notteam" if set to 1, don't spawn in team games +"notsingle" if set to 1, don't spawn in single player games +"wait" override the default wait before respawning. -1 = never respawn automatically, which can be used with targeted spawning. +"random" random number of plus or minus seconds varied from the respawn time +"count" override quantity or duration on most items. +*/ + +gitem_t bg_itemlist[] = +{ + { + NULL, + NULL, + { NULL, + NULL, + 0, 0} , +/* icon */ NULL, +/* pickup */ NULL, + 0, + 0, + 0, +/* precache */ "", +/* sounds */ "" + }, // leave index 0 alone + + // + // ARMOR + // + +/*QUAKED item_armor_shard (.3 .3 1) (-16 -16 -16) (16 16 16) suspended +*/ + /*{ + "item_armor_shard", + "sound/misc/ar1_pkup.wav", + { "models/powerups/armor/shard.md3", + "models/powerups/armor/shard_sphere.md3", + 0, 0} , + "icons/iconr_shard", + "Armor Shard", + 5, + IT_ARMOR, + 0, + "", + "" + },*/ + +/*QUAKED item_armor_combat (.3 .3 1) (-16 -16 -16) (16 16 16) suspended +*/ + /*{ + "item_armor_combat", + "sound/misc/ar2_pkup.wav", + { "models/powerups/armor/armor_yel.md3", + 0, 0, 0}, + "icons/iconr_yellow", + "Armor", + 50, + IT_ARMOR, + 0, + "", + "" + },*/ + +/*QUAKED item_armor_body (.3 .3 1) (-16 -16 -16) (16 16 16) suspended +*/ + /*{ + "item_armor_body", + "sound/misc/ar3_pkup.wav", + { "models/powerups/armor/armor_red.md3", + 0, 0, 0}, + "icons/iconr_red", + "Heavy Armor", + 100, + IT_ARMOR, + 0, + "", + "" + },*/ + + // + // health + // +/*QUAKED item_health_small (.3 .3 1) (-16 -16 -16) (16 16 16) suspended +*/ + /*{ + "item_health_small", + "sound/items/s_health.wav", + { "models/powerups/health/small_cross.md3", + "models/powerups/health/small_sphere.md3", + 0, 0 }, + "icons/iconh_green", + "5 Health", + 5, + IT_HEALTH, + 0, + "", + "" + },*/ + +/*QUAKED item_health (.3 .3 1) (-16 -16 -16) (16 16 16) suspended +*/ + /*{ + "item_health", + "sound/items/n_health.wav", + { "models/powerups/health/medium_cross.md3", + "models/powerups/health/medium_sphere.md3", + 0, 0 }, + "icons/iconh_yellow", + "25 Health", + 25, + IT_HEALTH, + 0, + "", + "" + },*/ + +/*QUAKED item_health_large (.3 .3 1) (-16 -16 -16) (16 16 16) suspended +*/ + /*{ + "item_health_large", + "sound/items/l_health.wav", + { "models/powerups/health/large_cross.md3", + "models/powerups/health/large_sphere.md3", + 0, 0 }, + "icons/iconh_red", + "50 Health", + 50, + IT_HEALTH, + 0, + "", + "" + },*/ + +/*QUAKED item_health_mega (.3 .3 1) (-16 -16 -16) (16 16 16) suspended +*/ + /*{ + "item_health_mega", + "sound/items/m_health.wav", + { "models/powerups/health/mega_cross.md3", + "models/powerups/health/mega_sphere.md3", + 0, 0 }, + "icons/iconh_mega", + "Mega Health", + 100, + IT_HEALTH, + 0, + "", + "" + },*/ + + + // + // WEAPONS + // + +/*QUAKED weapon_gauntlet (.3 .3 1) (-16 -16 -16) (16 16 16) suspended +*/ + +//TA:FIXME: must keep gauntlet and machinegun for now or bots have a fit and prevent game working + + { + "weapon_gauntlet", + "sound/misc/w_pkup.wav", + { "models/weapons2/gauntlet/gauntlet.md3", + 0, 0, 0}, + "icons/iconw_gauntlet", + "Gauntlet", + 0, + IT_WEAPON, + WP_GAUNTLET, + "", + "" + }, + +/*QUAKED weapon_venom (.3 .3 1) (-16 -16 -16) (16 16 16) suspended +*/ + { + "weapon_venom", + "sound/misc/w_pkup.wav", + { "models/weapons2/gauntlet/gauntlet.md3", + 0, 0, 0}, + "icons/iconw_gauntlet", + "Venom", + 0, + IT_WEAPON, + WP_VENOM, + "", + "" + }, + +/*QUAKED weapon_abuild (.3 .3 1) (-16 -16 -16) (16 16 16) suspended +*/ + { + "weapon_abuild", + "sound/misc/w_pkup.wav", + { "models/weapons2/gauntlet/gauntlet.md3", + 0, 0, 0}, + "icons/iconw_gauntlet", + "ABuild", + 0, + IT_WEAPON, + WP_ABUILD, + "", + "" + }, + +/*QUAKED weapon_hbuild (.3 .3 1) (-16 -16 -16) (16 16 16) suspended +*/ + { + "weapon_hbuild", + "sound/misc/w_pkup.wav", + { "models/weapons2/gauntlet/gauntlet.md3", + 0, 0, 0}, + "icons/iconw_gauntlet", + "HBuild", + 0, + IT_WEAPON, + WP_HBUILD, + "", + "" + }, + +/*QUAKED weapon_scanner (.3 .3 1) (-16 -16 -16) (16 16 16) suspended +*/ + { + "weapon_scanner", + "sound/misc/w_pkup.wav", + { "models/weapons2/shotgun/shotgun.md3", + 0, 0, 0}, + "icons/iconw_shotgun", + "Scanner", + 0, + IT_WEAPON, + WP_SCANNER, + "", + "" + }, + +/*QUAKED weapon_shotgun (.3 .3 1) (-16 -16 -16) (16 16 16) suspended +*/ + /*{ + "weapon_shotgun", + "sound/misc/w_pkup.wav", + { "models/weapons2/shotgun/shotgun.md3", + 0, 0, 0}, + "icons/iconw_shotgun", + "Shotgun", + 10, + IT_WEAPON, + WP_SHOTGUN, + "", + "" + },*/ + +/*QUAKED weapon_machinegun (.3 .3 1) (-16 -16 -16) (16 16 16) suspended +*/ + { + "weapon_machinegun", + "sound/misc/w_pkup.wav", + { "models/weapons2/machinegun/machinegun.md3", + 0, 0, 0}, + "icons/iconw_machinegun", + "Machinegun", + 40, + IT_WEAPON, + WP_MACHINEGUN, + "", + "" + }, + +/*QUAKED weapon_chaingun (.3 .3 1) (-16 -16 -16) (16 16 16) suspended +*/ + { + "weapon_chaingun", + "sound/misc/w_pkup.wav", + { "models/weapons2/machinegun/machinegun.md3", + 0, 0, 0}, + "icons/iconw_machinegun", + "Chaingun", + 40, + IT_WEAPON, + WP_CHAINGUN, + "", + "" + }, + +/*QUAKED weapon_ggrenade (.3 .3 1) (-16 -16 -16) (16 16 16) suspended +*/ + { + "weapon_ggrenade", + "sound/misc/w_pkup.wav", + { "models/weapons2/gauntlet/gauntlet.md3", + 0, 0, 0}, + "icons/iconw_gauntlet", + "Gas Grenade", + 0, + IT_WEAPON, + WP_GGRENADE, + "", + "" + }, + +/*QUAKED weapon_grenadelauncher (.3 .3 1) (-16 -16 -16) (16 16 16) suspended +*/ + /*{ + "weapon_grenadelauncher", + "sound/misc/w_pkup.wav", + { "models/weapons2/grenadel/grenadel.md3", + 0, 0, 0}, + "icons/iconw_grenade", + "Grenade Launcher", + 10, + IT_WEAPON, + WP_GRENADE_LAUNCHER, + "", + "sound/weapons/grenade/hgrenb1a.wav sound/weapons/grenade/hgrenb2a.wav" + },*/ + +/*QUAKED weapon_rocketlauncher (.3 .3 1) (-16 -16 -16) (16 16 16) suspended +*/ + /*{ + "weapon_rocketlauncher", + "sound/misc/w_pkup.wav", + { "models/weapons2/rocketl/rocketl.md3", + 0, 0, 0}, + "icons/iconw_rocket", + "Rocket Launcher", + 10, + IT_WEAPON, + WP_ROCKET_LAUNCHER, + "", + "" + },*/ + +/*QUAKED weapon_lightning (.3 .3 1) (-16 -16 -16) (16 16 16) suspended +*/ + /*{ + "weapon_lightning", + "sound/misc/w_pkup.wav", + { "models/weapons2/lightning/lightning.md3", + 0, 0, 0}, + "icons/iconw_lightning", + "Lightning Gun", + 100, + IT_WEAPON, + WP_LIGHTNING, + "", + "" + },*/ + +/*QUAKED weapon_railgun (.3 .3 1) (-16 -16 -16) (16 16 16) suspended +*/ + /*{ + "weapon_railgun", + "sound/misc/w_pkup.wav", + { "models/weapons2/railgun/railgun.md3", + 0, 0, 0}, + "icons/iconw_railgun", + "Railgun", + 10, + IT_WEAPON, + WP_RAILGUN, + "", + "" + },*/ + +/*QUAKED weapon_plasmagun (.3 .3 1) (-16 -16 -16) (16 16 16) suspended +*/ + { + "weapon_plasmagun", + "sound/misc/w_pkup.wav", + { "models/weapons2/plasma/plasma.md3", + 0, 0, 0}, + "icons/iconw_plasma", + "Plasma Gun", + 50, + IT_WEAPON, + WP_PLASMAGUN, + "", + "" + }, + +/*QUAKED weapon_flamer (.3 .3 1) (-16 -16 -16) (16 16 16) suspended +*/ + { + "weapon_flamer", + "sound/misc/w_pkup.wav", + { "models/weapons2/plasma/plasma.md3", + 0, 0, 0}, + "icons/iconw_plasma", + "Flame Thrower", + 50, + IT_WEAPON, + WP_FLAMER, + "", + "" + }, + +/*QUAKED weapon_bfg (.3 .3 1) (-16 -16 -16) (16 16 16) suspended +*/ + { + "weapon_bfg", + "sound/misc/w_pkup.wav", + { "models/weapons2/bfg/bfg.md3", + 0, 0, 0}, + "icons/iconw_bfg", + "Dual BFG", + 0, + IT_WEAPON, + WP_BFG, + "", + "" + }, + +/*QUAKED weapon_grapplinghook (.3 .3 1) (-16 -16 -16) (16 16 16) suspended +*/ + /*{ + "weapon_grapplinghook", + "sound/misc/w_pkup.wav", + { "models/weapons2/grapple/grapple.md3", + 0, 0, 0}, + "icons/iconw_grapple", + "Grappling Hook", + 0, + IT_WEAPON, + WP_GRAPPLING_HOOK, + "", + "" + },*/ + + // + // AMMO ITEMS + // + +/*QUAKED ammo_shells (.3 .3 1) (-16 -16 -16) (16 16 16) suspended +*/ + /*{ + "ammo_shells", + "sound/misc/am_pkup.wav", + { "models/powerups/ammo/shotgunam.md3", + 0, 0, 0}, + "icons/icona_shotgun", + "Shells", + 10, + IT_AMMO, + WP_SHOTGUN, + "", + "" + },*/ + +/*QUAKED ammo_bullets (.3 .3 1) (-16 -16 -16) (16 16 16) suspended +*/ + /*{ + "ammo_bullets", + "sound/misc/am_pkup.wav", + { "models/powerups/ammo/machinegunam.md3", + 0, 0, 0}, + "icons/icona_machinegun", + "Bullets", + 50, + IT_AMMO, + WP_MACHINEGUN, + "", + "" + },*/ + +/*QUAKED ammo_grenades (.3 .3 1) (-16 -16 -16) (16 16 16) suspended +*/ + /*{ + "ammo_grenades", + "sound/misc/am_pkup.wav", + { "models/powerups/ammo/grenadeam.md3", + 0, 0, 0}, + "icons/icona_grenade", + "Grenades", + 5, + IT_AMMO, + WP_GRENADE_LAUNCHER, + "", + "" + },*/ + +/*QUAKED ammo_cells (.3 .3 1) (-16 -16 -16) (16 16 16) suspended +*/ + /*{ + "ammo_cells", + "sound/misc/am_pkup.wav", + { "models/powerups/ammo/plasmaam.md3", + 0, 0, 0}, + "icons/icona_plasma", + "Cells", + 30, + IT_AMMO, + WP_PLASMAGUN, + "", + "" + },*/ + +/*QUAKED ammo_lightning (.3 .3 1) (-16 -16 -16) (16 16 16) suspended +*/ + /*{ + "ammo_lightning", + "sound/misc/am_pkup.wav", + { "models/powerups/ammo/lightningam.md3", + 0, 0, 0}, + "icons/icona_lightning", + "Lightning", + 60, + IT_AMMO, + WP_LIGHTNING, + "", + "" + },*/ + +/*QUAKED ammo_rockets (.3 .3 1) (-16 -16 -16) (16 16 16) suspended +*/ + /*{ + "ammo_rockets", + "sound/misc/am_pkup.wav", + { "models/powerups/ammo/rocketam.md3", + 0, 0, 0}, + "icons/icona_rocket", + "Rockets", + 5, + IT_AMMO, + WP_ROCKET_LAUNCHER, + "", + "" + },*/ + +/*QUAKED ammo_slugs (.3 .3 1) (-16 -16 -16) (16 16 16) suspended +*/ + /*{ + "ammo_slugs", + "sound/misc/am_pkup.wav", + { "models/powerups/ammo/railgunam.md3", + 0, 0, 0}, + "icons/icona_railgun", + "Slugs", + 10, + IT_AMMO, + WP_RAILGUN, + "", + "" + },*/ + +/*QUAKED ammo_bfg (.3 .3 1) (-16 -16 -16) (16 16 16) suspended +*/ + /*{ + "ammo_bfg", + "sound/misc/am_pkup.wav", + { "models/powerups/ammo/bfgam.md3", + 0, 0, 0}, + "icons/icona_bfg", + "Bfg Ammo", + 15, + IT_AMMO, + WP_BFG, + "", + "" + },*/ + + // + // HOLDABLE ITEMS + // +/*QUAKED holdable_teleporter (.3 .3 1) (-16 -16 -16) (16 16 16) suspended +*/ + /*{ + "holdable_teleporter", + "sound/items/holdable.wav", + { "models/powerups/holdable/teleporter.md3", + 0, 0, 0}, + "icons/teleporter", + "Personal Teleporter", + 60, + IT_HOLDABLE, + HI_TELEPORTER, + "", + "" + },*/ + +/*QUAKED holdable_medkit (.3 .3 1) (-16 -16 -16) (16 16 16) suspended +*/ + /*{ + "holdable_medkit", + "sound/items/holdable.wav", + { "models/powerups/holdable/medkit.md3", + "models/powerups/holdable/medkit_sphere.md3", + 0, 0}, + "icons/medkit", + "Medkit", + 60, + IT_HOLDABLE, + HI_MEDKIT, + "", + "sound/items/use_medkit.wav" + },*/ + + // + // POWERUP ITEMS + // +/*QUAKED item_quad (.3 .3 1) (-16 -16 -16) (16 16 16) suspended +*/ + /*{ + "item_quad", + "sound/items/quaddamage.wav", + { "models/powerups/instant/quad.md3", + "models/powerups/instant/quad_ring.md3", + 0, 0 }, + "icons/quad", + "Quad Damage", + 30, + IT_POWERUP, + PW_QUAD, + "", + "sound/items/damage2.wav sound/items/damage3.wav" + },*/ + +/*QUAKED item_enviro (.3 .3 1) (-16 -16 -16) (16 16 16) suspended +*/ + /*{ + "item_enviro", + "sound/items/protect.wav", + { "models/powerups/instant/enviro.md3", + "models/powerups/instant/enviro_ring.md3", + 0, 0 }, + "icons/envirosuit", + "Battle Suit", + 30, + IT_POWERUP, + PW_BATTLESUIT, + "", + "sound/items/airout.wav sound/items/protect3.wav" + },*/ + +/*QUAKED item_haste (.3 .3 1) (-16 -16 -16) (16 16 16) suspended +*/ + /*{ + "item_haste", + "sound/items/haste.wav", + { "models/powerups/instant/haste.md3", + "models/powerups/instant/haste_ring.md3", + 0, 0 }, + "icons/haste", + "Speed", + 30, + IT_POWERUP, + PW_HASTE, + "", + "" + },*/ + +/*QUAKED item_invis (.3 .3 1) (-16 -16 -16) (16 16 16) suspended +*/ + /*{ + "item_invis", + "sound/items/invisibility.wav", + { "models/powerups/instant/invis.md3", + "models/powerups/instant/invis_ring.md3", + 0, 0 }, + "icons/invis", + "Invisibility", + 30, + IT_POWERUP, + PW_INVIS, + "", + "" + },*/ + +/*QUAKED item_regen (.3 .3 1) (-16 -16 -16) (16 16 16) suspended +*/ + /*{ + "item_regen", + "sound/items/regeneration.wav", + { "models/powerups/instant/regen.md3", + "models/powerups/instant/regen_ring.md3", + 0, 0 }, + "icons/regen", + "Regeneration", + 30, + IT_POWERUP, + PW_REGEN, + "", + "sound/items/regen.wav" + },*/ + +/*QUAKED item_flight (.3 .3 1) (-16 -16 -16) (16 16 16) suspended +*/ + /*{ + "item_flight", + "sound/items/flight.wav", + { "models/powerups/instant/flight.md3", + "models/powerups/instant/flight_ring.md3", + 0, 0 }, + "icons/flight", + "Flight", + 60, + IT_POWERUP, + PW_FLIGHT, + "", + "sound/items/flight.wav" + },*/ + +/*QUAKED team_droid_spawn (0 0 1) (-16 -16 -16) (16 16 16) +TA: droid spawn item +*/ + { + "team_droid_spawn", + "sound/items/holdable.wav", + { "models/bitems/aspawn.md3", 0, 0, 0 }, + "icons/teleporter", //icon + "Droid Spawn", //pickup + 0, + IT_BUILDABLE, + BA_A_SPAWN, + "", //precache + "" //sounds + }, + +/*QUAKED team_droid_def1 (0 0 1) (-16 -16 -16) (16 16 16) +TA: droid defense item +*/ + { + "team_droid_def1", + "sound/items/holdable.wav", + { "models/bitems/adef1.md3", 0, 0, 0 }, + "icons/teleporter", //icon + "Droid Defense", //pickup + 0, + IT_BUILDABLE, + BA_A_DEF1, + "", //precache + "" //sounds + }, + +/*QUAKED team_human_spawn (0 0 1) (-16 -16 -16) (16 16 16) +TA: human spawn item +*/ + { + "team_human_spawn", + "sound/items/holdable.wav", + { "models/bitems/hspawn.md3", 0, 0, 0 }, + "icons/teleporter", //icon + "Human Spawn", //pickup + 0, + IT_BUILDABLE, + BA_H_SPAWN, + "", //precache + "" //sounds + }, + +/*QUAKED team_human_def1 (0 0 1) (-16 -16 -16) (16 16 16) +TA: human defense item +*/ + { + "team_human_def1", + "sound/items/holdable.wav", + { "models/bitems/turret-base.md3", "models/bitems/turret-top.md3", 0, 0 }, + "icons/teleporter", //icon + "Human Defense", //pickup + 0, + IT_BUILDABLE, + BA_H_DEF1, + "", //precache + "" //sounds + }, + +/*QUAKED team_human_mcu (0 0 1) (-16 -16 -16) (16 16 16) +TA: human defense item +*/ + { + "team_human_mcu", + "sound/items/holdable.wav", + { "models/bitems/adef1.md3", 0, 0, 0 }, + "icons/teleporter", //icon + "Human MCU", //pickup + 0, + IT_BUILDABLE, + BA_H_MCU, + "", //precache + "" //sounds + }, + +/*QUAKED upgrade_torch (0 0 1) (-16 -16 -16) (16 16 16) +*/ + { + "upgrade_torch", + "sound/items/holdable.wav", + { 0, 0, 0, 0 }, + "icons/teleporter", //icon + "Torch", //pickup + 0, + IT_UPGRADE, + UP_TORCH, + "", //precache + "" //sounds + }, + +/*QUAKED upgrade_nvg (0 0 1) (-16 -16 -16) (16 16 16) +*/ + { + "upgrade_nvg", + "sound/items/holdable.wav", + { 0, 0, 0, 0 }, + "icons/teleporter", //icon + "NVG", //pickup + 0, + IT_UPGRADE, + UP_NVG, + "", //precache + "" //sounds + }, + +/*QUAKED team_CTF_redflag (1 0 0) (-16 -16 -16) (16 16 16) +Only in CTF games +*/ + /*{ + "team_CTF_redflag", + "sound/teamplay/flagtk_red.wav", + { "models/flags/r_flag.md3", + 0, 0, 0 }, + "icons/iconf_red1", + "Red Flag", + 0, + IT_TEAM, + PW_REDFLAG, + "", + "sound/teamplay/flagcap_red.wav sound/teamplay/flagtk_red.wav sound/teamplay/flagret_red.wav" + },*/ + +/*QUAKED team_CTF_blueflag (0 0 1) (-16 -16 -16) (16 16 16) +Only in CTF games +*/ + /*{ + "team_CTF_blueflag", + "sound/teamplay/flagtk_blu.wav", + { "models/flags/b_flag.md3", + 0, 0, 0 }, + "icons/iconf_blu1", + "Blue Flag", + 0, + IT_TEAM, + PW_BLUEFLAG, + "", + "sound/teamplay/flagcap_blu.wav sound/teamplay/flagtk_blu.wav sound/teamplay/flagret_blu.wav" + },*/ + + // end of list marker + {NULL} +}; + +int bg_numItems = sizeof(bg_itemlist) / sizeof(bg_itemlist[0]) - 1; + + +/* +============== +BG_FindItemForPowerup +============== +*/ +gitem_t *BG_FindItemForPowerup( powerup_t pw ) { + int i; + + for ( i = 0 ; i < bg_numItems ; i++ ) { + if ( (bg_itemlist[i].giType == IT_POWERUP /*|| + bg_itemlist[i].giType == IT_TEAM || + bg_itemlist[i].giType == IT_PERSISTANT_POWERUP*/) && + bg_itemlist[i].giTag == pw ) { + return &bg_itemlist[i]; + } + } + + return NULL; +} + + +/* +============== +BG_FindItemForHoldable +============== +*/ +gitem_t *BG_FindItemForHoldable( holdable_t pw ) { + int i; + + for ( i = 0 ; i < bg_numItems ; i++ ) { + if ( bg_itemlist[i].giType == IT_HOLDABLE && bg_itemlist[i].giTag == pw ) { + return &bg_itemlist[i]; + } + } + + Com_Error( ERR_DROP, "HoldableItem not found" ); + + return NULL; +} + + +/* +=============== +BG_FindItemForWeapon + +=============== +*/ +gitem_t *BG_FindItemForWeapon( weapon_t weapon ) { + gitem_t *it; + + for ( it = bg_itemlist + 1 ; it->classname ; it++) { + if ( it->giType == IT_WEAPON && it->giTag == weapon ) { + return it; + } + } + + Com_Error( ERR_DROP, "Couldn't find item for weapon %i", weapon); + return NULL; +} + + +/* +=============== +BG_FindItemForBuildable + +TA: new function for finding buildable items +=============== +*/ +gitem_t *BG_FindItemForBuildable( buildable_t buildable ) { + gitem_t *it; + + for ( it = bg_itemlist + 1 ; it->classname ; it++) { + if ( it->giType == IT_BUILDABLE && it->giTag == buildable ) { + return it; + } + } + + Com_Error( ERR_DROP, "Couldn't find item for buildable %i", buildable); + return NULL; +} + + +/* +=============== +BG_FindItemForUpgrade + +TA: new function for finding upgrade items +=============== +*/ +gitem_t *BG_FindItemForUpgrade( upgrade_t upgrade ) { + gitem_t *it; + + for ( it = bg_itemlist + 1 ; it->classname ; it++) { + if ( it->giType == IT_UPGRADE && it->giTag == upgrade ) { + return it; + } + } + + Com_Error( ERR_DROP, "Couldn't find item for upgrade %i", upgrade); + return NULL; +} + + +/* +=============== +BG_FindItem + +=============== +*/ +gitem_t *BG_FindItem( const char *pickupName ) { + gitem_t *it; + + for ( it = bg_itemlist + 1 ; it->classname ; it++ ) { + if ( !Q_stricmp( it->pickup_name, pickupName ) ) + return it; + } + + return NULL; +} + +/* +============ +BG_PlayerTouchesItem + +Items can be picked up without actually touching their physical bounds to make +grabbing them easier +============ +*/ +qboolean BG_PlayerTouchesItem( playerState_t *ps, entityState_t *item, int atTime ) { + vec3_t origin; + + BG_EvaluateTrajectory( &item->pos, atTime, origin ); + + // we are ignoring ducked differences here + if ( ps->origin[0] - origin[0] > 44 + || ps->origin[0] - origin[0] < -50 + || ps->origin[1] - origin[1] > 36 + || ps->origin[1] - origin[1] < -36 + || ps->origin[2] - origin[2] > 36 + || ps->origin[2] - origin[2] < -36 ) { + return qfalse; + } + + return qtrue; +} + +/* +================ +BG_CanItemBeGrabbed + +Returns false if the item should not be picked up. +This needs to be the same for client side prediction and server use. +================ +*/ +qboolean BG_CanItemBeGrabbed( int gametype, const entityState_t *ent, const playerState_t *ps ) { + gitem_t *item; + + if ( ent->modelindex < 1 || ent->modelindex >= bg_numItems ) { + Com_Error( ERR_DROP, "BG_CanItemBeGrabbed: index out of range" ); + } + + item = &bg_itemlist[ent->modelindex]; + + switch( item->giType ) { + case IT_WEAPON: + return qtrue; // weapons are always picked up + + case IT_AMMO: + if ( ps->ammo[ item->giTag ] >= 200 ) { + return qfalse; // can't hold any more + } + return qtrue; + + case IT_ARMOR: + if ( ps->stats[STAT_ARMOR] >= ps->stats[STAT_MAX_HEALTH] * 2 ) { + return qfalse; + } + return qtrue; + + case IT_HEALTH: + // small and mega healths will go over the max, otherwise + // don't pick up if already at max + if ( item->quantity == 5 || item->quantity == 100 ) { + if ( ps->stats[STAT_HEALTH] >= ps->stats[STAT_MAX_HEALTH] * 2 ) { + return qfalse; + } + return qtrue; + } + + if ( ps->stats[STAT_HEALTH] >= ps->stats[STAT_MAX_HEALTH] ) { + return qfalse; + } + return qtrue; + + case IT_POWERUP: + return qtrue; // powerups are always picked up + + case IT_TEAM: // team items, such as flags + // ent->modelindex2 is non-zero on items if they are dropped + // we need to know this because we can pick up our dropped flag (and return it) + // but we can't pick up our flag at base + if (ps->persistant[PERS_TEAM] == TEAM_HUMANS) { + //TA: remove powerups + /*if (item->giTag == PW_BLUEFLAG || + (item->giTag == PW_REDFLAG && ent->modelindex2) || + (item->giTag == PW_REDFLAG && ps->powerups[PW_BLUEFLAG])) + return qtrue;*/ + } else if (ps->persistant[PERS_TEAM] == TEAM_DROIDS) { + /*if (item->giTag == PW_REDFLAG || + (item->giTag == PW_BLUEFLAG && ent->modelindex2) || + (item->giTag == PW_BLUEFLAG && ps->powerups[PW_REDFLAG])) + return qtrue;*/ + } + return qfalse; + + //TA: not using the q3 holdable items code + /*case IT_HOLDABLE: + // can only hold one item at a time + if ( ps->stats[STAT_HOLDABLE_ITEM] ) { + return qfalse; + } + return qtrue;*/ + + case IT_BAD: + Com_Error( ERR_DROP, "BG_CanItemBeGrabbed: IT_BAD" ); + } + + return qfalse; +} + +//====================================================================== + +/* +================ +BG_EvaluateTrajectory + +================ +*/ +void BG_EvaluateTrajectory( const trajectory_t *tr, int atTime, vec3_t result ) { + float deltaTime; + float phase; + + switch( tr->trType ) { + case TR_STATIONARY: + case TR_INTERPOLATE: + VectorCopy( tr->trBase, result ); + break; + case TR_LINEAR: + deltaTime = ( atTime - tr->trTime ) * 0.001; // milliseconds to seconds + VectorMA( tr->trBase, deltaTime, tr->trDelta, result ); + break; + case TR_SINE: + deltaTime = ( atTime - tr->trTime ) / (float) tr->trDuration; + phase = sin( deltaTime * M_PI * 2 ); + VectorMA( tr->trBase, phase, tr->trDelta, result ); + break; + case TR_LINEAR_STOP: + if ( atTime > tr->trTime + tr->trDuration ) { + atTime = tr->trTime + tr->trDuration; + } + deltaTime = ( atTime - tr->trTime ) * 0.001; // milliseconds to seconds + if ( deltaTime < 0 ) { + deltaTime = 0; + } + VectorMA( tr->trBase, deltaTime, tr->trDelta, result ); + break; + case TR_GRAVITY: + deltaTime = ( atTime - tr->trTime ) * 0.001; // milliseconds to seconds + VectorMA( tr->trBase, deltaTime, tr->trDelta, result ); + result[2] -= 0.5 * DEFAULT_GRAVITY * deltaTime * deltaTime; // FIXME: local gravity... + break; + default: + Com_Error( ERR_DROP, "BG_EvaluateTrajectory: unknown trType: %i", tr->trTime ); + break; + } +} + +/* +================ +BG_EvaluateTrajectoryDelta + +For determining velocity at a given time +================ +*/ +void BG_EvaluateTrajectoryDelta( const trajectory_t *tr, int atTime, vec3_t result ) { + float deltaTime; + float phase; + + switch( tr->trType ) { + case TR_STATIONARY: + case TR_INTERPOLATE: + VectorClear( result ); + break; + case TR_LINEAR: + VectorCopy( tr->trDelta, result ); + break; + case TR_SINE: + deltaTime = ( atTime - tr->trTime ) / (float) tr->trDuration; + phase = cos( deltaTime * M_PI * 2 ); // derivative of sin = cos + phase *= 0.5; + VectorScale( tr->trDelta, phase, result ); + break; + case TR_LINEAR_STOP: + if ( atTime > tr->trTime + tr->trDuration ) { + VectorClear( result ); + return; + } + VectorCopy( tr->trDelta, result ); + break; + case TR_GRAVITY: + deltaTime = ( atTime - tr->trTime ) * 0.001; // milliseconds to seconds + VectorCopy( tr->trDelta, result ); + result[2] -= DEFAULT_GRAVITY * deltaTime; // FIXME: local gravity... + break; + default: + Com_Error( ERR_DROP, "BG_EvaluateTrajectoryDelta: unknown trType: %i", tr->trTime ); + break; + } +} + +char *eventnames[] = { + "EV_NONE", + + "EV_FOOTSTEP", + "EV_FOOTSTEP_METAL", + "EV_FOOTSPLASH", + "EV_FOOTWADE", + "EV_SWIM", + + "EV_STEP_4", + "EV_STEP_8", + "EV_STEP_12", + "EV_STEP_16", + + "EV_FALL_SHORT", + "EV_FALL_MEDIUM", + "EV_FALL_FAR", + + "EV_JUMP_PAD", // boing sound at origin", jump sound on player + + "EV_JUMP", + "EV_WATER_TOUCH", // foot touches + "EV_WATER_LEAVE", // foot leaves + "EV_WATER_UNDER", // head touches + "EV_WATER_CLEAR", // head leaves + + "EV_ITEM_PICKUP", // normal item pickups are predictable + "EV_GLOBAL_ITEM_PICKUP", // powerup / team sounds are broadcast to everyone + + "EV_NOAMMO", + "EV_CHANGE_WEAPON", + "EV_FIRE_WEAPON", + + "EV_USE_ITEM0", + "EV_USE_ITEM1", + "EV_USE_ITEM2", + "EV_USE_ITEM3", + "EV_USE_ITEM4", + "EV_USE_ITEM5", + "EV_USE_ITEM6", + "EV_USE_ITEM7", + "EV_USE_ITEM8", + "EV_USE_ITEM9", + "EV_USE_ITEM10", + "EV_USE_ITEM11", + "EV_USE_ITEM12", + "EV_USE_ITEM13", + "EV_USE_ITEM14", + "EV_USE_ITEM15", + + "EV_ITEM_RESPAWN", + "EV_ITEM_POP", + "EV_PLAYER_TELEPORT_IN", + "EV_PLAYER_TELEPORT_OUT", + + "EV_GRENADE_BOUNCE", // eventParm will be the soundindex + + "EV_GENERAL_SOUND", + "EV_GLOBAL_SOUND", // no attenuation + "EV_GLOBAL_TEAM_SOUND", + + "EV_BULLET_HIT_FLESH", + "EV_BULLET_HIT_WALL", + + "EV_MISSILE_HIT", + "EV_MISSILE_MISS", + "EV_MISSILE_MISS_METAL", + "EV_RAILTRAIL", + "EV_SHOTGUN", + "EV_BULLET", // otherEntity is the shooter + + "EV_PAIN", + "EV_DEATH1", + "EV_DEATH2", + "EV_DEATH3", + "EV_OBITUARY", + + "EV_POWERUP_QUAD", + "EV_POWERUP_BATTLESUIT", + "EV_POWERUP_REGEN", + + "EV_GIB_PLAYER", // gib a previously living player + "EV_SCOREPLUM", // score plum + +//#ifdef MISSIONPACK + "EV_PROXIMITY_MINE_STICK", + "EV_PROXIMITY_MINE_TRIGGER", + "EV_KAMIKAZE", // kamikaze explodes + "EV_OBELISKEXPLODE", // obelisk explodes + "EV_INVUL_IMPACT", // invulnerability sphere impact + "EV_JUICED", // invulnerability juiced effect + "EV_LIGHTNINGBOLT", // lightning bolt bounced of invulnerability sphere +//#endif + + "EV_DEBUG_LINE", + "EV_STOPLOOPINGSOUND", + "EV_TAUNT" + +}; + +/* +=============== +BG_AddPredictableEventToPlayerstate + +Handles the sequence numbers +=============== +*/ + +void trap_Cvar_VariableStringBuffer( const char *var_name, char *buffer, int bufsize ); + +void BG_AddPredictableEventToPlayerstate( int newEvent, int eventParm, playerState_t *ps ) { +#ifdef _DEBUG + { + char buf[256]; + trap_Cvar_VariableStringBuffer("showevents", buf, sizeof(buf)); + if ( atof(buf) != 0 ) { +#ifdef QAGAME + Com_Printf(" game event svt %5d -> %5d: num = %20s parm %d\n", ps->pmove_framecount/*ps->commandTime*/, ps->eventSequence, eventnames[newEvent], eventParm); +#else + Com_Printf("Cgame event svt %5d -> %5d: num = %20s parm %d\n", ps->pmove_framecount/*ps->commandTime*/, ps->eventSequence, eventnames[newEvent], eventParm); +#endif + } + } +#endif + ps->events[ps->eventSequence & (MAX_PS_EVENTS-1)] = newEvent; + ps->eventParms[ps->eventSequence & (MAX_PS_EVENTS-1)] = eventParm; + ps->eventSequence++; +} + + +/* +======================== +BG_TouchJumpPad +======================== +*/ +void BG_TouchJumpPad( playerState_t *ps, entityState_t *jumppad ) { + vec3_t angles; + float p; + int effectNum; + + // spectators don't use jump pads + if ( ps->pm_type != PM_NORMAL ) { + return; + } + + // flying characters don't hit bounce pads + if ( ps->powerups[PW_FLIGHT] ) { + return; + } + + // if we didn't hit this same jumppad the previous frame + // then don't play the event sound again if we are in a fat trigger + if ( ps->jumppad_ent != jumppad->number ) { + + vectoangles( jumppad->origin2, angles); + p = fabs( AngleNormalize180( angles[PITCH] ) ); + if( p < 45 ) { + effectNum = 0; + } else { + effectNum = 1; + } + BG_AddPredictableEventToPlayerstate( EV_JUMP_PAD, effectNum, ps ); + } + // remember hitting this jumppad this frame + ps->jumppad_ent = jumppad->number; + ps->jumppad_frame = ps->pmove_framecount; + // give the player the velocity from the jumppad + VectorCopy( jumppad->origin2, ps->velocity ); +} + + +/* +======================== +BG_PlayerStateToEntityState + +This is done after each set of usercmd_t on the server, +and after local prediction on the client +======================== +*/ +void BG_PlayerStateToEntityState( playerState_t *ps, entityState_t *s, qboolean snap ) { + int i; + vec3_t ceilingNormal = { 0, 0, -1 }; + + if ( ps->pm_type == PM_INTERMISSION || ps->pm_type == PM_SPECTATOR ) { + s->eType = ET_INVISIBLE; + } else if ( ps->stats[STAT_HEALTH] <= GIB_HEALTH ) { + s->eType = ET_INVISIBLE; + } else { + s->eType = ET_PLAYER; + } + + s->number = ps->clientNum; + + s->pos.trType = TR_INTERPOLATE; + VectorCopy( ps->origin, s->pos.trBase ); + if ( snap ) { + SnapVector( s->pos.trBase ); + } + //set the trDelta for flag direction + VectorCopy( ps->velocity, s->pos.trDelta ); + + s->apos.trType = TR_INTERPOLATE; + VectorCopy( ps->viewangles, s->apos.trBase ); + if ( snap ) { + SnapVector( s->apos.trBase ); + } + + //TA: i need for other things :) + //s->angles2[YAW] = ps->movementDir; + s->time2 = ps->movementDir; + s->legsAnim = ps->legsAnim; + s->torsoAnim = ps->torsoAnim; + s->clientNum = ps->clientNum; // ET_PLAYER looks here instead of at number + // so corpses can also reference the proper config + s->eFlags = ps->eFlags; + if ( ps->stats[STAT_HEALTH] <= 0 ) { + s->eFlags |= EF_DEAD; + } else { + s->eFlags &= ~EF_DEAD; + } + + if ( ps->externalEvent ) { + s->event = ps->externalEvent; + s->eventParm = ps->externalEventParm; + } else { + int seq; + + if ( ps->entityEventSequence < ps->eventSequence - MAX_PS_EVENTS) { + ps->entityEventSequence = ps->eventSequence - MAX_PS_EVENTS; + } + seq = (ps->entityEventSequence-1) & (MAX_PS_EVENTS-1); + s->event = ps->events[ seq ] | ( ( ps->entityEventSequence & 3 ) << 8 ); + s->eventParm = ps->eventParms[ seq ]; + if ( ps->entityEventSequence < ps->eventSequence ) { + ps->entityEventSequence++; + } + } + + s->weapon = ps->weapon; + s->groundEntityNum = ps->groundEntityNum; + + /*s->powerups = 0; + for ( i = 0 ; i < MAX_POWERUPS ; i++ ) { + if ( ps->powerups[ i ] ) { + s->powerups |= 1 << i; + } + }*/ + + //TA: use powerups field to store team/class info: + s->powerups = ps->stats[ STAT_PTEAM ] | ( ps->stats[ STAT_PCLASS ] << 8 ); + + //TA: have to get the surfNormal thru somehow... + if( ps->stats[ STAT_STATE ] & SS_GPISROTVEC ) + { + VectorCopy( ceilingNormal, s->angles2 ); + } + else + { + VectorCopy( ps->grapplePoint, s->angles2 ); + } + + s->loopSound = ps->loopSound; + s->generic1 = ps->generic1; +} + + +/* +======================== +BG_PlayerStateToEntityStateExtraPolate + +This is done after each set of usercmd_t on the server, +and after local prediction on the client +======================== +*/ +void BG_PlayerStateToEntityStateExtraPolate( playerState_t *ps, entityState_t *s, int time, qboolean snap ) { + int i; + vec3_t ceilingNormal = { 0, 0, -1 }; + + if ( ps->pm_type == PM_INTERMISSION || ps->pm_type == PM_SPECTATOR ) { + s->eType = ET_INVISIBLE; + } else if ( ps->stats[STAT_HEALTH] <= GIB_HEALTH ) { + s->eType = ET_INVISIBLE; + } else { + s->eType = ET_PLAYER; + } + + s->number = ps->clientNum; + + s->pos.trType = TR_LINEAR_STOP; + VectorCopy( ps->origin, s->pos.trBase ); + if ( snap ) { + SnapVector( s->pos.trBase ); + } + // set the trDelta for flag direction and linear prediction + VectorCopy( ps->velocity, s->pos.trDelta ); + // set the time for linear prediction + s->pos.trTime = time; + // set maximum extra polation time + s->pos.trDuration = 50; // 1000 / sv_fps (default = 20) + + s->apos.trType = TR_INTERPOLATE; + VectorCopy( ps->viewangles, s->apos.trBase ); + if ( snap ) { + SnapVector( s->apos.trBase ); + } + + //TA: i need for other things :) + //s->angles2[YAW] = ps->movementDir; + s->time2 = ps->movementDir; + s->legsAnim = ps->legsAnim; + s->torsoAnim = ps->torsoAnim; + s->clientNum = ps->clientNum; // ET_PLAYER looks here instead of at number + // so corpses can also reference the proper config + s->eFlags = ps->eFlags; + if ( ps->stats[STAT_HEALTH] <= 0 ) { + s->eFlags |= EF_DEAD; + } else { + s->eFlags &= ~EF_DEAD; + } + + if ( ps->externalEvent ) { + s->event = ps->externalEvent; + s->eventParm = ps->externalEventParm; + } else { + int seq; + + if ( ps->entityEventSequence < ps->eventSequence - MAX_PS_EVENTS) { + ps->entityEventSequence = ps->eventSequence - MAX_PS_EVENTS; + } + seq = (ps->entityEventSequence-1) & (MAX_PS_EVENTS-1); + s->event = ps->events[ seq ] | ( ( ps->entityEventSequence & 3 ) << 8 ); + s->eventParm = ps->eventParms[ seq ]; + if ( ps->entityEventSequence < ps->eventSequence ) { + ps->entityEventSequence++; + } + } + + s->weapon = ps->weapon; + s->groundEntityNum = ps->groundEntityNum; + + /*s->powerups = 0; + for ( i = 0 ; i < MAX_POWERUPS ; i++ ) { + if ( ps->powerups[ i ] ) { + s->powerups |= 1 << i; + } + }*/ + + //TA: use powerups field to store team/class info: + s->powerups = ps->stats[ STAT_PTEAM ] | ( ps->stats[ STAT_PCLASS ] << 8 ); + + //TA: have to get the surfNormal thru somehow... + if( ps->stats[ STAT_STATE ] & SS_GPISROTVEC ) + { + VectorCopy( ceilingNormal, s->angles2 ); + } + else + { + VectorCopy( ps->grapplePoint, s->angles2 ); + } + + s->loopSound = ps->loopSound; + s->generic1 = ps->generic1; +} + + +//TA: extract the ammo quantity from the array +void BG_unpackAmmoArray( int weapon, int ammo[ ], int ammo2[ ], int *quan, int *clips, int *maxclips ) +{ + int ammoarray[32]; + int i; + + for( i = 0; i <= 15; i++ ) + ammoarray[ i ] = ammo[ i ]; + + for( i = 16; i <= 31; i++ ) + ammoarray[ i ] = ammo2[ i - 16 ]; + + if( quan != NULL ) + *quan = ammoarray[ weapon ] & 0x03FF; + + if( clips != NULL ) + *clips = ( ammoarray[ weapon ] >> 10 ) & 0x07; + + if( maxclips != NULL ) + *maxclips = ( ammoarray[ weapon ] >> 13 ) & 0x07; +} + +//TA: pack the ammo quantity into the array +void BG_packAmmoArray( int weapon, int ammo[ ], int ammo2[ ], int quan, int clips, int maxclips ) +{ + int weaponvalue; + + weaponvalue = quan | ( clips << 10 ) | ( maxclips << 13 ); + + if( weapon <= 15 ) + ammo[ weapon ] = weaponvalue; + else if( weapon >= 16 ) + ammo2[ weapon - 16 ] = weaponvalue; +} + +//TA: check whether infinite ammo +qboolean BG_infiniteAmmo( int weapon ) +{ + switch( weapon ) + { + case WP_VENOM: + case WP_ABUILD: + case WP_HBUILD: + case WP_SCANNER: + return qtrue; + break; + + //nothing else has infinite ammo + default: + return qfalse; + break; + } +} + +//TA: pack weapons into the array +void BG_packWeapon( int weapon, int stats[ ] ) +{ + int weaponList, i; + + weaponList = ( stats[ STAT_WEAPONS ] & 0x0000FFFF ) | ( ( stats[ STAT_WEAPONS2 ] << 16 ) & 0xFFFF0000 ); + + weaponList |= ( 1 << weapon ); + + stats[ STAT_WEAPONS ] = weaponList & 0x0000FFFF; + stats[ STAT_WEAPONS2 ] = ( weaponList & 0xFFFF0000 ) >> 16; + +} + +//TA: check whether array contains weapon +qboolean BG_gotWeapon( int weapon, int stats[ ] ) +{ + int weaponList, i; + + weaponList = ( stats[ STAT_WEAPONS ] & 0x0000FFFF ) | ( ( stats[ STAT_WEAPONS2 ] << 16 ) & 0xFFFF0000 ); + + return( weaponList & ( 1 << weapon ) ); +} + +//TA: pack items into array +void BG_packItem( int item, int stats[ ] ) +{ + stats[ STAT_ITEMS ] |= ( 1 << item ); +} + +//TA: remove items from array +void BG_removeItem( int item, int stats[ ] ) +{ + stats[ STAT_ITEMS ] &= ~( 1 << item ); +} + +//TA: check if item is in array +qboolean BG_gotItem( int item, int stats[ ] ) +{ + return( stats[ STAT_ITEMS ] & ( 1 << item ) ); +} + +//TA: set item active in array +void BG_activateItem( int item, int stats[ ] ) +{ + stats[ STAT_ACTIVEITEMS ] |= ( 1 << item ); +} + +//TA: set item deactive in array +void BG_deactivateItem( int item, int stats[ ] ) +{ + stats[ STAT_ACTIVEITEMS ] &= ~( 1 << item ); +} + +//TA: check if item active in array +qboolean BG_activated( int item, int stats[ ] ) +{ + return( stats[ STAT_ACTIVEITEMS ] & ( 1 << item ) ); +} + +//TA: set attributes in array +void BG_packAttributes( int fov, int bob, int steptime, int stats[ ] ) +{ + stats[ STAT_ATTRIBS ] = ( ( (int)( (float)fov/10.0 ) & 0x1F ) << 10 ) | ( ( bob & 0x1F ) << 5 ) | ( (int)( (float)steptime/25.0 ) & 0x1F ); +} + +//TA: get attributes from array +void BG_unpackAttributes( int *fov, int *bob, int *steptime, int stats[ ] ) +{ + if( fov != NULL ) + *fov = ( ( stats[ STAT_ATTRIBS ] >> 10 ) & 0x1F ) * 10; + + if( bob != NULL ) + *bob = ( stats[ STAT_ATTRIBS ] >> 5 ) & 0x1F; + + if( steptime != NULL ) + *steptime = ( stats[ STAT_ATTRIBS ] & 0x1F ) * 25; +} diff --git a/src/game/bg_pmove.c b/src/game/bg_pmove.c new file mode 100644 index 00000000..e7be5798 --- /dev/null +++ b/src/game/bg_pmove.c @@ -0,0 +1,2818 @@ +// Copyright (C) 1999-2000 Id Software, Inc. +// +// bg_pmove.c -- both games player movement code +// takes a playerstate and a usercmd as input and returns a modifed playerstate + +/* + * Portions Copyright (C) 2000-2001 Tim Angus + * + * This program 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, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ + +/* To assertain which portions are licensed under the GPL and which are + * licensed by Id Software, Inc. please run a diff between the equivalent + * versions of the "Tremulous" modification and the unmodified "Quake3" + * game source code. + */ + +#include "q_shared.h" +#include "bg_public.h" +#include "bg_local.h" + +pmove_t *pm; +pml_t pml; + +//TA: wall climbing struct for EVERY client. Not c/s communicated so +// size isn't that important +wcl_t wcl[ MAX_CLIENTS ]; + +// movement parameters +float pm_stopspeed = 100.0f; +float pm_duckScale = 0.25f; +float pm_swimScale = 0.50f; +float pm_wadeScale = 0.70f; + +float pm_accelerate = 10.0f; +float pm_airaccelerate = 1.0f; +float pm_wateraccelerate = 4.0f; +float pm_flyaccelerate = 8.0f; + +float pm_friction = 6.0f; +float pm_waterfriction = 1.0f; +float pm_flightfriction = 3.0f; +float pm_spectatorfriction = 5.0f; + +int c_pmove = 0; + +/* +================ +PM_AddSmoothOp + +Add a smoothing operation to the smoothing queue +================ +*/ +static void PM_AddSmoothOp( vec3_t rotAxis, float rotAngle ) +{ + int i; + + //hack to prevent multiple identical rotations being added to the queue + //this happens often because groundtrace is called twice a frame + for( i = 0; i < MAXSMOOTHS; i++ ) + { + if( wcl[ pm->ps->clientNum ].sList[ i ].time >= pm->cmd.serverTime-100 ) + return; + + /*if( VectorCompare( sList[ i ].rotAxis, rotAxis ) ) + return;*/ + + } + + //iterate through smoothing array + for( i = 0; i < MAXSMOOTHS; i++ ) + { + //if a smooth has been performed it can be overwritten + if( wcl[ pm->ps->clientNum ].sList[ i ].time + SMOOTHTIME < pm->cmd.serverTime ) + { + //copy the new smooth into the array and stop + VectorCopy( rotAxis, wcl[ pm->ps->clientNum ].sList[ i ].rotAxis ); + wcl[ pm->ps->clientNum ].sList[ i ].rotAngle = rotAngle; + wcl[ pm->ps->clientNum ].sList[ i ].time = pm->cmd.serverTime; + return; + } + } + + //there are no free smooth slots +} + + +/* +================ +PM_PerformSmoothOps + +Perform all the smoothing operations in the smoothing queue +================ +*/ +static qboolean PM_PerformSmoothOps( vec3_t in[3], vec3_t out[3] ) +{ + int i; + float stLocal, sFraction; + vec3_t localIn[3], localOut[3]; + qboolean performed = qfalse; + + AxisCopy( in, localIn ); + + //iterate through smoothing array + for( i = 0; i < MAXSMOOTHS; i++ ) + { + //perform smooth + if( ( pm->cmd.serverTime < wcl[ pm->ps->clientNum ].sList[ i ].time + SMOOTHTIME ) && + ( VectorLength( wcl[ pm->ps->clientNum ].sList[ i ].rotAxis ) != 0 ) ) + { + //the rotAxis /should/ never be of zero length but q3 *will* crash if it is... + //better to be safe than sorry :) + + stLocal = 1.0 - ( ( ( wcl[ pm->ps->clientNum ].sList[ i ].time + SMOOTHTIME ) - pm->cmd.serverTime ) / SMOOTHTIME ); + + //some ppl might prefer this smoothing function: + //sFraction = -( 1.0 - sin( stLocal * M_PI / 2 ) ); + sFraction = -( cos( stLocal * M_PI ) + 1 ) / 2; + + RotatePointAroundVector( localOut[0], wcl[ pm->ps->clientNum ].sList[ i ].rotAxis, localIn[0], sFraction * wcl[ pm->ps->clientNum ].sList[ i ].rotAngle ); + RotatePointAroundVector( localOut[1], wcl[ pm->ps->clientNum ].sList[ i ].rotAxis, localIn[1], sFraction * wcl[ pm->ps->clientNum ].sList[ i ].rotAngle ); + RotatePointAroundVector( localOut[2], wcl[ pm->ps->clientNum ].sList[ i ].rotAxis, localIn[2], sFraction * wcl[ pm->ps->clientNum ].sList[ i ].rotAngle ); + + AxisCopy( localOut, localIn ); + performed = qtrue; + } + } + + AxisCopy( localOut, out ); + + return performed; +} + + +/* +=============== +PM_AddEvent + +=============== +*/ +void PM_AddEvent( int newEvent ) { + BG_AddPredictableEventToPlayerstate( newEvent, 0, pm->ps ); +} + +/* +=============== +PM_AddTouchEnt +=============== +*/ +void PM_AddTouchEnt( int entityNum ) { + int i; + + if ( entityNum == ENTITYNUM_WORLD ) { + return; + } + if ( pm->numtouch == MAXTOUCH ) { + return; + } + + // see if it is already added + for ( i = 0 ; i < pm->numtouch ; i++ ) { + if ( pm->touchents[ i ] == entityNum ) { + return; + } + } + + // add it + pm->touchents[pm->numtouch] = entityNum; + pm->numtouch++; +} + +/* +=================== +PM_StartTorsoAnim +=================== +*/ +static void PM_StartTorsoAnim( int anim ) { + if ( pm->ps->pm_type >= PM_DEAD ) { + return; + } + pm->ps->torsoAnim = ( ( pm->ps->torsoAnim & ANIM_TOGGLEBIT ) ^ ANIM_TOGGLEBIT ) + | anim; +} +static void PM_StartLegsAnim( int anim ) { + if ( pm->ps->pm_type >= PM_DEAD ) { + return; + } + if ( pm->ps->legsTimer > 0 ) { + return; // a high priority animation is running + } + pm->ps->legsAnim = ( ( pm->ps->legsAnim & ( ANIM_TOGGLEBIT | ANIM_WALLCLIMBING ) ) ^ ANIM_TOGGLEBIT ) + | anim; +} + +static void PM_ContinueLegsAnim( int anim ) { + if ( ( pm->ps->legsAnim & ~( ANIM_TOGGLEBIT | ANIM_WALLCLIMBING ) ) == anim ) { + return; + } + if ( pm->ps->legsTimer > 0 ) { + return; // a high priority animation is running + } + PM_StartLegsAnim( anim ); +} + +static void PM_ContinueTorsoAnim( int anim ) { + if ( ( pm->ps->torsoAnim & ~( ANIM_TOGGLEBIT | ANIM_WALLCLIMBING ) ) == anim ) { + return; + } + if ( pm->ps->torsoTimer > 0 ) { + return; // a high priority animation is running + } + PM_StartTorsoAnim( anim ); +} + +static void PM_ForceLegsAnim( int anim ) { + pm->ps->legsTimer = 0; + PM_StartLegsAnim( anim ); +} + + +/* +================== +PM_ClipVelocity + +Slide off of the impacting surface +================== +*/ +void PM_ClipVelocity( vec3_t in, vec3_t normal, vec3_t out, float overbounce ) { + float backoff; + float change; + int i; + + backoff = DotProduct (in, normal); + + if ( backoff < 0 ) { + backoff *= overbounce; + } else { + backoff /= overbounce; + } + + for ( i=0 ; i<3 ; i++ ) { + change = normal[i]*backoff; + out[i] = in[i] - change; + } +} + + +/* +================== +PM_Friction + +Handles both ground friction and water friction +================== +*/ +static void PM_Friction( void ) { + vec3_t vec; + float *vel; + float speed, newspeed, control; + float drop; + + vel = pm->ps->velocity; + + //TA: make sure vertical velocity is NOT set to zero when wall climbing + VectorCopy( vel, vec ); + if ( pml.walking && + pm->ps->stats[ STAT_STATE ] & ~SS_WALLCLIMBING ) { + vec[2] = 0; // ignore slope movement + } + + speed = VectorLength(vec); + if (speed < 1) { + vel[0] = 0; + vel[1] = 0; // allow sinking underwater + // FIXME: still have z friction underwater? + return; + } + + drop = 0; + + // apply ground friction + if ( pm->waterlevel <= 1 ) { + if ( pml.walking && !(pml.groundTrace.surfaceFlags & SURF_SLICK) ) { + // if getting knocked back, no friction + if ( ! (pm->ps->pm_flags & PMF_TIME_KNOCKBACK) ) { + control = speed < pm_stopspeed ? pm_stopspeed : speed; + drop += control*pm_friction*pml.frametime; + } + } + } + + // apply water friction even if just wading + if ( pm->waterlevel ) { + drop += speed*pm_waterfriction*pm->waterlevel*pml.frametime; + } + + // apply flying friction + if ( pm->ps->pm_type == PM_SPECTATOR ) { + drop += speed*pm_spectatorfriction*pml.frametime; + } + + // scale the velocity + newspeed = speed - drop; + if (newspeed < 0) { + newspeed = 0; + } + newspeed /= speed; + + vel[0] = vel[0] * newspeed; + vel[1] = vel[1] * newspeed; + vel[2] = vel[2] * newspeed; +} + + +/* +============== +PM_Accelerate + +Handles user intended acceleration +============== +*/ +static void PM_Accelerate( vec3_t wishdir, float wishspeed, float accel ) { +#if 1 + // q2 style + int i; + float addspeed, accelspeed, currentspeed; + + currentspeed = DotProduct (pm->ps->velocity, wishdir); + addspeed = wishspeed - currentspeed; + if (addspeed <= 0) { + return; + } + accelspeed = accel*pml.frametime*wishspeed; + if (accelspeed > addspeed) { + accelspeed = addspeed; + } + + for (i=0 ; i<3 ; i++) { + pm->ps->velocity[i] += accelspeed*wishdir[i]; + } +#else + // proper way (avoids strafe jump maxspeed bug), but feels bad + vec3_t wishVelocity; + vec3_t pushDir; + float pushLen; + float canPush; + + VectorScale( wishdir, wishspeed, wishVelocity ); + VectorSubtract( wishVelocity, pm->ps->velocity, pushDir ); + pushLen = VectorNormalize( pushDir ); + + canPush = accel*pml.frametime*wishspeed; + if (canPush > pushLen) { + canPush = pushLen; + } + + VectorMA( pm->ps->velocity, canPush, pushDir, pm->ps->velocity ); +#endif +} + + + +/* +============ +PM_CmdScale + +Returns the scale factor to apply to cmd movements +This allows the clients to use axial -127 to 127 values for all directions +without getting a sqrt(2) distortion in speed. +============ +*/ +static float PM_CmdScale( usercmd_t *cmd ) { + int max; + float total; + float scale; + float modifier = 1.0; + static int time; + int dTime; + int aForward, aRight; + + dTime = pm->cmd.serverTime - time; + time = pm->cmd.serverTime; + + if( pm->ps->stats[ STAT_PTEAM ] == PTE_HUMANS ) + { + if( !( pm->ps->stats[ STAT_STATE ] & SS_SPEEDBOOST ) ) + { + //if not sprinting + modifier *= 0.8; + } + else + { + //subtract stamina + pm->ps->stats[ STAT_STAMINA ] -= (dTime/4); + } + + aForward = abs( cmd->forwardmove ); + aRight = abs( cmd->rightmove ); + + if( ( aForward <= 64 && aForward > 5 ) && ( aRight <= 64 && aRight > 5 ) ) + { + //restore stamina + pm->ps->stats[ STAT_STAMINA ] += (dTime/5); + } + else if( aForward <= 5 && aRight <= 5 ) + { + //restore stamina faster + pm->ps->stats[ STAT_STAMINA ] += (dTime/4); + } + + if( cmd->forwardmove < 0 ) + { + //can't run backwards + modifier *= 0.5; + } + else if( cmd->rightmove ) + { + //can't move that fast sideways + modifier *= 0.75; + } + + //cap stamina + if( pm->ps->stats[ STAT_STAMINA ] > 1000 ) + pm->ps->stats[ STAT_STAMINA ] = 1000; + if( pm->ps->stats[ STAT_STAMINA ] < -1000 ) + pm->ps->stats[ STAT_STAMINA ] = -1000; + + //if not trying to run then not trying to sprint + if( abs( cmd->forwardmove ) <= 64 ) + pm->ps->stats[ STAT_STATE ] &= ~SS_SPEEDBOOST; + + //must have +ve stamina to jump + if( pm->ps->stats[ STAT_STAMINA ] < 0 ) + cmd->upmove = 0; + + //slow down once stamina depletes + if( pm->ps->stats[ STAT_STAMINA ] <= -500 ) + modifier *= (float)( pm->ps->stats[ STAT_STAMINA ] + 1000 ) / 500.0f; + } + + if( !( pm->ps->stats[ STAT_ABILITIES ] & SCA_CANJUMP ) ) + cmd->upmove = 0; + + max = abs( cmd->forwardmove ); + if ( abs( cmd->rightmove ) > max ) { + max = abs( cmd->rightmove ); + } + if ( abs( cmd->upmove ) > max ) { + max = abs( cmd->upmove ); + } + if ( !max ) { + return 0; + } + + if( ( pm->ps->stats[ STAT_ABILITIES ] & SCA_WALLCLIMBER ) && + ( pm->ps->stats[ STAT_STATE ] & SS_WALLCLIMBING ) ) + { + total = sqrt( cmd->forwardmove * cmd->forwardmove + cmd->rightmove * cmd->rightmove ); + } + else + { + total = sqrt( cmd->forwardmove * cmd->forwardmove + + cmd->rightmove * cmd->rightmove + cmd->upmove * cmd->upmove ); + } + scale = (float)pm->ps->speed * max / ( 127.0 * total ) * modifier; + + return scale; +} + + +/* +================ +PM_SetMovementDir + +Determine the rotation of the legs reletive +to the facing dir +================ +*/ +static void PM_SetMovementDir( void ) { + if ( pm->cmd.forwardmove || pm->cmd.rightmove ) { + if ( pm->cmd.rightmove == 0 && pm->cmd.forwardmove > 0 ) { + pm->ps->movementDir = 0; + } else if ( pm->cmd.rightmove < 0 && pm->cmd.forwardmove > 0 ) { + pm->ps->movementDir = 1; + } else if ( pm->cmd.rightmove < 0 && pm->cmd.forwardmove == 0 ) { + pm->ps->movementDir = 2; + } else if ( pm->cmd.rightmove < 0 && pm->cmd.forwardmove < 0 ) { + pm->ps->movementDir = 3; + } else if ( pm->cmd.rightmove == 0 && pm->cmd.forwardmove < 0 ) { + pm->ps->movementDir = 4; + } else if ( pm->cmd.rightmove > 0 && pm->cmd.forwardmove < 0 ) { + pm->ps->movementDir = 5; + } else if ( pm->cmd.rightmove > 0 && pm->cmd.forwardmove == 0 ) { + pm->ps->movementDir = 6; + } else if ( pm->cmd.rightmove > 0 && pm->cmd.forwardmove > 0 ) { + pm->ps->movementDir = 7; + } + } else { + // if they aren't actively going directly sideways, + // change the animation to the diagonal so they + // don't stop too crooked + if ( pm->ps->movementDir == 2 ) { + pm->ps->movementDir = 1; + } else if ( pm->ps->movementDir == 6 ) { + pm->ps->movementDir = 7; + } + } +} + + +/* +============= +PM_CheckJump +============= +*/ +static qboolean PM_CheckJump( void ) { + if( !( pm->ps->stats[ STAT_ABILITIES ] & SCA_CANJUMP ) ) return qfalse; + + if( ( pm->ps->stats[ STAT_PTEAM ] == PTE_HUMANS ) && + ( pm->ps->stats[ STAT_STAMINA ] < 0 ) ) + return qfalse; + + if ( pm->ps->pm_flags & PMF_RESPAWNED ) { + return qfalse; // don't allow jump until all buttons are up + } + + if ( pm->cmd.upmove < 10 ) { + // not holding jump + return qfalse; + } + + // must wait for jump to be released + if ( pm->ps->pm_flags & PMF_JUMP_HELD ) { + // clear upmove so cmdscale doesn't lower running speed + pm->cmd.upmove = 0; + return qfalse; + } + + pml.groundPlane = qfalse; // jumping away + pml.walking = qfalse; + pm->ps->pm_flags |= PMF_JUMP_HELD; + + //TA: take some stamina off + if( pm->ps->stats[ STAT_PTEAM ] == PTE_HUMANS ) + pm->ps->stats[ STAT_STAMINA ] -= 500; + + pm->ps->groundEntityNum = ENTITYNUM_NONE; + pm->ps->velocity[2] = JUMP_VELOCITY; + PM_AddEvent( EV_JUMP ); + + if ( pm->cmd.forwardmove >= 0 ) { + PM_ForceLegsAnim( LEGS_JUMP ); + pm->ps->pm_flags &= ~PMF_BACKWARDS_JUMP; + } else { + PM_ForceLegsAnim( LEGS_JUMPB ); + pm->ps->pm_flags |= PMF_BACKWARDS_JUMP; + } + + return qtrue; +} + +/* +============= +PM_CheckWaterJump +============= +*/ +static qboolean PM_CheckWaterJump( void ) { + vec3_t spot; + int cont; + vec3_t flatforward; + + if (pm->ps->pm_time) { + return qfalse; + } + + // check for water jump + if ( pm->waterlevel != 2 ) { + return qfalse; + } + + flatforward[0] = pml.forward[0]; + flatforward[1] = pml.forward[1]; + flatforward[2] = 0; + VectorNormalize (flatforward); + + VectorMA (pm->ps->origin, 30, flatforward, spot); + spot[2] += 4; + cont = pm->pointcontents (spot, pm->ps->clientNum ); + if ( !(cont & CONTENTS_SOLID) ) { + return qfalse; + } + + spot[2] += 16; + cont = pm->pointcontents (spot, pm->ps->clientNum ); + if ( cont ) { + return qfalse; + } + + // jump out of water + VectorScale (pml.forward, 200, pm->ps->velocity); + pm->ps->velocity[2] = 350; + + pm->ps->pm_flags |= PMF_TIME_WATERJUMP; + pm->ps->pm_time = 2000; + + return qtrue; +} + +//============================================================================ + + +/* +=================== +PM_WaterJumpMove + +Flying out of the water +=================== +*/ +static void PM_WaterJumpMove( void ) { + // waterjump has no control, but falls + + PM_StepSlideMove( qtrue ); + + pm->ps->velocity[2] -= pm->ps->gravity * pml.frametime; + if (pm->ps->velocity[2] < 0) { + // cancel as soon as we are falling down again + pm->ps->pm_flags &= ~PMF_ALL_TIMES; + pm->ps->pm_time = 0; + } +} + +/* +=================== +PM_WaterMove + +=================== +*/ +static void PM_WaterMove( void ) { + int i; + vec3_t wishvel; + float wishspeed; + vec3_t wishdir; + float scale; + float vel; + + if ( PM_CheckWaterJump() ) { + PM_WaterJumpMove(); + return; + } +#if 0 + // jump = head for surface + if ( pm->cmd.upmove >= 10 ) { + if (pm->ps->velocity[2] > -300) { + if ( pm->watertype == CONTENTS_WATER ) { + pm->ps->velocity[2] = 100; + } else if (pm->watertype == CONTENTS_SLIME) { + pm->ps->velocity[2] = 80; + } else { + pm->ps->velocity[2] = 50; + } + } + } +#endif + PM_Friction (); + + scale = PM_CmdScale( &pm->cmd ); + // + // user intentions + // + if ( !scale ) { + wishvel[0] = 0; + wishvel[1] = 0; + wishvel[2] = -60; // sink towards bottom + } else { + for (i=0 ; i<3 ; i++) + wishvel[i] = scale * pml.forward[i]*pm->cmd.forwardmove + scale * pml.right[i]*pm->cmd.rightmove; + + wishvel[2] += scale * pm->cmd.upmove; + } + + VectorCopy (wishvel, wishdir); + wishspeed = VectorNormalize(wishdir); + + if ( wishspeed > pm->ps->speed * pm_swimScale ) { + wishspeed = pm->ps->speed * pm_swimScale; + } + + PM_Accelerate (wishdir, wishspeed, pm_wateraccelerate); + + // make sure we can go up slopes easily under water + if ( pml.groundPlane && DotProduct( pm->ps->velocity, pml.groundTrace.plane.normal ) < 0 ) { + vel = VectorLength(pm->ps->velocity); + // slide along the ground plane + PM_ClipVelocity (pm->ps->velocity, pml.groundTrace.plane.normal, + pm->ps->velocity, OVERCLIP ); + + VectorNormalize(pm->ps->velocity); + VectorScale(pm->ps->velocity, vel, pm->ps->velocity); + } + + PM_SlideMove( qfalse ); +} + + + +/* +=================== +PM_FlyMove + +Only with the flight powerup +=================== +*/ +static void PM_FlyMove( void ) { + int i; + vec3_t wishvel; + float wishspeed; + vec3_t wishdir; + float scale; + + // normal slowdown + PM_Friction (); + + scale = PM_CmdScale( &pm->cmd ); + // + // user intentions + // + if ( !scale ) { + wishvel[0] = 0; + wishvel[1] = 0; + wishvel[2] = 0; + } else { + for (i=0 ; i<3 ; i++) { + wishvel[i] = scale * pml.forward[i]*pm->cmd.forwardmove + scale * pml.right[i]*pm->cmd.rightmove; + } + + wishvel[2] += scale * pm->cmd.upmove; + } + + VectorCopy (wishvel, wishdir); + wishspeed = VectorNormalize(wishdir); + + PM_Accelerate (wishdir, wishspeed, pm_flyaccelerate); + + PM_StepSlideMove( qfalse ); +} + + +/* +=================== +PM_AirMove + +=================== +*/ +static void PM_AirMove( void ) { + int i; + vec3_t wishvel; + float fmove, smove; + vec3_t wishdir; + float wishspeed; + float scale; + usercmd_t cmd; + + PM_Friction(); + + fmove = pm->cmd.forwardmove; + smove = pm->cmd.rightmove; + + cmd = pm->cmd; + scale = PM_CmdScale( &cmd ); + + // set the movementDir so clients can rotate the legs for strafing + PM_SetMovementDir(); + + // project moves down to flat plane + pml.forward[2] = 0; + pml.right[2] = 0; + VectorNormalize (pml.forward); + VectorNormalize (pml.right); + + for ( i = 0 ; i < 2 ; i++ ) { + wishvel[i] = pml.forward[i]*fmove + pml.right[i]*smove; + } + wishvel[2] = 0; + + VectorCopy (wishvel, wishdir); + wishspeed = VectorNormalize(wishdir); + wishspeed *= scale; + + // not on ground, so little effect on velocity + PM_Accelerate (wishdir, wishspeed, pm_airaccelerate); + + // we may have a ground plane that is very steep, even + // though we don't have a groundentity + // slide along the steep plane + if ( pml.groundPlane ) { + PM_ClipVelocity (pm->ps->velocity, pml.groundTrace.plane.normal, + pm->ps->velocity, OVERCLIP ); + } + +#if 0 + //ZOID: If we are on the grapple, try stair-stepping + //this allows a player to use the grapple to pull himself + //over a ledge + if (pm->ps->pm_flags & PMF_GRAPPLE_PULL) + PM_StepSlideMove ( qtrue ); + else + PM_SlideMove ( qtrue ); +#endif + + PM_StepSlideMove ( qtrue ); +} + +/* +=================== +PM_GrappleMove + +=================== +*/ +static void PM_GrappleMove( void ) { + vec3_t vel, v; + float vlen; + + VectorScale(pml.forward, -16, v); + VectorAdd(pm->ps->grapplePoint, v, v); + VectorSubtract(v, pm->ps->origin, vel); + vlen = VectorLength(vel); + VectorNormalize( vel ); + + if (vlen <= 100) + VectorScale(vel, 10 * vlen, vel); + else + VectorScale(vel, 800, vel); + + VectorCopy(vel, pm->ps->velocity); + + pml.groundPlane = qfalse; +} + + +/* +=================== +PM_ClimbMove + +=================== +*/ +static void PM_ClimbMove( void ) { + int i; + vec3_t wishvel; + float fmove, smove; + vec3_t wishdir; + float wishspeed; + float scale; + usercmd_t cmd; + float accelerate; + float vel; + + if ( pm->waterlevel > 2 && DotProduct( pml.forward, pml.groundTrace.plane.normal ) > 0 ) { + // begin swimming + PM_WaterMove(); + return; + } + + + if ( PM_CheckJump () ) { + // jumped away + if ( pm->waterlevel > 1 ) { + PM_WaterMove(); + } else { + PM_AirMove(); + } + return; + } + + PM_Friction (); + + fmove = pm->cmd.forwardmove; + smove = pm->cmd.rightmove; + + cmd = pm->cmd; + scale = PM_CmdScale( &cmd ); + + // set the movementDir so clients can rotate the legs for strafing + PM_SetMovementDir(); + + // project the forward and right directions onto the ground plane + PM_ClipVelocity (pml.forward, pml.groundTrace.plane.normal, pml.forward, OVERCLIP ); + PM_ClipVelocity (pml.right, pml.groundTrace.plane.normal, pml.right, OVERCLIP ); + // + VectorNormalize (pml.forward); + VectorNormalize (pml.right); + + for ( i = 0 ; i < 3 ; i++ ) { + wishvel[i] = pml.forward[i]*fmove + pml.right[i]*smove; + } + // when going up or down slopes the wish velocity should Not be zero +// wishvel[2] = 0; + + VectorCopy (wishvel, wishdir); + wishspeed = VectorNormalize(wishdir); + wishspeed *= scale; + + // clamp the speed lower if ducking + if ( pm->ps->pm_flags & PMF_DUCKED ) { + if ( wishspeed > pm->ps->speed * pm_duckScale ) { + wishspeed = pm->ps->speed * pm_duckScale; + } + } + + // clamp the speed lower if wading or walking on the bottom + if ( pm->waterlevel ) { + float waterScale; + + waterScale = pm->waterlevel / 3.0; + waterScale = 1.0 - ( 1.0 - pm_swimScale ) * waterScale; + if ( wishspeed > pm->ps->speed * waterScale ) { + wishspeed = pm->ps->speed * waterScale; + } + } + + // when a player gets hit, they temporarily lose + // full control, which allows them to be moved a bit + if ( ( pml.groundTrace.surfaceFlags & SURF_SLICK ) || pm->ps->pm_flags & PMF_TIME_KNOCKBACK ) { + accelerate = pm_airaccelerate; + } else { + accelerate = pm_accelerate; + } + + PM_Accelerate (wishdir, wishspeed, accelerate); + + //Com_Printf("velocity = %1.1f %1.1f %1.1f\n", pm->ps->velocity[0], pm->ps->velocity[1], pm->ps->velocity[2]); + //Com_Printf("velocity1 = %1.1f\n", VectorLength(pm->ps->velocity)); + + if ( ( pml.groundTrace.surfaceFlags & SURF_SLICK ) || pm->ps->pm_flags & PMF_TIME_KNOCKBACK ) { + pm->ps->velocity[2] -= pm->ps->gravity * pml.frametime; + } else { + // don't reset the z velocity for slopes +// pm->ps->velocity[2] = 0; + } + + vel = VectorLength(pm->ps->velocity); + + // slide along the ground plane + PM_ClipVelocity (pm->ps->velocity, pml.groundTrace.plane.normal, + pm->ps->velocity, OVERCLIP ); + + // don't decrease velocity when going up or down a slope + VectorNormalize(pm->ps->velocity); + VectorScale(pm->ps->velocity, vel, pm->ps->velocity); + + // don't do anything if standing still + if (!pm->ps->velocity[0] && !pm->ps->velocity[1] && !pm->ps->velocity[2]) { + return; + } + + PM_StepSlideMove( qfalse ); + + //Com_Printf("velocity2 = %1.1f\n", VectorLength(pm->ps->velocity)); + +} + + +/* +=================== +PM_WalkMove + +=================== +*/ +static void PM_WalkMove( void ) { + int i; + vec3_t wishvel; + float fmove, smove; + vec3_t wishdir; + float wishspeed; + float scale; + usercmd_t cmd; + float accelerate; + float vel; + + if ( pm->waterlevel > 2 && DotProduct( pml.forward, pml.groundTrace.plane.normal ) > 0 ) { + // begin swimming + PM_WaterMove(); + return; + } + + + if ( PM_CheckJump () ) { + // jumped away + if ( pm->waterlevel > 1 ) { + PM_WaterMove(); + } else { + PM_AirMove(); + } + return; + } + + PM_Friction (); + + fmove = pm->cmd.forwardmove; + smove = pm->cmd.rightmove; + + cmd = pm->cmd; + scale = PM_CmdScale( &cmd ); + + // set the movementDir so clients can rotate the legs for strafing + PM_SetMovementDir(); + + // project moves down to flat plane + pml.forward[2] = 0; + pml.right[2] = 0; + + // project the forward and right directions onto the ground plane + PM_ClipVelocity (pml.forward, pml.groundTrace.plane.normal, pml.forward, OVERCLIP ); + PM_ClipVelocity (pml.right, pml.groundTrace.plane.normal, pml.right, OVERCLIP ); + // + VectorNormalize (pml.forward); + VectorNormalize (pml.right); + + for ( i = 0 ; i < 3 ; i++ ) { + wishvel[i] = pml.forward[i]*fmove + pml.right[i]*smove; + } + // when going up or down slopes the wish velocity should Not be zero +// wishvel[2] = 0; + + VectorCopy (wishvel, wishdir); + wishspeed = VectorNormalize(wishdir); + wishspeed *= scale; + + // clamp the speed lower if ducking + if ( pm->ps->pm_flags & PMF_DUCKED ) { + if ( wishspeed > pm->ps->speed * pm_duckScale ) { + wishspeed = pm->ps->speed * pm_duckScale; + } + } + + // clamp the speed lower if wading or walking on the bottom + if ( pm->waterlevel ) { + float waterScale; + + waterScale = pm->waterlevel / 3.0; + waterScale = 1.0 - ( 1.0 - pm_swimScale ) * waterScale; + if ( wishspeed > pm->ps->speed * waterScale ) { + wishspeed = pm->ps->speed * waterScale; + } + } + + // when a player gets hit, they temporarily lose + // full control, which allows them to be moved a bit + if ( ( pml.groundTrace.surfaceFlags & SURF_SLICK ) || pm->ps->pm_flags & PMF_TIME_KNOCKBACK ) { + accelerate = pm_airaccelerate; + } else { + accelerate = pm_accelerate; + } + + PM_Accelerate (wishdir, wishspeed, accelerate); + + //Com_Printf("velocity = %1.1f %1.1f %1.1f\n", pm->ps->velocity[0], pm->ps->velocity[1], pm->ps->velocity[2]); + //Com_Printf("velocity1 = %1.1f\n", VectorLength(pm->ps->velocity)); + + if ( ( pml.groundTrace.surfaceFlags & SURF_SLICK ) || pm->ps->pm_flags & PMF_TIME_KNOCKBACK ) { + pm->ps->velocity[2] -= pm->ps->gravity * pml.frametime; + } else { + // don't reset the z velocity for slopes +// pm->ps->velocity[2] = 0; + } + + vel = VectorLength(pm->ps->velocity); + + // slide along the ground plane + PM_ClipVelocity (pm->ps->velocity, pml.groundTrace.plane.normal, + pm->ps->velocity, OVERCLIP ); + + // don't decrease velocity when going up or down a slope + VectorNormalize(pm->ps->velocity); + VectorScale(pm->ps->velocity, vel, pm->ps->velocity); + + // don't do anything if standing still + if (!pm->ps->velocity[0] && !pm->ps->velocity[1]) { + return; + } + + PM_StepSlideMove( qfalse ); + + //Com_Printf("velocity2 = %1.1f\n", VectorLength(pm->ps->velocity)); + +} + + +/* +============== +PM_DeadMove +============== +*/ +static void PM_DeadMove( void ) { + float forward; + + if ( !pml.walking ) { + return; + } + + // extra friction + + forward = VectorLength (pm->ps->velocity); + forward -= 20; + if ( forward <= 0 ) { + VectorClear (pm->ps->velocity); + } else { + VectorNormalize (pm->ps->velocity); + VectorScale (pm->ps->velocity, forward, pm->ps->velocity); + } +} + + +/* +=============== +PM_NoclipMove +=============== +*/ +static void PM_NoclipMove( void ) { + float speed, drop, friction, control, newspeed; + int i; + vec3_t wishvel; + float fmove, smove; + vec3_t wishdir; + float wishspeed; + float scale; + + pm->ps->viewheight = DEFAULT_VIEWHEIGHT; + + // friction + + speed = VectorLength (pm->ps->velocity); + if (speed < 1) + { + VectorCopy (vec3_origin, pm->ps->velocity); + } + else + { + drop = 0; + + friction = pm_friction*1.5; // extra friction + control = speed < pm_stopspeed ? pm_stopspeed : speed; + drop += control*friction*pml.frametime; + + // scale the velocity + newspeed = speed - drop; + if (newspeed < 0) + newspeed = 0; + newspeed /= speed; + + VectorScale (pm->ps->velocity, newspeed, pm->ps->velocity); + } + + // accelerate + scale = PM_CmdScale( &pm->cmd ); + + fmove = pm->cmd.forwardmove; + smove = pm->cmd.rightmove; + + for (i=0 ; i<3 ; i++) + wishvel[i] = pml.forward[i]*fmove + pml.right[i]*smove; + wishvel[2] += pm->cmd.upmove; + + VectorCopy (wishvel, wishdir); + wishspeed = VectorNormalize(wishdir); + wishspeed *= scale; + + PM_Accelerate( wishdir, wishspeed, pm_accelerate ); + + // move + VectorMA (pm->ps->origin, pml.frametime, pm->ps->velocity, pm->ps->origin); +} + +//============================================================================ + +/* +================ +PM_FootstepForSurface + +Returns an event number apropriate for the groundsurface +================ +*/ +static int PM_FootstepForSurface( void ) +{ + //TA: + if ( pm->ps->stats[ STAT_STATE ] & SS_CREEPSLOWED ) + return EV_FOOTSTEP_SQUELCH; + + if ( pml.groundTrace.surfaceFlags & SURF_NOSTEPS ) + return 0; + + if ( pml.groundTrace.surfaceFlags & SURF_METALSTEPS ) + return EV_FOOTSTEP_METAL; + + return EV_FOOTSTEP; +} + + +/* +================= +PM_CrashLand + +Check for hard landings that generate sound events +================= +*/ +static void PM_CrashLand( void ) { + float delta; + float dist; + float vel, acc; + float t; + float a, b, c, den; + + // decide which landing animation to use + if ( pm->ps->pm_flags & PMF_BACKWARDS_JUMP ) { + PM_ForceLegsAnim( LEGS_LANDB ); + } else { + PM_ForceLegsAnim( LEGS_LAND ); + } + + pm->ps->legsTimer = TIMER_LAND; + + // calculate the exact velocity on landing + dist = pm->ps->origin[2] - pml.previous_origin[2]; + vel = pml.previous_velocity[2]; + acc = -pm->ps->gravity; + + a = acc / 2; + b = vel; + c = -dist; + + den = b * b - 4 * a * c; + if ( den < 0 ) { + return; + } + t = (-b - sqrt( den ) ) / ( 2 * a ); + + delta = vel + t * acc; + delta = delta*delta * 0.0001; + + // ducking while falling doubles damage + if ( pm->ps->pm_flags & PMF_DUCKED ) { + delta *= 2; + } + + // never take falling damage if completely underwater + if ( pm->waterlevel == 3 ) { + return; + } + + // reduce falling damage if there is standing water + if ( pm->waterlevel == 2 ) { + delta *= 0.25; + } + if ( pm->waterlevel == 1 ) { + delta *= 0.5; + } + + if ( delta < 1 ) { + return; + } + + // create a local entity event to play the sound + + // SURF_NODAMAGE is used for bounce pads where you don't ever + // want to take damage or play a crunch sound + if ( !(pml.groundTrace.surfaceFlags & SURF_NODAMAGE) ) { + if ( delta > 60 ) { + PM_AddEvent( EV_FALL_FAR ); + } else if ( delta > 40 ) { + // this is a pain grunt, so don't play it if dead + if ( pm->ps->stats[STAT_HEALTH] > 0 ) { + PM_AddEvent( EV_FALL_MEDIUM ); + } + } else if ( delta > 7 ) { + PM_AddEvent( EV_FALL_SHORT ); + } else { + PM_AddEvent( PM_FootstepForSurface() ); + } + } + + // start footstep cycle over + pm->ps->bobCycle = 0; +} + + +/* +============= +PM_CorrectAllSolid +============= +*/ +static int PM_CorrectAllSolid( trace_t *trace ) { + int i, j, k; + vec3_t point; + + if ( pm->debugLevel ) { + Com_Printf("%i:allsolid\n", c_pmove); + } + + // jitter around + for (i = -1; i <= 1; i++) { + for (j = -1; j <= 1; j++) { + for (k = -1; k <= 1; k++) { + VectorCopy(pm->ps->origin, point); + point[0] += (float) i; + point[1] += (float) j; + point[2] += (float) k; + pm->trace (trace, point, pm->mins, pm->maxs, point, pm->ps->clientNum, pm->tracemask); + if ( !trace->allsolid ) { + point[0] = pm->ps->origin[0]; + point[1] = pm->ps->origin[1]; + point[2] = pm->ps->origin[2] - 0.25; + + pm->trace (trace, pm->ps->origin, pm->mins, pm->maxs, point, pm->ps->clientNum, pm->tracemask); + pml.groundTrace = *trace; + return qtrue; + } + } + } + } + + pm->ps->groundEntityNum = ENTITYNUM_NONE; + pml.groundPlane = qfalse; + pml.walking = qfalse; + + return qfalse; +} + + +/* +============= +PM_GroundTraceMissed + +The ground trace didn't hit a surface, so we are in freefall +============= +*/ +static void PM_GroundTraceMissed( void ) { + trace_t trace; + vec3_t point; + + if ( pm->ps->groundEntityNum != ENTITYNUM_NONE ) { + // we just transitioned into freefall + if ( pm->debugLevel ) { + Com_Printf("%i:lift\n", c_pmove); + } + + // if they aren't in a jumping animation and the ground is a ways away, force into it + // if we didn't do the trace, the player would be backflipping down staircases + VectorCopy( pm->ps->origin, point ); + point[2] -= 64; + + pm->trace (&trace, pm->ps->origin, pm->mins, pm->maxs, point, pm->ps->clientNum, pm->tracemask); + if ( trace.fraction == 1.0 ) { + if ( pm->cmd.forwardmove >= 0 ) { + PM_ForceLegsAnim( LEGS_JUMP ); + pm->ps->pm_flags &= ~PMF_BACKWARDS_JUMP; + } else { + PM_ForceLegsAnim( LEGS_JUMPB ); + pm->ps->pm_flags |= PMF_BACKWARDS_JUMP; + } + } + } + + pm->ps->groundEntityNum = ENTITYNUM_NONE; + pml.groundPlane = qfalse; + pml.walking = qfalse; +} + + +/* +============= +PM_GroundClimbTrace +============= +*/ +static void PM_GroundClimbTrace( void ) +{ + vec3_t surfNormal, movedir, forward, right, point, srotAxis; + vec3_t refNormal = { 0, 0, 1 }; + vec3_t ceilingNormal = { 0, 0, -1 }; + float toAngles[3], surfAngles[3], srotAngle; + trace_t trace; + int i; + qboolean smoothed = qtrue; + + //TA: If we're on the ceiling then grapplePoint is a rotation normal.. otherwise its a surface normal. + // would have been nice if Carmack had left a few random variables in the ps struct for mod makers + if( pm->ps->stats[ STAT_STATE ] & SS_GPISROTVEC ) + VectorCopy( ceilingNormal, surfNormal ); + else + VectorCopy( pm->ps->grapplePoint, surfNormal ); + + //construct a vector which reflects the direction the player is looking wrt the surface normal + AngleVectors( wcl[ pm->ps->clientNum ].nonSvangles, forward, NULL, NULL ); + CrossProduct( forward, surfNormal, right ); + VectorNormalize( right ); + CrossProduct( surfNormal, right, movedir ); + VectorNormalize( movedir ); + + //rotate this direction vector based upon what direction the player is /trying/ to move to + /*if( pm->cmd.forwardmove || pm->cmd.rightmove ) + { + if( ( pm->cmd.rightmove < 0 ) && ( pm->cmd.forwardmove > 0 ) ) + RotatePointAroundVector( movedir, surfNormal, movedir, 45 ); + else if( ( pm->cmd.rightmove < 0 ) && ( pm->cmd.forwardmove == 0 ) ) + RotatePointAroundVector( movedir, surfNormal, movedir, 90 ); + else if( ( pm->cmd.rightmove < 0 ) && ( pm->cmd.forwardmove < 0 ) ) + RotatePointAroundVector( movedir, surfNormal, movedir, 135 ); + else if( ( pm->cmd.rightmove == 0 ) && ( pm->cmd.forwardmove < 0 ) ) + RotatePointAroundVector( movedir, surfNormal, movedir, 180 ); + else if( ( pm->cmd.rightmove > 0 ) && ( pm->cmd.forwardmove < 0 ) ) + RotatePointAroundVector( movedir, surfNormal, movedir, -135 ); + else if( ( pm->cmd.rightmove > 0 ) && ( pm->cmd.forwardmove == 0 ) ) + RotatePointAroundVector( movedir, surfNormal, movedir, -90 ); + else if( ( pm->cmd.rightmove > 0 ) && ( pm->cmd.forwardmove > 0 ) ) + RotatePointAroundVector( movedir, surfNormal, movedir, -45 ); + }*/ + + //TA: 3am, code make no sense, but code work... leave it be... + if( pm->cmd.forwardmove < 0 ) + VectorInverse( movedir ); + //RotatePointAroundVector( movedir, surfNormal, movedir, 180 ); + + for(i = 0; i <= 3; i++) + { + + switch ( i ) + { + case 0: + //trace into direction we are moving + VectorMA( pm->ps->origin, 0.25, movedir, point ); + pm->trace (&trace, pm->ps->origin, pm->mins, pm->maxs, point, pm->ps->clientNum, pm->tracemask); + break; + + case 1: + //trace straight down anto "ground" surface + VectorMA( pm->ps->origin, -0.25, surfNormal, point ); + pm->trace (&trace, pm->ps->origin, pm->mins, pm->maxs, point, pm->ps->clientNum, pm->tracemask); + break; + + case 2: + //trace "underneath" BBOX so we can traverse angles > 180deg + //TA: I can't believe this actually works :0 - its gotta be messing with something + // I would like a better way if one exists... + if( pml.groundPlane != qfalse ) + { + VectorMA( pm->ps->origin, -16, surfNormal, point ); + VectorMA( point, -16, movedir, point ); + pm->trace (&trace, pm->ps->origin, pm->mins, pm->maxs, point, pm->ps->clientNum, pm->tracemask); + } + break; + + case 3: + //fall back so we don't have to modify PM_GroundTrace too much + VectorCopy( pm->ps->origin, point ); + point[2] = pm->ps->origin[2] - 0.25; + pm->trace (&trace, pm->ps->origin, pm->mins, pm->maxs, point, pm->ps->clientNum, pm->tracemask); + break; + } + + //experimental: slow down speed around transitions + pm->ps->stats[ STAT_STATE ] &= ~SS_WALLTRANSIDING; + + //if we hit something + if( trace.fraction < 1.0 && !( trace.surfaceFlags & ( SURF_SKY | SURF_NOIMPACT ) ) && !( trace.entityNum != 1022 && i != 3 ) ) + { + if( i == 2 ) + VectorCopy( trace.endpos, pm->ps->origin ); + + //if the trace result and old surface normal are different then we must have transided to a new + //surface... do some stuff... + if( !VectorCompare( trace.plane.normal, surfNormal ) ) + { + //experimental: slow down speed around transitions + pm->ps->stats[ STAT_STATE ] |= SS_WALLTRANSIDING; + + //if the trace result or the old vector is not the floor or ceiling correct the YAW angle + if( !VectorCompare( trace.plane.normal, refNormal ) && !VectorCompare( surfNormal, refNormal ) && + !VectorCompare( trace.plane.normal, ceilingNormal ) && !VectorCompare( surfNormal, ceilingNormal ) ) + { + vectoangles( trace.plane.normal, toAngles ); + vectoangles( surfNormal, surfAngles ); + + pm->ps->delta_angles[1] -= ANGLE2SHORT( surfAngles[1] - toAngles[1] ); + } + + //transition from wall to ceiling + //normal for subsequent viewangle rotations + if( VectorCompare( trace.plane.normal, ceilingNormal ) ) + { + CrossProduct( surfNormal, trace.plane.normal, pm->ps->grapplePoint ); + VectorNormalize( pm->ps->grapplePoint ); + pm->ps->stats[ STAT_STATE ] |= SS_GPISROTVEC; + } + + //transition from ceiling to wall + //we need to do some different angle correction here cos GPISROTVEC + if( VectorCompare( surfNormal, ceilingNormal ) ) + { + vectoangles( trace.plane.normal, toAngles ); + vectoangles( pm->ps->grapplePoint, surfAngles ); + + pm->ps->delta_angles[1] -= ANGLE2SHORT( ( ( surfAngles[1] - toAngles[1] ) * 2 ) - 180 ); + } + + //TA: smooth transitions + CrossProduct( surfNormal, trace.plane.normal, srotAxis ); + VectorNormalize( srotAxis ); + srotAngle = abs( RAD2DEG( arccos( DotProduct( surfNormal, trace.plane.normal ) ) ) ); + + PM_AddSmoothOp( srotAxis, srotAngle ); + smoothed = qfalse; + } + + pml.groundTrace = trace; + + //so everything knows where we're wallclimbing + pm->ps->stats[ STAT_STATE ] |= SS_WALLCLIMBING; + pm->ps->legsAnim |= ANIM_WALLCLIMBING; + + //if we're not stuck to the ceiling then set grapplePoint to be a surface normal + if( !VectorCompare( trace.plane.normal, ceilingNormal ) ) + { + //so we know what surface we're stuck to + VectorCopy( trace.plane.normal, pm->ps->grapplePoint ); + pm->ps->stats[ STAT_STATE ] &= ~SS_GPISROTVEC; + } + + //so that surf -> empty space is smoothed + wcl[ pm->ps->clientNum ].justFallen = qtrue; + VectorCopy( pm->ps->grapplePoint, wcl[ pm->ps->clientNum ].lastNormal ); + + //IMPORTANT: break out of the for loop if we've hit something + break; + } + else if ( trace.allsolid ) + { + // do something corrective if the trace starts in a solid... + //TA: fuck knows what this does with all my new stuff :( + if ( !PM_CorrectAllSolid(&trace) ) + return; + } + } + + if ( trace.fraction >= 1.0 ) + { + // if the trace didn't hit anything, we are in free fall + //Com_Printf("trace missed\n"); + PM_GroundTraceMissed(); + pml.groundPlane = qfalse; + pml.walking = qfalse; + pm->ps->stats[ STAT_STATE ] &= ~SS_WALLCLIMBING; + pm->ps->legsAnim &= ~ANIM_WALLCLIMBING; + + if( wcl[ pm->ps->clientNum ].justFallen && !smoothed ) + { + if( pm->ps->stats[ STAT_STATE ] & SS_GPISROTVEC ) + { + //FIXME: need some delta-correction here + //pm->ps->delta_angles[1] -= ANGLE2SHORT( ( ( nonSvangles[1] - pm->ps->grapplePoint[1] ) * 2) ); + AngleVectors( pm->ps->viewangles, NULL, srotAxis, NULL ); + + srotAngle = 180; + } + else + { + CrossProduct( wcl[ pm->ps->clientNum ].lastNormal, refNormal, srotAxis ); + VectorNormalize( srotAxis ); + srotAngle = abs( RAD2DEG( arccos( DotProduct( refNormal, wcl[ pm->ps->clientNum ].lastNormal ) ) ) ); + } + + PM_AddSmoothOp( srotAxis, srotAngle ); + } + + pm->ps->stats[ STAT_STATE ] &= ~SS_GPISROTVEC; + + //we get very bizarre effects if we don't do this :0 + VectorCopy( refNormal, pm->ps->grapplePoint ); + wcl[ pm->ps->clientNum ].justFallen = qfalse; + return; + } + + pml.groundPlane = qtrue; + pml.walking = qtrue; + + // hitting solid ground will end a waterjump + if (pm->ps->pm_flags & PMF_TIME_WATERJUMP) + { + pm->ps->pm_flags &= ~(PMF_TIME_WATERJUMP | PMF_TIME_LAND); + pm->ps->pm_time = 0; + } + + pm->ps->groundEntityNum = trace.entityNum; + + // don't reset the z velocity for slopes +// pm->ps->velocity[2] = 0; + + PM_AddTouchEnt( trace.entityNum ); +} + + +/* +============= +PM_GroundTrace +============= +*/ +static void PM_GroundTrace( void ) { + vec3_t point, forward, srotAxis; + vec3_t refNormal = { 0, 0, 1 }; + vec3_t ceilingNormal = { 0, 0, -1 }; + trace_t trace; + float srotAngle; + + if( ( pm->ps->stats[ STAT_ABILITIES ] & SCA_WALLCLIMBER ) && ( pm->cmd.upmove < 0 ) ) + { + PM_GroundClimbTrace( ); + return; + } + + pm->ps->stats[ STAT_STATE ] &= ~SS_WALLCLIMBING; + pm->ps->legsAnim &= ~ANIM_WALLCLIMBING; + + //make sure that the surfNormal is reset to the ground + VectorCopy( refNormal, pm->ps->grapplePoint ); + + point[0] = pm->ps->origin[0]; + point[1] = pm->ps->origin[1]; + point[2] = pm->ps->origin[2] - 0.25; + + pm->trace (&trace, pm->ps->origin, pm->mins, pm->maxs, point, pm->ps->clientNum, pm->tracemask); + pml.groundTrace = trace; + + // do something corrective if the trace starts in a solid... + if ( trace.allsolid ) { + if ( !PM_CorrectAllSolid(&trace) ) + return; + } + + if( wcl[ pm->ps->clientNum ].justFallen ) + { + if( pm->ps->stats[ STAT_STATE ] & SS_GPISROTVEC ) + { + //FIXME: need some delta-correction here + //pm->ps->delta_angles[1] -= ANGLE2SHORT( ( ( nonSvangles[1] - pm->ps->grapplePoint[1] ) * 2) ); + AngleVectors( wcl[ pm->ps->clientNum ].nonSvangles, NULL, srotAxis, NULL ); + + srotAngle = 180; + } + else + { + CrossProduct( wcl[ pm->ps->clientNum ].lastNormal, refNormal, srotAxis ); + VectorNormalize( srotAxis ); + + srotAngle = abs( RAD2DEG( arccos( DotProduct( refNormal, wcl[ pm->ps->clientNum ].lastNormal ) ) ) ); + } + + PM_AddSmoothOp( srotAxis, srotAngle ); + } + + //things have already been smoothed.. + wcl[ pm->ps->clientNum ].justFallen = qfalse; + + // if the trace didn't hit anything, we are in free fall + if ( trace.fraction == 1.0 ) { + PM_GroundTraceMissed(); + pml.groundPlane = qfalse; + pml.walking = qfalse; + + pm->ps->stats[ STAT_STATE ] &= ~SS_GPISROTVEC; + + //we get very bizarre effects if we don't do this :0 + VectorCopy( refNormal, pm->ps->grapplePoint ); + + return; + } + + // check if getting thrown off the ground + if ( pm->ps->velocity[2] > 0 && DotProduct( pm->ps->velocity, trace.plane.normal ) > 10 ) { + if ( pm->debugLevel ) { + Com_Printf("%i:kickoff\n", c_pmove); + } + // go into jump animation + if ( pm->cmd.forwardmove >= 0 ) { + PM_ForceLegsAnim( LEGS_JUMP ); + pm->ps->pm_flags &= ~PMF_BACKWARDS_JUMP; + } else { + PM_ForceLegsAnim( LEGS_JUMPB ); + pm->ps->pm_flags |= PMF_BACKWARDS_JUMP; + } + + pm->ps->groundEntityNum = ENTITYNUM_NONE; + pml.groundPlane = qfalse; + pml.walking = qfalse; + return; + } + + // slopes that are too steep will not be considered onground + if ( trace.plane.normal[2] < MIN_WALK_NORMAL ) { + if ( pm->debugLevel ) { + Com_Printf("%i:steep\n", c_pmove); + } + // FIXME: if they can't slide down the slope, let them + // walk (sharp crevices) + pm->ps->groundEntityNum = ENTITYNUM_NONE; + pml.groundPlane = qtrue; + pml.walking = qfalse; + return; + } + + pml.groundPlane = qtrue; + pml.walking = qtrue; + + // hitting solid ground will end a waterjump + if (pm->ps->pm_flags & PMF_TIME_WATERJUMP) + { + pm->ps->pm_flags &= ~(PMF_TIME_WATERJUMP | PMF_TIME_LAND); + pm->ps->pm_time = 0; + } + + if ( pm->ps->groundEntityNum == ENTITYNUM_NONE ) { + // just hit the ground + if ( pm->debugLevel ) { + Com_Printf("%i:Land\n", c_pmove); + } + + if( pm->ps->stats[ STAT_ABILITIES ] & SCA_TAKESFALLDAMAGE ) + PM_CrashLand(); + + // don't do landing time if we were just going down a slope + if ( pml.previous_velocity[2] < -200 ) { + // don't allow another jump for a little while + pm->ps->pm_flags |= PMF_TIME_LAND; + pm->ps->pm_time = 250; + } + } + + pm->ps->groundEntityNum = trace.entityNum; + + // don't reset the z velocity for slopes +// pm->ps->velocity[2] = 0; + + PM_AddTouchEnt( trace.entityNum ); +} + + +/* +============= +PM_SetWaterLevel FIXME: avoid this twice? certainly if not moving +============= +*/ +static void PM_SetWaterLevel( void ) { + vec3_t point; + int cont; + int sample1; + int sample2; + + // + // get waterlevel, accounting for ducking + // + pm->waterlevel = 0; + pm->watertype = 0; + + point[0] = pm->ps->origin[0]; + point[1] = pm->ps->origin[1]; + point[2] = pm->ps->origin[2] + MINS_Z + 1; + cont = pm->pointcontents( point, pm->ps->clientNum ); + + if ( cont & MASK_WATER ) { + sample2 = pm->ps->viewheight - MINS_Z; + sample1 = sample2 / 2; + + pm->watertype = cont; + pm->waterlevel = 1; + point[2] = pm->ps->origin[2] + MINS_Z + sample1; + cont = pm->pointcontents (point, pm->ps->clientNum ); + if ( cont & MASK_WATER ) { + pm->waterlevel = 2; + point[2] = pm->ps->origin[2] + MINS_Z + sample2; + cont = pm->pointcontents (point, pm->ps->clientNum ); + if ( cont & MASK_WATER ){ + pm->waterlevel = 3; + } + } + } + +} + + + +/* +============== +PM_CheckDuck + +Sets mins, maxs, and pm->ps->viewheight +============== +*/ +static void PM_CheckDuck (void) +{ + trace_t trace; + vec3_t PCmins, PCmaxs, PCcmaxs; + int PCvh, PCcvh; + + switch( pm->ps->stats[ STAT_PCLASS ] ) + { + case PCL_D_BUILDER: + VectorSet( PCmins, -15, -15, -20 ); + VectorSet( PCmaxs, 15, 15, 20 ); + VectorSet( PCcmaxs, 15, 15, 20 ); + PCvh = 12; + PCcvh = 12; + break; + + case PCL_D_BASE: + VectorSet( PCmins, -15, -15, -15 ); + VectorSet( PCmaxs, 15, 15, 15 ); + VectorSet( PCcmaxs, 15, 15, 15 ); + PCvh = 4; + PCcvh = 4; + break; + + case PCL_H_BASE: + VectorSet( PCmins, -15, -15, -24 ); + VectorSet( PCmaxs, 15, 15, 32 ); + VectorSet( PCcmaxs, 15, 15, 16 ); + PCvh = 26; + PCcvh = 12; + break; + + default: + VectorSet( PCmins, -15, -15, MINS_Z ); + VectorSet( PCmaxs, 15, 15, 32 ); + VectorSet( PCcmaxs, 15, 15, 16 ); + PCvh = DEFAULT_VIEWHEIGHT; + PCcvh = CROUCH_VIEWHEIGHT; + } + + //TA: iD bug? you can still crouch when you're a spectator + if( pm->ps->persistant[PERS_TEAM] == TEAM_SPECTATOR ) + PCcvh = PCvh; + + pm->mins[0] = PCmins[0]; + pm->mins[1] = PCmins[1]; + + pm->maxs[0] = PCmaxs[0]; + pm->maxs[1] = PCmaxs[1]; + + pm->mins[2] = PCmins[2]; + + if (pm->ps->pm_type == PM_DEAD) + { + pm->maxs[2] = -8; + pm->ps->viewheight = DEAD_VIEWHEIGHT; + return; + } + + //TA: If the standing and crouching viewheights are the same the class can't crouch + if ( ( pm->cmd.upmove < 0 ) && ( PCvh != PCcvh ) ) + { // duck + pm->ps->pm_flags |= PMF_DUCKED; + } + else + { // stand up if possible + if (pm->ps->pm_flags & PMF_DUCKED) + { + // try to stand up + pm->maxs[2] = PCmaxs[2]; + pm->trace (&trace, pm->ps->origin, pm->mins, pm->maxs, pm->ps->origin, pm->ps->clientNum, pm->tracemask ); + if (!trace.allsolid) + pm->ps->pm_flags &= ~PMF_DUCKED; + } + } + + if (pm->ps->pm_flags & PMF_DUCKED) + { + pm->maxs[2] = PCcmaxs[2]; + pm->ps->viewheight = PCcvh; + } + else + { + pm->maxs[2] = PCmaxs[2]; + pm->ps->viewheight = PCvh; + } +} + + + +//=================================================================== + + +/* +=============== +PM_Footsteps +=============== +*/ +static void PM_Footsteps( void ) { + float bobmove; + int old; + qboolean footstep; + + // + // calculate speed and cycle to be used for + // all cyclic walking effects + // + if( ( pm->ps->stats[STAT_ABILITIES] & SCA_WALLCLIMBER ) && ( pml.groundPlane ) ) + { + pm->xyspeed = DotProduct(pm->ps->velocity, pml.groundTrace.plane.normal); + } + else + pm->xyspeed = sqrt( pm->ps->velocity[0] * pm->ps->velocity[0] + + pm->ps->velocity[1] * pm->ps->velocity[1] ); + + if ( pm->ps->groundEntityNum == ENTITYNUM_NONE ) { + // airborne leaves position in cycle intact, but doesn't advance + if ( pm->waterlevel > 1 ) { + PM_ContinueLegsAnim( LEGS_SWIM ); + } + return; + } + + // if not trying to move + if ( !pm->cmd.forwardmove && !pm->cmd.rightmove ) { + if ( pm->xyspeed < 5 ) { + pm->ps->bobCycle = 0; // start at beginning of cycle again + if ( pm->ps->pm_flags & PMF_DUCKED ) { + PM_ContinueLegsAnim( LEGS_IDLECR ); + } else { + PM_ContinueLegsAnim( LEGS_IDLE ); + } + } + return; + } + + + footstep = qfalse; + + if ( pm->ps->pm_flags & PMF_DUCKED ) { + bobmove = 0.5; // ducked characters bob much faster + if ( pm->ps->pm_flags & PMF_BACKWARDS_RUN ) { + PM_ContinueLegsAnim( LEGS_BACKCR ); + } + else { + PM_ContinueLegsAnim( LEGS_WALKCR ); + } + // ducked characters never play footsteps + /* + } else if ( pm->ps->pm_flags & PMF_BACKWARDS_RUN ) { + if ( !( pm->cmd.buttons & BUTTON_WALKING ) ) { + bobmove = 0.4; // faster speeds bob faster + footstep = qtrue; + } else { + bobmove = 0.3; + } + PM_ContinueLegsAnim( LEGS_BACK ); + */ + } else { + //TA: switch walking/running anims based on speed + //if ( !( pm->cmd.buttons & BUTTON_WALKING ) ) { + if ( pm->xyspeed > 160 ) { + bobmove = 0.4f; // faster speeds bob faster + if ( pm->ps->pm_flags & PMF_BACKWARDS_RUN ) { + PM_ContinueLegsAnim( LEGS_BACK ); + } + else { + PM_ContinueLegsAnim( LEGS_RUN ); + } + footstep = qtrue; + } else { + bobmove = 0.3f; // walking bobs slow + if ( pm->ps->pm_flags & PMF_BACKWARDS_RUN ) { + PM_ContinueLegsAnim( LEGS_BACKWALK ); + } + else { + PM_ContinueLegsAnim( LEGS_WALK ); + } + } + } + + // check for footstep / splash sounds + old = pm->ps->bobCycle; + pm->ps->bobCycle = (int)( old + bobmove * pml.msec ) & 255; + + // if we just crossed a cycle boundary, play an apropriate footstep event + if ( ( ( old + 64 ) ^ ( pm->ps->bobCycle + 64 ) ) & 128 ) { + if ( pm->waterlevel == 0 ) { + // on ground will only play sounds if running + if ( footstep && !pm->noFootsteps ) { + PM_AddEvent( PM_FootstepForSurface() ); + } + } else if ( pm->waterlevel == 1 ) { + // splashing + PM_AddEvent( EV_FOOTSPLASH ); + } else if ( pm->waterlevel == 2 ) { + // wading / swimming at surface + PM_AddEvent( EV_SWIM ); + } else if ( pm->waterlevel == 3 ) { + // no sound when completely underwater + + } + } +} + +/* +============== +PM_WaterEvents + +Generate sound events for entering and leaving water +============== +*/ +static void PM_WaterEvents( void ) { // FIXME? + // + // if just entered a water volume, play a sound + // + if (!pml.previous_waterlevel && pm->waterlevel) { + PM_AddEvent( EV_WATER_TOUCH ); + } + + // + // if just completely exited a water volume, play a sound + // + if (pml.previous_waterlevel && !pm->waterlevel) { + PM_AddEvent( EV_WATER_LEAVE ); + } + + // + // check for head just going under water + // + if (pml.previous_waterlevel != 3 && pm->waterlevel == 3) { + PM_AddEvent( EV_WATER_UNDER ); + } + + // + // check for head just coming out of water + // + if (pml.previous_waterlevel == 3 && pm->waterlevel != 3) { + PM_AddEvent( EV_WATER_CLEAR ); + } +} + + +/* +=============== +PM_BeginWeaponChange +=============== +*/ +static void PM_BeginWeaponChange( int weapon ) { + if ( weapon <= WP_NONE || weapon >= WP_NUM_WEAPONS ) { + return; + } + + if ( !BG_gotWeapon( weapon, pm->ps->stats ) ) { + return; + } + + if ( pm->ps->weaponstate == WEAPON_DROPPING ) { + return; + } + + PM_AddEvent( EV_CHANGE_WEAPON ); + pm->ps->weaponstate = WEAPON_DROPPING; + pm->ps->weaponTime += 200; + PM_StartTorsoAnim( TORSO_DROP ); +} + + +/* +=============== +PM_FinishWeaponChange +=============== +*/ +static void PM_FinishWeaponChange( void ) { + int weapon; + + weapon = pm->cmd.weapon; + if ( weapon < WP_NONE || weapon >= WP_NUM_WEAPONS ) { + weapon = WP_NONE; + } + + if ( !BG_gotWeapon( weapon, pm->ps->stats ) ) { + weapon = WP_NONE; + } + + pm->ps->weapon = weapon; + pm->ps->weaponstate = WEAPON_RAISING; + pm->ps->weaponTime += 250; + PM_StartTorsoAnim( TORSO_RAISE ); +} + + +/* +============== +PM_TorsoAnimation + +============== +*/ +static void PM_TorsoAnimation( void ) { + if ( pm->ps->weaponstate == WEAPON_READY ) { + if ( pm->ps->weapon == WP_GAUNTLET ) { + PM_ContinueTorsoAnim( TORSO_STAND2 ); + } else { + PM_ContinueTorsoAnim( TORSO_STAND ); + } + return; + } +} + + +/* +============== +PM_Weapon + +Generates weapon events and modifes the weapon counter +============== +*/ +static void PM_Weapon( void ) { + int addTime; + int ammo, clips, maxclips; + static int chainGunAddTime; + + // don't allow attack until all buttons are up + if ( pm->ps->pm_flags & PMF_RESPAWNED ) { + return; + } + + // ignore if spectator + if ( pm->ps->persistant[PERS_TEAM] == TEAM_SPECTATOR ) { + return; + } + + // check for dead player + if ( pm->ps->stats[STAT_HEALTH] <= 0 ) { + pm->ps->weapon = WP_NONE; + return; + } + + // check for item using + //TA: not using q3 holdable item code + /*if ( pm->cmd.buttons & BUTTON_USE_HOLDABLE ) + { + if ( ! ( pm->ps->pm_flags & PMF_USE_ITEM_HELD ) ) + { + if ( bg_itemlist[pm->ps->stats[STAT_HOLDABLE_ITEM]].giTag == HI_MEDKIT + && pm->ps->stats[STAT_HEALTH] >= pm->ps->stats[STAT_MAX_HEALTH] ) + { + // don't use medkit if at max health + } + else + { + pm->ps->pm_flags |= PMF_USE_ITEM_HELD; + PM_AddEvent( EV_USE_ITEM0 + bg_itemlist[pm->ps->stats[STAT_HOLDABLE_ITEM]].giTag ); + pm->ps->stats[STAT_HOLDABLE_ITEM] = 0; + } + return; + } + } + else + { + pm->ps->pm_flags &= ~PMF_USE_ITEM_HELD; + }*/ + + + // make weapon function + if ( pm->ps->weaponTime > 0 ) { + pm->ps->weaponTime -= pml.msec; + } + + // check for weapon change + // can't change if weapon is firing, but can change + // again if lowering or raising + if ( pm->ps->weaponTime <= 0 || pm->ps->weaponstate != WEAPON_FIRING ) { + //TA: must press use to switch weapons + if( pm->cmd.buttons & BUTTON_USE_HOLDABLE ) + { + if( !( pm->ps->pm_flags & PMF_USE_ITEM_HELD ) ) + { + if( pm->cmd.weapon <= 32 ) + { + //if trying to select a weapon, select it + if ( pm->ps->weapon != pm->cmd.weapon ) + PM_BeginWeaponChange( pm->cmd.weapon ); + } + else if( pm->cmd.weapon > 32 ) + { + //if trying to toggle an upgrade, toggle it + if( BG_gotItem( pm->cmd.weapon - 32, pm->ps->stats ) ) //sanity check + { + if( BG_activated( pm->cmd.weapon - 32, pm->ps->stats ) ) + { + BG_deactivateItem( pm->cmd.weapon - 32, pm->ps->stats ); + } + else + { + BG_activateItem( pm->cmd.weapon - 32, pm->ps->stats ); + } + } + } + pm->ps->pm_flags |= PMF_USE_ITEM_HELD; + } + } + else + pm->ps->pm_flags &= ~PMF_USE_ITEM_HELD; + } + + if ( pm->ps->weaponTime > 0 ) { + return; + } + + // change weapon if time + if ( pm->ps->weaponstate == WEAPON_DROPPING ) { + PM_FinishWeaponChange(); + return; + } + + if ( pm->ps->weaponstate == WEAPON_RAISING ) { + pm->ps->weaponstate = WEAPON_READY; + if ( pm->ps->weapon == WP_GAUNTLET ) { + PM_StartTorsoAnim( TORSO_STAND2 ); + } else { + PM_StartTorsoAnim( TORSO_STAND ); + } + return; + } + + if( pm->ps->weapon == WP_SCANNER ) + return; //doesn't actually do anything + + // check for fire + if ( ! (pm->cmd.buttons & BUTTON_ATTACK) ) { + pm->ps->weaponTime = 0; + pm->ps->weaponstate = WEAPON_READY; + chainGunAddTime = 120; + return; + } + + // start the animation even if out of ammo + if ( pm->ps->weapon == WP_GAUNTLET ) { + // the guantlet only "fires" when it actually hits something + if ( !pm->gauntletHit ) { + pm->ps->weaponTime = 0; + pm->ps->weaponstate = WEAPON_READY; + return; + } + PM_StartTorsoAnim( TORSO_ATTACK2 ); + } else { + PM_StartTorsoAnim( TORSO_ATTACK ); + } + + BG_unpackAmmoArray( pm->ps->weapon, pm->ps->ammo, pm->ps->powerups, &ammo, &clips, &maxclips ); + + // check for out of ammo + if ( !ammo && !clips && !BG_infiniteAmmo( pm->ps->weapon ) ) { + PM_AddEvent( EV_NOAMMO ); + pm->ps->weaponTime += 200; + return; + } + + //done reloading so give em some ammo + if( pm->ps->weaponstate == WEAPON_RELOADING ) + { + + switch( pm->ps->weapon ) + { + default: + case WP_MACHINEGUN: + clips--; + ammo = CS_MG; + break; + + } + + BG_packAmmoArray( pm->ps->weapon, pm->ps->ammo, pm->ps->powerups, ammo, clips, maxclips ); + } + + // check for end of clip + if ( !ammo && clips ) + { + pm->ps->weaponstate = WEAPON_RELOADING; + + switch( pm->ps->weapon ) + { + default: + case WP_MACHINEGUN: + addTime = 2000; + break; + + } + + pm->ps->weaponTime += addTime; + return; + } + + pm->ps->weaponstate = WEAPON_FIRING; + + // take an ammo away if not infinite + if( !BG_infiniteAmmo( pm->ps->weapon ) ) + { + ammo--; + BG_packAmmoArray( pm->ps->weapon, pm->ps->ammo, pm->ps->powerups, ammo, clips, maxclips ); + } + + // fire weapon + PM_AddEvent( EV_FIRE_WEAPON ); + + switch( pm->ps->weapon ) { + default: + case WP_GAUNTLET: + addTime = 400; + break; + case WP_LIGHTNING: + addTime = 50; + break; + case WP_SHOTGUN: + addTime = 1000; + break; + case WP_MACHINEGUN: + addTime = 100; + break; + case WP_CHAINGUN: + if( chainGunAddTime > 30 ) chainGunAddTime -= 2; + addTime = chainGunAddTime; + break; + case WP_GRENADE_LAUNCHER: + addTime = 800; + break; + case WP_ROCKET_LAUNCHER: + addTime = 800; + break; + case WP_FLAMER: + addTime = 80; + break; + case WP_RAILGUN: + addTime = 1500; + break; + case WP_BFG: + addTime = 200; + break; + case WP_GRAPPLING_HOOK: + addTime = 400; + break; + case WP_VENOM: + addTime = 200; + break; + case WP_ABUILD: + addTime = 1000; + break; + case WP_HBUILD: + addTime = 1000; + break; + case WP_SCANNER: + addTime = 1000; //abritutary since scaner doesn't "fire" + break; + } + + /*if ( pm->ps->powerups[PW_HASTE] ) { + addTime /= 1.3; + }*/ + + if( pm->ps->weapon == WP_CHAINGUN ) + { + if( pm->ps->pm_flags & PMF_DUCKED ) + { + pm->ps->delta_angles[ PITCH ] -= ANGLE2SHORT( ( ( random() * 0.5 ) - 0.125 ) * ( 30 / (float)addTime ) ); + pm->ps->delta_angles[ YAW ] -= ANGLE2SHORT( ( ( random() * 0.5 ) - 0.25 ) * ( 30.0 / (float)addTime ) ); + } + else + { + pm->ps->delta_angles[ PITCH ] -= ANGLE2SHORT( ( ( random() * 8 ) - 2 ) * ( 30.0 / (float)addTime ) ); + pm->ps->delta_angles[ YAW ] -= ANGLE2SHORT( ( ( random() * 8 ) - 4 ) * ( 30.0 / (float)addTime ) ); + } + } + + pm->ps->weaponTime += addTime; +} + +/* +================ +PM_Animate +================ +*/ +static void PM_Animate( void ) { + if ( pm->cmd.buttons & BUTTON_GESTURE ) { + if ( pm->ps->torsoTimer == 0 ) { + PM_StartTorsoAnim( TORSO_GESTURE ); + pm->ps->torsoTimer = TIMER_GESTURE; + PM_AddEvent( EV_TAUNT ); + } + } +} + + +/* +================ +PM_DropTimers +================ +*/ +static void PM_DropTimers( void ) { + // drop misc timing counter + if ( pm->ps->pm_time ) { + if ( pml.msec >= pm->ps->pm_time ) { + pm->ps->pm_flags &= ~PMF_ALL_TIMES; + pm->ps->pm_time = 0; + } else { + pm->ps->pm_time -= pml.msec; + } + } + + // drop animation counter + if ( pm->ps->legsTimer > 0 ) { + pm->ps->legsTimer -= pml.msec; + if ( pm->ps->legsTimer < 0 ) { + pm->ps->legsTimer = 0; + } + } + + if ( pm->ps->torsoTimer > 0 ) { + pm->ps->torsoTimer -= pml.msec; + if ( pm->ps->torsoTimer < 0 ) { + pm->ps->torsoTimer = 0; + } + } +} + + +/* +================ +PM_UpdateViewAngles + +This can be used as another entry point when only the viewangles +are being updated isntead of a full move +================ +*/ +void PM_UpdateViewAngles( playerState_t *ps, const usercmd_t *cmd ) { + short temp[3]; + int i; + vec3_t surfNormal, xNormal; + vec3_t axis[3], rotaxis[3], smoothaxis[3]; + vec3_t refNormal = { 0, 0, 1 }; + vec3_t ceilingNormal = { 0, 0, -1 }; + float rotAngle; + vec3_t tempang, tempang2; + + if ( ps->pm_type == PM_INTERMISSION || ps->pm_type == PM_SPINTERMISSION ) { + return; // no view changes at all + } + + if ( ps->pm_type != PM_SPECTATOR && ps->stats[STAT_HEALTH] <= 0 ) { + return; // no view changes at all + } + + // circularly clamp the angles with deltas + for (i=0 ; i<3 ; i++) { + temp[i] = cmd->angles[i] + ps->delta_angles[i]; + + if ( i == PITCH ) { + // don't let the player look up or down more than 90 degrees + if ( temp[i] > 16000 ) { + ps->delta_angles[i] = 16000 - cmd->angles[i]; + temp[i] = 16000; + } else if ( temp[i] < -16000 ) { + ps->delta_angles[i] = -16000 - cmd->angles[i]; + temp[i] = -16000; + } + } + tempang[i] = SHORT2ANGLE( temp[i] ); + } + + /*Com_Printf( "%1.2f ", ps->grapplePoint[ 0 ] ); + Com_Printf( "%1.2f ", ps->grapplePoint[ 1 ] ); + Com_Printf( "%1.2f ", ps->grapplePoint[ 2 ] ); + Com_Printf( "\n" );*/ + + //convert viewangles -> axis + AnglesToAxis( tempang, axis ); + + //the grapplePoint being a surfNormal rotation Normal hack... see above :) + if( ps->stats[ STAT_STATE ] & SS_GPISROTVEC ) + { + VectorCopy( ceilingNormal, surfNormal ); + VectorCopy( ps->grapplePoint, xNormal ); + } + else + { + //cross the reference normal and the surface normal to get the rotation axis + VectorCopy( ps->grapplePoint, surfNormal ); + if( surfNormal[2] <= 0 ) + { + CrossProduct( refNormal, surfNormal, xNormal ); + } + else + { + CrossProduct( surfNormal, refNormal, xNormal ); + } + + VectorNormalize( xNormal ); + } + + //if we're a wall climber.. and we're climbing rotate the axis + if( ( pm->ps->stats[ STAT_ABILITIES ] & SCA_WALLCLIMBER ) && + ( pm->ps->stats[ STAT_STATE ] & SS_WALLCLIMBING ) && + ( VectorLength( xNormal ) != 0 ) ) + { + //if the normal pointing straight down then the rotAngle will always be 180deg + if( surfNormal[2] == -1 ) + rotAngle = 180; + /*else if( abs( RAD2DEG( arccos( DotProduct( ps->grapplePoint, surfNormal ) ) ) ) <= 90 ) + { + //WEIRD-BUG_WORKAROUND: + //for some reason surface normals on curves in a specfic quadrant are inverted.. dunno why + VectorInverse( surfNormal ); + rotAngle = -RAD2DEG( arccos( DotProduct( surfNormal, refNormal ) ) ); + }*/ + else + rotAngle = -RAD2DEG( arccos( DotProduct( surfNormal, refNormal ) ) ); + + //hmmm could get away with only one rotation and some clever stuff later... but i'm lazy + RotatePointAroundVector( rotaxis[0], xNormal, axis[0], rotAngle ); + RotatePointAroundVector( rotaxis[1], xNormal, axis[1], rotAngle ); + RotatePointAroundVector( rotaxis[2], xNormal, axis[2], rotAngle ); + //MAJOR FIXME: try to reduce the number of vector rotations.. they kill the QVM + } + else + { + //we can't wall climb/aren't wall climbing + rotAngle = 0; + AxisCopy( axis, rotaxis ); + } + + AxisToAngles( rotaxis, wcl[ pm->ps->clientNum ].nonSvangles ); + + //force angles to -180 <= x <= 180 + for( i = 0; i < 3; i++ ) + { + while( wcl[ pm->ps->clientNum ].nonSvangles[ i ] > 180 ) + wcl[ pm->ps->clientNum ].nonSvangles[ i ] -= 360; + + while( wcl[ pm->ps->clientNum ].nonSvangles[ i ] < 180 ) + wcl[ pm->ps->clientNum ].nonSvangles[ i ] += 360; + } + //AnglesSubtract( wcl[ pm->ps->clientNum ].nonSvangles, 0, wcl[ pm->ps->clientNum ].nonSvangles ); + + //smooth transitions + if( !PM_PerformSmoothOps( rotaxis, smoothaxis ) ) + AxisCopy( rotaxis, smoothaxis ); + + //convert the new axis back to angles + AxisToAngles( smoothaxis, tempang2 ); + + //force angles to -180 <= x <= 180 + //AnglesSubtract( tempang2, 0, tempang2 ); + for( i = 0; i < 3; i++ ) + { + while( tempang2[ i ] > 180 ) + tempang2[ i ] -= 360; + + while( tempang2[ i ] < 180 ) + tempang2[ i ] += 360; + } + + //actually set the viewangles + for (i=0 ; i<3 ; i++) { + ps->viewangles[i] = tempang2[i]; + } +} + + +/* +================ +PmoveSingle + +================ +*/ +void trap_SnapVector( float *v ); + +void PmoveSingle (pmove_t *pmove) +{ + int ammo, clips, maxclips; + pm = pmove; + + BG_unpackAmmoArray( pm->ps->weapon, pm->ps->ammo, pm->ps->powerups, &ammo, &clips, &maxclips ); + + // this counter lets us debug movement problems with a journal + // by setting a conditional breakpoint fot the previous frame + c_pmove++; + + // clear results + pm->numtouch = 0; + pm->watertype = 0; + pm->waterlevel = 0; + + if ( pm->ps->stats[STAT_HEALTH] <= 0 ) { + pm->tracemask &= ~CONTENTS_BODY; // corpses can fly through bodies + } + + // make sure walking button is clear if they are running, to avoid + // proxy no-footsteps cheats + if ( abs( pm->cmd.forwardmove ) > 64 || abs( pm->cmd.rightmove ) > 64 ) { + pm->cmd.buttons &= ~BUTTON_WALKING; + } + + // set the talk balloon flag + if ( pm->cmd.buttons & BUTTON_TALK ) { + pm->ps->eFlags |= EF_TALK; + } else { + pm->ps->eFlags &= ~EF_TALK; + } + + // set the firing flag for continuous beam weapons + if ( !(pm->ps->pm_flags & PMF_RESPAWNED) && pm->ps->pm_type != PM_INTERMISSION + && ( pm->cmd.buttons & BUTTON_ATTACK ) && ( ammo > 0 || clips > 0 ) ) { + pm->ps->eFlags |= EF_FIRING; + } else { + pm->ps->eFlags &= ~EF_FIRING; + } + + // clear the respawned flag if attack and use are cleared + if ( pm->ps->stats[STAT_HEALTH] > 0 && + !( pm->cmd.buttons & (BUTTON_ATTACK | BUTTON_USE_HOLDABLE) ) ) { + pm->ps->pm_flags &= ~PMF_RESPAWNED; + } + + // if talk button is down, dissallow all other input + // this is to prevent any possible intercept proxy from + // adding fake talk balloons + if ( pmove->cmd.buttons & BUTTON_TALK ) { + pmove->cmd.buttons = BUTTON_TALK; + pmove->cmd.forwardmove = 0; + pmove->cmd.rightmove = 0; + pmove->cmd.upmove = 0; + } + + // clear all pmove local vars + memset (&pml, 0, sizeof(pml)); + + // determine the time + pml.msec = pmove->cmd.serverTime - pm->ps->commandTime; + if ( pml.msec < 1 ) { + pml.msec = 1; + } else if ( pml.msec > 200 ) { + pml.msec = 200; + } + pm->ps->commandTime = pmove->cmd.serverTime; + + // save old org in case we get stuck + VectorCopy (pm->ps->origin, pml.previous_origin); + + // save old velocity for crashlanding + VectorCopy (pm->ps->velocity, pml.previous_velocity); + + pml.frametime = pml.msec * 0.001; + + AngleVectors (pm->ps->viewangles, pml.forward, pml.right, pml.up); + + if ( pm->cmd.upmove < 10 ) { + // not holding jump + pm->ps->pm_flags &= ~PMF_JUMP_HELD; + } + + // decide if backpedaling animations should be used + if ( pm->cmd.forwardmove < 0 ) { + pm->ps->pm_flags |= PMF_BACKWARDS_RUN; + } else if ( pm->cmd.forwardmove > 0 || ( pm->cmd.forwardmove == 0 && pm->cmd.rightmove ) ) { + pm->ps->pm_flags &= ~PMF_BACKWARDS_RUN; + } + + if ( pm->ps->pm_type >= PM_DEAD ) { + pm->cmd.forwardmove = 0; + pm->cmd.rightmove = 0; + pm->cmd.upmove = 0; + } + + if ( pm->ps->pm_type == PM_SPECTATOR ) { + // update the viewangles + PM_UpdateViewAngles( pm->ps, &pm->cmd ); + PM_CheckDuck (); + PM_FlyMove (); + PM_DropTimers (); + return; + } + + if ( pm->ps->pm_type == PM_NOCLIP ) { + PM_NoclipMove (); + PM_DropTimers (); + return; + } + + if (pm->ps->pm_type == PM_FREEZE) { + return; // no movement at all + } + + if ( pm->ps->pm_type == PM_INTERMISSION || pm->ps->pm_type == PM_SPINTERMISSION ) { + return; // no movement at all + } + + // set watertype, and waterlevel + PM_SetWaterLevel(); + pml.previous_waterlevel = pmove->waterlevel; + + // set mins, maxs, and viewheight + PM_CheckDuck (); + + // set groundentity + PM_GroundTrace(); + // update the viewangles + PM_UpdateViewAngles( pm->ps, &pm->cmd ); + + if ( pm->ps->pm_type == PM_DEAD ) { + PM_DeadMove (); + } + + PM_DropTimers(); + + /*if ( pm->ps->powerups[PW_FLIGHT] ) { + // flight powerup doesn't allow jump and has different friction + PM_FlyMove(); + } else*/ if (pm->ps->pm_flags & PMF_GRAPPLE_PULL) { + PM_GrappleMove(); + // We can wiggle a bit + PM_AirMove(); + } else if (pm->ps->pm_flags & PMF_TIME_WATERJUMP) { + PM_WaterJumpMove(); + } else if ( pm->waterlevel > 1 ) { + // swimming + PM_WaterMove(); + } else if ( pml.walking ) { + if( ( pm->ps->stats[ STAT_ABILITIES ] & SCA_WALLCLIMBER ) && + ( pm->ps->stats[ STAT_STATE ] & SS_WALLCLIMBING ) ) + PM_ClimbMove(); //TA: walking on any surface + else + PM_WalkMove(); // walking on ground + } else { + // airborne + PM_AirMove(); + } + + PM_Animate(); + + // set groundentity, watertype, and waterlevel + PM_GroundTrace(); + //TA: must update after every GroundTrace() - yet more clock cycles down the drain :( (14 vec rotations/frame) + // update the viewangles + PM_UpdateViewAngles( pm->ps, &pm->cmd ); + + PM_SetWaterLevel(); + + // weapons + PM_Weapon(); + + // torso animation + PM_TorsoAnimation(); + + // footstep events / legs animations + PM_Footsteps(); + + // entering / leaving water splashes + PM_WaterEvents(); + + // snap some parts of playerstate to save network bandwidth + trap_SnapVector( pm->ps->velocity ); +} + + +/* +================ +Pmove + +Can be called by either the server or the client +================ +*/ +void Pmove (pmove_t *pmove) { + int finalTime; + + finalTime = pmove->cmd.serverTime; + + if ( finalTime < pmove->ps->commandTime ) { + return; // should not happen + } + + if ( finalTime > pmove->ps->commandTime + 1000 ) { + pmove->ps->commandTime = finalTime - 1000; + } + + pmove->ps->pmove_framecount = (pmove->ps->pmove_framecount+1) & ((1<<PS_PMOVEFRAMECOUNTBITS)-1); + + // chop the move up if it is too long, to prevent framerate + // dependent behavior + while ( pmove->ps->commandTime != finalTime ) { + int msec; + + msec = finalTime - pmove->ps->commandTime; + + if ( pmove->pmove_fixed ) { + if ( msec > pmove->pmove_msec ) { + msec = pmove->pmove_msec; + } + } + else { + if ( msec > 66 ) { + msec = 66; + } + } + + + pmove->cmd.serverTime = pmove->ps->commandTime + msec; + PmoveSingle( pmove ); + + if ( pmove->ps->pm_flags & PMF_JUMP_HELD ) { + pmove->cmd.upmove = 20; + } + } + +} diff --git a/src/game/bg_public.h b/src/game/bg_public.h new file mode 100644 index 00000000..fb8ffc66 --- /dev/null +++ b/src/game/bg_public.h @@ -0,0 +1,783 @@ +// Copyright (C) 1999-2000 Id Software, Inc. +// +// bg_public.h -- definitions shared by both the server game and client game modules + +/* + * Portions Copyright (C) 2000-2001 Tim Angus + * + * This program 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, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ + +/* To assertain which portions are licensed under the GPL and which are + * licensed by Id Software, Inc. please run a diff between the equivalent + * versions of the "Tremulous" modification and the unmodified "Quake3" + * game source code. + */ + +// because games can change separately from the main system version, we need a +// second version that must match between game and cgame +#define GAME_VERSION "baseq3-1" + +#define DEFAULT_GRAVITY 800 +#define GIB_HEALTH -40 +#define ARMOR_PROTECTION 0.66 + +#define MAX_ITEMS 256 + +#define RANK_TIED_FLAG 0x4000 + +#define DEFAULT_SHOTGUN_SPREAD 700 +#define DEFAULT_SHOTGUN_COUNT 11 + +#define ITEM_RADIUS 15 // item sizes are needed for client side pickup detection + +#define LIGHTNING_RANGE 768 + +#define SCORE_NOT_PRESENT -9999 // for the CS_SCORES[12] when only one player is present + +#define VOTE_TIME 30000 // 30 seconds before vote times out + +#define MINS_Z -24 +#define DEFAULT_VIEWHEIGHT 26 +#define CROUCH_VIEWHEIGHT 12 +#define DEAD_VIEWHEIGHT -16 + +// +// config strings are a general means of communicating variable length strings +// from the server to all connected clients. +// + +// CS_SERVERINFO and CS_SYSTEMINFO are defined in q_shared.h +#define CS_MUSIC 2 +#define CS_MESSAGE 3 // from the map worldspawn's message field +#define CS_MOTD 4 // g_motd string for server message of the day +#define CS_WARMUP 5 // server time when the match will be restarted +#define CS_SCORES1 6 +#define CS_SCORES2 7 +#define CS_VOTE_TIME 8 +#define CS_VOTE_STRING 9 +#define CS_VOTE_YES 10 +#define CS_VOTE_NO 11 + +#define CS_TEAMVOTE_TIME 12 +#define CS_TEAMVOTE_STRING 14 +#define CS_TEAMVOTE_YES 16 +#define CS_TEAMVOTE_NO 18 + +#define CS_GAME_VERSION 20 +#define CS_LEVEL_START_TIME 21 // so the timer only shows the current level +#define CS_INTERMISSION 22 // when 1, fraglimit/timelimit has been hit and intermission will start in a second or two +#define CS_FLAGSTATUS 23 // string indicating flag status in CTF +#define CS_SHADERSTATE 24 +#define CS_BOTINFO 25 + +#define CS_ITEMS 27 // string of 0's and 1's that tell which items are present + +//TA: extra stuff: +#define CS_ABPOINTS 28 +#define CS_HBPOINTS 29 + +#define CS_MODELS 32 +#define CS_SOUNDS (CS_MODELS+MAX_MODELS) +#define CS_PLAYERS (CS_SOUNDS+MAX_SOUNDS) +#define MAX_PRECACHES 32 +#define CS_LOCATIONS (CS_PLAYERS+MAX_CLIENTS+MAX_PRECACHES) + +#define CS_MAX (CS_LOCATIONS+MAX_LOCATIONS) + +#if (CS_MAX) > MAX_CONFIGSTRINGS +#error overflow: (CS_MAX) > MAX_CONFIGSTRINGS +#endif + +typedef enum { + GT_FFA, // free for all + GT_TOURNAMENT, // one on one tournament + GT_SINGLE_PLAYER, // single player ffa + + //-- team games go after this -- + + GT_TEAM, // team deathmatch + GT_CTF, // capture the flag + GT_1FCTF, + GT_OBELISK, + GT_HARVESTER, + + GT_MAX_GAME_TYPE +} gametype_t; + +typedef enum { GENDER_MALE, GENDER_FEMALE, GENDER_NEUTER } gender_t; + +/* +=================================================================================== + +PMOVE MODULE + +The pmove code takes a player_state_t and a usercmd_t and generates a new player_state_t +and some other output data. Used for local prediction on the client game and true +movement on the server game. +=================================================================================== +*/ + +typedef enum { + PM_NORMAL, // can accelerate and turn + PM_NOCLIP, // noclip movement + PM_SPECTATOR, // still run into walls + PM_DEAD, // no acceleration or turning, but free falling + PM_FREEZE, // stuck in place with no control + PM_INTERMISSION, // no movement or status bar + PM_SPINTERMISSION // no movement or status bar +} pmtype_t; + +typedef enum { + WEAPON_READY, + WEAPON_RAISING, + WEAPON_DROPPING, + WEAPON_FIRING, + WEAPON_RELOADING +} weaponstate_t; + +//TA: clip-size defines +#define CS_MG 30 //clip-size +#define CS_CG 500 +#define CS_BFG 100 +#define CS_FLAMER 400 + +//TA: bitmasks to get ammo, clips and maxclips out of ammo array +#define BM_AMMO 0x3F +#define BM_CLIPS 0xC0 + +//TA: bitmasks to get weapons out of weapons store +#define BM_SWB 0x0000FFFF +#define BM_SW2B 0xFFFF0000 + +//TA: buildable item type +#define BIT_DROIDS 1 +#define BIT_HUMANS 2 + + +// pmove->pm_flags +#define PMF_DUCKED 1 +#define PMF_JUMP_HELD 2 +#define PMF_BACKWARDS_JUMP 8 // go into backwards land +#define PMF_BACKWARDS_RUN 16 // coast down to backwards run +#define PMF_TIME_LAND 32 // pm_time is time before rejump +#define PMF_TIME_KNOCKBACK 64 // pm_time is an air-accelerate only time +#define PMF_TIME_WATERJUMP 256 // pm_time is waterjump +#define PMF_RESPAWNED 512 // clear after attack and jump buttons come up +#define PMF_USE_ITEM_HELD 1024 +#define PMF_GRAPPLE_PULL 2048 // pull towards grapple location +#define PMF_FOLLOW 4096 // spectate following another player +#define PMF_SCOREBOARD 8192 // spectate as a scoreboard +#define PMF_INVULEXPAND 16384 // invulnerability sphere set to full size + + +#define PMF_ALL_TIMES (PMF_TIME_WATERJUMP|PMF_TIME_LAND|PMF_TIME_KNOCKBACK) + +#define MAXTOUCH 32 +typedef struct { + // state (in / out) + playerState_t *ps; + + // command (in) + usercmd_t cmd; + int tracemask; // collide against these types of surfaces + int debugLevel; // if set, diagnostic output will be printed + qboolean noFootsteps; // if the game is setup for no footsteps by the server + qboolean gauntletHit; // true if a gauntlet attack would actually hit something + + int framecount; + + // results (out) + int numtouch; + int touchents[MAXTOUCH]; + + vec3_t mins, maxs; // bounding box size + + int watertype; + int waterlevel; + + float xyspeed; + + // for fixed msec Pmove + int pmove_fixed; + int pmove_msec; + + // callbacks to test the world + // these will be different functions during game and cgame + void (*trace)( trace_t *results, const vec3_t start, const vec3_t mins, const vec3_t maxs, const vec3_t end, int passEntityNum, int contentMask ); + int (*pointcontents)( const vec3_t point, int passEntityNum ); +} pmove_t; + +// if a full pmove isn't done on the client, you can just update the angles +void PM_UpdateViewAngles( playerState_t *ps, const usercmd_t *cmd ); +void Pmove (pmove_t *pmove); + +//=================================================================================== + + +// player_state->stats[] indexes +typedef enum { + STAT_HEALTH, + STAT_ITEMS, + STAT_ACTIVEITEMS, + STAT_WEAPONS, // 16 bit fields + STAT_WEAPONS2, //TA: another 16 bits to push the max weapon count up + STAT_ARMOR, + STAT_DEAD_YAW, // look this direction when dead (FIXME: get rid of?) + STAT_CLIENTS_READY, // bit mask of clients wishing to exit the intermission (FIXME: configstring?) + STAT_MAX_HEALTH, // health / armor limit, changable by handicap + STAT_PCLASS, //TA: player class (for droids AND humans) + STAT_PTEAM, //TA: player team + STAT_ABILITIES, //TA: client abilities (based on class) + STAT_ATTRIBS, + STAT_STAMINA, //TA: stamina (human only) + STAT_STATE //TA: client states e.g. wall climbing +} statIndex_t; + +#define SCA_WALLCLIMBER 1 +#define SCA_TAKESFALLDAMAGE 2 +#define SCA_CANZOOM 4 +#define SCA_CANJUMP 8 +#define SCA_NOWEAPONDRIFT 16 + +#define SS_WALLCLIMBING 1 +#define SS_GPISROTVEC 2 +#define SS_CREEPSLOWED 4 +#define SS_WALLTRANSIDING 8 +#define SS_SPEEDBOOST 16 + + +// player_state->persistant[] indexes +// these fields are the only part of player_state that isn't +// cleared on respawn +typedef enum { + PERS_SCORE, // !!! MUST NOT CHANGE, SERVER AND GAME BOTH REFERENCE !!! + PERS_HITS, // total points damage inflicted so damage beeps can sound on change + PERS_RANK, + PERS_TEAM, + PERS_SPAWN_COUNT, // incremented every respawn + PERS_PLAYEREVENTS, // 16 bits that can be flipped for events + PERS_REWARD, // a reward_t + PERS_ATTACKER, // clientnum of last damage inflicter + PERS_KILLED, // count of the number of times you died + // these were added for single player awards tracking + PERS_IMPRESSIVE_COUNT, + PERS_EXCELLENT_COUNT, + PERS_GAUNTLET_FRAG_COUNT, + PERS_ACCURACY_SHOTS, + PERS_ACCURACY_HITS, + //TA: FIXME: /\ get rid of award counts to make some room + + //TA: extra gubbins + PERS_POINTS, + PERS_TOTALPOINTS +} persEnum_t; + + +// entityState_t->eFlags +#define EF_DEAD 0x00000001 // don't draw a foe marker over players with EF_DEAD +#define EF_TELEPORT_BIT 0x00000004 // toggled every time the origin abruptly changes +#define EF_AWARD_EXCELLENT 0x00000008 // draw an excellent sprite +#define EF_BOUNCE 0x00000010 // for missiles +#define EF_BOUNCE_HALF 0x00000020 // for missiles +#define EF_AWARD_GAUNTLET 0x00000040 // draw a gauntlet sprite +#define EF_NODRAW 0x00000080 // may have an event, but no model (unspawned items) +#define EF_FIRING 0x00000100 // for lightning gun +#define EF_MOVER_STOP 0x00000400 // will push otherwise +#define EF_TALK 0x00001000 // draw a talk balloon +#define EF_CONNECTION 0x00002000 // draw a connection trouble sprite +#define EF_VOTED 0x00004000 // already cast a vote +#define EF_TEAMVOTED 0x00008000 // already cast a vote +#define EF_AWARD_IMPRESSIVE 0x00010000 // draw an impressive sprite + +typedef enum { + PW_NONE, + + PW_QUAD, + PW_BATTLESUIT, + PW_HASTE, + PW_INVIS, + PW_REGEN, + PW_FLIGHT, + + PW_REDFLAG, + PW_BLUEFLAG, + PW_BALL, + + PW_NUM_POWERUPS +} powerup_t; + +typedef enum { + HI_NONE, + + HI_TELEPORTER, + HI_MEDKIT, + + HI_NUM_HOLDABLE +} holdable_t; + +typedef enum { + WP_NONE, + + WP_GAUNTLET, + WP_MACHINEGUN, + WP_CHAINGUN, + WP_SHOTGUN, + WP_GRENADE_LAUNCHER, + WP_ROCKET_LAUNCHER, + WP_LIGHTNING, + WP_RAILGUN, + WP_FLAMER, + WP_PLASMAGUN, + WP_BFG, + WP_GRAPPLING_HOOK, + WP_VENOM, + WP_HBUILD, + WP_ABUILD, + WP_SCANNER, + WP_GGRENADE, + + WP_NUM_WEAPONS +} weapon_t; + +typedef enum { + UP_NONE, + + UP_TORCH, + UP_NVG, + UP_CHESTARMOUR, + UP_LIMBARMOUR, + UP_HELMET, + UP_ANTITOXIN, + UP_BATTPACK, + UP_JETPACK, + UP_THREATHELMET, + UP_BATTLESUIT, + UP_IMPANTKIT, + + UP_NUM_UPGRADES +} upgrade_t; + +typedef enum { + BA_NONE, + + BA_A_SPAWN, + BA_A_DEF1, + BA_H_SPAWN, + BA_H_DEF1, + BA_H_MCU, + + BA_NUM_BUILDABLES +} buildable_t; + +// reward sounds (stored in ps->persistant[PERS_PLAYEREVENTS]) +#define PLAYEREVENT_DENIEDREWARD 0x0001 +#define PLAYEREVENT_GAUNTLETREWARD 0x0002 +#define PLAYEREVENT_HOLYSHIT 0x0004 + +// entityState_t->event values +// entity events are for effects that take place reletive +// to an existing entities origin. Very network efficient. + +// two bits at the top of the entityState->event field +// will be incremented with each change in the event so +// that an identical event started twice in a row can +// be distinguished. And off the value with ~EV_EVENT_BITS +// to retrieve the actual event number +#define EV_EVENT_BIT1 0x00000100 +#define EV_EVENT_BIT2 0x00000200 +#define EV_EVENT_BITS (EV_EVENT_BIT1|EV_EVENT_BIT2) + +typedef enum { + EV_NONE, + + EV_FOOTSTEP, + EV_FOOTSTEP_METAL, + EV_FOOTSTEP_SQUELCH, + EV_FOOTSPLASH, + EV_FOOTWADE, + EV_SWIM, + + EV_STEP_4, + EV_STEP_8, + EV_STEP_12, + EV_STEP_16, + + EV_FALL_SHORT, + EV_FALL_MEDIUM, + EV_FALL_FAR, + + EV_JUMP_PAD, // boing sound at origin, jump sound on player + + EV_JUMP, + EV_WATER_TOUCH, // foot touches + EV_WATER_LEAVE, // foot leaves + EV_WATER_UNDER, // head touches + EV_WATER_CLEAR, // head leaves + + EV_ITEM_PICKUP, // normal item pickups are predictable + EV_GLOBAL_ITEM_PICKUP, // powerup / team sounds are broadcast to everyone + + EV_NOAMMO, + EV_CHANGE_WEAPON, + EV_FIRE_WEAPON, + + EV_USE_ITEM0, + EV_USE_ITEM1, + EV_USE_ITEM2, + EV_USE_ITEM3, + EV_USE_ITEM4, + EV_USE_ITEM5, + EV_USE_ITEM6, + EV_USE_ITEM7, + EV_USE_ITEM8, + EV_USE_ITEM9, + EV_USE_ITEM10, + EV_USE_ITEM11, + EV_USE_ITEM12, + EV_USE_ITEM13, + EV_USE_ITEM14, + EV_USE_ITEM15, + + EV_ITEM_RESPAWN, + EV_ITEM_GROW, //droid items that grow + EV_ITEM_POP, + EV_PLAYER_TELEPORT_IN, + EV_PLAYER_TELEPORT_OUT, + + EV_GRENADE_BOUNCE, // eventParm will be the soundindex + + EV_GENERAL_SOUND, + EV_GLOBAL_SOUND, // no attenuation + + EV_BULLET_HIT_FLESH, + EV_BULLET_HIT_WALL, + + EV_MISSILE_HIT, + EV_MISSILE_MISS, + EV_MISSILE_MISS_METAL, + EV_ITEM_EXPLOSION, //TA: human item explosions + EV_RAILTRAIL, + EV_SHOTGUN, + EV_BULLET, // otherEntity is the shooter + + EV_PAIN, + EV_DEATH1, + EV_DEATH2, + EV_DEATH3, + EV_OBITUARY, + + EV_POWERUP_QUAD, + EV_POWERUP_BATTLESUIT, + EV_POWERUP_REGEN, + + EV_GIB_PLAYER, // gib a previously living player + EV_GIB_GENERIC, //TA: generic green gib for droids + + EV_DEBUG_LINE, + EV_STOPLOOPINGSOUND, + EV_TAUNT, + EV_TAUNT_YES, + EV_TAUNT_NO, + EV_TAUNT_FOLLOWME, + EV_TAUNT_GETFLAG, + EV_TAUNT_GUARDBASE, + EV_TAUNT_PATROL, + + EV_MENU //TA: menu event + +} entity_event_t; + +typedef enum +{ + MN_TEAM, + MN_DROID, + MN_HUMAN, + MN_ABUILD, + MN_HBUILD +} dynMenu_t; + +// animations +typedef enum { + BOTH_DEATH1, + BOTH_DEAD1, + BOTH_DEATH2, + BOTH_DEAD2, + BOTH_DEATH3, + BOTH_DEAD3, + + TORSO_GESTURE, + + TORSO_ATTACK, + TORSO_ATTACK2, + + TORSO_DROP, + TORSO_RAISE, + + TORSO_STAND, + TORSO_STAND2, + + LEGS_WALKCR, + LEGS_WALK, + LEGS_RUN, + LEGS_BACK, + LEGS_SWIM, + + LEGS_JUMP, + LEGS_LAND, + + LEGS_JUMPB, + LEGS_LANDB, + + LEGS_IDLE, + LEGS_IDLECR, + + LEGS_TURN, + +#ifdef NEW_ANIMS + TORSO_GETFLAG, + TORSO_GUARDBASE, + TORSO_PATROL, + TORSO_FOLLOWME, + TORSO_AFFIRMATIVE, + TORSO_NEGATIVE, +#endif + + MAX_ANIMATIONS, + + LEGS_BACKCR, + LEGS_BACKWALK, + FLAG_RUN, + FLAG_STAND, + FLAG_STAND2RUN, + + MAX_TOTALANIMATIONS +} animNumber_t; + + +typedef struct animation_s { + int firstFrame; + int numFrames; + int loopFrames; // 0 to numFrames + int frameLerp; // msec between frames + int initialLerp; // msec to get to first frame + int reversed; // true if animation is reversed + int flipflop; // true if animation should flipflop back to base +} animation_t; + + +// flip the togglebit every time an animation +// changes so a restart of the same anim can be detected +#define ANIM_TOGGLEBIT 128 +#define ANIM_WALLCLIMBING 64 + + +typedef enum { + TEAM_FREE, + TEAM_HUMANS, + TEAM_DROIDS, + TEAM_SPECTATOR, + + TEAM_NUM_TEAMS +} team_t; + +// Time between location updates +#define TEAM_LOCATION_UPDATE_TIME 1000 + +// How many players on the overlay +#define TEAM_MAXOVERLAY 32 + +//FIXME: switch to enums at some point +//TA: player classes +#define PCL_D_BUILDER 1 +#define PCL_D_BASE 2 +#define PCL_D_OFF1 3 +#define PCL_D_OFF2 4 +#define PCL_D_OFF3 5 +#define PCL_D_OFF4 6 +#define PCL_D_OFF5 7 +#define PCL_D_OFF6 8 +#define PCL_D_OFF7 9 +#define PCL_D_OFF8 10 + +#define PCL_H_BASE 11 + +//TA: player teams +#define PTE_NONE 0 +#define PTE_DROIDS 1 +#define PTE_HUMANS 2 + + +// means of death +typedef enum { + MOD_UNKNOWN, + MOD_SHOTGUN, + MOD_GAUNTLET, + MOD_MACHINEGUN, + MOD_CHAINGUN, + MOD_GRENADE, + MOD_GRENADE_SPLASH, + MOD_ROCKET, + MOD_ROCKET_SPLASH, + MOD_FLAMER, + MOD_FLAMER_SPLASH, + MOD_RAILGUN, + MOD_LIGHTNING, + MOD_BFG, + MOD_BFG_SPLASH, + MOD_WATER, + MOD_SLIME, + MOD_LAVA, + MOD_CRUSH, + MOD_TELEFRAG, + MOD_FALLING, + MOD_SUICIDE, + MOD_TARGET_LASER, + MOD_TRIGGER_HURT, + MOD_GRAPPLE, + MOD_VENOM, + MOD_HSPAWN, + MOD_ASPAWN +} meansOfDeath_t; + + +//--------------------------------------------------------- + +// gitem_t->type +typedef enum { + IT_BAD, + IT_WEAPON, // EFX: rotate + upscale + minlight + IT_BUILDABLE, //TA: gitem_t->type for buildable items (spawns etc.) + IT_UPGRADE, //TA: gitem_t->type for human upgrades + IT_AMMO, // EFX: rotate + IT_ARMOR, // EFX: rotate + minlight + IT_HEALTH, // EFX: static external sphere + rotating internal + IT_POWERUP, // instant on, timer based + // EFX: rotate + external ring that rotates + IT_HOLDABLE, // single use, holdable item + // EFX: rotate + bob + IT_TEAM +} itemType_t; + +#define MAX_ITEM_MODELS 4 + +typedef struct gitem_s { + char *classname; // spawning name + char *pickup_sound; + char *world_model[MAX_ITEM_MODELS]; + + char *icon; + char *pickup_name; // for printing on pickup + + int quantity; // for ammo how much, or duration of powerup + itemType_t giType; // IT_* flags + + int giTag; + + char *precaches; // string of all models and images this item will use + char *sounds; // string of all sounds this item will use +} gitem_t; + +// included in both the game dll and the client +extern gitem_t bg_itemlist[]; +extern int bg_numItems; + +gitem_t *BG_FindItem( const char *pickupName ); +gitem_t *BG_FindItemForWeapon( weapon_t weapon ); +gitem_t *BG_FindItemForBuildable( buildable_t buildable ); +gitem_t *BG_FindItemForUpgrade( upgrade_t upgrade ); +gitem_t *BG_FindItemForPowerup( powerup_t pw ); +gitem_t *BG_FindItemForHoldable( holdable_t pw ); +#define ITEM_INDEX(x) ((x)-bg_itemlist) + +qboolean BG_CanItemBeGrabbed( int gametype, const entityState_t *ent, const playerState_t *ps ); + + +// g_dmflags->integer flags +#define DF_NO_FALLING 8 +#define DF_FIXED_FOV 16 +#define DF_NO_FOOTSTEPS 32 + +// content masks +#define MASK_ALL (-1) +#define MASK_SOLID (CONTENTS_SOLID) +#define MASK_PLAYERSOLID (CONTENTS_SOLID|CONTENTS_PLAYERCLIP|CONTENTS_BODY) +#define MASK_DEADSOLID (CONTENTS_SOLID|CONTENTS_PLAYERCLIP) +#define MASK_WATER (CONTENTS_WATER|CONTENTS_LAVA|CONTENTS_SLIME) +#define MASK_OPAQUE (CONTENTS_SOLID|CONTENTS_SLIME|CONTENTS_LAVA) +#define MASK_SHOT (CONTENTS_SOLID|CONTENTS_BODY|CONTENTS_CORPSE) + + +// +// entityState_t->eType +// +typedef enum { + ET_GENERAL, + ET_PLAYER, + ET_ITEM, + + ET_BUILDABLE, //TA: buildable type + ET_CREEP, //TA: creep type + + ET_MISSILE, + ET_MOVER, + ET_BEAM, + ET_PORTAL, + ET_SPEAKER, + ET_PUSH_TRIGGER, + ET_TELEPORT_TRIGGER, + ET_INVISIBLE, + ET_GRAPPLE, // grapple hooked on wall + + ET_TORCH, //TA: torch type + ET_CORPSE, + + ET_EVENTS // any of the EV_* events can be added freestanding + // by setting eType to ET_EVENTS + eventNum + // this avoids having to set eFlags and eventNum +} entityType_t; + +void BG_EvaluateTrajectory( const trajectory_t *tr, int atTime, vec3_t result ); +void BG_EvaluateTrajectoryDelta( const trajectory_t *tr, int atTime, vec3_t result ); + +void BG_AddPredictableEventToPlayerstate( int newEvent, int eventParm, playerState_t *ps ); + +void BG_TouchJumpPad( playerState_t *ps, entityState_t *jumppad ); + +void BG_PlayerStateToEntityState( playerState_t *ps, entityState_t *s, qboolean snap ); +void BG_PlayerStateToEntityStateExtraPolate( playerState_t *ps, entityState_t *s, int time, qboolean snap ); + +qboolean BG_PlayerTouchesItem( playerState_t *ps, entityState_t *item, int atTime ); + +//TA: extra bits: (which I apparently dont need) +/*void BG_unpackAmmoArray( int weapon, int ammo[ ], int ammo2[ ], int *quan, int *clips, int *maxclips ); +void BG_packAmmoArray( int weapon, int ammo[ ], int ammo2[ ], int quan, int clips, int maxclips ); +qboolean BG_infiniteAmmo( int weapon ); +void BG_packWeapon( int weapon, int stats[ ] ); +qboolean BG_gotWeapon( int weapon, int stats[ ] );*/ + +#define CREEP_BASESIZE 120 + +#define ARENAS_PER_TIER 4 +#define MAX_ARENAS 1024 +#define MAX_ARENAS_TEXT 8192 + +#define MAX_BOTS 1024 +#define MAX_BOTS_TEXT 8192 + +//TA: conceptually should live in q_shared.h +void AxisToAngles( vec3_t axis[3], vec3_t angles); +float arccos( float x ); + diff --git a/src/game/bg_slidemove.c b/src/game/bg_slidemove.c new file mode 100644 index 00000000..00ca010a --- /dev/null +++ b/src/game/bg_slidemove.c @@ -0,0 +1,327 @@ +// Copyright (C) 1999-2000 Id Software, Inc. +// +// bg_slidemove.c -- part of bg_pmove functionality + +/* + * Portions Copyright (C) 2000-2001 Tim Angus + * + * This program 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, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ + +/* To assertain which portions are licensed under the GPL and which are + * licensed by Id Software, Inc. please run a diff between the equivalent + * versions of the "Tremulous" modification and the unmodified "Quake3" + * game source code. + */ + +#include "q_shared.h" +#include "bg_public.h" +#include "bg_local.h" + +/* + +input: origin, velocity, bounds, groundPlane, trace function + +output: origin, velocity, impacts, stairup boolean + +*/ + +/* +================== +PM_SlideMove + +Returns qtrue if the velocity was clipped in some way +================== +*/ +#define MAX_CLIP_PLANES 5 +qboolean PM_SlideMove( qboolean gravity ) { + int bumpcount, numbumps; + vec3_t dir; + float d; + int numplanes; + vec3_t planes[MAX_CLIP_PLANES]; + vec3_t primal_velocity; + vec3_t clipVelocity; + int i, j, k; + trace_t trace; + vec3_t end; + float time_left; + float into; + vec3_t endVelocity; + vec3_t endClipVelocity; + + numbumps = 4; + + VectorCopy (pm->ps->velocity, primal_velocity); + + if ( gravity ) { + VectorCopy( pm->ps->velocity, endVelocity ); + endVelocity[2] -= pm->ps->gravity * pml.frametime; + pm->ps->velocity[2] = ( pm->ps->velocity[2] + endVelocity[2] ) * 0.5; + primal_velocity[2] = endVelocity[2]; + if ( pml.groundPlane ) { + // slide along the ground plane + PM_ClipVelocity (pm->ps->velocity, pml.groundTrace.plane.normal, + pm->ps->velocity, OVERCLIP ); + } + } + + time_left = pml.frametime; + + // never turn against the ground plane + if ( pml.groundPlane ) { + numplanes = 1; + VectorCopy( pml.groundTrace.plane.normal, planes[0] ); + } else { + numplanes = 0; + } + + // never turn against original velocity + VectorNormalize2( pm->ps->velocity, planes[numplanes] ); + numplanes++; + + for ( bumpcount=0 ; bumpcount < numbumps ; bumpcount++ ) { + + // calculate position we are trying to move to + VectorMA( pm->ps->origin, time_left, pm->ps->velocity, end ); + + // see if we can make it there + pm->trace ( &trace, pm->ps->origin, pm->mins, pm->maxs, end, pm->ps->clientNum, pm->tracemask); + + if (trace.allsolid) { + // entity is completely trapped in another solid + pm->ps->velocity[2] = 0; // don't build up falling damage, but allow sideways acceleration + return qtrue; + } + + if (trace.fraction > 0) { + // actually covered some distance + VectorCopy (trace.endpos, pm->ps->origin); + } + + if (trace.fraction == 1) { + break; // moved the entire distance + } + + // save entity for contact + PM_AddTouchEnt( trace.entityNum ); + + time_left -= time_left * trace.fraction; + + if (numplanes >= MAX_CLIP_PLANES) { + // this shouldn't really happen + VectorClear( pm->ps->velocity ); + return qtrue; + } + + // + // if this is the same plane we hit before, nudge velocity + // out along it, which fixes some epsilon issues with + // non-axial planes + // + for ( i = 0 ; i < numplanes ; i++ ) { + if ( DotProduct( trace.plane.normal, planes[i] ) > 0.99 ) { + VectorAdd( trace.plane.normal, pm->ps->velocity, pm->ps->velocity ); + break; + } + } + if ( i < numplanes ) { + continue; + } + VectorCopy (trace.plane.normal, planes[numplanes]); + numplanes++; + + // + // modify velocity so it parallels all of the clip planes + // + + // find a plane that it enters + for ( i = 0 ; i < numplanes ; i++ ) { + into = DotProduct( pm->ps->velocity, planes[i] ); + if ( into >= 0.1 ) { + continue; // move doesn't interact with the plane + } + + // see how hard we are hitting things + if ( -into > pml.impactSpeed ) { + pml.impactSpeed = -into; + } + + // slide along the plane + PM_ClipVelocity (pm->ps->velocity, planes[i], clipVelocity, OVERCLIP ); + + // slide along the plane + PM_ClipVelocity (endVelocity, planes[i], endClipVelocity, OVERCLIP ); + + // see if there is a second plane that the new move enters + for ( j = 0 ; j < numplanes ; j++ ) { + if ( j == i ) { + continue; + } + if ( DotProduct( clipVelocity, planes[j] ) >= 0.1 ) { + continue; // move doesn't interact with the plane + } + + // try clipping the move to the plane + PM_ClipVelocity( clipVelocity, planes[j], clipVelocity, OVERCLIP ); + PM_ClipVelocity( endClipVelocity, planes[j], endClipVelocity, OVERCLIP ); + + // see if it goes back into the first clip plane + if ( DotProduct( clipVelocity, planes[i] ) >= 0 ) { + continue; + } + + // slide the original velocity along the crease + CrossProduct (planes[i], planes[j], dir); + VectorNormalize( dir ); + d = DotProduct( dir, pm->ps->velocity ); + VectorScale( dir, d, clipVelocity ); + + CrossProduct (planes[i], planes[j], dir); + VectorNormalize( dir ); + d = DotProduct( dir, endVelocity ); + VectorScale( dir, d, endClipVelocity ); + + // see if there is a third plane the the new move enters + for ( k = 0 ; k < numplanes ; k++ ) { + if ( k == i || k == j ) { + continue; + } + if ( DotProduct( clipVelocity, planes[k] ) >= 0.1 ) { + continue; // move doesn't interact with the plane + } + + // stop dead at a tripple plane interaction + VectorClear( pm->ps->velocity ); + return qtrue; + } + } + + // if we have fixed all interactions, try another move + VectorCopy( clipVelocity, pm->ps->velocity ); + VectorCopy( endClipVelocity, endVelocity ); + break; + } + } + + if ( gravity ) { + VectorCopy( endVelocity, pm->ps->velocity ); + } + + // don't change velocity if in a timer (FIXME: is this correct?) + if ( pm->ps->pm_time ) { + VectorCopy( primal_velocity, pm->ps->velocity ); + } + + return ( bumpcount != 0 ); +} + +/* +================== +PM_StepSlideMove + +================== +*/ +void PM_StepSlideMove( qboolean gravity ) { + vec3_t start_o, start_v; + vec3_t down_o, down_v; + trace_t trace; +// float down_dist, up_dist; +// vec3_t delta, delta2; + vec3_t up, down; + + VectorCopy (pm->ps->origin, start_o); + VectorCopy (pm->ps->velocity, start_v); + + if ( PM_SlideMove( gravity ) == 0 ) { + return; // we got exactly where we wanted to go first try + } + + VectorCopy(start_o, down); + down[2] -= STEPSIZE; + pm->trace (&trace, start_o, pm->mins, pm->maxs, down, pm->ps->clientNum, pm->tracemask); + VectorSet(up, 0, 0, 1); + // never step up when you still have up velocity + if ( pm->ps->velocity[2] > 0 && (trace.fraction == 1.0 || + DotProduct(trace.plane.normal, up) < 0.7)) { + return; + } + + VectorCopy (pm->ps->origin, down_o); + VectorCopy (pm->ps->velocity, down_v); + + VectorCopy (start_o, up); + up[2] += STEPSIZE; + + // test the player position if they were a stepheight higher + pm->trace (&trace, up, pm->mins, pm->maxs, up, pm->ps->clientNum, pm->tracemask); + if ( trace.allsolid ) { + if ( pm->debugLevel ) { + Com_Printf("%i:bend can't step\n", c_pmove); + } + return; // can't step up + } + + // try slidemove from this position + VectorCopy (up, pm->ps->origin); + VectorCopy (start_v, pm->ps->velocity); + + PM_SlideMove( gravity ); + + // push down the final amount + VectorCopy (pm->ps->origin, down); + down[2] -= STEPSIZE; + pm->trace (&trace, pm->ps->origin, pm->mins, pm->maxs, down, pm->ps->clientNum, pm->tracemask); + if ( !trace.allsolid ) { + VectorCopy (trace.endpos, pm->ps->origin); + } + if ( trace.fraction < 1.0 ) { + PM_ClipVelocity( pm->ps->velocity, trace.plane.normal, pm->ps->velocity, OVERCLIP ); + } + +#if 0 + // if the down trace can trace back to the original position directly, don't step + pm->trace( &trace, pm->ps->origin, pm->mins, pm->maxs, start_o, pm->ps->clientNum, pm->tracemask); + if ( trace.fraction == 1.0 ) { + // use the original move + VectorCopy (down_o, pm->ps->origin); + VectorCopy (down_v, pm->ps->velocity); + if ( pm->debugLevel ) { + Com_Printf("%i:bend\n", c_pmove); + } + } else +#endif + { + // use the step move + float delta; + + delta = pm->ps->origin[2] - start_o[2]; + if ( delta > 2 ) { + if ( delta < 7 ) { + PM_AddEvent( EV_STEP_4 ); + } else if ( delta < 11 ) { + PM_AddEvent( EV_STEP_8 ); + } else if ( delta < 15 ) { + PM_AddEvent( EV_STEP_12 ); + } else { + PM_AddEvent( EV_STEP_16 ); + } + } + if ( pm->debugLevel ) { + Com_Printf("%i:stepped\n", c_pmove); + } + } +} + diff --git a/src/game/g_active.c b/src/game/g_active.c new file mode 100644 index 00000000..d8519184 --- /dev/null +++ b/src/game/g_active.c @@ -0,0 +1,1059 @@ +// Copyright (C) 1999-2000 Id Software, Inc. +// + +/* + * Portions Copyright (C) 2000-2001 Tim Angus + * + * This program 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, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ + +/* To assertain which portions are licensed under the GPL and which are + * licensed by Id Software, Inc. please run a diff between the equivalent + * versions of the "Tremulous" modification and the unmodified "Quake3" + * game source code. + */ + +#include "g_local.h" + +/* +=============== +G_DamageFeedback + +Called just before a snapshot is sent to the given player. +Totals up all damage and generates both the player_state_t +damage values to that client for pain blends and kicks, and +global pain sound events for all clients. +=============== +*/ +void P_DamageFeedback( gentity_t *player ) { + gclient_t *client; + float count; + vec3_t angles; + + client = player->client; + if ( client->ps.pm_type == PM_DEAD ) { + return; + } + + // total points of damage shot at the player this frame + count = client->damage_blood + client->damage_armor; + if ( count == 0 ) { + return; // didn't take any damage + } + + if ( count > 255 ) { + count = 255; + } + + // send the information to the client + + // world damage (falling, slime, etc) uses a special code + // to make the blend blob centered instead of positional + if ( client->damage_fromWorld ) { + client->ps.damagePitch = 255; + client->ps.damageYaw = 255; + + client->damage_fromWorld = qfalse; + } else { + vectoangles( client->damage_from, angles ); + client->ps.damagePitch = angles[PITCH]/360.0 * 256; + client->ps.damageYaw = angles[YAW]/360.0 * 256; + } + + // play an apropriate pain sound + if ( (level.time > player->pain_debounce_time) && !(player->flags & FL_GODMODE) ) { + player->pain_debounce_time = level.time + 700; + G_AddEvent( player, EV_PAIN, player->health ); + client->ps.damageEvent++; + } + + + client->ps.damageCount = count; + + // + // clear totals + // + client->damage_blood = 0; + client->damage_armor = 0; + client->damage_knockback = 0; +} + + + +/* +============= +P_WorldEffects + +Check for lava / slime contents and drowning +============= +*/ +void P_WorldEffects( gentity_t *ent ) { + qboolean envirosuit; + int waterlevel; + + if ( ent->client->noclip ) { + ent->client->airOutTime = level.time + 12000; // don't need air + return; + } + + waterlevel = ent->waterlevel; + + envirosuit = /*ent->client->ps.powerups[PW_BATTLESUIT]*/ 0 > level.time; + + // + // check for drowning + // + if ( waterlevel == 3 ) { + // envirosuit give air + if ( envirosuit ) { + ent->client->airOutTime = level.time + 10000; + } + + // if out of air, start drowning + if ( ent->client->airOutTime < level.time) { + // drown! + ent->client->airOutTime += 1000; + if ( ent->health > 0 ) { + // take more damage the longer underwater + ent->damage += 2; + if (ent->damage > 15) + ent->damage = 15; + + // play a gurp sound instead of a normal pain sound + if (ent->health <= ent->damage) { + G_Sound(ent, CHAN_VOICE, G_SoundIndex("*drown.wav")); + } else if (rand()&1) { + G_Sound(ent, CHAN_VOICE, G_SoundIndex("sound/player/gurp1.wav")); + } else { + G_Sound(ent, CHAN_VOICE, G_SoundIndex("sound/player/gurp2.wav")); + } + + // don't play a normal pain sound + ent->pain_debounce_time = level.time + 200; + + G_Damage (ent, NULL, NULL, NULL, NULL, + ent->damage, DAMAGE_NO_ARMOR, MOD_WATER); + } + } + } else { + ent->client->airOutTime = level.time + 12000; + ent->damage = 2; + } + + // + // check for sizzle damage (move to pmove?) + // + if (waterlevel && + (ent->watertype&(CONTENTS_LAVA|CONTENTS_SLIME)) ) { + if (ent->health > 0 + && ent->pain_debounce_time <= level.time ) { + + if ( envirosuit ) { + G_AddEvent( ent, EV_POWERUP_BATTLESUIT, 0 ); + } else { + if (ent->watertype & CONTENTS_LAVA) { + G_Damage (ent, NULL, NULL, NULL, NULL, + 30*waterlevel, 0, MOD_LAVA); + } + + if (ent->watertype & CONTENTS_SLIME) { + G_Damage (ent, NULL, NULL, NULL, NULL, + 10*waterlevel, 0, MOD_SLIME); + } + } + } + } +} + + + +/* +=============== +G_SetClientSound +=============== +*/ +void G_SetClientSound( gentity_t *ent ) { + if (ent->waterlevel && (ent->watertype&(CONTENTS_LAVA|CONTENTS_SLIME)) ) { + ent->client->ps.loopSound = level.snd_fry; + } else { + ent->client->ps.loopSound = 0; + } +} + + + +//============================================================== + +/* +============== +ClientImpacts +============== +*/ +void ClientImpacts( gentity_t *ent, pmove_t *pm ) { + int i, j; + trace_t trace; + gentity_t *other; + + memset( &trace, 0, sizeof( trace ) ); + for (i=0 ; i<pm->numtouch ; i++) { + for (j=0 ; j<i ; j++) { + if (pm->touchents[j] == pm->touchents[i] ) { + break; + } + } + if (j != i) { + continue; // duplicated + } + other = &g_entities[ pm->touchents[i] ]; + + if ( ( ent->r.svFlags & SVF_BOT ) && ( ent->touch ) ) { + ent->touch( ent, other, &trace ); + } + + if ( !other->touch ) { + continue; + } + + other->touch( other, ent, &trace ); + } + +} + +/* +============ +G_TouchTriggers + +Find all trigger entities that ent's current position touches. +Spectators will only interact with teleporters. +============ +*/ +void G_TouchTriggers( gentity_t *ent ) { + int i, num; + int touch[MAX_GENTITIES]; + gentity_t *hit; + trace_t trace; + vec3_t mins, maxs; + static vec3_t range = { 40, 40, 52 }; + + if ( !ent->client ) { + return; + } + + // dead clients don't activate triggers! + if ( ent->client->ps.stats[STAT_HEALTH] <= 0 ) { + return; + } + + VectorSubtract( ent->client->ps.origin, range, mins ); + VectorAdd( ent->client->ps.origin, range, maxs ); + + num = trap_EntitiesInBox( mins, maxs, touch, MAX_GENTITIES ); + + // can't use ent->absmin, because that has a one unit pad + VectorAdd( ent->client->ps.origin, ent->r.mins, mins ); + VectorAdd( ent->client->ps.origin, ent->r.maxs, maxs ); + + for ( i=0 ; i<num ; i++ ) { + hit = &g_entities[touch[i]]; + + if ( !hit->touch && !ent->touch ) { + continue; + } + if ( !( hit->r.contents & CONTENTS_TRIGGER ) ) { + continue; + } + + // ignore most entities if a spectator + if ( ent->client->sess.sessionTeam == TEAM_SPECTATOR ) { + if ( hit->s.eType != ET_TELEPORT_TRIGGER && + // this is ugly but adding a new ET_? type will + // most likely cause network incompatibilities + hit->touch != Touch_DoorTrigger) { + continue; + } + } + + // use seperate code for determining if an item is picked up + // so you don't have to actually contact its bounding box + if ( hit->s.eType == ET_ITEM || hit->s.eType == ET_BUILDABLE ) { + if ( !BG_PlayerTouchesItem( &ent->client->ps, &hit->s, level.time ) ) { + continue; + } + } else { + if ( !trap_EntityContact( mins, maxs, hit ) ) { + continue; + } + } + + memset( &trace, 0, sizeof(trace) ); + + if ( hit->touch ) { + hit->touch (hit, ent, &trace); + } + + if ( ( ent->r.svFlags & SVF_BOT ) && ( ent->touch ) ) { + ent->touch( ent, hit, &trace ); + } + } + + // if we didn't touch a jump pad this pmove frame + if ( ent->client->ps.jumppad_frame != ent->client->ps.pmove_framecount ) { + ent->client->ps.jumppad_frame = 0; + ent->client->ps.jumppad_ent = 0; + } +} + +/* +================= +SpectatorThink +================= +*/ +void SpectatorThink( gentity_t *ent, usercmd_t *ucmd ) { + pmove_t pm; + gclient_t *client; + + client = ent->client; + + if ( client->sess.spectatorState != SPECTATOR_FOLLOW ) { + + if( client->sess.spectatorState == SPECTATOR_LOCKED ) + client->ps.pm_type = PM_FREEZE; + else + client->ps.pm_type = PM_SPECTATOR; + + client->ps.speed = 400; // faster than normal + + // set up for pmove + memset (&pm, 0, sizeof(pm)); + pm.ps = &client->ps; + pm.cmd = *ucmd; + pm.tracemask = MASK_PLAYERSOLID & ~CONTENTS_BODY; // spectators can fly through bodies + pm.trace = trap_Trace; + pm.pointcontents = trap_PointContents; + + // perform a pmove + Pmove (&pm); + + // save results of pmove + VectorCopy( client->ps.origin, ent->s.origin ); + + G_TouchTriggers( ent ); + trap_UnlinkEntity( ent ); + } + + client->oldbuttons = client->buttons; + client->buttons = ucmd->buttons; + + if ( ( client->buttons & BUTTON_ATTACK ) && !( client->oldbuttons & BUTTON_ATTACK ) ) + { + if( client->pers.pteam == PTE_NONE ) + { + G_AddPredictableEvent( ent, EV_MENU, MN_TEAM ); + } + else if( client->pers.pteam == PTE_DROIDS ) + { + G_AddPredictableEvent( ent, EV_MENU, MN_DROID ); + } + else if( client->pers.pteam == PTE_HUMANS ) + { + G_AddPredictableEvent( ent, EV_MENU, MN_HUMAN ); + } + } + + // attack button cycles through spectators + //TA: messes with the menus + /*if ( ( client->buttons & BUTTON_ATTACK ) && + !( client->oldbuttons & BUTTON_ATTACK ) && + ( client->sess.spectatorState != SPECTATOR_LOCKED ) ) + Cmd_FollowCycle_f( ent, 1 );*/ +} + + + +/* +================= +ClientInactivityTimer + +Returns qfalse if the client is dropped +================= +*/ +qboolean ClientInactivityTimer( gclient_t *client ) { + if ( ! g_inactivity.integer ) { + // give everyone some time, so if the operator sets g_inactivity during + // gameplay, everyone isn't kicked + client->inactivityTime = level.time + 60 * 1000; + client->inactivityWarning = qfalse; + } else if ( client->pers.cmd.forwardmove || + client->pers.cmd.rightmove || + client->pers.cmd.upmove || + (client->pers.cmd.buttons & BUTTON_ATTACK) ) { + client->inactivityTime = level.time + g_inactivity.integer * 1000; + client->inactivityWarning = qfalse; + } else if ( !client->pers.localClient ) { + if ( level.time > client->inactivityTime ) { + trap_DropClient( client - level.clients, "Dropped due to inactivity" ); + return qfalse; + } + if ( level.time > client->inactivityTime - 10000 && !client->inactivityWarning ) { + client->inactivityWarning = qtrue; + trap_SendServerCommand( client - level.clients, "cp \"Ten seconds until inactivity drop!\n\"" ); + } + } + return qtrue; +} + +/* +================== +ClientTimerActions + +Actions that happen once a second +================== +*/ +void ClientTimerActions( gentity_t *ent, int msec ) { + gclient_t *client; + + client = ent->client; + client->timeResidual += msec; + + while ( client->timeResidual >= 1000 ) { + client->timeResidual -= 1000; + + // regenerate + /*if ( client->ps.powerups[PW_REGEN] ) + { + if ( ent->health < client->ps.stats[STAT_MAX_HEALTH]) + { + ent->health += 15; + if ( ent->health > client->ps.stats[STAT_MAX_HEALTH] * 1.1 ) + { + ent->health = client->ps.stats[STAT_MAX_HEALTH] * 1.1; + } + G_AddEvent( ent, EV_POWERUP_REGEN, 0 ); + } + else if ( ent->health < client->ps.stats[STAT_MAX_HEALTH] * 2) + { + ent->health += 5; + if ( ent->health > client->ps.stats[STAT_MAX_HEALTH] * 2 ) + { + ent->health = client->ps.stats[STAT_MAX_HEALTH] * 2; + } + G_AddEvent( ent, EV_POWERUP_REGEN, 0 ); + } + } + else + { + // count down health when over max + if ( ent->health > client->ps.stats[STAT_MAX_HEALTH] ) + { + //TA: dont count health and armo(u)r down + //ent->health--; + } + }*/ + + // count down armor when over max + if ( client->ps.stats[STAT_ARMOR] > client->ps.stats[STAT_MAX_HEALTH] ) { + //client->ps.stats[STAT_ARMOR]--; + } + + } +} + +/* +==================== +ClientIntermissionThink +==================== +*/ +void ClientIntermissionThink( gclient_t *client ) { + client->ps.eFlags &= ~EF_TALK; + client->ps.eFlags &= ~EF_FIRING; + + // the level will exit when everyone wants to or after timeouts + + // swap and latch button actions + client->oldbuttons = client->buttons; + client->buttons = client->pers.cmd.buttons; + if ( client->buttons & ( BUTTON_ATTACK | BUTTON_USE_HOLDABLE ) & ( client->oldbuttons ^ client->buttons ) ) { + client->readyToExit = 1; + } +} + + +/* +================ +ClientEvents + +Events will be passed on to the clients for presentation, +but any server game effects are handled here +================ +*/ +void ClientEvents( gentity_t *ent, int oldEventSequence ) { + int i, j; + int event; + gclient_t *client; + int damage; + vec3_t dir; + vec3_t origin, angles; +// qboolean fired; + gitem_t *item; + gentity_t *drop; + + client = ent->client; + + if ( oldEventSequence < client->ps.eventSequence - MAX_PS_EVENTS ) { + oldEventSequence = client->ps.eventSequence - MAX_PS_EVENTS; + } + for ( i = oldEventSequence ; i < client->ps.eventSequence ; i++ ) { + event = client->ps.events[ i & (MAX_PS_EVENTS-1) ]; + + switch ( event ) { + case EV_FALL_MEDIUM: + case EV_FALL_FAR: + if ( ent->s.eType != ET_PLAYER ) { + break; // not in the player model + } + if ( g_dmflags.integer & DF_NO_FALLING ) { + break; + } + if ( event == EV_FALL_FAR ) { + damage = 10; + } else { + damage = 5; + } + VectorSet (dir, 0, 0, 1); + ent->pain_debounce_time = level.time + 200; // no normal pain sound + G_Damage (ent, NULL, NULL, NULL, NULL, damage, 0, MOD_FALLING); + break; + + case EV_FIRE_WEAPON: + FireWeapon( ent ); + break; + + case EV_USE_ITEM1: // teleporter + // drop flags in CTF + item = NULL; + j = 0; + + /*if ( ent->client->ps.powerups[ PW_REDFLAG ] ) { + item = BG_FindItemForPowerup( PW_REDFLAG ); + i = PW_REDFLAG; + } else if ( ent->client->ps.powerups[ PW_BLUEFLAG ] ) { + item = BG_FindItemForPowerup( PW_BLUEFLAG ); + i = PW_BLUEFLAG; + }*/ + + if ( item ) { + drop = Drop_Item( ent, item, 0 ); + // decide how many seconds it has left + drop->count = ( ent->client->ps.powerups[ j ] - level.time ) / 1000; + if ( drop->count < 1 ) { + drop->count = 1; + } + + ent->client->ps.powerups[ j ] = 0; + } + + SelectSpawnPoint( ent->client->ps.origin, origin, angles ); + TeleportPlayer( ent, origin, angles ); + break; + + case EV_USE_ITEM2: // medkit + ent->health = ent->client->ps.stats[STAT_MAX_HEALTH]; + break; + + default: + break; + } + } + +} + +/* +============== +StuckInOtherClient +============== +*/ +static int StuckInOtherClient(gentity_t *ent) { + int i; + gentity_t *ent2; + + ent2 = &g_entities[0]; + for ( i = 0; i < MAX_CLIENTS; i++, ent2++ ) { + if ( ent2 == ent ) { + continue; + } + if ( !ent2->inuse ) { + continue; + } + if ( !ent2->client ) { + continue; + } + if ( ent2->health <= 0 ) { + continue; + } + // + if (ent2->r.absmin[0] > ent->r.absmax[0]) + continue; + if (ent2->r.absmin[1] > ent->r.absmax[1]) + continue; + if (ent2->r.absmin[2] > ent->r.absmax[2]) + continue; + if (ent2->r.absmax[0] < ent->r.absmin[0]) + continue; + if (ent2->r.absmax[1] < ent->r.absmin[1]) + continue; + if (ent2->r.absmax[2] < ent->r.absmin[2]) + continue; + return qtrue; + } + return qfalse; +} + +//TA: rip bots +//void BotTestSolid(vec3_t origin); + +/* +============== +ClientThink + +This will be called once for each client frame, which will +usually be a couple times for each server frame on fast clients. + +If "g_synchronousClients 1" is set, this will be called exactly +once for each server frame, which makes for smooth demo recording. +============== +*/ +void ClientThink_real( gentity_t *ent ) { + gclient_t *client; + pmove_t pm; + int oldEventSequence; + int msec; + usercmd_t *ucmd; + float speed; + + //TA: torch + gentity_t *light; + + //TA: creep variables + gentity_t *creepNode; + vec3_t temp_v; + int i; + qboolean cSlowed = qfalse; + + //Com_Printf( "%d\n", G_LuminanceAtPoint( ent->s.origin ) ); + + client = ent->client; + + // don't think if the client is not yet connected (and thus not yet spawned in) + if (client->pers.connected != CON_CONNECTED) { + return; + } + + // mark the time, so the connection sprite can be removed + ucmd = &ent->client->pers.cmd; + + // sanity check the command time to prevent speedup cheating + if ( ucmd->serverTime > level.time + 200 ) { + ucmd->serverTime = level.time + 200; +// G_Printf("serverTime <<<<<\n" ); + } + if ( ucmd->serverTime < level.time - 1000 ) { + ucmd->serverTime = level.time - 1000; +// G_Printf("serverTime >>>>>\n" ); + } + + msec = ucmd->serverTime - client->ps.commandTime; + // following others may result in bad times, but we still want + // to check for follow toggles + if ( msec < 1 && client->sess.spectatorState != SPECTATOR_FOLLOW ) { + return; + } + if ( msec > 200 ) { + msec = 200; + } + + if ( pmove_msec.integer < 8 ) { + trap_Cvar_Set("pmove_msec", "8"); + } + else if (pmove_msec.integer > 33) { + trap_Cvar_Set("pmove_msec", "33"); + } + + if ( pmove_fixed.integer || client->pers.pmoveFixed ) { + ucmd->serverTime = ((ucmd->serverTime + pmove_msec.integer-1) / pmove_msec.integer) * pmove_msec.integer; + //if (ucmd->serverTime - client->ps.commandTime <= 0) + // return; + } + + // + // check for exiting intermission + // + if ( level.intermissiontime ) { + ClientIntermissionThink( client ); + return; + } + + // spectators don't do much + if ( client->sess.sessionTeam == TEAM_SPECTATOR ) { + if ( client->sess.spectatorState == SPECTATOR_SCOREBOARD ) { + return; + } + SpectatorThink( ent, ucmd ); + return; + } + + // check for inactivity timer, but never drop the local client of a non-dedicated server + if ( !ClientInactivityTimer( client ) ) { + return; + } + + // clear the rewards if time + if ( level.time > client->rewardTime ) { + client->ps.eFlags &= ~(EF_AWARD_IMPRESSIVE | EF_AWARD_EXCELLENT | EF_AWARD_GAUNTLET ); + } + + if ( client->noclip ) { + client->ps.pm_type = PM_NOCLIP; + } else if ( client->ps.stats[STAT_HEALTH] <= 0 ) { + client->ps.pm_type = PM_DEAD; + } else { + client->ps.pm_type = PM_NORMAL; + } + + client->ps.gravity = g_gravity.value; + + // set speed + client->ps.speed = g_speed.value * client->classSpeed; + + //TA: slow player if standing in creep + for ( i = 1, creepNode = g_entities + i; i < level.num_entities; i++, creepNode++ ) + { + if( !Q_stricmp( creepNode->classname, "team_droid_creep" ) ) + { + VectorSubtract( client->ps.origin, creepNode->s.origin, temp_v ); + + if( ( VectorLength( temp_v ) <= creepNode->s.frame ) && + ( temp_v[ 2 ] <= 21 ) && //assumes mins of player is (x, x, -24) + ( client->ps.stats[ STAT_PTEAM ] == PTE_HUMANS ) ) + { + client->ps.speed *= 0.5; + client->ps.stats[ STAT_STATE ] |= SS_CREEPSLOWED; + cSlowed = qtrue; + break; + } + } + } + + if( !cSlowed ) + client->ps.stats[ STAT_STATE ] &= ~SS_CREEPSLOWED; + + /*if ( client->ps.powerups[PW_HASTE] ) { + client->ps.speed *= 1.3; + }*/ + + // Let go of the hook if we aren't firing + if ( client->ps.weapon == WP_GRAPPLING_HOOK && + client->hook && !( ucmd->buttons & BUTTON_ATTACK ) ) { + Weapon_HookFree(client->hook); + } + + //TA: torch stuff + if( client->torch == NULL && BG_activated( UP_TORCH, client->ps.stats ) ) + { + light = G_Spawn( ); + light->s.eType = ET_TORCH; + light->r.ownerNum = ent->s.number; + light->parent = ent; + client->torch = light; + } + else if( client->torch != NULL && !BG_activated( UP_TORCH, client->ps.stats ) ) + { + G_FreeEntity( client->torch ); + trap_LinkEntity( client->torch ); + client->torch = NULL; + } + + + if( client->torch != NULL ) + ShineTorch( client->torch ); + + // set up for pmove + oldEventSequence = client->ps.eventSequence; + + memset (&pm, 0, sizeof(pm)); + + //TA: gauntlet is a NULL weapon to be given to builder classes + // check for the hit-scan gauntlet, don't let the action + // go through as an attack unless it actually hits something + /*if ( client->ps.weapon == WP_GAUNTLET && !( ucmd->buttons & BUTTON_TALK ) && + ( ucmd->buttons & BUTTON_ATTACK ) && client->ps.weaponTime <= 0 ) { + pm.gauntletHit = CheckGauntletAttack( ent ); + }*/ + pm.gauntletHit = qfalse; + + if ( ent->flags & FL_FORCE_GESTURE ) { + ent->flags &= ~FL_FORCE_GESTURE; + ent->client->pers.cmd.buttons |= BUTTON_GESTURE; + } + + pm.ps = &client->ps; + pm.cmd = *ucmd; + if ( pm.ps->pm_type == PM_DEAD ) { + pm.tracemask = MASK_PLAYERSOLID & ~CONTENTS_BODY; + } + else if ( ent->r.svFlags & SVF_BOT ) { + pm.tracemask = MASK_PLAYERSOLID | CONTENTS_BOTCLIP; + } + else { + pm.tracemask = MASK_PLAYERSOLID; + } + pm.trace = trap_Trace; + pm.pointcontents = trap_PointContents; + pm.debugLevel = g_debugMove.integer; + pm.noFootsteps = ( g_dmflags.integer & DF_NO_FOOTSTEPS ) > 0; + + pm.pmove_fixed = pmove_fixed.integer | client->pers.pmoveFixed; + pm.pmove_msec = pmove_msec.integer; + + VectorCopy( client->ps.origin, client->oldOrigin ); + + Pmove (&pm); + + // save results of pmove + if ( ent->client->ps.eventSequence != oldEventSequence ) { + ent->eventTime = level.time; + } + if (g_smoothClients.integer) { + BG_PlayerStateToEntityStateExtraPolate( &ent->client->ps, &ent->s, ent->client->ps.commandTime, qtrue ); + } + else { + BG_PlayerStateToEntityState( &ent->client->ps, &ent->s, qtrue ); + } + if ( !( ent->client->ps.eFlags & EF_FIRING ) ) { + client->fireHeld = qfalse; // for grapple + } + + // use the snapped origin for linking so it matches client predicted versions + VectorCopy( ent->s.pos.trBase, ent->r.currentOrigin ); + + VectorCopy (pm.mins, ent->r.mins); + VectorCopy (pm.maxs, ent->r.maxs); + + ent->waterlevel = pm.waterlevel; + ent->watertype = pm.watertype; + + // execute client events + ClientEvents( ent, oldEventSequence ); + + // link entity now, after any personal teleporters have been used + trap_LinkEntity (ent); + if ( !ent->client->noclip ) { + G_TouchTriggers( ent ); + } + + // NOTE: now copy the exact origin over otherwise clients can be snapped into solid + VectorCopy( ent->client->ps.origin, ent->r.currentOrigin ); + + //test for solid areas in the AAS file + //TA: rip bots + //BotTestSolid(ent->r.currentOrigin); + + // touch other objects + ClientImpacts( ent, &pm ); + + // save results of triggers and client events + if (ent->client->ps.eventSequence != oldEventSequence) { + ent->eventTime = level.time; + } + + // swap and latch button actions + client->oldbuttons = client->buttons; + client->buttons = ucmd->buttons; + client->latched_buttons |= client->buttons & ~client->oldbuttons; + + // check for respawning + if ( client->ps.stats[STAT_HEALTH] <= 0 ) { + // wait for the attack button to be pressed + if ( level.time > client->respawnTime ) { + // forcerespawn is to prevent users from waiting out powerups + if ( g_forcerespawn.integer > 0 && + ( level.time - client->respawnTime ) > g_forcerespawn.integer * 1000 ) { + respawn( ent ); + return; + } + + // pressing attack or use is the normal respawn method + if ( ucmd->buttons & ( BUTTON_ATTACK | BUTTON_USE_HOLDABLE ) ) { + respawn( ent ); + } + } + return; + } + + // perform once-a-second actions + ClientTimerActions( ent, msec ); +} + +/* +================== +ClientThink + +A new command has arrived from the client +================== +*/ +void ClientThink( int clientNum ) { + gentity_t *ent; + + ent = g_entities + clientNum; + trap_GetUsercmd( clientNum, &ent->client->pers.cmd ); + + // mark the time we got info, so we can display the + // phone jack if they don't get any for a while + ent->client->lastCmdTime = level.time; + + if ( !(ent->r.svFlags & SVF_BOT) && !g_synchronousClients.integer ) { + ClientThink_real( ent ); + } +} + + +void G_RunClient( gentity_t *ent ) { + if ( !(ent->r.svFlags & SVF_BOT) && !g_synchronousClients.integer ) { + return; + } + ent->client->pers.cmd.serverTime = level.time; + ClientThink_real( ent ); +} + + +/* +================== +SpectatorClientEndFrame + +================== +*/ +void SpectatorClientEndFrame( gentity_t *ent ) { + gclient_t *cl; + + // if we are doing a chase cam or a remote view, grab the latest info + if ( ent->client->sess.spectatorState == SPECTATOR_FOLLOW ) { + int clientNum, flags; + + clientNum = ent->client->sess.spectatorClient; + + // team follow1 and team follow2 go to whatever clients are playing + if ( clientNum == -1 ) { + clientNum = level.follow1; + } else if ( clientNum == -2 ) { + clientNum = level.follow2; + } + if ( clientNum >= 0 ) { + cl = &level.clients[ clientNum ]; + if ( cl->pers.connected == CON_CONNECTED && cl->sess.sessionTeam != TEAM_SPECTATOR ) { + flags = (cl->ps.eFlags & ~(EF_VOTED | EF_TEAMVOTED)) | (ent->client->ps.eFlags & (EF_VOTED | EF_TEAMVOTED)); + ent->client->ps = cl->ps; + ent->client->ps.pm_flags |= PMF_FOLLOW; + ent->client->ps.eFlags = flags; + return; + } else { + // drop them to free spectators unless they are dedicated camera followers + if ( ent->client->sess.spectatorClient >= 0 ) { + ent->client->sess.spectatorState = SPECTATOR_FREE; + ClientBegin( ent->client - level.clients ); + } + } + } + } + + if ( ent->client->sess.spectatorState == SPECTATOR_SCOREBOARD ) { + ent->client->ps.pm_flags |= PMF_SCOREBOARD; + } else { + ent->client->ps.pm_flags &= ~PMF_SCOREBOARD; + } +} + +/* +============== +ClientEndFrame + +Called at the end of each server frame for each connected client +A fast client will have multiple ClientThink for each ClientEdFrame, +while a slow client may have multiple ClientEndFrame between ClientThink. +============== +*/ +void ClientEndFrame( gentity_t *ent ) { + int i; + clientPersistant_t *pers; + + if ( ent->client->sess.sessionTeam == TEAM_SPECTATOR ) { + SpectatorClientEndFrame( ent ); + return; + } + + pers = &ent->client->pers; + + // turn off any expired powerups + for ( i = 0 ; i < MAX_POWERUPS ; i++ ) { + if ( ent->client->ps.powerups[ i ] < level.time ) { + ent->client->ps.powerups[ i ] = 0; + } + } + + // save network bandwidth +#if 0 + if ( !g_synchronousClients->integer && ent->client->ps.pm_type == PM_NORMAL ) { + // FIXME: this must change eventually for non-sync demo recording + VectorClear( ent->client->ps.viewangles ); + } +#endif + + // + // If the end of unit layout is displayed, don't give + // the player any normal movement attributes + // + if ( level.intermissiontime ) { + return; + } + + // burn from lava, etc + P_WorldEffects (ent); + + // apply all the damage taken this frame + P_DamageFeedback (ent); + + // add the EF_CONNECTION flag if we haven't gotten commands recently + if ( level.time - ent->client->lastCmdTime > 1000 ) { + ent->s.eFlags |= EF_CONNECTION; + } else { + ent->s.eFlags &= ~EF_CONNECTION; + } + + ent->client->ps.stats[STAT_HEALTH] = ent->health; // FIXME: get rid of ent->health... + + G_SetClientSound (ent); + + // set the latest infor + if (g_smoothClients.integer) { + BG_PlayerStateToEntityStateExtraPolate( &ent->client->ps, &ent->s, ent->client->ps.commandTime, qtrue ); + } + else { + BG_PlayerStateToEntityState( &ent->client->ps, &ent->s, qtrue ); + } + + // set the bit for the reachability area the client is currently in + // i = trap_AAS_PointReachabilityAreaIndex( ent->client->ps.origin ); + // ent->client->areabits[i >> 3] |= 1 << (i & 7); +} + + diff --git a/src/game/g_buildable.c b/src/game/g_buildable.c new file mode 100644 index 00000000..c8b8ab55 --- /dev/null +++ b/src/game/g_buildable.c @@ -0,0 +1,627 @@ +/* + * Portions Copyright (C) 2000-2001 Tim Angus + * + * This program 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, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ + +/* To assertain which portions are licensed under the GPL and which are + * licensed by Id Software, Inc. please run a diff between the equivalent + * versions of the "Tremulous" modification and the unmodified "Quake3" + * game source code. + */ + +#include "g_local.h" + + +/* +================ +nullDieFunction + +hack to prevent compilers complaining about function pointer -> NULL conversion +================ +*/ +void nullDieFunction( gentity_t *self, gentity_t *inflictor, gentity_t *attacker, int damage, int mod ) +{ +} + + +/* +================ +DSpawn_Melt + +Called when an droid spawn dies +================ +*/ +void DSpawn_Melt( gentity_t *self ) +{ + G_SelectiveRadiusDamage( self->s.pos.trBase, self->parent, 2, + self->splashRadius, self, self->splashMethodOfDeath, PTE_DROIDS ); + + if( ( self->timestamp + 10000 ) > level.time ) + { + self->nextthink = level.time + 500; + trap_LinkEntity( self ); + } + else + G_FreeEntity( self ); + + //update spawn counts + CalculateRanks( ); +} + +/* +================ +DSpawn_Die + +Called when an droid spawn dies +================ +*/ +void DSpawn_Die( gentity_t *self, gentity_t *inflictor, gentity_t *attacker, int damage, int mod ) +{ + vec3_t dir; + + // we don't have a valid direction, so just point straight up + dir[0] = dir[1] = 0; + dir[2] = 1; + + G_SelectiveRadiusDamage( self->s.pos.trBase, self->parent, self->splashDamage, + self->splashRadius, self, self->splashMethodOfDeath, PTE_DROIDS ); + + self->s.modelindex = 0; //don't draw the model once its destroyed + G_AddEvent( self, EV_GIB_GENERIC, DirToByte( dir ) ); + self->r.contents = CONTENTS_TRIGGER; + self->timestamp = level.time; + self->die = nullDieFunction; + self->think = DSpawn_Melt; + self->nextthink = level.time + 500; //wait .5 seconds before damaging others + + trap_LinkEntity( self ); +} + + +/* +================ +DDef1_Die + +Called when an droid spawn dies +================ +*/ +void DDef1_Die( gentity_t *self, gentity_t *inflictor, gentity_t *attacker, int damage, int mod ) +{ + vec3_t dir; + + // we don't have a valid direction, so just point straight up + dir[0] = dir[1] = 0; + dir[2] = 1; + + G_SelectiveRadiusDamage( self->s.pos.trBase, self->parent, self->splashDamage, + self->splashRadius, self, self->splashMethodOfDeath, PTE_DROIDS ); + + self->s.modelindex = 0; //don't draw the model once its destroyed + G_AddEvent( self, EV_GIB_GENERIC, DirToByte( dir ) ); + self->r.contents = CONTENTS_TRIGGER; + self->timestamp = level.time; + self->freeAfterEvent = qtrue; + self->die = nullDieFunction; +} + + +/* +================ +DSpawn_Think + +think function for Droid Spawn +================ +*/ +void DSpawn_Think( gentity_t *self ) +{ + if( self->parentNode == NULL ) + self->parentNode = createCreepNode( self->s.origin ); + + self->nextthink = level.time + 100; +} + + +/* +================ +DDef1_Think + +think function for Droid Spawn +================ +*/ +void DDef1_Think( gentity_t *self ) +{ + int i; + gentity_t *ent; + gentity_t *closestSpawn; + int distance = 0; + int minDistance = 10000; + vec3_t temp_v; + vec3_t dir = { 0, 0, 1 }; //up + + if( ( self->parentNode == NULL ) || !self->parentNode->inuse ) + { + for ( i = 1, ent = g_entities + i; i < level.num_entities; i++, ent++ ) + { + if( !Q_stricmp( ent->classname, "team_droid_spawn" ) ) + { + VectorSubtract( self->s.origin, ent->s.origin, temp_v ); + distance = VectorLength( temp_v ); + if( distance < minDistance ) + { + closestSpawn = ent; + minDistance = distance; + } + } + } + + if( minDistance <= CREEP_BASESIZE ) + self->parentNode = closestSpawn; + else + { + G_Damage( self, NULL, NULL, NULL, NULL, 10000, 0, MOD_SUICIDE ); + return; + } + } + + G_SelectiveRadiusDamage( self->s.pos.trBase, self->parent, self->splashDamage, + self->splashRadius, self, self->splashMethodOfDeath, PTE_DROIDS ); + + self->nextthink = level.time + 100; +} + +//TA: the following defense turret code was written by +// "fuzzysteve" (fuzzysteve@quakefiles.com) and +// Anthony "inolen" Pesch (www.inolen.com) +//with modifications by me of course :) +#define HDEF1_RANGE 500 +#define HDEF1_ANGULARSPEED 15 +#define HDEF1_FIRINGSPEED 200 +#define HDEF1_ACCURACYTOLERANCE 10 +#define HDEF1_VERTICALCAP 20 + +/* +================ +hdef1_trackenemy + +Used by HDef1_Think to track enemy location +================ +*/ +qboolean hdef1_trackenemy( gentity_t *self ) +{ + vec3_t dirToTarget, angleToTarget, angularDiff; + + VectorSubtract( self->enemy->s.pos.trBase, self->s.pos.trBase, dirToTarget ); + VectorNormalize( dirToTarget ); + vectoangles( dirToTarget, angleToTarget ); + + angularDiff[ PITCH ] = AngleSubtract( self->turloc[ PITCH ], angleToTarget[ PITCH ] ); + angularDiff[ YAW ] = AngleSubtract( self->turloc[ YAW ], angleToTarget[ YAW ] ); + + if( angularDiff[ PITCH ] < -HDEF1_ACCURACYTOLERANCE ) + self->turloc[ PITCH ] += HDEF1_ANGULARSPEED; + else if( angularDiff[ PITCH ] > HDEF1_ACCURACYTOLERANCE ) + self->turloc[ PITCH ] -= HDEF1_ANGULARSPEED; + else + self->turloc[ PITCH ] = angleToTarget[ PITCH ]; + + if( self->turloc[ PITCH ] < -HDEF1_VERTICALCAP ) + self->turloc[ PITCH ] = -HDEF1_VERTICALCAP; + + if( self->turloc[ PITCH ] > HDEF1_VERTICALCAP ) + self->turloc[ PITCH ] = HDEF1_VERTICALCAP; + + if( angularDiff[ YAW ] < -HDEF1_ACCURACYTOLERANCE ) + self->turloc[ YAW ] += HDEF1_ANGULARSPEED; + else if( angularDiff[ YAW ] > HDEF1_ACCURACYTOLERANCE ) + self->turloc[ YAW ] -= HDEF1_ANGULARSPEED; + else + self->turloc[ YAW ] = angleToTarget[ YAW ]; + + VectorCopy( self->turloc, self->s.angles2 ); + + trap_LinkEntity( self ); + + if( abs( angleToTarget[ YAW ] - self->turloc[ YAW ] ) <= HDEF1_ACCURACYTOLERANCE && + abs( angleToTarget[ PITCH ] - self->turloc[ PITCH ] ) <= HDEF1_ACCURACYTOLERANCE ) + return qtrue; + + return qfalse; +} + +/* +================ +hdef1_fireonemeny + +Used by HDef1_Think to fire at enemy +================ +*/ +void hdef1_fireonenemy( gentity_t *self ) +{ + vec3_t aimVector; + + AngleVectors( self->turloc, aimVector, NULL, NULL ); + //fire_flamer( self, self->s.pos.trBase, aimVector ); + fire_plasma( self, self->s.pos.trBase, aimVector ); + G_AddEvent( self, EV_FIRE_WEAPON, 0 ); + self->count = level.time + HDEF1_FIRINGSPEED; +} + +/* +================ +hdef1_checktarget + +Used by HDef1_Think to check enemies for validity +================ +*/ +qboolean hdef1_checktarget(gentity_t *self, gentity_t *target) +{ + vec3_t distance; + trace_t trace; + + if( !target ) // Do we have a target? + return qfalse; + if( !target->inuse ) // Does the target still exist? + return qfalse; + if( target == self ) // is the target us? + return qfalse; + if( !target->client ) // is the target a bot or player? + return qfalse; + if( target->client->ps.stats[ STAT_PTEAM ] == PTE_HUMANS ) // is the target one of us? + return qfalse; + if( target->client->sess.sessionTeam == TEAM_SPECTATOR ) // is the target alive? + return qfalse; + if( target->health <= 0 ) // is the target still alive? + return qfalse; + + VectorSubtract( target->r.currentOrigin, self->r.currentOrigin, distance ); + if( VectorLength( distance ) > HDEF1_RANGE ) // is the target within range? + return qfalse; + + trap_Trace( &trace, self->s.pos.trBase, NULL, NULL, target->s.pos.trBase, self->s.number, MASK_SHOT ); + if ( trace.contents & CONTENTS_SOLID ) // can we see the target? + return qfalse; + + return qtrue; +} + + +/* +================ +hdef1_findenemy + +Used by HDef1_Think to locate enemy gentities +================ +*/ +void hdef1_findenemy( gentity_t *ent ) +{ + gentity_t *target; + + target = g_entities; + + for (; target < &g_entities[ level.num_entities ]; target++) + { + if( !hdef1_checktarget( ent, target ) ) + continue; + + ent->enemy = target; + return; + } + + ent->enemy = NULL; +} + + +/* +================ +HDef1_Think + +think function for Human Defense +================ +*/ +void HDef1_Think( gentity_t *self ) +{ + self->nextthink = level.time + 50; + + if( !hdef1_checktarget( self, self->enemy) ) + hdef1_findenemy( self ); + if( !self->enemy ) + return; + + if( hdef1_trackenemy( self ) && ( self->count < level.time ) ) + hdef1_fireonenemy( self ); +} + + +/* +================ +HSpawn_blast + +Called when a human spawn explodes +think function +================ +*/ +void HSpawn_Blast( gentity_t *self ) +{ + G_RadiusDamage( self->s.pos.trBase, self->parent, self->splashDamage, + self->splashRadius, self, self->splashMethodOfDeath ); + + G_FreeEntity( self ); + trap_LinkEntity( self ); + + //update spawn counts + CalculateRanks( ); +} + + +/* +================ +HSpawn_die + +Called when a human spawn dies +================ +*/ +void HSpawn_Die( gentity_t *self, gentity_t *inflictor, gentity_t *attacker, int damage, int mod ) +{ + vec3_t dir; + + // we don't have a valid direction, so just point straight up + dir[0] = dir[1] = 0; + dir[2] = 1; + + self->s.modelindex = 0; //don't draw the model once its destroyed + G_AddEvent( self, EV_ITEM_EXPLOSION, DirToByte( dir ) ); + self->r.contents = CONTENTS_TRIGGER; + self->timestamp = level.time; + self->die = nullDieFunction; + self->think = HSpawn_Blast; + self->nextthink = level.time + 1000; //wait 1.5 seconds before damaging others + + trap_LinkEntity( self ); +} + + +/* +================ +itemFits + +Checks to see if an item fits in a specific area +================ +*/ +qboolean itemFits( gentity_t *ent, gitem_t *item, int distance ) +{ + vec3_t forward; + vec3_t angles; + vec3_t player_origin, entity_origin; + vec3_t mins, maxs; + vec3_t temp_v; + trace_t tr1, tr2; + int i; + gentity_t *creepent; + qboolean creeptest; + qboolean nearcreep = qfalse; + + VectorCopy( ent->s.apos.trBase, angles ); + angles[PITCH] = 0; // always forward + + AngleVectors( angles, forward, NULL, NULL ); + VectorCopy( ent->s.pos.trBase, player_origin ); + VectorMA( player_origin, distance, forward, entity_origin ); + + if( !Q_stricmp( item->classname, "team_droid_spawn" ) ) + { + creeptest = qfalse; + VectorSet( mins, -15, -15, -15 ); + VectorSet( maxs, 15, 15, 15 ); + } + else if( !Q_stricmp( item->classname, "team_droid_def1" ) ) + { + creeptest = qtrue; + VectorSet( mins, -15, -15, -15 ); + VectorSet( maxs, 15, 15, 15 ); + } + else if( !Q_stricmp( item->classname, "team_human_spawn" ) ) + { + creeptest = qfalse; + VectorSet( mins, -40, -40, -4 ); + VectorSet( maxs, 40, 40, 4 ); + } + else if( !Q_stricmp( item->classname, "team_human_def1" ) ) + { + creeptest = qfalse; + VectorSet( mins, -24, -24, -11 ); + VectorSet( maxs, 24, 24, 11 ); + } + else if( !Q_stricmp( item->classname, "team_human_mcu" ) ) + { + creeptest = qfalse; + VectorSet( mins, -15, -15, -15 ); + VectorSet( maxs, 15, 15, 15 ); + } + + trap_Trace( &tr1, entity_origin, mins, maxs, entity_origin, ent->s.number, MASK_PLAYERSOLID ); + trap_Trace( &tr2, player_origin, NULL, NULL, entity_origin, ent->s.number, MASK_PLAYERSOLID ); + + if( creeptest ) + { + for ( i = 1, creepent = g_entities + i; i < level.num_entities; i++, creepent++ ) + { + if( !Q_stricmp( creepent->classname, "team_droid_spawn" ) ) + { + VectorSubtract( entity_origin, creepent->s.origin, temp_v ); + if( VectorLength( temp_v ) <= CREEP_BASESIZE ) + { + nearcreep = qtrue; + break; + } + } + } + } + else + nearcreep = qtrue; + + if( tr1.fraction >= 1.0 && tr2.fraction >= 1.0 && nearcreep ) + return qtrue; + else + return qfalse; +} + + +/* +================ +Build_Item + +Spawns an item and tosses it forward +================ +*/ +gentity_t *Build_Item( gentity_t *ent, gitem_t *item, int distance ) { + vec3_t forward; + vec3_t angles; + vec3_t origin; + gentity_t *built; + + VectorCopy( ent->s.apos.trBase, angles ); + angles[PITCH] = 0; // always forward + + AngleVectors( angles, forward, NULL, NULL ); + VectorCopy( ent->s.pos.trBase, origin ); + VectorMA( origin, distance, forward, origin ); + + built = G_Spawn(); + + built->s.eType = ET_BUILDABLE; + built->s.modelindex = item - bg_itemlist; // store item number in modelindex + + built->classname = item->classname; + built->item = item; + + if( !Q_stricmp( item->classname, "team_droid_spawn" ) ) + { + VectorSet( built->r.mins, -15, -15, -15 ); + VectorSet( built->r.maxs, 15, 15, 15 ); + + built->biteam = BIT_DROIDS; + built->takedamage = qtrue; + built->health = 1000; + built->damage = 50; + built->splashDamage = 50; + built->splashRadius = 200; + built->splashMethodOfDeath = MOD_ASPAWN; + built->s.modelindex2 = BIT_DROIDS; + G_AddEvent( built, EV_ITEM_GROW, 0 ); + //built->touch = ASpawn_Touch; + built->die = DSpawn_Die; + //built->pain = ASpawn_Pain; + built->think = DSpawn_Think; + built->nextthink = level.time + 100; + } + else if( !Q_stricmp( item->classname, "team_droid_def1" ) ) + { + VectorSet( built->r.mins, -15, -15, -15 ); + VectorSet( built->r.maxs, 15, 15, 15 ); + + built->biteam = BIT_DROIDS; + built->takedamage = qtrue; + built->health = 1000; + built->damage = 50; + built->splashDamage = 20; + built->splashRadius = 50; + built->splashMethodOfDeath = MOD_ASPAWN; + built->s.modelindex2 = BIT_DROIDS; + G_AddEvent( built, EV_ITEM_GROW, 0 ); + //built->touch = ASpawn_Touch; + built->die = DDef1_Die; + //built->pain = ASpawn_Pain; + built->think = DDef1_Think; + built->nextthink = level.time + 100; + } + else if( !Q_stricmp( item->classname, "team_human_spawn" ) ) + { + VectorSet( built->r.mins, -40, -40, -4 ); + VectorSet( built->r.maxs, 40, 40, 4 ); + + built->biteam = BIT_HUMANS; + built->takedamage = qtrue; + built->health = 1000; + built->damage = 50; + built->splashDamage = 50; + built->splashRadius = 150; + built->splashMethodOfDeath = MOD_HSPAWN; + built->s.modelindex2 = BIT_HUMANS; + //built->touch = HSpawn_Touch; + //built->think = HSpawn_Think; + //built->nextthink = level.time + 1000; + built->die = HSpawn_Die; + //built->pain = HSpawn_Pain; + } + else if( !Q_stricmp( item->classname, "team_human_def1" ) ) + { + VectorSet( built->r.mins, -24, -24, -11 ); + VectorSet( built->r.maxs, 24, 24, 11 ); + + built->biteam = BIT_HUMANS; + built->takedamage = qtrue; + built->health = 1000; + built->damage = 50; + built->splashDamage = 20; + built->splashRadius = 50; + built->splashMethodOfDeath = MOD_HSPAWN; + built->s.modelindex2 = BIT_HUMANS; + //built->touch = ASpawn_Touch; + built->die = HSpawn_Die; + //built->pain = ASpawn_Pain; + built->think = HDef1_Think; + built->enemy = NULL; + built->nextthink = level.time + 50; + } + else if( !Q_stricmp( item->classname, "team_human_mcu" ) ) + { + VectorSet( built->r.mins, -15, -15, -15 ); + VectorSet( built->r.maxs, 15, 15, 15 ); + + built->biteam = BIT_HUMANS; + built->takedamage = qtrue; + built->health = 1000; + built->damage = 50; + built->splashDamage = 50; + built->splashRadius = 150; + built->splashMethodOfDeath = MOD_HSPAWN; + built->s.modelindex2 = BIT_HUMANS; + //built->touch = HSpawn_Touch; + //built->think = HSpawn_Think; + //built->nextthink = level.time + 1000; + built->die = HSpawn_Die; + //built->pain = HSpawn_Pain; + } + + built->s.number = built - g_entities; + built->r.contents = CONTENTS_BODY; + built->clipmask = MASK_PLAYERSOLID; + + G_SetOrigin( built, origin ); + VectorCopy( angles, built->s.angles ); + built->turloc[ YAW ] = built->s.angles2[ YAW ] = angles[ YAW ]; + VectorCopy( origin, built->s.origin ); + built->s.pos.trType = TR_GRAVITY; + built->s.pos.trTime = level.time; + + //update spawn counts + CalculateRanks( ); + + trap_LinkEntity (built); + + return built; +} + diff --git a/src/game/g_client.c b/src/game/g_client.c new file mode 100644 index 00000000..af9fdee7 --- /dev/null +++ b/src/game/g_client.c @@ -0,0 +1,1543 @@ +// Copyright (C) 1999-2000 Id Software, Inc. +// +/* + * Portions Copyright (C) 2000-2001 Tim Angus + * + * This program 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, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ + +/* To assertain which portions are licensed under the GPL and which are + * licensed by Id Software, Inc. please run a diff between the equivalent + * versions of the "Tremulous" modification and the unmodified "Quake3" + * game source code. + */ + +#include "g_local.h" + +// g_client.c -- client functions that don't happen every frame + +static vec3_t playerMins = {-15, -15, -24}; +static vec3_t playerMaxs = {15, 15, 32}; + +/*QUAKED info_player_deathmatch (1 0 1) (-16 -16 -24) (16 16 32) initial +potential spawning position for deathmatch games. +The first time a player enters the game, they will be at an 'initial' spot. +Targets will be fired when someone spawns in on them. +"nobots" will prevent bots from using this spot. +"nohumans" will prevent non-bots from using this spot. +*/ +void SP_info_player_deathmatch( gentity_t *ent ) { + int i; + + G_SpawnInt( "nobots", "0", &i); + if ( i ) { + ent->flags |= FL_NO_BOTS; + } + G_SpawnInt( "nohumans", "0", &i ); + if ( i ) { + ent->flags |= FL_NO_HUMANS; + } +} + +/*QUAKED info_player_start (1 0 0) (-16 -16 -24) (16 16 32) +equivelant to info_player_deathmatch +*/ +void SP_info_player_start(gentity_t *ent) { + ent->classname = "info_player_deathmatch"; + SP_info_player_deathmatch( ent ); +} + +/*QUAKED info_player_intermission (1 0 1) (-16 -16 -24) (16 16 32) +The intermission will be viewed from this point. Target an info_notnull for the view direction. +*/ +void SP_info_player_intermission( gentity_t *ent ) { + +} + +/*QUAKED info_droid_intermission (1 0 1) (-16 -16 -24) (16 16 32) +The intermission will be viewed from this point. Target an info_notnull for the view direction. +*/ +void SP_info_droid_intermission( gentity_t *ent ) { + +} + +/*QUAKED info_human_intermission (1 0 1) (-16 -16 -24) (16 16 32) +The intermission will be viewed from this point. Target an info_notnull for the view direction. +*/ +void SP_info_human_intermission( gentity_t *ent ) { + +} + + + +/* +======================================================================= + + SelectSpawnPoint + +======================================================================= +*/ + +/* +================ +SpotWouldTelefrag + +================ +*/ +qboolean SpotWouldTelefrag( gentity_t *spot ) { + int i, num; + int touch[MAX_GENTITIES]; + gentity_t *hit; + vec3_t mins, maxs; + + VectorAdd( spot->s.origin, playerMins, mins ); + VectorAdd( spot->s.origin, playerMaxs, maxs ); + num = trap_EntitiesInBox( mins, maxs, touch, MAX_GENTITIES ); + + for (i=0 ; i<num ; i++) { + hit = &g_entities[touch[i]]; + //if ( hit->client && hit->client->ps.stats[STAT_HEALTH] > 0 ) { + if( hit->client ) { + return qtrue; + } + + } + + return qfalse; +} + +/* +================ +SelectNearestDeathmatchSpawnPoint + +Find the spot that we DON'T want to use +================ +*/ +#define MAX_SPAWN_POINTS 128 +gentity_t *SelectNearestDeathmatchSpawnPoint( vec3_t from ) { + gentity_t *spot; + vec3_t delta; + float dist, nearestDist; + gentity_t *nearestSpot; + + nearestDist = 999999; + nearestSpot = NULL; + spot = NULL; + + while ((spot = G_Find (spot, FOFS(classname), "info_player_deathmatch")) != NULL) { + + VectorSubtract( spot->s.origin, from, delta ); + dist = VectorLength( delta ); + if ( dist < nearestDist ) { + nearestDist = dist; + nearestSpot = spot; + } + } + + return nearestSpot; +} + + +/* +================ +SelectRandomDeathmatchSpawnPoint + +go to a random point that doesn't telefrag +================ +*/ +#define MAX_SPAWN_POINTS 128 +gentity_t *SelectRandomDeathmatchSpawnPoint( void ) { + gentity_t *spot; + int count; + int selection; + gentity_t *spots[MAX_SPAWN_POINTS]; + + count = 0; + spot = NULL; + + while ((spot = G_Find (spot, FOFS(classname), "info_player_deathmatch")) != NULL) { + if ( SpotWouldTelefrag( spot ) ) { + continue; + } + spots[ count ] = spot; + count++; + } + + if ( !count ) { // no spots that won't telefrag + return G_Find( NULL, FOFS(classname), "info_player_deathmatch"); + } + + selection = rand() % count; + return spots[ selection ]; +} + + +/* +=========== +SelectRandomFurthestSpawnPoint + +Chooses a player start, deathmatch start, etc +============ +*/ +gentity_t *SelectRandomFurthestSpawnPoint ( vec3_t avoidPoint, vec3_t origin, vec3_t angles ) { + gentity_t *spot; + vec3_t delta; + float dist; + float list_dist[64]; + gentity_t *list_spot[64]; + int numSpots, rnd, i, j; + + numSpots = 0; + spot = NULL; + + while ((spot = G_Find (spot, FOFS(classname), "info_player_deathmatch")) != NULL) { + if ( SpotWouldTelefrag( spot ) ) { + continue; + } + VectorSubtract( spot->s.origin, avoidPoint, delta ); + dist = VectorLength( delta ); + for (i = 0; i < numSpots; i++) { + if ( dist > list_dist[i] ) { + if ( numSpots >= 64 ) + numSpots = 64-1; + for (j = numSpots; j > i; j--) { + list_dist[j] = list_dist[j-1]; + list_spot[j] = list_spot[j-1]; + } + list_dist[i] = dist; + list_spot[i] = spot; + numSpots++; + if (numSpots > 64) + numSpots = 64; + break; + } + } + if (i >= numSpots && numSpots < 64) { + list_dist[numSpots] = dist; + list_spot[numSpots] = spot; + numSpots++; + } + } + if (!numSpots) { + spot = G_Find( NULL, FOFS(classname), "info_player_deathmatch"); + if (!spot) + G_Error( "Couldn't find a spawn point" ); + VectorCopy (spot->s.origin, origin); + origin[2] += 9; + VectorCopy (spot->s.angles, angles); + return spot; + } + + // select a random spot from the spawn points furthest away + rnd = random() * (numSpots / 2); + + VectorCopy (list_spot[rnd]->s.origin, origin); + origin[2] += 9; + VectorCopy (list_spot[rnd]->s.angles, angles); + + return list_spot[rnd]; +} + + +/* +================ +SelectDroidSpawnPoint + +go to a random point that doesn't telefrag +================ +*/ +gentity_t *SelectDroidSpawnPoint( void ) { + gentity_t *spot; + int count; + int selection; + gentity_t *spots[MAX_SPAWN_POINTS]; + + count = 0; + spot = NULL; + + while ((spot = G_Find (spot, FOFS(classname), "team_droid_spawn")) != NULL) { + if ( SpotWouldTelefrag( spot ) || ( spot->health <= 0 ) ) { + continue; + } + spots[ count ] = spot; + count++; + } + + if ( !count ) { // no spots that won't telefrag + spot = G_Find( NULL, FOFS(classname), "team_droid_spawn"); + if( spot->health > 0 ) + return spot; + else + return NULL; + + } + + selection = rand() % count; + return spots[ selection ]; +} + + +/* +================ +SelectHumanSpawnPoint + +go to a random point that doesn't telefrag +================ +*/ +gentity_t *SelectHumanSpawnPoint( void ) { + gentity_t *spot; + int count; + int selection; + gentity_t *spots[MAX_SPAWN_POINTS]; + + count = 0; + spot = NULL; + + while ((spot = G_Find (spot, FOFS(classname), "team_human_spawn")) != NULL) { + if ( SpotWouldTelefrag( spot ) || ( spot->health <= 0 ) ) { + continue; + } + spots[ count ] = spot; + count++; + } + + if ( !count ) { // no spots that won't telefrag + spot = G_Find( NULL, FOFS(classname), "team_human_spawn"); + if( spot->health > 0 ) + return spot; + else + return NULL; + } + + selection = rand() % count; + return spots[ selection ]; +} + + +/* +=========== +SelectSpawnPoint + +Chooses a player start, deathmatch start, etc +============ +*/ +gentity_t *SelectSpawnPoint ( vec3_t avoidPoint, vec3_t origin, vec3_t angles ) { + return SelectRandomFurthestSpawnPoint( avoidPoint, origin, angles ); + + /* + gentity_t *spot; + gentity_t *nearestSpot; + + nearestSpot = SelectNearestDeathmatchSpawnPoint( avoidPoint ); + + spot = SelectRandomDeathmatchSpawnPoint ( ); + if ( spot == nearestSpot ) { + // roll again if it would be real close to point of death + spot = SelectRandomDeathmatchSpawnPoint ( ); + if ( spot == nearestSpot ) { + // last try + spot = SelectRandomDeathmatchSpawnPoint ( ); + } + } + + // find a single player start spot + if (!spot) { + G_Error( "Couldn't find a spawn point" ); + } + + VectorCopy (spot->s.origin, origin); + origin[2] += 9; + VectorCopy (spot->s.angles, angles); + + return spot; + */ +} + + +/* +=========== +SelectTremulousSpawnPoint + +Chooses a player start, deathmatch start, etc +============ +*/ +gentity_t *SelectTremulousSpawnPoint( int team, vec3_t origin, vec3_t angles ) +{ + gentity_t *spot; + + if( team == PTE_DROIDS ) + spot = SelectDroidSpawnPoint( ); + else if( team == PTE_HUMANS ) + spot = SelectHumanSpawnPoint( ); + + //no available spots + if( !spot ) + { + return NULL; + } + + // find a single player start spot + if (!spot) { + //G_Error( "Couldn't find a spawn point" ); + } + + //TA: why isn't spot->s.origin being set? + VectorCopy (spot->s.pos.trBase, origin); + VectorCopy (spot->s.angles, angles); + + if( team == PTE_DROIDS ) + origin[2] += 40; + else if( team == PTE_HUMANS ) + origin[2] += 29; + + return spot; + +} + + +/* +=========== +SelectInitialSpawnPoint + +Try to find a spawn point marked 'initial', otherwise +use normal spawn selection. +============ +*/ +gentity_t *SelectInitialSpawnPoint( vec3_t origin, vec3_t angles ) { + gentity_t *spot; + + spot = NULL; + while ((spot = G_Find (spot, FOFS(classname), "info_player_deathmatch")) != NULL) { + if ( spot->spawnflags & 1 ) { + break; + } + } + + if ( !spot || SpotWouldTelefrag( spot ) ) { + return SelectSpawnPoint( vec3_origin, origin, angles ); + } + + VectorCopy (spot->s.origin, origin); + origin[2] += 9; + VectorCopy (spot->s.angles, angles); + + return spot; +} + +/* +=========== +SelectSpectatorSpawnPoint + +============ +*/ +gentity_t *SelectSpectatorSpawnPoint( vec3_t origin, vec3_t angles ) { + FindIntermissionPoint(); + + VectorCopy( level.intermission_origin, origin ); + VectorCopy( level.intermission_angle, angles ); + + return NULL; +} + + +/* +=========== +SelectDroidLockSpawnPoint + +Try to find a spawn point for droid intermission otherwise +use normal intermission spawn. +============ +*/ +gentity_t *SelectDroidLockSpawnPoint( vec3_t origin, vec3_t angles ) { + gentity_t *spot; + + spot = NULL; + spot = G_Find (spot, FOFS(classname), "info_droid_intermission"); + + if ( !spot ) { + return SelectSpectatorSpawnPoint( origin, angles ); + } + + VectorCopy (spot->s.origin, origin); + VectorCopy (spot->s.angles, angles); + + return spot; +} + + +/* +=========== +SelectHumanLockSpawnPoint + +Try to find a spawn point for human intermission otherwise +use normal intermission spawn. +============ +*/ +gentity_t *SelectHumanLockSpawnPoint( vec3_t origin, vec3_t angles ) { + gentity_t *spot; + + spot = NULL; + spot = G_Find (spot, FOFS(classname), "info_human_intermission"); + + if ( !spot ) { + return SelectSpectatorSpawnPoint( origin, angles ); + } + + VectorCopy (spot->s.origin, origin); + VectorCopy (spot->s.angles, angles); + + return spot; +} + + +/* +======================================================================= + +BODYQUE + +======================================================================= +*/ + +#if BODY_QUEUE_SIZE +/* +=============== +InitBodyQue +=============== +*/ +void InitBodyQue (void) { + int i; + gentity_t *ent; + + level.bodyQueIndex = 0; + for (i=0; i<BODY_QUEUE_SIZE ; i++) { + ent = G_Spawn(); + ent->classname = "bodyque"; + ent->neverFree = qtrue; + level.bodyQue[i] = ent; + } +} +#endif + +/* +============= +BodySink + +After sitting around for five seconds, fall into the ground and dissapear +============= +*/ +void BodySink( gentity_t *ent ) { + if ( level.time - ent->timestamp > 6500 ) { + // the body ques are never actually freed, they are just unlinked + trap_UnlinkEntity( ent ); + ent->physicsObject = qfalse; + return; + } + ent->nextthink = level.time + 100; + ent->s.pos.trBase[2] -= 1; +} + +/* +============= +CopyToBodyQue + +A player is respawning, so make an entity that looks +just like the existing corpse to leave behind. +============= +*/ +void CopyToBodyQue( gentity_t *ent ) { + gentity_t *body; + int contents; + + //TA: not really the place for this.. but hey.. + if( ent->client->torch != NULL && BG_activated( UP_TORCH, ent->client->ps.stats ) ) + { + G_FreeEntity( ent->client->torch ); + trap_UnlinkEntity( ent->client->torch ); + ent->client->torch = NULL; + } + + trap_UnlinkEntity (ent); + + // if client is in a nodrop area, don't leave the body + contents = trap_PointContents( ent->s.origin, -1 ); + if ( contents & CONTENTS_NODROP ) { + return; + } + + body = G_Spawn( ); + body->classname = "corpse"; + body->s = ent->s; + body->r.s = body->s; + body->s.eFlags = EF_DEAD; + body->s.eType = ET_CORPSE; + body->s.number = body - g_entities; + body->timestamp = level.time; + body->physicsObject = qtrue; + body->s.event = 0; + body->r.contents = CONTENTS_BODY; + body->clipmask = MASK_PLAYERSOLID; + + switch ( body->s.legsAnim & ~ANIM_TOGGLEBIT ) { + case BOTH_DEATH1: + case BOTH_DEAD1: + body->s.torsoAnim = body->s.legsAnim = BOTH_DEAD1; + break; + case BOTH_DEATH2: + case BOTH_DEAD2: + body->s.torsoAnim = body->s.legsAnim = BOTH_DEAD2; + break; + case BOTH_DEATH3: + case BOTH_DEAD3: + default: + body->s.torsoAnim = body->s.legsAnim = BOTH_DEAD3; + break; + } + + //body->die = body_die; + + // don't take more damage if already gibbed + if ( ent->health <= GIB_HEALTH ) { + body->takedamage = qfalse; + } else { + body->takedamage = qtrue; + } + + //make the make player entity disappear + ent->takedamage = qfalse; + ent->s.eType = ET_INVISIBLE; + ent->r.contents = 0; + ent->s.solid = 0; + ent->r.s.solid = 0; + body->health = ent->health = ent->client->ps.stats[STAT_HEALTH]; + ent->health = ent->client->ps.stats[STAT_HEALTH] = GIB_HEALTH - 1; + + //FIXME: change body dimensions + VectorSet( body->r.mins, -15, -15, -15 ); + VectorSet( body->r.maxs, 15, 15, 15 ); + VectorSet( body->r.absmin, -15, -15, -15 ); + VectorSet( body->r.absmax, 15, 15, 15 ); + + if ( body->s.groundEntityNum == ENTITYNUM_NONE ) + { + body->s.pos.trType = TR_GRAVITY; + body->s.pos.trTime = level.time; + VectorCopy( ent->client->ps.velocity, body->s.pos.trDelta ); + } + else + { + body->s.pos.trType = TR_STATIONARY; + } + + body->s.pos.trTime = level.time; + + VectorCopy ( body->s.pos.trBase, body->r.currentOrigin ); + trap_LinkEntity( body ); + +} + +//====================================================================== + + +/* +================== +SetClientViewAngle + +================== +*/ +void SetClientViewAngle( gentity_t *ent, vec3_t angle ) { + int i; + + // set the delta angle + for (i=0 ; i<3 ; i++) { + int cmdAngle; + + cmdAngle = ANGLE2SHORT(angle[i]); + ent->client->ps.delta_angles[i] = cmdAngle - ent->client->pers.cmd.angles[i]; + } + VectorCopy( angle, ent->s.angles ); + VectorCopy (ent->s.angles, ent->client->ps.viewangles); +} + +/* +================ +respawn +================ +*/ +void respawn( gentity_t *ent ) { + gentity_t *tent; + + CopyToBodyQue (ent); + + //TA: Clients can't respawn - they must go thru the class cmd + ClientSpawn(ent); + + //FIXME: need different spawn/respawn functions for different teams + + // add a teleportation effect + //tent = G_TempEntity( ent->client->ps.origin, EV_PLAYER_TELEPORT_IN ); + //tent->s.clientNum = ent->s.clientNum; +} + +/* +================ +TeamCount + +Returns number of players on a team +================ +*/ +team_t TeamCount( int ignoreClientNum, int team ) { + int i; + int count = 0; + + for ( i = 0 ; i < level.maxclients ; i++ ) { + if ( i == ignoreClientNum ) { + continue; + } + if ( level.clients[i].pers.connected == CON_DISCONNECTED ) { + continue; + } + if ( level.clients[i].sess.sessionTeam == team ) { + count++; + } + } + + return count; +} + + +/* +================ +TeamLeader + +Returns the client number of the team leader +================ +*/ +int TeamLeader( int team ) { + int i; + + for ( i = 0 ; i < level.maxclients ; i++ ) { + if ( level.clients[i].pers.connected == CON_DISCONNECTED ) { + continue; + } + if ( level.clients[i].sess.sessionTeam == team ) { + if ( level.clients[i].sess.teamLeader ) + return i; + } + } + + return -1; +} + + +/* +================ +PickTeam + +================ +*/ +team_t PickTeam( int ignoreClientNum ) { + int counts[TEAM_NUM_TEAMS]; + + counts[TEAM_DROIDS] = TeamCount( ignoreClientNum, TEAM_DROIDS ); + counts[TEAM_HUMANS] = TeamCount( ignoreClientNum, TEAM_HUMANS ); + + if ( counts[TEAM_DROIDS] > counts[TEAM_HUMANS] ) { + return TEAM_HUMANS; + } + if ( counts[TEAM_HUMANS] > counts[TEAM_DROIDS] ) { + return TEAM_DROIDS; + } + // equal team count, so join the team with the lowest score + if ( level.teamScores[TEAM_DROIDS] > level.teamScores[TEAM_HUMANS] ) { + return TEAM_HUMANS; + } + return TEAM_DROIDS; +} + +/* +=========== +ForceClientSkin + +Forces a client's skin (for teamplay) +=========== +*/ +static void ForceClientSkin( gclient_t *client, char *model, const char *skin ) { + char *p; + + if ((p = Q_strrchr(model, '/')) != 0) { + *p = 0; + } + + Q_strcat(model, MAX_QPATH, "/"); + Q_strcat(model, MAX_QPATH, skin); +} + + +/* +=========== +ClientCheckName +============ +*/ +static void ClientCleanName( const char *in, char *out, int outSize ) { + int len, colorlessLen; + char ch; + char *p; + int spaces; + + //save room for trailing null byte + outSize--; + + len = 0; + colorlessLen = 0; + p = out; + *p = 0; + spaces = 0; + + while( 1 ) { + ch = *in++; + if( !ch ) { + break; + } + + // don't allow leading spaces + if( !*p && ch == ' ' ) { + continue; + } + + // check colors + if( ch == Q_COLOR_ESCAPE ) { + // solo trailing carat is not a color prefix + if( !*in ) { + break; + } + + // don't allow black in a name, period + if( ColorIndex(*in) == 0 ) { + in++; + continue; + } + + // make sure room in dest for both chars + if( len > outSize - 2 ) { + break; + } + + *out++ = ch; + *out++ = *in++; + len += 2; + continue; + } + + // don't allow too many consecutive spaces + if( ch == ' ' ) { + spaces++; + if( spaces > 3 ) { + continue; + } + } + else { + spaces = 0; + } + + if( len > outSize - 1 ) { + break; + } + + *out++ = ch; + colorlessLen++; + len++; + } + *out = 0; + + // don't allow empty names + if( *p == 0 || colorlessLen == 0 ) { + Q_strncpyz( p, "UnnamedPlayer", outSize ); + } +} + + +/* +=========== +ClientUserInfoChanged + +Called from ClientConnect when the player first connects and +directly by the server system when the player updates a userinfo variable. + +The game can override any of the settings and call trap_SetUserinfo +if desired. +============ +*/ +void ClientUserinfoChanged( int clientNum ) { + gentity_t *ent; + int teamTask, teamLeader, team, health; + char *s; + char model[MAX_QPATH]; + char oldname[MAX_STRING_CHARS]; + gclient_t *client; + char c1[MAX_INFO_STRING]; + char redTeam[MAX_INFO_STRING]; + char blueTeam[MAX_INFO_STRING]; + char userinfo[MAX_INFO_STRING]; + + ent = g_entities + clientNum; + client = ent->client; + + trap_GetUserinfo( clientNum, userinfo, sizeof( userinfo ) ); + + // check for malformed or illegal info strings + if ( !Info_Validate(userinfo) ) { + strcpy (userinfo, "\\name\\badinfo"); + } + + // check for local client + s = Info_ValueForKey( userinfo, "ip" ); + if ( !strcmp( s, "localhost" ) ) { + client->pers.localClient = qtrue; + } + + // check the item prediction + s = Info_ValueForKey( userinfo, "cg_predictItems" ); + if ( !atoi( s ) ) { + client->pers.predictItemPickup = qfalse; + } else { + client->pers.predictItemPickup = qtrue; + } + + // set name + Q_strncpyz ( oldname, client->pers.netname, sizeof( oldname ) ); + s = Info_ValueForKey (userinfo, "name"); + ClientCleanName( s, client->pers.netname, sizeof(client->pers.netname) ); + + if ( client->sess.sessionTeam == TEAM_SPECTATOR ) { + if ( client->sess.spectatorState == SPECTATOR_SCOREBOARD ) { + Q_strncpyz( client->pers.netname, "scoreboard", sizeof(client->pers.netname) ); + } + } + + if ( client->pers.connected == CON_CONNECTED ) { + if ( strcmp( oldname, client->pers.netname ) ) { + trap_SendServerCommand( -1, va("print \"%s" S_COLOR_WHITE " renamed to %s\n\"", oldname, + client->pers.netname) ); + } + } + + // set max health + health = atoi( Info_ValueForKey( userinfo, "handicap" ) ); + client->pers.maxHealth = health; + if ( client->pers.maxHealth < 1 || client->pers.maxHealth > 100 ) { + client->pers.maxHealth = 100; + } + //client->ps.stats[STAT_MAX_HEALTH] = client->pers.maxHealth; + + // set model + //Q_strncpyz( model, Info_ValueForKey (userinfo, "model"), sizeof( model ) ); + switch( client->pers.pclass ) + { + case PCL_D_BASE: + Q_strncpyz( model, "klesk", sizeof( model ) ); + break; + case PCL_D_BUILDER: + Q_strncpyz( model, "lucy", sizeof( model ) ); + break; + case PCL_H_BASE: + Q_strncpyz( model, "sarge", sizeof( model ) ); + break; + default: + Q_strncpyz( model, "grunt", sizeof( model ) ); + } + + // team + switch( client->sess.sessionTeam ) { + case TEAM_HUMANS: + ForceClientSkin(client, model, "red"); + break; + case TEAM_DROIDS: + ForceClientSkin(client, model, "blue"); + break; + } + if ( g_gametype.integer >= GT_TEAM && client->sess.sessionTeam == TEAM_SPECTATOR ) { + // don't ever use a default skin in teamplay, it would just waste memory + ForceClientSkin(client, model, "red"); + } + + + + // teamInfo + s = Info_ValueForKey( userinfo, "teamoverlay" ); + if ( ! *s || atoi( s ) != 0 ) { + client->pers.teamInfo = qtrue; + } else { + client->pers.teamInfo = qfalse; + } + + // team task (0 = none, 1 = offence, 2 = defence) + teamTask = atoi(Info_ValueForKey(userinfo, "teamtask")); + // team Leader (1 = leader, 0 is normal player) + teamLeader = client->sess.teamLeader; + + // colors + strcpy(c1, Info_ValueForKey( userinfo, "color" )); + strcpy(redTeam, "humans"); + strcpy(blueTeam, "droids"); + + // send over a subset of the userinfo keys so other clients can + // print scoreboards, display models, and play custom sounds + if ( ent->r.svFlags & SVF_BOT ) { + s = va("n\\%s\\t\\%i\\model\\%s\\hmodel\\%s\\c1\\%s\\hc\\%i\\w\\%i\\l\\%i\\skill\\%s\\tt\\%d\\tl\\%d", + client->pers.netname, client->sess.sessionTeam, model, model, c1, + client->pers.maxHealth, client->sess.wins, client->sess.losses, + Info_ValueForKey( userinfo, "skill" ), teamTask, teamLeader ); + } else { + s = va("n\\%s\\t\\%i\\model\\%s\\hmodel\\%s\\g_redteam\\%s\\g_blueteam\\%s\\c1\\%s\\hc\\%i\\w\\%i\\l\\%i\\tt\\%d\\tl\\%d", + client->pers.netname, client->sess.sessionTeam, model, model, redTeam, blueTeam, c1, + client->pers.maxHealth, client->sess.wins, client->sess.losses, teamTask, teamLeader); + } + + trap_SetConfigstring( CS_PLAYERS+clientNum, s ); + + G_LogPrintf( "ClientUserinfoChanged: %i %s\n", clientNum, s ); +} + + +/* +=========== +ClientConnect + +Called when a player begins connecting to the server. +Called again for every map change or tournement restart. + +The session information will be valid after exit. + +Return NULL if the client should be allowed, otherwise return +a string with the reason for denial. + +Otherwise, the client will be sent the current gamestate +and will eventually get to ClientBegin. + +firstTime will be qtrue the very first time a client connects +to the server machine, but qfalse on map changes and tournement +restarts. +============ +*/ +char *ClientConnect( int clientNum, qboolean firstTime, qboolean isBot ) { + char *value; + gclient_t *client; + char userinfo[MAX_INFO_STRING]; + gentity_t *ent; + + ent = &g_entities[ clientNum ]; + + trap_GetUserinfo( clientNum, userinfo, sizeof( userinfo ) ); + + // check to see if they are on the banned IP list + value = Info_ValueForKey (userinfo, "ip"); + if ( G_FilterPacket( value ) ) { + return "Banned."; + } + + // check for a password + value = Info_ValueForKey (userinfo, "password"); + if ( g_password.string[0] && Q_stricmp( g_password.string, "none" ) && + strcmp( g_password.string, value) != 0) { + return "Invalid password"; + } + + // they can connect + ent->client = level.clients + clientNum; + client = ent->client; + + memset( client, 0, sizeof(*client) ); + + client->pers.connected = CON_CONNECTING; + + // read or initialize the session data + if ( firstTime || level.newSession ) { + G_InitSessionData( client, userinfo ); + } + G_ReadSessionData( client ); + + //TA: rip bots + /*if( isBot ) { + ent->r.svFlags |= SVF_BOT; + ent->inuse = qtrue; + if( !G_BotConnect( clientNum, !firstTime ) ) { + return "BotConnectfailed"; + } + }*/ + + // get and distribute relevent paramters + G_LogPrintf( "ClientConnect: %i\n", clientNum ); + ClientUserinfoChanged( clientNum ); + + // don't do the "xxx connected" messages if they were caried over from previous level + if ( firstTime ) { + trap_SendServerCommand( -1, va("print \"%s" S_COLOR_WHITE " connected\n\"", client->pers.netname) ); + } + + if ( g_gametype.integer >= GT_TEAM && + client->sess.sessionTeam != TEAM_SPECTATOR ) { + BroadcastTeamChange( client, -1 ); + } + + // count current clients and rank for scoreboard + CalculateRanks(); + + return NULL; +} + +/* +=========== +ClientBegin + +called when a client has finished connecting, and is ready +to be placed into the level. This will happen every level load, +and on transition between teams, but doesn't happen on respawns +============ +*/ +void ClientBegin( int clientNum ) { + gentity_t *ent; + gclient_t *client; + gentity_t *tent; + int flags; + + ent = g_entities + clientNum; + + //TA: rip bots + /*if( ent->botDelayBegin ) { + G_QueueBotBegin( clientNum ); + ent->botDelayBegin = qfalse; + return; + }*/ + + client = level.clients + clientNum; + + if ( ent->r.linked ) { + trap_UnlinkEntity( ent ); + } + G_InitGentity( ent ); + ent->touch = 0; + ent->pain = 0; + ent->client = client; + + client->pers.connected = CON_CONNECTED; + client->pers.enterTime = level.time; + client->pers.teamState.state = TEAM_BEGIN; + + // save eflags around this, because changing teams will + // cause this to happen with a valid entity, and we + // want to make sure the teleport bit is set right + // so the viewpoint doesn't interpolate through the + // world to the new position + flags = client->ps.eFlags; + memset( &client->ps, 0, sizeof( client->ps ) ); + client->ps.eFlags = flags; + + // locate ent at a spawn point + + ClientSpawn( ent ); + + if ( client->sess.sessionTeam != TEAM_SPECTATOR ) { + // send event + tent = G_TempEntity( ent->client->ps.origin, EV_PLAYER_TELEPORT_IN ); + tent->s.clientNum = ent->s.clientNum; + + if ( g_gametype.integer != GT_TOURNAMENT ) { + trap_SendServerCommand( -1, va("print \"%s" S_COLOR_WHITE " entered the game\n\"", client->pers.netname) ); + } + } + G_LogPrintf( "ClientBegin: %i\n", clientNum ); + + // count current clients and rank for scoreboard + CalculateRanks(); +} + +/* +=========== +ClientSpawn + +Called every time a client is placed fresh in the world: +after the first ClientBegin, and after each respawn +Initializes all non-persistant parts of playerState +============ +*/ +void ClientSpawn(gentity_t *ent) { + int index; + vec3_t spawn_origin, spawn_angles; + gclient_t *client; + int i; + clientPersistant_t saved; + clientSession_t savedSess; + int persistant[MAX_PERSISTANT]; + gentity_t *spawnPoint; + int flags; + int savedPing; + int ammoIndex, ammoSubIndex; + int teamLocal; + int accuracy_hits, accuracy_shots; + int savedEvents[MAX_PS_EVENTS]; + int eventSequence; + char userinfo[MAX_INFO_STRING]; + + index = ent - g_entities; + client = ent->client; + + teamLocal = client->pers.pteam; + + //TA: only start client if chosen a class and joined a team + if( client->pers.pclass == 0 && teamLocal == 0 ) + { + client->sess.sessionTeam = TEAM_SPECTATOR; + client->sess.spectatorState = SPECTATOR_FREE; + } + else if( client->pers.pclass == 0 ) + { + client->sess.sessionTeam = TEAM_SPECTATOR; + client->sess.spectatorState = SPECTATOR_LOCKED; + } + + // find a spawn point + // do it before setting health back up, so farthest + // ranging doesn't count this client + if ( client->sess.sessionTeam == TEAM_SPECTATOR ) + { + if( teamLocal == PTE_NONE ) + spawnPoint = SelectSpectatorSpawnPoint ( spawn_origin, spawn_angles); + else if( teamLocal == PTE_DROIDS ) + spawnPoint = SelectDroidLockSpawnPoint ( spawn_origin, spawn_angles); + else if( teamLocal == PTE_HUMANS ) + spawnPoint = SelectHumanLockSpawnPoint ( spawn_origin, spawn_angles); + } + else + { + // don't spawn near existing origin if possible + spawnPoint = SelectTremulousSpawnPoint( teamLocal, spawn_origin, spawn_angles ); + + if( spawnPoint == NULL ) + { + trap_SendServerCommand( ent-g_entities, va("print \"No suitable spawns available\n\"" ) ); + return; + } + } + client->pers.teamState.state = TEAM_ACTIVE; + + // toggle the teleport bit so the client knows to not lerp + flags = ent->client->ps.eFlags & ( EF_TELEPORT_BIT | EF_VOTED | EF_TEAMVOTED ); + flags ^= EF_TELEPORT_BIT; + + // clear everything but the persistant data + + saved = client->pers; + savedSess = client->sess; + savedPing = client->ps.ping; + accuracy_hits = client->accuracy_hits; + accuracy_shots = client->accuracy_shots; + for ( i = 0 ; i < MAX_PERSISTANT ; i++ ) { + persistant[i] = client->ps.persistant[i]; + } + // also save the predictable events otherwise we might get double or dropped events + for (i = 0; i < MAX_PS_EVENTS; i++) { + savedEvents[i] = client->ps.events[i]; + } + eventSequence = client->ps.eventSequence; + memset (client, 0, sizeof(*client)); + + client->pers = saved; + client->sess = savedSess; + client->ps.ping = savedPing; + client->accuracy_hits = accuracy_hits; + client->accuracy_shots = accuracy_shots; + client->lastkilled_client = -1; + for ( i = 0 ; i < MAX_PERSISTANT ; i++ ) { + client->ps.persistant[i] = persistant[i]; + } + for (i = 0; i < MAX_PS_EVENTS; i++) { + client->ps.events[i] = savedEvents[i]; + } + client->ps.eventSequence = eventSequence; + + if( client->sess.sessionTeam == TEAM_SPECTATOR ) + { + if( teamLocal == PTE_DROIDS ) + G_AddEvent( ent, EV_MENU, MN_DROID ); + else if( teamLocal == PTE_HUMANS ) + G_AddEvent( ent, EV_MENU, MN_HUMAN ); + } + + // increment the spawncount so the client will detect the respawn + client->ps.persistant[PERS_SPAWN_COUNT]++; + client->ps.persistant[PERS_TEAM] = client->sess.sessionTeam; + + client->airOutTime = level.time + 12000; + + trap_GetUserinfo( index, userinfo, sizeof(userinfo) ); + client->ps.eFlags = flags; + + //Com_Printf( "ent->client->pers->pclass = %i\n", ent->client->pers.pclass ); + + ent->s.groundEntityNum = ENTITYNUM_NONE; + ent->client = &level.clients[index]; + ent->takedamage = qtrue; + ent->inuse = qtrue; + ent->classname = "player"; + ent->r.contents = CONTENTS_BODY; + ent->clipmask = MASK_PLAYERSOLID; + ent->die = player_die; + ent->waterlevel = 0; + ent->watertype = 0; + ent->flags = 0; + + client->ps.stats[ STAT_WEAPONS ] = 0; + client->ps.stats[ STAT_WEAPONS2 ] = 0; + + // clear entity values + switch( ent->client->pers.pclass ) + { + case PCL_D_BUILDER: + client->pers.maxHealth = 50; + client->ps.stats[STAT_MAX_HEALTH] = 50; + client->ps.stats[STAT_ARMOR] = 50; + + client->ps.eFlags = flags; + + VectorCopy (playerMins, ent->r.mins); + VectorCopy (playerMaxs, ent->r.maxs); + + client->ps.clientNum = index; + + BG_packWeapon( WP_ABUILD, client->ps.stats ); + BG_packAmmoArray( WP_ABUILD, client->ps.ammo, client->ps.powerups, 0, 0, 0 ); + + client->ps.stats[ STAT_ABILITIES ] |= SCA_TAKESFALLDAMAGE; + BG_packAttributes( 80, 15, 350, client->ps.stats ); + client->classSpeed = 0.5; + break; + + case PCL_D_BASE: + client->pers.maxHealth = 25; + client->ps.stats[STAT_MAX_HEALTH] = 25; + client->ps.eFlags = flags; + + VectorCopy (playerMins, ent->r.mins); + VectorCopy (playerMaxs, ent->r.maxs); + + client->ps.clientNum = index; + + BG_packWeapon( WP_VENOM, client->ps.stats ); + BG_packAmmoArray( WP_VENOM, client->ps.ammo, client->ps.powerups, 0, 0, 0 ); + + client->ps.stats[ STAT_ABILITIES ] |= SCA_WALLCLIMBER; + client->ps.stats[ STAT_ABILITIES ] |= SCA_CANJUMP; + client->ps.stats[ STAT_ABILITIES ] |= SCA_NOWEAPONDRIFT; + BG_packAttributes( 140, 0, 25, client->ps.stats ); + client->classSpeed = 2.0; + break; + + case PCL_H_BASE: + client->pers.maxHealth = 100; + client->ps.stats[STAT_MAX_HEALTH] = 100; + client->ps.stats[STAT_ARMOR] = 50; + + client->ps.eFlags = flags; + + VectorCopy (playerMins, ent->r.mins); + VectorCopy (playerMaxs, ent->r.maxs); + + client->ps.clientNum = index; + + /*BG_packWeapon( WP_MACHINEGUN, client->ps.stats ); + BG_packAmmoArray( WP_MACHINEGUN, client->ps.ammo, client->ps.powerups, CS_MG, 4, 4 );*/ + + client->ps.stats[ STAT_ABILITIES ] |= SCA_TAKESFALLDAMAGE; + client->ps.stats[ STAT_ABILITIES ] |= SCA_CANJUMP; + BG_packAttributes( 90, 2, 200, client->ps.stats ); + client->classSpeed = 1.0; + break; + + //eventually remove this case (or report an error) when all classes implemented + default: + client->ps.stats[STAT_MAX_HEALTH] = client->pers.maxHealth; + client->ps.eFlags = flags; + + VectorCopy (playerMins, ent->r.mins); + VectorCopy (playerMaxs, ent->r.maxs); + + client->ps.clientNum = index; + + BG_packWeapon( WP_MACHINEGUN, client->ps.stats ); + BG_packAmmoArray( WP_MACHINEGUN, client->ps.ammo, client->ps.powerups, 100, 0, 0 ); + + BG_packWeapon( WP_GAUNTLET, client->ps.stats ); + BG_packAmmoArray( WP_GAUNTLET, client->ps.ammo, client->ps.powerups, 0, 0, 0 ); + + BG_packAmmoArray( WP_GRAPPLING_HOOK, client->ps.ammo, client->ps.powerups, 0, 0, 0 ); + + client->ps.stats[ STAT_ABILITIES ] |= SCA_TAKESFALLDAMAGE; + client->ps.stats[ STAT_ABILITIES ] |= SCA_CANZOOM; + client->ps.stats[ STAT_ABILITIES ] |= SCA_CANJUMP; + BG_packAttributes( 90, 2, 200, client->ps.stats ); + client->classSpeed = 1.0; + + } + + ent->client->ps.stats[ STAT_PCLASS ] = ent->client->pers.pclass; + ent->client->ps.stats[ STAT_PTEAM ] = ent->client->pers.pteam; + + + // health will count down towards max_health + ent->health = client->ps.stats[STAT_HEALTH] = client->ps.stats[STAT_MAX_HEALTH]; //* 1.25; + + G_SetOrigin( ent, spawn_origin ); + VectorCopy( spawn_origin, client->ps.origin ); + + // the respawned flag will be cleared after the attack and jump keys come up + client->ps.pm_flags |= PMF_RESPAWNED; + + trap_GetUsercmd( client - level.clients, &ent->client->pers.cmd ); + SetClientViewAngle( ent, spawn_angles ); + + if ( client->sess.sessionTeam == TEAM_SPECTATOR ) { + + } else { + G_KillBox( ent ); + trap_LinkEntity (ent); + + // force the base weapon up + client->ps.weapon = WP_NONE; + client->ps.weaponstate = WEAPON_READY; + + } + + // don't allow full run speed for a bit + client->ps.pm_flags |= PMF_TIME_KNOCKBACK; + client->ps.pm_time = 100; + + client->respawnTime = level.time; + client->inactivityTime = level.time + g_inactivity.integer * 1000; + client->latched_buttons = 0; + + // set default animations + client->ps.torsoAnim = TORSO_STAND; + client->ps.legsAnim = LEGS_IDLE; + + if ( level.intermissiontime ) { + MoveClientToIntermission( ent ); + } else { + // fire the targets of the spawn point + G_UseTargets( spawnPoint, ent ); + + // select the highest weapon number available, after any + // spawn given items have fired + client->ps.weapon = 1; + for ( i = WP_NUM_WEAPONS - 1 ; i > 0 ; i-- ) { + if ( BG_gotWeapon( i, client->ps.stats ) ) { + client->ps.weapon = i; + break; + } + } + } + + // run a client frame to drop exactly to the floor, + // initialize animations and other things + client->ps.commandTime = level.time - 100; + ent->client->pers.cmd.serverTime = level.time; + ClientThink( ent-g_entities ); + + // positively link the client, even if the command times are weird + if ( client->sess.sessionTeam != TEAM_SPECTATOR ) { + BG_PlayerStateToEntityState( &client->ps, &ent->s, qtrue ); + VectorCopy( ent->client->ps.origin, ent->r.currentOrigin ); + trap_LinkEntity( ent ); + } + + //TA: must do this here so the number of active clients is calculated + CalculateRanks(); + + // run the presend to set anything else + ClientEndFrame( ent ); + + // clear entity state values + BG_PlayerStateToEntityState( &client->ps, &ent->s, qtrue ); +} + + +/* +=========== +ClientDisconnect + +Called when a player drops from the server. +Will not be called between levels. + +This should NOT be called directly by any game logic, +call trap_DropClient(), which will call this and do +server system housekeeping. +============ +*/ +void ClientDisconnect( int clientNum ) { + gentity_t *ent; + gentity_t *tent; + int i; + + ent = g_entities + clientNum; + if ( !ent->client ) { + return; + } + + // stop any following clients + for ( i = 0 ; i < level.maxclients ; i++ ) { + if ( level.clients[i].sess.sessionTeam == TEAM_SPECTATOR + && level.clients[i].sess.spectatorState == SPECTATOR_FOLLOW + && level.clients[i].sess.spectatorClient == clientNum ) { + StopFollowing( &g_entities[i] ); + } + } + + // send effect if they were completely connected + if ( ent->client->pers.connected == CON_CONNECTED + && ent->client->sess.sessionTeam != TEAM_SPECTATOR ) { + tent = G_TempEntity( ent->client->ps.origin, EV_PLAYER_TELEPORT_OUT ); + tent->s.clientNum = ent->s.clientNum; + + // They don't get to take powerups with them! + // Especially important for stuff like CTF flags + TossClientItems ( ent ); + } + + G_LogPrintf( "ClientDisconnect: %i\n", clientNum ); + + // if we are playing in tourney mode and losing, give a win to the other player + if ( ( g_gametype.integer == GT_TOURNAMENT ) + && !level.intermissiontime + && !level.warmupTime && level.sortedClients[1] == clientNum ) { + level.clients[ level.sortedClients[0] ].sess.wins++; + ClientUserinfoChanged( level.sortedClients[0] ); + } + + trap_UnlinkEntity (ent); + ent->s.modelindex = 0; + ent->inuse = qfalse; + ent->classname = "disconnected"; + ent->client->pers.connected = CON_DISCONNECTED; + ent->client->ps.persistant[PERS_TEAM] = TEAM_FREE; + ent->client->sess.sessionTeam = TEAM_FREE; + + trap_SetConfigstring( CS_PLAYERS + clientNum, ""); + + CalculateRanks(); + + //TA: rip bots + /*if ( ent->r.svFlags & SVF_BOT ) { + BotAIShutdownClient( clientNum ); + }*/ +} diff --git a/src/game/g_cmds.c b/src/game/g_cmds.c new file mode 100644 index 00000000..3fcc088b --- /dev/null +++ b/src/game/g_cmds.c @@ -0,0 +1,2050 @@ +// Copyright (C) 1999-2000 Id Software, Inc. +// + +/* + * Portions Copyright (C) 2000-2001 Tim Angus + * + * This program 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, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ + +/* To assertain which portions are licensed under the GPL and which are + * licensed by Id Software, Inc. please run a diff between the equivalent + * versions of the "Tremulous" modification and the unmodified "Quake3" + * game source code. + */ + +#include "g_local.h" + +#include "../ta_ui/menudef.h" // for the voice chats + +/* +================== +DeathmatchScoreboardMessage + +================== +*/ +void DeathmatchScoreboardMessage( gentity_t *ent ) { + char entry[1024]; + char string[1400]; + int stringlength; + int i, j; + gclient_t *cl; + int numSorted; + int scoreFlags; + + // send the latest information on all clients + string[0] = 0; + stringlength = 0; + scoreFlags = 0; + + numSorted = level.numConnectedClients; + + for (i=0 ; i < numSorted ; i++) { + int ping; + + cl = &level.clients[level.sortedClients[i]]; + + if ( cl->pers.connected == CON_CONNECTING ) { + ping = -1; + } else { + ping = cl->ps.ping < 999 ? cl->ps.ping : 999; + } + Com_sprintf (entry, sizeof(entry), + " %i %i %i %i %i %i", level.sortedClients[i], + cl->ps.persistant[PERS_SCORE], ping, (level.time - cl->pers.enterTime)/60000, + scoreFlags, g_entities[level.sortedClients[i]].s.powerups); + j = strlen(entry); + if (stringlength + j > 1024) + break; + strcpy (string + stringlength, entry); + stringlength += j; + } + + trap_SendServerCommand( ent-g_entities, va("scores %i %i %i%s", i, + level.teamScores[TEAM_HUMANS], level.teamScores[TEAM_DROIDS], + string ) ); +} + + +/* +================== +Cmd_Score_f + +Request current scoreboard information +================== +*/ +void Cmd_Score_f( gentity_t *ent ) { + DeathmatchScoreboardMessage( ent ); +} + + + +/* +================== +CheatsOk +================== +*/ +qboolean CheatsOk( gentity_t *ent ) { + if ( !g_cheats.integer ) { + trap_SendServerCommand( ent-g_entities, va("print \"Cheats are not enabled on this server.\n\"")); + return qfalse; + } + if ( ent->health <= 0 ) { + trap_SendServerCommand( ent-g_entities, va("print \"You must be alive to use this command.\n\"")); + return qfalse; + } + return qtrue; +} + + +/* +================== +ConcatArgs +================== +*/ +char *ConcatArgs( int start ) { + int i, c, tlen; + static char line[MAX_STRING_CHARS]; + int len; + char arg[MAX_STRING_CHARS]; + + len = 0; + c = trap_Argc(); + for ( i = start ; i < c ; i++ ) { + trap_Argv( i, arg, sizeof( arg ) ); + tlen = strlen( arg ); + if ( len + tlen >= MAX_STRING_CHARS - 1 ) { + break; + } + memcpy( line + len, arg, tlen ); + len += tlen; + if ( i != c - 1 ) { + line[len] = ' '; + len++; + } + } + + line[len] = 0; + + return line; +} + +/* +================== +SanitizeString + +Remove case and control characters +================== +*/ +void SanitizeString( char *in, char *out ) { + while ( *in ) { + if ( *in == 27 ) { + in += 2; // skip color code + continue; + } + if ( *in < 32 ) { + in++; + continue; + } + *out++ = tolower( *in++ ); + } + + *out = 0; +} + +/* +================== +ClientNumberFromString + +Returns a player number for either a number or name string +Returns -1 if invalid +================== +*/ +int ClientNumberFromString( gentity_t *to, char *s ) { + gclient_t *cl; + int idnum; + char s2[MAX_STRING_CHARS]; + char n2[MAX_STRING_CHARS]; + + // numeric values are just slot numbers + if (s[0] >= '0' && s[0] <= '9') { + idnum = atoi( s ); + if ( idnum < 0 || idnum >= level.maxclients ) { + trap_SendServerCommand( to-g_entities, va("print \"Bad client slot: %i\n\"", idnum)); + return -1; + } + + cl = &level.clients[idnum]; + if ( cl->pers.connected != CON_CONNECTED ) { + trap_SendServerCommand( to-g_entities, va("print \"Client %i is not active\n\"", idnum)); + return -1; + } + return idnum; + } + + // check for a name match + SanitizeString( s, s2 ); + for ( idnum=0,cl=level.clients ; idnum < level.maxclients ; idnum++,cl++ ) { + if ( cl->pers.connected != CON_CONNECTED ) { + continue; + } + SanitizeString( cl->pers.netname, n2 ); + if ( !strcmp( n2, s2 ) ) { + return idnum; + } + } + + trap_SendServerCommand( to-g_entities, va("print \"User %s is not on the server\n\"", s)); + return -1; +} + +/* +================== +Cmd_Give_f + +Give items to a client +================== +*/ +void Cmd_Give_f (gentity_t *ent) +{ + char *name; + gitem_t *it; + int i; + qboolean give_all; + gentity_t *it_ent; + trace_t trace; + + if ( !CheatsOk( ent ) ) { + return; + } + + name = ConcatArgs( 1 ); + + if (Q_stricmp(name, "all") == 0) + give_all = qtrue; + else + give_all = qfalse; + + if (give_all || Q_stricmp( name, "health") == 0) + { + ent->health = ent->client->ps.stats[STAT_MAX_HEALTH]; + if (!give_all) + return; + } + + if (give_all || Q_stricmp(name, "weapons") == 0) + { + BG_packWeapon( (1 << WP_NUM_WEAPONS) - 1 - ( 1 << WP_GRAPPLING_HOOK ) - ( 1 << WP_NONE ), ent->client->ps.stats ); + if (!give_all) + return; + } + + if (give_all || Q_stricmp(name, "ammo") == 0) + { + for ( i = 0 ; i < MAX_WEAPONS ; i++ ) { + BG_packAmmoArray( i, ent->client->ps.ammo, ent->client->ps.powerups, 999, 0, 0 ); + } + if (!give_all) + return; + } + + if (give_all || Q_stricmp(name, "armor") == 0) + { + ent->client->ps.stats[STAT_ARMOR] = 200; + + if (!give_all) + return; + } + + // spawn a specific item right on the player + if ( !give_all ) { + it = BG_FindItem (name); + if (!it) { + return; + } + + it_ent = G_Spawn(); + VectorCopy( ent->r.currentOrigin, it_ent->s.origin ); + it_ent->classname = it->classname; + G_SpawnItem (it_ent, it); + FinishSpawningItem(it_ent ); + memset( &trace, 0, sizeof( trace ) ); + Touch_Item (it_ent, ent, &trace); + if (it_ent->inuse) { + G_FreeEntity( it_ent ); + } + } +} + + +/* +================== +Cmd_God_f + +Sets client to godmode + +argv(0) god +================== +*/ +void Cmd_God_f (gentity_t *ent) +{ + char *msg; + + if ( !CheatsOk( ent ) ) { + return; + } + + ent->flags ^= FL_GODMODE; + if (!(ent->flags & FL_GODMODE) ) + msg = "godmode OFF\n"; + else + msg = "godmode ON\n"; + + trap_SendServerCommand( ent-g_entities, va("print \"%s\"", msg)); +} + + +/* +================== +Cmd_Notarget_f + +Sets client to notarget + +argv(0) notarget +================== +*/ +void Cmd_Notarget_f( gentity_t *ent ) { + char *msg; + + if ( !CheatsOk( ent ) ) { + return; + } + + ent->flags ^= FL_NOTARGET; + if (!(ent->flags & FL_NOTARGET) ) + msg = "notarget OFF\n"; + else + msg = "notarget ON\n"; + + trap_SendServerCommand( ent-g_entities, va("print \"%s\"", msg)); +} + + +/* +================== +Cmd_Noclip_f + +argv(0) noclip +================== +*/ +void Cmd_Noclip_f( gentity_t *ent ) { + char *msg; + + if ( !CheatsOk( ent ) ) { + return; + } + + if ( ent->client->noclip ) { + msg = "noclip OFF\n"; + } else { + msg = "noclip ON\n"; + } + ent->client->noclip = !ent->client->noclip; + + trap_SendServerCommand( ent-g_entities, va("print \"%s\"", msg)); +} + + +/* +================== +Cmd_LevelShot_f + +This is just to help generate the level pictures +for the menus. It goes to the intermission immediately +and sends over a command to the client to resize the view, +hide the scoreboard, and take a special screenshot +================== +*/ +void Cmd_LevelShot_f( gentity_t *ent ) { + if ( !CheatsOk( ent ) ) { + return; + } + + // doesn't work in single player + if ( g_gametype.integer != 0 ) { + trap_SendServerCommand( ent-g_entities, + "print \"Must be in g_gametype 0 for levelshot\n\"" ); + return; + } + + BeginIntermission(); + trap_SendServerCommand( ent-g_entities, "clientLevelShot" ); +} + + +void Cmd_TeamTask_f( gentity_t *ent ) { + char userinfo[MAX_INFO_STRING]; + char arg[MAX_TOKEN_CHARS]; + int task; + int client = ent->client - level.clients; + + if ( trap_Argc() != 2 ) { + return; + } + trap_Argv( 1, arg, sizeof( arg ) ); + task = atoi( arg ); + + trap_GetUserinfo(client, userinfo, sizeof(userinfo)); + Info_SetValueForKey(userinfo, "teamtask", va("%d", task)); + trap_SetUserinfo(client, userinfo); + ClientUserinfoChanged(client); +} + + +/* +================= +Cmd_Kill_f +================= +*/ +void Cmd_Kill_f( gentity_t *ent ) { + if ( ent->client->sess.sessionTeam == TEAM_SPECTATOR ) { + return; + } + if (ent->health <= 0) { + return; + } + ent->flags &= ~FL_GODMODE; + ent->client->ps.stats[STAT_HEALTH] = ent->health = 0; + player_die (ent, ent, ent, 100000, MOD_SUICIDE); +} + +/* +================= +BroadCastTeamChange + +Let everyone know about a team change +================= +*/ +void BroadcastTeamChange( gclient_t *client, int oldTeam ) +{ + if ( client->sess.sessionTeam == TEAM_HUMANS ) { + trap_SendServerCommand( -1, va("cp \"%s" S_COLOR_WHITE " joined the humans.\n\"", + client->pers.netname) ); + } else if ( client->sess.sessionTeam == TEAM_DROIDS ) { + trap_SendServerCommand( -1, va("cp \"%s" S_COLOR_WHITE " joined the droids.\n\"", + client->pers.netname)); + } else if ( client->sess.sessionTeam == TEAM_SPECTATOR && oldTeam != TEAM_SPECTATOR ) { + trap_SendServerCommand( -1, va("cp \"%s" S_COLOR_WHITE " joined the spectators.\n\"", + client->pers.netname)); + } else if ( client->sess.sessionTeam == TEAM_FREE ) { + trap_SendServerCommand( -1, va("cp \"%s" S_COLOR_WHITE " joined the battle.\n\"", + client->pers.netname)); + } +} + +/* +================= +SetTeam +================= +*/ +void SetTeam( gentity_t *ent, char *s ) { + int team, oldTeam; + gclient_t *client; + int clientNum; + spectatorState_t specState; + int specClient; + + // + // see what change is requested + // + client = ent->client; + + clientNum = client - level.clients; + specClient = 0; + + specState = SPECTATOR_NOT; + if ( !Q_stricmp( s, "scoreboard" ) || !Q_stricmp( s, "score" ) ) { + team = TEAM_SPECTATOR; + specState = SPECTATOR_SCOREBOARD; + } else if ( !Q_stricmp( s, "follow1" ) ) { + team = TEAM_SPECTATOR; + specState = SPECTATOR_FOLLOW; + specClient = -1; + } else if ( !Q_stricmp( s, "follow2" ) ) { + team = TEAM_SPECTATOR; + specState = SPECTATOR_FOLLOW; + specClient = -2; + } else if ( !Q_stricmp( s, "spectator" ) || !Q_stricmp( s, "s" ) ) { + team = TEAM_SPECTATOR; + specState = SPECTATOR_FREE; + } else if ( g_gametype.integer >= GT_TEAM ) { + // if running a team game, assign player to one of the teams + specState = SPECTATOR_NOT; + if ( !Q_stricmp( s, "humans" ) || !Q_stricmp( s, "h" ) ) { + team = TEAM_HUMANS; + } else if ( !Q_stricmp( s, "droids" ) || !Q_stricmp( s, "d" ) ) { + team = TEAM_DROIDS; + } else { + // pick the team with the least number of players + team = PickTeam( clientNum ); + } + + if ( g_teamForceBalance.integer ) { + int counts[TEAM_NUM_TEAMS]; + + counts[TEAM_DROIDS] = TeamCount( ent->client->ps.clientNum, TEAM_DROIDS ); + counts[TEAM_HUMANS] = TeamCount( ent->client->ps.clientNum, TEAM_HUMANS ); + + // We allow a spread of two + if ( team == TEAM_HUMANS && counts[TEAM_HUMANS] - counts[TEAM_DROIDS] > 1 ) { + trap_SendServerCommand( ent->client->ps.clientNum, + "cp \"Humans team has too many players.\n\"" ); + return; // ignore the request + } + if ( team == TEAM_DROIDS && counts[TEAM_DROIDS] - counts[TEAM_HUMANS] > 1 ) { + trap_SendServerCommand( ent->client->ps.clientNum, + "cp \"Droids team has too many players.\n\"" ); + return; // ignore the request + } + + // It's ok, the team we are switching to has less or same number of players + } + + } else { + // force them to spectators if there aren't any spots free + team = TEAM_FREE; + } + + // override decision if limiting the players + if ( (g_gametype.integer == GT_TOURNAMENT) + && level.numNonSpectatorClients >= 2 ) { + team = TEAM_SPECTATOR; + } else if ( g_maxGameClients.integer > 0 && + level.numNonSpectatorClients >= g_maxGameClients.integer ) { + team = TEAM_SPECTATOR; + } + + // + // decide if we will allow the change + // + oldTeam = client->sess.sessionTeam; + if ( team == oldTeam && team != TEAM_SPECTATOR ) { + return; + } + + // + // execute the team change + // + + // he starts at 'base' + client->pers.teamState.state = TEAM_BEGIN; + if ( oldTeam != TEAM_SPECTATOR ) { + // Kill him (makes sure he loses flags, etc) + ent->flags &= ~FL_GODMODE; + ent->client->ps.stats[STAT_HEALTH] = ent->health = 0; + player_die (ent, ent, ent, 100000, MOD_SUICIDE); + + } + // they go to the end of the line for tournements + if ( team == TEAM_SPECTATOR ) { + client->sess.spectatorTime = level.time; + } + + client->sess.sessionTeam = team; + client->sess.spectatorState = specState; + client->sess.spectatorClient = specClient; + + BroadcastTeamChange( client, oldTeam ); + + // get and distribute relevent paramters + ClientUserinfoChanged( clientNum ); + + ClientBegin( clientNum ); +} + +/* +================= +StopFollowing + +If the client being followed leaves the game, or you just want to drop +to free floating spectator mode +================= +*/ +void StopFollowing( gentity_t *ent ) { + ent->client->ps.persistant[ PERS_TEAM ] = TEAM_SPECTATOR; + ent->client->sess.sessionTeam = TEAM_SPECTATOR; + ent->client->sess.spectatorState = SPECTATOR_FREE; + ent->client->ps.pm_flags &= ~PMF_FOLLOW; + ent->r.svFlags &= ~SVF_BOT; + ent->client->ps.clientNum = ent - g_entities; +} + +/* +================= +Cmd_Team_f +================= +*/ +void Cmd_Team_f( gentity_t *ent ) { + int oldTeam; + char s[MAX_TOKEN_CHARS]; + + //TA: rip out the q3a team system :) + + oldTeam = ent->client->pers.pteam; + + trap_Argv( 1, s, sizeof( s ) ); + + if( !strlen( s ) ) + { + trap_SendServerCommand( ent-g_entities, va("print \"team: %i\n\"", ent->client->pers.pteam ) ); + return; + } + + if(!Q_stricmp(s, "0")) + ent->client->pers.pteam = PTE_NONE; + else if(!Q_stricmp(s, "1")) + ent->client->pers.pteam = PTE_DROIDS; + else if(!Q_stricmp(s, "2")) + ent->client->pers.pteam = PTE_HUMANS; + + if( oldTeam != ent->client->pers.pteam ) + { + ent->client->pers.pclass = 0; + ClientSpawn( ent ); + } + + //FIXME: put some team change broadcast code here. +} + + +/* +================= +Cmd_Follow_f +================= +*/ +void Cmd_Follow_f( gentity_t *ent ) { + int i; + char arg[MAX_TOKEN_CHARS]; + + if ( trap_Argc() != 2 ) { + if ( ent->client->sess.spectatorState == SPECTATOR_FOLLOW ) { + StopFollowing( ent ); + } + return; + } + + trap_Argv( 1, arg, sizeof( arg ) ); + i = ClientNumberFromString( ent, arg ); + if ( i == -1 ) { + return; + } + + // can't follow self + if ( &level.clients[ i ] == ent->client ) { + return; + } + + // can't follow another spectator + if ( level.clients[ i ].sess.sessionTeam == TEAM_SPECTATOR ) { + return; + } + + // if they are playing a tournement game, count as a loss + if ( ( g_gametype.integer == GT_TOURNAMENT ) + && ent->client->sess.sessionTeam == TEAM_FREE ) { + ent->client->sess.losses++; + } + + // first set them to spectator + if ( ent->client->sess.sessionTeam != TEAM_SPECTATOR ) { + SetTeam( ent, "spectator" ); + } + + ent->client->sess.spectatorState = SPECTATOR_FOLLOW; + ent->client->sess.spectatorClient = i; +} + +/* +================= +Cmd_FollowCycle_f +================= +*/ +void Cmd_FollowCycle_f( gentity_t *ent, int dir ) { + int clientnum; + int original; + + // if they are playing a tournement game, count as a loss + if ( ( g_gametype.integer == GT_TOURNAMENT ) + && ent->client->sess.sessionTeam == TEAM_FREE ) { + ent->client->sess.losses++; + } + // first set them to spectator + if ( ent->client->sess.spectatorState == SPECTATOR_NOT ) { + SetTeam( ent, "spectator" ); + } + + if ( dir != 1 && dir != -1 ) { + G_Error( "Cmd_FollowCycle_f: bad dir %i", dir ); + } + + clientnum = ent->client->sess.spectatorClient; + original = clientnum; + do { + clientnum += dir; + if ( clientnum >= level.maxclients ) { + clientnum = 0; + } + if ( clientnum < 0 ) { + clientnum = level.maxclients - 1; + } + + // can only follow connected clients + if ( level.clients[ clientnum ].pers.connected != CON_CONNECTED ) { + continue; + } + + // can't follow another spectator + if ( level.clients[ clientnum ].sess.sessionTeam == TEAM_SPECTATOR ) { + continue; + } + + // this is good, we can use it + ent->client->sess.spectatorClient = clientnum; + ent->client->sess.spectatorState = SPECTATOR_FOLLOW; + return; + } while ( clientnum != original ); + + // leave it where it was +} + + +/* +================== +G_Say +================== +*/ +static void G_SayTo( gentity_t *ent, gentity_t *other, int mode, int color, const char *name, const char *message ) { + if (!other) { + return; + } + if (!other->inuse) { + return; + } + if (!other->client) { + return; + } + if ( mode == SAY_TEAM && !OnSameTeam(ent, other) ) { + return; + } + // no chatting to players in tournements + if ( ( g_gametype.integer == GT_TOURNAMENT ) + && other->client->sess.sessionTeam == TEAM_FREE + && ent->client->sess.sessionTeam != TEAM_FREE ) { + return; + } + + trap_SendServerCommand( other-g_entities, va("%s \"%s%c%c%s\"", + mode == SAY_TEAM ? "tchat" : "chat", + name, Q_COLOR_ESCAPE, color, message)); +} + +#define EC "\x19" + +void G_Say( gentity_t *ent, gentity_t *target, int mode, const char *chatText ) { + int j; + gentity_t *other; + int color; + char name[64]; + // don't let text be too long for malicious reasons + char text[MAX_SAY_TEXT]; + char location[64]; + + if ( g_gametype.integer < GT_TEAM && mode == SAY_TEAM ) { + mode = SAY_ALL; + } + + switch ( mode ) { + default: + case SAY_ALL: + G_LogPrintf( "say: %s: %s\n", ent->client->pers.netname, chatText ); + Com_sprintf (name, sizeof(name), "%s%c%c"EC": ", ent->client->pers.netname, Q_COLOR_ESCAPE, COLOR_WHITE ); + color = COLOR_GREEN; + break; + case SAY_TEAM: + G_LogPrintf( "sayteam: %s: %s\n", ent->client->pers.netname, chatText ); + if (Team_GetLocationMsg(ent, location, sizeof(location))) + Com_sprintf (name, sizeof(name), EC"(%s%c%c"EC") (%s)"EC": ", + ent->client->pers.netname, Q_COLOR_ESCAPE, COLOR_WHITE, location); + else + Com_sprintf (name, sizeof(name), EC"(%s%c%c"EC")"EC": ", + ent->client->pers.netname, Q_COLOR_ESCAPE, COLOR_WHITE ); + color = COLOR_CYAN; + break; + case SAY_TELL: + if (target && g_gametype.integer >= GT_TEAM && + target->client->sess.sessionTeam == ent->client->sess.sessionTeam && + Team_GetLocationMsg(ent, location, sizeof(location))) + Com_sprintf (name, sizeof(name), EC"[%s%c%c"EC"] (%s)"EC": ", ent->client->pers.netname, Q_COLOR_ESCAPE, COLOR_WHITE, location ); + else + Com_sprintf (name, sizeof(name), EC"[%s%c%c"EC"]"EC": ", ent->client->pers.netname, Q_COLOR_ESCAPE, COLOR_WHITE ); + color = COLOR_MAGENTA; + break; + } + + Q_strncpyz( text, chatText, sizeof(text) ); + + if ( target ) { + G_SayTo( ent, target, mode, color, name, text ); + return; + } + + // echo the text to the console + if ( g_dedicated.integer ) { + G_Printf( "%s%s\n", name, text); + } + + // send it to all the apropriate clients + for (j = 0; j < level.maxclients; j++) { + other = &g_entities[j]; + G_SayTo( ent, other, mode, color, name, text ); + } +} + + +/* +================== +Cmd_Say_f +================== +*/ +static void Cmd_Say_f( gentity_t *ent, int mode, qboolean arg0 ) { + char *p; + + if ( trap_Argc () < 2 && !arg0 ) { + return; + } + + if (arg0) + { + p = ConcatArgs( 0 ); + } + else + { + p = ConcatArgs( 1 ); + } + + G_Say( ent, NULL, mode, p ); +} + +/* +================== +Cmd_Tell_f +================== +*/ +static void Cmd_Tell_f( gentity_t *ent ) { + int targetNum; + gentity_t *target; + char *p; + char arg[MAX_TOKEN_CHARS]; + + if ( trap_Argc () < 2 ) { + return; + } + + trap_Argv( 1, arg, sizeof( arg ) ); + targetNum = atoi( arg ); + if ( targetNum < 0 || targetNum >= level.maxclients ) { + return; + } + + target = &g_entities[targetNum]; + if ( !target || !target->inuse || !target->client ) { + return; + } + + p = ConcatArgs( 2 ); + + G_LogPrintf( "tell: %s to %s: %s\n", ent->client->pers.netname, target->client->pers.netname, p ); + G_Say( ent, target, SAY_TELL, p ); + // don't tell to the player self if it was already directed to this player + // also don't send the chat back to a bot + if ( ent != target && !(ent->r.svFlags & SVF_BOT)) { + G_Say( ent, ent, SAY_TELL, p ); + } +} + + +static void G_VoiceTo( gentity_t *ent, gentity_t *other, int mode, const char *id, qboolean voiceonly ) { + int color; + char *cmd; + + if (!other) { + return; + } + if (!other->inuse) { + return; + } + if (!other->client) { + return; + } + if ( mode == SAY_TEAM && !OnSameTeam(ent, other) ) { + return; + } + // no chatting to players in tournements + if ( (g_gametype.integer == GT_TOURNAMENT )) { + return; + } + + if (mode == SAY_TEAM) { + color = COLOR_CYAN; + cmd = "vtchat"; + } + else if (mode == SAY_TELL) { + color = COLOR_MAGENTA; + cmd = "vtell"; + } + else { + color = COLOR_GREEN; + cmd = "vchat"; + } + + trap_SendServerCommand( other-g_entities, va("%s %d %d %d %s", cmd, voiceonly, ent->s.number, color, id)); +} + + +void G_Voice( gentity_t *ent, gentity_t *target, int mode, const char *id, qboolean voiceonly ) { + int j; + gentity_t *other; + + if ( g_gametype.integer < GT_TEAM && mode == SAY_TEAM ) { + mode = SAY_ALL; + } + + if ( target ) { + G_VoiceTo( ent, target, mode, id, voiceonly ); + return; + } + + // echo the text to the console + if ( g_dedicated.integer ) { + G_Printf( "voice: %s %s\n", ent->client->pers.netname, id); + } + + // send it to all the apropriate clients + for (j = 0; j < level.maxclients; j++) { + other = &g_entities[j]; + G_VoiceTo( ent, other, mode, id, voiceonly ); + } +} + + +/* +================== +Cmd_Voice_f +================== +*/ +static void Cmd_Voice_f( gentity_t *ent, int mode, qboolean arg0, qboolean voiceonly ) { + char *p; + + if ( trap_Argc () < 2 && !arg0 ) { + return; + } + + if (arg0) + { + p = ConcatArgs( 0 ); + } + else + { + p = ConcatArgs( 1 ); + } + + G_Voice( ent, NULL, mode, p, voiceonly ); +} + + +/* +================== +Cmd_VoiceTell_f +================== +*/ +static void Cmd_VoiceTell_f( gentity_t *ent, qboolean voiceonly ) { + int targetNum; + gentity_t *target; + char *id; + char arg[MAX_TOKEN_CHARS]; + + if ( trap_Argc () < 2 ) { + return; + } + + trap_Argv( 1, arg, sizeof( arg ) ); + targetNum = atoi( arg ); + if ( targetNum < 0 || targetNum >= level.maxclients ) { + return; + } + + target = &g_entities[targetNum]; + if ( !target || !target->inuse || !target->client ) { + return; + } + + id = ConcatArgs( 2 ); + + G_LogPrintf( "vtell: %s to %s: %s\n", ent->client->pers.netname, target->client->pers.netname, id ); + G_Voice( ent, target, SAY_TELL, id, voiceonly ); + // don't tell to the player self if it was already directed to this player + // also don't send the chat back to a bot + if ( ent != target && !(ent->r.svFlags & SVF_BOT)) { + G_Voice( ent, ent, SAY_TELL, id, voiceonly ); + } +} + + +/* +================== +Cmd_VoiceTaunt_f +================== +*/ +static void Cmd_VoiceTaunt_f( gentity_t *ent ) { + gentity_t *who; + int i; + + if (!ent->client) { + return; + } + + // insult someone who just killed you + if (ent->enemy && ent->enemy->client && ent->enemy->client->lastkilled_client == ent->s.number) { + // i am a dead corpse + if (!(ent->enemy->r.svFlags & SVF_BOT)) { + //G_Voice( ent, ent->enemy, SAY_TELL, VOICECHAT_DEATHINSULT, qfalse ); + } + if (!(ent->r.svFlags & SVF_BOT)) { + //G_Voice( ent, ent, SAY_TELL, VOICECHAT_DEATHINSULT, qfalse ); + } + ent->enemy = NULL; + return; + } + // insult someone you just killed + if (ent->client->lastkilled_client >= 0 && ent->client->lastkilled_client != ent->s.number) { + who = g_entities + ent->client->lastkilled_client; + if (who->client) { + // who is the person I just killed + if (who->client->lasthurt_mod == MOD_GAUNTLET) { + if (!(who->r.svFlags & SVF_BOT)) { + //G_Voice( ent, who, SAY_TELL, VOICECHAT_KILLGAUNTLET, qfalse ); // and I killed them with a gauntlet + } + if (!(ent->r.svFlags & SVF_BOT)) { + //G_Voice( ent, ent, SAY_TELL, VOICECHAT_KILLGAUNTLET, qfalse ); + } + } else { + if (!(who->r.svFlags & SVF_BOT)) { + //G_Voice( ent, who, SAY_TELL, VOICECHAT_KILLINSULT, qfalse ); // and I killed them with something else + } + if (!(ent->r.svFlags & SVF_BOT)) { + //G_Voice( ent, ent, SAY_TELL, VOICECHAT_KILLINSULT, qfalse ); + } + } + ent->client->lastkilled_client = -1; + return; + } + } + + if (g_gametype.integer >= GT_TEAM) { + // praise a team mate who just got a reward + for(i = 0; i < MAX_CLIENTS; i++) { + who = g_entities + i; + if (who->client && who != ent && who->client->sess.sessionTeam == ent->client->sess.sessionTeam) { + if (who->client->rewardTime > level.time) { + if (!(who->r.svFlags & SVF_BOT)) { + //G_Voice( ent, who, SAY_TELL, VOICECHAT_PRAISE, qfalse ); + } + if (!(ent->r.svFlags & SVF_BOT)) { + //G_Voice( ent, ent, SAY_TELL, VOICECHAT_PRAISE, qfalse ); + } + return; + } + } + } + } + + // just say something + //G_Voice( ent, NULL, SAY_ALL, VOICECHAT_TAUNT, qfalse ); +} + + +static char *gc_orders[] = { + "hold your position", + "hold this position", + "come here", + "cover me", + "guard location", + "search and destroy", + "report" +}; + +void Cmd_GameCommand_f( gentity_t *ent ) { + int player; + int order; + char str[MAX_TOKEN_CHARS]; + + trap_Argv( 1, str, sizeof( str ) ); + player = atoi( str ); + trap_Argv( 2, str, sizeof( str ) ); + order = atoi( str ); + + if ( player < 0 || player >= MAX_CLIENTS ) { + return; + } + if ( order < 0 || order > sizeof(gc_orders)/sizeof(char *) ) { + return; + } + G_Say( ent, &g_entities[player], SAY_TELL, gc_orders[order] ); + G_Say( ent, ent, SAY_TELL, gc_orders[order] ); +} + +/* +================== +Cmd_Where_f +================== +*/ +void Cmd_Where_f( gentity_t *ent ) { + trap_SendServerCommand( ent-g_entities, va("print \"%s\n\"", vtos( ent->s.origin ) ) ); +} + +static const char *gameNames[] = { + "Free For All", + "Tournament", + "Single Player", + "Team Deathmatch", + "Capture the Flag", + "One Flag CTF", + "Overload", + "Harvester" +}; + +/* +================== +Cmd_CallVote_f +================== +*/ +void Cmd_CallVote_f( gentity_t *ent ) { + int i; + char arg1[MAX_STRING_TOKENS]; + char arg2[MAX_STRING_TOKENS]; + + if ( !g_allowVote.integer ) { + trap_SendServerCommand( ent-g_entities, "print \"Voting not allowed here.\n\"" ); + return; + } + + if ( level.voteTime ) { + trap_SendServerCommand( ent-g_entities, "print \"A vote is already in progress.\n\"" ); + return; + } + if ( ent->client->pers.voteCount >= MAX_VOTE_COUNT ) { + trap_SendServerCommand( ent-g_entities, "print \"You have called the maximum number of votes.\n\"" ); + return; + } + if ( ent->client->sess.sessionTeam == TEAM_SPECTATOR ) { + trap_SendServerCommand( ent-g_entities, "print \"Not allowed to call a vote as spectator.\n\"" ); + return; + } + + // make sure it is a valid command to vote on + trap_Argv( 1, arg1, sizeof( arg1 ) ); + trap_Argv( 2, arg2, sizeof( arg2 ) ); + + if( strchr( arg1, ';' ) || strchr( arg2, ';' ) ) { + trap_SendServerCommand( ent-g_entities, "print \"Invalid vote string.\n\"" ); + return; + } + + if ( !Q_stricmp( arg1, "map_restart" ) ) { + } else if ( !Q_stricmp( arg1, "nextmap" ) ) { + } else if ( !Q_stricmp( arg1, "map" ) ) { + } else if ( !Q_stricmp( arg1, "g_gametype" ) ) { + } else if ( !Q_stricmp( arg1, "kick" ) ) { + } else if ( !Q_stricmp( arg1, "clientkick" ) ) { + } else if ( !Q_stricmp( arg1, "g_doWarmup" ) ) { + } else if ( !Q_stricmp( arg1, "timelimit" ) ) { + } else if ( !Q_stricmp( arg1, "fraglimit" ) ) { + } else { + trap_SendServerCommand( ent-g_entities, "print \"Invalid vote string.\n\"" ); + trap_SendServerCommand( ent-g_entities, "print \"Vote commands are: map_restart, nextmap, map <mapname>, g_gametype <n>, kick <player>, clientkick <clientnum>, g_doWarmup, timelimit <time>, fraglimit <frags>.\n\"" ); + return; + } + + // if there is still a vote to be executed + if ( level.voteExecuteTime ) { + level.voteExecuteTime = 0; + trap_SendConsoleCommand( EXEC_APPEND, va("%s\n", level.voteString ) ); + } + + // special case for g_gametype, check for bad values + if ( !Q_stricmp( arg1, "g_gametype" ) ) { + i = atoi( arg2 ); + if( i == GT_SINGLE_PLAYER || i < GT_FFA || i >= GT_MAX_GAME_TYPE) { + trap_SendServerCommand( ent-g_entities, "print \"Invalid gametype.\n\"" ); + return; + } + + Com_sprintf( level.voteString, sizeof( level.voteString ), "%s %d", arg1, i ); + Com_sprintf( level.voteDisplayString, sizeof( level.voteDisplayString ), "%s %s", arg1, gameNames[i] ); + } else if ( !Q_stricmp( arg1, "map" ) ) { + // special case for map changes, we want to reset the nextmap setting + // this allows a player to change maps, but not upset the map rotation + char s[MAX_STRING_CHARS]; + + trap_Cvar_VariableStringBuffer( "nextmap", s, sizeof(s) ); + if (*s) { + Com_sprintf( level.voteString, sizeof( level.voteString ), "%s %s; set nextmap \"%s\"", arg1, arg2, s ); + } else { + Com_sprintf( level.voteString, sizeof( level.voteString ), "%s %s", arg1, arg2 ); + } + Com_sprintf( level.voteDisplayString, sizeof( level.voteDisplayString ), "%s", level.voteString ); + } else if ( !Q_stricmp( arg1, "nextmap" ) ) { + char s[MAX_STRING_CHARS]; + + trap_Cvar_VariableStringBuffer( "nextmap", s, sizeof(s) ); + if (!*s) { + trap_SendServerCommand( ent-g_entities, "print \"nextmap not set.\n\"" ); + return; + } + Com_sprintf( level.voteString, sizeof( level.voteString ), "vstr nextmap"); + Com_sprintf( level.voteDisplayString, sizeof( level.voteDisplayString ), "%s", level.voteString ); + } else { + Com_sprintf( level.voteString, sizeof( level.voteString ), "%s \"%s\"", arg1, arg2 ); + Com_sprintf( level.voteDisplayString, sizeof( level.voteDisplayString ), "%s", level.voteString ); + } + + trap_SendServerCommand( -1, va("print \"%s called a vote.\n\"", ent->client->pers.netname ) ); + + // start the voting, the caller autoamtically votes yes + level.voteTime = level.time; + level.voteYes = 1; + level.voteNo = 0; + + for ( i = 0 ; i < level.maxclients ; i++ ) { + level.clients[i].ps.eFlags &= ~EF_VOTED; + } + ent->client->ps.eFlags |= EF_VOTED; + + trap_SetConfigstring( CS_VOTE_TIME, va("%i", level.voteTime ) ); + trap_SetConfigstring( CS_VOTE_STRING, level.voteDisplayString ); + trap_SetConfigstring( CS_VOTE_YES, va("%i", level.voteYes ) ); + trap_SetConfigstring( CS_VOTE_NO, va("%i", level.voteNo ) ); +} + +/* +================== +Cmd_Vote_f +================== +*/ +void Cmd_Vote_f( gentity_t *ent ) { + char msg[64]; + + if ( !level.voteTime ) { + trap_SendServerCommand( ent-g_entities, "print \"No vote in progress.\n\"" ); + return; + } + if ( ent->client->ps.eFlags & EF_VOTED ) { + trap_SendServerCommand( ent-g_entities, "print \"Vote already cast.\n\"" ); + return; + } + + if ( ent->client->sess.sessionTeam == TEAM_SPECTATOR ) { + trap_SendServerCommand( ent-g_entities, "print \"Not allowed to vote as spectator.\n\"" ); + return; + } + + trap_SendServerCommand( ent-g_entities, "print \"Vote cast.\n\"" ); + + ent->client->ps.eFlags |= EF_VOTED; + + trap_Argv( 1, msg, sizeof( msg ) ); + + if ( msg[0] == 'y' || msg[1] == 'Y' || msg[1] == '1' ) { + level.voteYes++; + trap_SetConfigstring( CS_VOTE_YES, va("%i", level.voteYes ) ); + } else { + level.voteNo++; + trap_SetConfigstring( CS_VOTE_NO, va("%i", level.voteNo ) ); + } + + // a majority will be determined in G_CheckVote, which will also account + // for players entering or leaving +} + +/* +================== +Cmd_CallTeamVote_f +================== +*/ +void Cmd_CallTeamVote_f( gentity_t *ent ) { + int i, team, cs_offset; + char arg1[MAX_STRING_TOKENS]; + char arg2[MAX_STRING_TOKENS]; + + team = ent->client->sess.sessionTeam; + if ( team == TEAM_HUMANS ) + cs_offset = 0; + else if ( team == TEAM_DROIDS ) + cs_offset = 1; + else + return; + + if ( !g_allowVote.integer ) { + trap_SendServerCommand( ent-g_entities, "print \"Voting not allowed here.\n\"" ); + return; + } + + if ( level.teamVoteTime[cs_offset] ) { + trap_SendServerCommand( ent-g_entities, "print \"A team vote is already in progress.\n\"" ); + return; + } + if ( ent->client->pers.teamVoteCount >= MAX_VOTE_COUNT ) { + trap_SendServerCommand( ent-g_entities, "print \"You have called the maximum number of team votes.\n\"" ); + return; + } + if ( ent->client->sess.sessionTeam == TEAM_SPECTATOR ) { + trap_SendServerCommand( ent-g_entities, "print \"Not allowed to call a vote as spectator.\n\"" ); + return; + } + + // make sure it is a valid command to vote on + trap_Argv( 1, arg1, sizeof( arg1 ) ); + arg2[0] = '\0'; + for ( i = 2; i < trap_Argc(); i++ ) { + if (i > 2) + strcat(arg2, " "); + trap_Argv( i, &arg2[strlen(arg2)], sizeof( arg2 ) - strlen(arg2) ); + } + + if( strchr( arg1, ';' ) || strchr( arg2, ';' ) ) { + trap_SendServerCommand( ent-g_entities, "print \"Invalid vote string.\n\"" ); + return; + } + + + if ( !Q_stricmp( arg1, "leader" ) ) { + char netname[MAX_NETNAME], leader[MAX_NETNAME]; + + if ( !arg2[0] ) { + i = ent->client->ps.clientNum; + } + else { + // numeric values are just slot numbers + for (i = 0; i < 3; i++) { + if ( !arg2[i] || arg2[i] < '0' || arg2[i] > '9' ) + break; + } + if ( i >= 3 || !arg2[i]) { + i = atoi( arg2 ); + if ( i < 0 || i >= level.maxclients ) { + trap_SendServerCommand( ent-g_entities, va("print \"Bad client slot: %i\n\"", i) ); + return; + } + + if ( !g_entities[i].inuse ) { + trap_SendServerCommand( ent-g_entities, va("print \"Client %i is not active\n\"", i) ); + return; + } + } + else { + Q_strncpyz(leader, arg2, sizeof(leader)); + Q_CleanStr(leader); + for ( i = 0 ; i < level.maxclients ; i++ ) { + if ( level.clients[i].pers.connected == CON_DISCONNECTED ) + continue; + if (level.clients[i].sess.sessionTeam != team) + continue; + Q_strncpyz(netname, level.clients[i].pers.netname, sizeof(netname)); + Q_CleanStr(netname); + if ( !Q_stricmp(netname, leader) ) { + break; + } + } + if ( i >= level.maxclients ) { + trap_SendServerCommand( ent-g_entities, va("print \"%s is not a valid player on your team.\n\"", arg2) ); + return; + } + } + } + Com_sprintf(arg2, sizeof(arg2), "%d", i); + } else { + trap_SendServerCommand( ent-g_entities, "print \"Invalid vote string.\n\"" ); + trap_SendServerCommand( ent-g_entities, "print \"Team vote commands are: leader <player>.\n\"" ); + return; + } + + Com_sprintf( level.teamVoteString[cs_offset], sizeof( level.teamVoteString[cs_offset] ), "%s %s", arg1, arg2 ); + + for ( i = 0 ; i < level.maxclients ; i++ ) { + if ( level.clients[i].pers.connected == CON_DISCONNECTED ) + continue; + if (level.clients[i].sess.sessionTeam == team) + trap_SendServerCommand( i, va("print \"%s called a team vote.\n\"", ent->client->pers.netname ) ); + } + + // start the voting, the caller autoamtically votes yes + level.teamVoteTime[cs_offset] = level.time; + level.teamVoteYes[cs_offset] = 1; + level.teamVoteNo[cs_offset] = 0; + + for ( i = 0 ; i < level.maxclients ; i++ ) { + if (level.clients[i].sess.sessionTeam == team) + level.clients[i].ps.eFlags &= ~EF_TEAMVOTED; + } + ent->client->ps.eFlags |= EF_TEAMVOTED; + + trap_SetConfigstring( CS_TEAMVOTE_TIME + cs_offset, va("%i", level.teamVoteTime[cs_offset] ) ); + trap_SetConfigstring( CS_TEAMVOTE_STRING + cs_offset, level.teamVoteString[cs_offset] ); + trap_SetConfigstring( CS_TEAMVOTE_YES + cs_offset, va("%i", level.teamVoteYes[cs_offset] ) ); + trap_SetConfigstring( CS_TEAMVOTE_NO + cs_offset, va("%i", level.teamVoteNo[cs_offset] ) ); +} + + +/* +================== +Cmd_TeamVote_f +================== +*/ +void Cmd_TeamVote_f( gentity_t *ent ) { + int team, cs_offset; + char msg[64]; + + team = ent->client->sess.sessionTeam; + if ( team == TEAM_HUMANS ) + cs_offset = 0; + else if ( team == TEAM_DROIDS ) + cs_offset = 1; + else + return; + + if ( !level.teamVoteTime[cs_offset] ) { + trap_SendServerCommand( ent-g_entities, "print \"No team vote in progress.\n\"" ); + return; + } + if ( ent->client->ps.eFlags & EF_TEAMVOTED ) { + trap_SendServerCommand( ent-g_entities, "print \"Team vote already cast.\n\"" ); + return; + } + if ( ent->client->sess.sessionTeam == TEAM_SPECTATOR ) { + trap_SendServerCommand( ent-g_entities, "print \"Not allowed to vote as spectator.\n\"" ); + return; + } + + trap_SendServerCommand( ent-g_entities, "print \"Team vote cast.\n\"" ); + + ent->client->ps.eFlags |= EF_TEAMVOTED; + + trap_Argv( 1, msg, sizeof( msg ) ); + + if ( msg[0] == 'y' || msg[1] == 'Y' || msg[1] == '1' ) { + level.teamVoteYes[cs_offset]++; + trap_SetConfigstring( CS_TEAMVOTE_YES + cs_offset, va("%i", level.teamVoteYes[cs_offset] ) ); + } else { + level.teamVoteNo[cs_offset]++; + trap_SetConfigstring( CS_TEAMVOTE_NO + cs_offset, va("%i", level.teamVoteNo[cs_offset] ) ); + } + + // a majority will be determined in TeamCheckVote, which will also account + // for players entering or leaving +} + + +/* +================= +Cmd_SetViewpos_f +================= +*/ +void Cmd_SetViewpos_f( gentity_t *ent ) { + vec3_t origin, angles; + char buffer[MAX_TOKEN_CHARS]; + int i; + + if ( !g_cheats.integer ) { + trap_SendServerCommand( ent-g_entities, va("print \"Cheats are not enabled on this server.\n\"")); + return; + } + if ( trap_Argc() != 5 ) { + trap_SendServerCommand( ent-g_entities, va("print \"usage: setviewpos x y z yaw\n\"")); + return; + } + + VectorClear( angles ); + for ( i = 0 ; i < 3 ; i++ ) { + trap_Argv( i + 1, buffer, sizeof( buffer ) ); + origin[i] = atof( buffer ); + } + + trap_Argv( 4, buffer, sizeof( buffer ) ); + angles[YAW] = atof( buffer ); + + TeleportPlayer( ent, origin, angles ); +} + + +/* +================= +Cmd_Stats_f +================= +*/ +void Cmd_Stats_f( gentity_t *ent ) { +/* + int max, n, i; + + max = trap_AAS_PointReachabilityAreaIndex( NULL ); + + n = 0; + for ( i = 0; i < max; i++ ) { + if ( ent->client->areabits[i >> 3] & (1 << (i & 7)) ) + n++; + } + + //trap_SendServerCommand( ent-g_entities, va("print \"visited %d of %d areas\n\"", n, max)); + trap_SendServerCommand( ent-g_entities, va("print \"%d%% level coverage\n\"", n * 100 / max)); +*/ +} + + +/* +================= +Cmd_TorchOff_f +================= +*/ +void Cmd_TorchOff_f( gentity_t *ent ) +{ + G_FreeEntity( ent->client->torch ); + trap_LinkEntity( ent->client->torch ); + ent->client->torch = NULL; +} + + +/* +================= +Cmd_Class_f +================= +*/ +void Cmd_Class_f( gentity_t *ent ) +{ + char s[ MAX_TOKEN_CHARS ]; + qboolean dontSpawn = qfalse; + int clientNum; + + clientNum = ent->client - level.clients; + trap_Argv( 1, s, sizeof( s ) ); + + if( !strlen( s ) && ( ent->client->pers.pteam != PTE_HUMANS ) ) + { + trap_SendServerCommand( ent-g_entities, va("print \"class number: %i\n\"", ent->client->pers.pclass ) ); + return; + } + + if( ent->client->pers.pteam == PTE_DROIDS ) + { + if( !Q_stricmp(s, "0") ) + ent->client->pers.pclass = PCL_D_BUILDER; + else if( !Q_stricmp(s, "1") ) + ent->client->pers.pclass = PCL_D_BASE; + else if( !Q_stricmp(s, "2") ) + ent->client->pers.pclass = PCL_D_OFF1; + else if( !Q_stricmp(s, "3") ) + ent->client->pers.pclass = PCL_D_OFF2; + else if( !Q_stricmp(s, "4") ) + ent->client->pers.pclass = PCL_D_OFF3; + else if( !Q_stricmp(s, "5") ) + ent->client->pers.pclass = PCL_D_OFF4; + else if( !Q_stricmp(s, "6") ) + ent->client->pers.pclass = PCL_D_OFF5; + else if( !Q_stricmp(s, "7") ) + ent->client->pers.pclass = PCL_D_OFF6; + else if( !Q_stricmp(s, "8") ) + ent->client->pers.pclass = PCL_D_OFF7; + else if( !Q_stricmp(s, "9") ) + ent->client->pers.pclass = PCL_D_OFF8; + else + { + trap_SendServerCommand( ent-g_entities, va("print \"Unknown class\n\"" ) ); + dontSpawn = qtrue; + } + + if( !dontSpawn ) + { + if( ent->client->torch != NULL ) + Cmd_TorchOff_f( ent ); + + ent->client->sess.sessionTeam = TEAM_FREE; + ClientUserinfoChanged( clientNum ); + ClientSpawn( ent ); + } + } + else if( ent->client->pers.pteam == PTE_HUMANS ) + { + ent->client->pers.pclass = PCL_H_BASE; + + if( ent->client->torch != NULL ) + Cmd_TorchOff_f( ent ); + + ent->client->sess.sessionTeam = TEAM_FREE; + ClientUserinfoChanged( clientNum ); + ClientSpawn( ent ); + } + else if( ent->client->pers.pteam == PTE_NONE ) + { + ent->client->pers.pclass = 0; + ent->client->sess.sessionTeam = TEAM_FREE; + ClientSpawn( ent ); + trap_SendServerCommand( ent-g_entities, va("print \"Join a team first\n\"" ) ); + } + else + { + trap_SendServerCommand( ent-g_entities, va("print \"?\n\"" ) ); + } +} + + +/* +================= +Cmd_Destroy_f +================= +*/ +void Cmd_Destroy_f( gentity_t *ent ) +{ + vec3_t forward, end; + trace_t tr; + gentity_t *traceEnt; + + AngleVectors( ent->client->ps.viewangles, forward, NULL, NULL ); + VectorMA( ent->client->ps.origin, 50, forward, end ); + + trap_Trace( &tr, ent->client->ps.origin, NULL, NULL, end, ent->s.number, MASK_PLAYERSOLID ); + traceEnt = &g_entities[ tr.entityNum ]; + + if( tr.fraction < 1.0 && + ( traceEnt->s.eType == ET_BUILDABLE ) && + ( traceEnt->biteam == ent->client->pers.pteam ) && + ( ( ent->client->ps.weapon == WP_ABUILD ) || + ( ent->client->ps.weapon == WP_HBUILD ) ) ) + G_Damage( traceEnt, ent, ent, forward, tr.endpos, 10000, 0, MOD_SUICIDE ); + +} + + +/* +================= +Cmd_ActivateItem_f + +Activate an item +================= +*/ +void Cmd_ActivateItem_f( gentity_t *ent ) +{ + char s[ MAX_TOKEN_CHARS ]; + + trap_Argv( 1, s, sizeof( s ) ); + + if( ent->client->pers.pteam != PTE_HUMANS ) + return; + + if( !Q_stricmp( s, "nvg" ) ) + { + if( BG_gotItem( UP_NVG, ent->client->ps.stats ) ) + BG_activateItem( UP_NVG, ent->client->ps.stats ); + else + trap_SendServerCommand( ent-g_entities, va("print \"You don't have the NVG\n\"" ) ); + } + else if( !Q_stricmp( s, "torch" ) ) + { + if( BG_gotItem( UP_TORCH, ent->client->ps.stats ) ) + BG_activateItem( UP_TORCH, ent->client->ps.stats ); + else + trap_SendServerCommand( ent-g_entities, va("print \"You don't have the torch\n\"" ) ); + } +} + + +/* +================= +Cmd_DeActivateItem_f + +Deactivate an item +================= +*/ +void Cmd_DeActivateItem_f( gentity_t *ent ) +{ + char s[ MAX_TOKEN_CHARS ]; + + trap_Argv( 1, s, sizeof( s ) ); + + if( ent->client->pers.pteam != PTE_HUMANS ) + return; + + if( !Q_stricmp( s, "nvg" ) ) + { + if( BG_gotItem( UP_NVG, ent->client->ps.stats ) ) + BG_deactivateItem( UP_NVG, ent->client->ps.stats ); + else + trap_SendServerCommand( ent-g_entities, va("print \"You don't have the NVG\n\"" ) ); + } + else if( !Q_stricmp( s, "torch" ) ) + { + if( BG_gotItem( UP_TORCH, ent->client->ps.stats ) ) + BG_deactivateItem( UP_TORCH, ent->client->ps.stats ); + else + trap_SendServerCommand( ent-g_entities, va("print \"You don't have the torch\n\"" ) ); + } +} + + +/* +================= +Cmd_ToggleItem_f +================= +*/ +void Cmd_ToggleItem_f( gentity_t *ent ) +{ + char s[ MAX_TOKEN_CHARS ]; + + trap_Argv( 1, s, sizeof( s ) ); + + if( ent->client->pers.pteam != PTE_HUMANS ) + return; + + if( !Q_stricmp( s, "nvg" ) ) + { + if( BG_gotItem( UP_NVG, ent->client->ps.stats ) ) + { + if( BG_activated( UP_NVG, ent->client->ps.stats ) ) + BG_deactivateItem( UP_NVG, ent->client->ps.stats ); + else + BG_activateItem( UP_NVG, ent->client->ps.stats ); + } + else + trap_SendServerCommand( ent-g_entities, va("print \"You don't have the NVG\n\"" ) ); + } + else if( !Q_stricmp( s, "torch" ) ) + { + if( BG_gotItem( UP_TORCH, ent->client->ps.stats ) ) + { + if( BG_activated( UP_TORCH, ent->client->ps.stats ) ) + BG_deactivateItem( UP_TORCH, ent->client->ps.stats ); + else + BG_activateItem( UP_TORCH, ent->client->ps.stats ); + } + else + trap_SendServerCommand( ent-g_entities, va("print \"You don't have the torch\n\"" ) ); + } +} + + +/* +================= +Cmd_Buy_f +================= +*/ +void Cmd_Buy_f( gentity_t *ent ) +{ + char s[ MAX_TOKEN_CHARS ]; + + trap_Argv( 1, s, sizeof( s ) ); + + if( ent->client->pers.pteam != PTE_HUMANS ) + return; + + + if( !Q_stricmp( s, "rifle" ) ) + { + BG_packWeapon( WP_MACHINEGUN, ent->client->ps.stats ); + BG_packAmmoArray( WP_MACHINEGUN, ent->client->ps.ammo, ent->client->ps.powerups, CS_MG, 3, 3 ); + ent->client->ps.weapon = WP_MACHINEGUN; + } + else if( !Q_stricmp( s, "chaingun" ) ) + { + BG_packWeapon( WP_CHAINGUN, ent->client->ps.stats ); + BG_packAmmoArray( WP_CHAINGUN, ent->client->ps.ammo, ent->client->ps.powerups, CS_CG, 0, 0 ); + ent->client->ps.weapon = WP_CHAINGUN; + } + else if( !Q_stricmp( s, "scanner" ) ) + { + BG_packWeapon( WP_SCANNER, ent->client->ps.stats ); + BG_packAmmoArray( WP_SCANNER, ent->client->ps.ammo, ent->client->ps.powerups, 0, 0, 0 ); + ent->client->ps.weapon = WP_SCANNER; + } + else if( !Q_stricmp( s, "flamer" ) ) + { + BG_packWeapon( WP_FLAMER, ent->client->ps.stats ); + BG_packAmmoArray( WP_FLAMER, ent->client->ps.ammo, ent->client->ps.powerups, CS_FLAMER, 0, 0 ); + ent->client->ps.weapon = WP_FLAMER; + } + else if( !Q_stricmp( s, "ckit" ) ) + { + BG_packWeapon( WP_HBUILD, ent->client->ps.stats ); + BG_packAmmoArray( WP_HBUILD, ent->client->ps.ammo, ent->client->ps.powerups, 0, 0, 0 ); + ent->client->ps.weapon = WP_HBUILD; + } + else if( !Q_stricmp( s, "ggrenade" ) ) + { + BG_packWeapon( WP_GGRENADE, ent->client->ps.stats ); + BG_packAmmoArray( WP_GGRENADE, ent->client->ps.ammo, ent->client->ps.powerups, 1, 0, 0 ); + ent->client->ps.weapon = WP_GGRENADE; + } + else if( !Q_stricmp( s, "carmour" ) ) + { + BG_packItem( UP_CHESTARMOUR, ent->client->ps.stats ); + } + else if( !Q_stricmp( s, "nvg" ) ) + { + BG_packItem( UP_NVG, ent->client->ps.stats ); + } + else if( !Q_stricmp( s, "torch" ) ) + { + BG_packItem( UP_TORCH, ent->client->ps.stats ); + } + + //subtract from funds +} + + +/* +================= +Cmd_Build_f +================= +*/ +void Cmd_Build_f( gentity_t *ent ) +{ + gitem_t *item; + char s[ MAX_TOKEN_CHARS ]; + int buildable; + qboolean dontBuild = qfalse; + + + trap_Argv( 1, s, sizeof( s ) ); + + if( ( ent->client->pers.pteam == PTE_DROIDS ) && + BG_gotWeapon( WP_ABUILD, ent->client->ps.stats ) && + ( ent->client->ps.weapon == WP_ABUILD ) ) + { + if( !Q_stricmp(s, "0") || !Q_stricmp(s, "spawn") ) + buildable = BA_A_SPAWN; + else if( !Q_stricmp(s, "1") || !Q_stricmp(s, "defense1") ) + buildable = BA_A_DEF1; + else + { + trap_SendServerCommand( ent-g_entities, va("print \"Unknown item\n\"" ) ); + dontBuild = qtrue; + } + + if( !dontBuild ) + { + item = BG_FindItemForBuildable( buildable ); + if( itemFits( ent, item, -50 ) ) + { + Build_Item( ent, item, -50 ); + } + else + trap_SendServerCommand( ent-g_entities, va("print \"Location is not suitable\n\"" ) ); + } + } + else if( ( ent->client->pers.pteam == PTE_HUMANS ) && + BG_gotWeapon( WP_HBUILD, ent->client->ps.stats ) && + ( ent->client->ps.weapon == WP_HBUILD ) ) + { + if( !Q_stricmp(s, "0") || !Q_stricmp(s, "spawn") ) + buildable = BA_H_SPAWN; + else if( !Q_stricmp(s, "1") || !Q_stricmp(s, "defense1") ) + buildable = BA_H_DEF1; + else if( !Q_stricmp(s, "2") || !Q_stricmp(s, "mcu") ) + buildable = BA_H_MCU; + else + { + trap_SendServerCommand( ent-g_entities, va("print \"Unknown item\n\"" ) ); + dontBuild = qtrue; + } + + if( !dontBuild ) + { + item = BG_FindItemForBuildable( buildable ); + if( itemFits( ent, item, 80 ) ) + { + Build_Item( ent, item, 80 ); + } + else + trap_SendServerCommand( ent-g_entities, va("print \"Location is not suitable\n\"" ) ); + } + } + else if( ent->client->pers.pteam == PTE_NONE ) + trap_SendServerCommand( ent-g_entities, va("print \"Join a team first\n\"" ) ); + else + trap_SendServerCommand( ent-g_entities, va("print \"Class is unable to build\n\"" ) ); +} + + +//TA: so we can print to the console from anywhere +/* +================= +Cmd_Echo_f +================= +*/ +void Cmd_Echo_f( gentity_t *ent ) +{ + char s[ MAX_TOKEN_CHARS ]; + + trap_Argv( 1, s, sizeof( s ) ); + + trap_SendServerCommand( ent-g_entities, va("print \"%s\n\"", s ) ); +} + + +/* +================= +Cmd_Boost_f +================= +*/ +void Cmd_Boost_f( gentity_t *ent ) +{ + if( ( ent->client->ps.stats[ STAT_PTEAM ] == PTE_HUMANS ) && + ( ent->client->ps.stats[ STAT_STAMINA ] > 0 ) ) + ent->client->ps.stats[ STAT_STATE ] |= SS_SPEEDBOOST; +} + + +/* +================= +ClientCommand +================= +*/ +void ClientCommand( int clientNum ) { + gentity_t *ent; + char cmd[MAX_TOKEN_CHARS]; + + ent = g_entities + clientNum; + if ( !ent->client ) { + return; // not fully in game yet + } + + trap_Argv( 0, cmd, sizeof( cmd ) ); + + if (Q_stricmp (cmd, "say") == 0) { + Cmd_Say_f (ent, SAY_ALL, qfalse); + return; + } + if (Q_stricmp (cmd, "say_team") == 0) { + Cmd_Say_f (ent, SAY_TEAM, qfalse); + return; + } + if (Q_stricmp (cmd, "tell") == 0) { + Cmd_Tell_f ( ent ); + return; + } + if (Q_stricmp (cmd, "vsay") == 0) { + Cmd_Voice_f (ent, SAY_ALL, qfalse, qfalse); + return; + } + if (Q_stricmp (cmd, "vsay_team") == 0) { + Cmd_Voice_f (ent, SAY_TEAM, qfalse, qfalse); + return; + } + if (Q_stricmp (cmd, "vtell") == 0) { + Cmd_VoiceTell_f ( ent, qfalse ); + return; + } + if (Q_stricmp (cmd, "vosay") == 0) { + Cmd_Voice_f (ent, SAY_ALL, qfalse, qtrue); + return; + } + if (Q_stricmp (cmd, "vosay_team") == 0) { + Cmd_Voice_f (ent, SAY_TEAM, qfalse, qtrue); + return; + } + if (Q_stricmp (cmd, "votell") == 0) { + Cmd_VoiceTell_f ( ent, qtrue ); + return; + } + if (Q_stricmp (cmd, "vtaunt") == 0) { + Cmd_VoiceTaunt_f ( ent ); + return; + } + if (Q_stricmp (cmd, "score") == 0) { + Cmd_Score_f (ent); + return; + } + + // ignore all other commands when at intermission + if (level.intermissiontime) { + Cmd_Say_f (ent, qfalse, qtrue); + return; + } + + if (Q_stricmp (cmd, "give") == 0) + Cmd_Give_f (ent); + else if (Q_stricmp (cmd, "god") == 0) + Cmd_God_f (ent); + else if (Q_stricmp (cmd, "notarget") == 0) + Cmd_Notarget_f (ent); + else if (Q_stricmp (cmd, "noclip") == 0) + Cmd_Noclip_f (ent); + else if (Q_stricmp (cmd, "kill") == 0) + Cmd_Kill_f (ent); + else if (Q_stricmp (cmd, "teamtask") == 0) + Cmd_TeamTask_f (ent); + else if (Q_stricmp (cmd, "levelshot") == 0) + Cmd_LevelShot_f (ent); + else if (Q_stricmp (cmd, "follow") == 0) + Cmd_Follow_f (ent); + else if (Q_stricmp (cmd, "follownext") == 0) + Cmd_FollowCycle_f (ent, 1); + else if (Q_stricmp (cmd, "followprev") == 0) + Cmd_FollowCycle_f (ent, -1); + else if (Q_stricmp (cmd, "team") == 0) + Cmd_Team_f (ent); + else if (Q_stricmp (cmd, "class") == 0) + Cmd_Class_f( ent ); + else if (Q_stricmp (cmd, "build") == 0) + Cmd_Build_f( ent ); + else if (Q_stricmp (cmd, "buy") == 0) + Cmd_Buy_f( ent ); + else if (Q_stricmp (cmd, "itemact") == 0) + Cmd_ActivateItem_f( ent ); + else if (Q_stricmp (cmd, "itemdeact") == 0) + Cmd_DeActivateItem_f( ent ); + else if (Q_stricmp (cmd, "itemtoggle") == 0) + Cmd_ToggleItem_f( ent ); + else if (Q_stricmp (cmd, "destroy") == 0) + Cmd_Destroy_f( ent ); + else if (Q_stricmp (cmd, "echo") == 0) + Cmd_Echo_f( ent ); + else if (Q_stricmp (cmd, "boost") == 0) + Cmd_Boost_f( ent ); + else if (Q_stricmp (cmd, "where") == 0) + Cmd_Where_f (ent); + else if (Q_stricmp (cmd, "callvote") == 0) + Cmd_CallVote_f (ent); + else if (Q_stricmp (cmd, "vote") == 0) + Cmd_Vote_f (ent); + else if (Q_stricmp (cmd, "callteamvote") == 0) + Cmd_CallTeamVote_f (ent); + else if (Q_stricmp (cmd, "teamvote") == 0) + Cmd_TeamVote_f (ent); + else if (Q_stricmp (cmd, "gc") == 0) + Cmd_GameCommand_f( ent ); + else if (Q_stricmp (cmd, "setviewpos") == 0) + Cmd_SetViewpos_f( ent ); + else if (Q_stricmp (cmd, "stats") == 0) + Cmd_Stats_f( ent ); + else + trap_SendServerCommand( clientNum, va("print \"unknown cmd %s\n\"", cmd ) ); +} diff --git a/src/game/g_combat.c b/src/game/g_combat.c new file mode 100644 index 00000000..b98654e7 --- /dev/null +++ b/src/game/g_combat.c @@ -0,0 +1,1193 @@ +// Copyright (C) 1999-2000 Id Software, Inc. +// +// g_combat.c + +/* + * Portions Copyright (C) 2000-2001 Tim Angus + * + * This program 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, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ + +/* To assertain which portions are licensed under the GPL and which are + * licensed by Id Software, Inc. please run a diff between the equivalent + * versions of the "Tremulous" modification and the unmodified "Quake3" + * game source code. + */ + +#include "g_local.h" + + +/* +============ +AddScore + +Adds score to both the client and his team +============ +*/ +void AddScore( gentity_t *ent, int score ) { + if ( !ent->client ) { + return; + } + // no scoring during pre-match warmup + if ( level.warmupTime ) { + return; + } + ent->client->ps.persistant[PERS_SCORE] += score; + if (g_gametype.integer == GT_TEAM) + level.teamScores[ ent->client->ps.persistant[PERS_TEAM] ] += score; + CalculateRanks(); +} + + +/* +============ +AddPoints + +Adds points to both the client and his team +============ +*/ +void AddPoints( gentity_t *ent, int score ) +{ + if ( !ent->client ) { + return; + } + // no scoring during pre-match warmup + if ( level.warmupTime ) { + return; + } + ent->client->ps.persistant[PERS_POINTS] += score; + ent->client->ps.persistant[PERS_TOTALPOINTS] += score; + + /*if (g_gametype.integer == GT_TEAM) + level.teamScores[ ent->client->ps.persistant[PERS_TEAM] ] += score; + CalculateRanks();*/ +} + + +/* +============ +CalculatePoints + +Calculates the points to given to a client +============ +*/ +int CalculatePoints( gentity_t *victim, gentity_t *attacker ) +{ + int victim_value, attacker_value; + + if( !victim->client || !attacker->client ) + return 0; + + /*switch( victim->client->ps.stats[STAT_PCLASS] ) + { + case PCL_D_BUILDER: + victim_value = 1; + break; + case PCL_H_BUILDER: + victim_value = 1; + break; + case PCL_D_BASE: + victim_value = 2; + break; + case PCL_H_BASE: + victim_value = 2; + break; + case PCL_D_OFF1: + victim_value = 3; + break; + case PCL_H_OFF1: + victim_value = 3; + break; + case PCL_D_OFF2: + victim_value = 4; + break; + case PCL_H_OFF2: + victim_value = 4; + break; + case PCL_D_OFF3: + victim_value = 5; + break; + case PCL_H_OFF3: + victim_value = 5; + break; + case PCL_D_OFF4: + victim_value = 6; + break; + case PCL_H_OFF4: + victim_value = 6; + break; + case PCL_D_OFF5: + victim_value = 7; + break; + case PCL_H_OFF5: + victim_value = 7; + break; + case PCL_D_OFF6: + victim_value = 8; + break; + case PCL_H_OFF6: + victim_value = 8; + break; + case PCL_D_OFF7: + victim_value = 9; + break; + case PCL_H_OFF7: + victim_value = 9; + break; + case PCL_D_OFF8: + victim_value = 10; + break; + case PCL_H_OFF8: + victim_value = 10; + break; + default: + victim_value = 1; + } + + switch( attacker->client->ps.stats[STAT_PCLASS] ) + { + case PCL_D_BUILDER: + attacker_value = 1; + break; + case PCL_H_BUILDER: + attacker_value = 1; + break; + case PCL_D_BASE: + attacker_value = 2; + break; + case PCL_H_BASE: + attacker_value = 2; + break; + case PCL_D_OFF1: + attacker_value = 3; + break; + case PCL_H_OFF1: + attacker_value = 3; + break; + case PCL_D_OFF2: + attacker_value = 4; + break; + case PCL_H_OFF2: + attacker_value = 4; + break; + case PCL_D_OFF3: + attacker_value = 5; + break; + case PCL_H_OFF3: + attacker_value = 5; + break; + case PCL_D_OFF4: + attacker_value = 6; + break; + case PCL_H_OFF4: + attacker_value = 6; + break; + case PCL_D_OFF5: + attacker_value = 7; + break; + case PCL_H_OFF5: + attacker_value = 7; + break; + case PCL_D_OFF6: + attacker_value = 8; + break; + case PCL_H_OFF6: + attacker_value = 8; + break; + case PCL_D_OFF7: + attacker_value = 9; + break; + case PCL_H_OFF7: + attacker_value = 9; + break; + case PCL_D_OFF8: + attacker_value = 10; + break; + case PCL_H_OFF8: + attacker_value = 10; + break; + default: + attacker_value = 1; + } + + return ( victim_value / attacker_value ) * 10;*/ + + return 1; + +} + + +/* +================= +TossClientItems + +Toss the weapon and powerups for the killed player +================= +*/ +void TossClientItems( gentity_t *self ) { + gitem_t *item; + int weapon; + float angle; + int i; + gentity_t *drop; + int ammo, clips, maxclips; + + // drop the weapon if not a gauntlet or machinegun + weapon = self->s.weapon; + + BG_unpackAmmoArray( weapon, self->client->ps.ammo, self->client->ps.powerups, &ammo, &clips, &maxclips ); + + // make a special check to see if they are changing to a new + // weapon that isn't the mg or gauntlet. Without this, a client + // can pick up a weapon, be killed, and not drop the weapon because + // their weapon change hasn't completed yet and they are still holding the MG. + if ( weapon == WP_MACHINEGUN || weapon == WP_GRAPPLING_HOOK ) { + if ( self->client->ps.weaponstate == WEAPON_DROPPING ) { + weapon = self->client->pers.cmd.weapon; + } + if ( !BG_gotWeapon( weapon, self->client->ps.stats ) ) { + weapon = WP_NONE; + } + } + + if( weapon > WP_MACHINEGUN && weapon != WP_GRAPPLING_HOOK && + ( ammo > 0 || clips > 0 ) ) { + // find the item type for this weapon + item = BG_FindItemForWeapon( weapon ); + + //TA: never drop weapons... + // spawn the item + //Drop_Item( self, item, 0 ); + } + + // drop all the powerups if not in teamplay + if ( g_gametype.integer != GT_TEAM ) { + angle = 45; + /*for ( i = 1 ; i < PW_NUM_POWERUPS ; i++ ) { + if ( self->client->ps.powerups[ i ] > level.time ) { + item = BG_FindItemForPowerup( i ); + if ( !item ) { + continue; + } + //TA: ...or powerups + /*drop = Drop_Item( self, item, angle ); + // decide how many seconds it has left + drop->count = ( self->client->ps.powerups[ i ] - level.time ) / 1000; + if ( drop->count < 1 ) { + drop->count = 1; + } + angle += 45; + } + }*/ + } +} + + +/* +================== +LookAtKiller +================== +*/ +void LookAtKiller( gentity_t *self, gentity_t *inflictor, gentity_t *attacker ) { + vec3_t dir; + vec3_t angles; + + if ( attacker && attacker != self ) { + VectorSubtract (attacker->s.pos.trBase, self->s.pos.trBase, dir); + } else if ( inflictor && inflictor != self ) { + VectorSubtract (inflictor->s.pos.trBase, self->s.pos.trBase, dir); + } else { + self->client->ps.stats[STAT_DEAD_YAW] = self->s.angles[YAW]; + return; + } + + self->client->ps.stats[STAT_DEAD_YAW] = vectoyaw ( dir ); + + angles[YAW] = vectoyaw ( dir ); + angles[PITCH] = 0; + angles[ROLL] = 0; +} + +/* +================== +GibEntity +================== +*/ +void GibEntity( gentity_t *self, int killer ) { + G_AddEvent( self, EV_GIB_PLAYER, killer ); + self->takedamage = qfalse; + self->s.eType = ET_INVISIBLE; + self->r.contents = 0; +} + +/* +================== +body_die +================== +*/ +void body_die( gentity_t *self, gentity_t *inflictor, gentity_t *attacker, int damage, int meansOfDeath ) { + if ( self->health > GIB_HEALTH ) { + return; + } + if ( !g_blood.integer ) { + self->health = GIB_HEALTH+1; + return; + } + + GibEntity( self, 0 ); +} + + +// these are just for logging, the client prints its own messages +char *modNames[] = { + "MOD_UNKNOWN", + "MOD_SHOTGUN", + "MOD_GAUNTLET", + "MOD_MACHINEGUN", + "MOD_CHAINGUN", + "MOD_GRENADE", + "MOD_GRENADE_SPLASH", + "MOD_ROCKET", + "MOD_ROCKET_SPLASH", + "MOD_FLAMER", + "MOD_FLAMER_SPLASH", + "MOD_RAILGUN", + "MOD_LIGHTNING", + "MOD_BFG", + "MOD_BFG_SPLASH", + "MOD_WATER", + "MOD_SLIME", + "MOD_LAVA", + "MOD_CRUSH", + "MOD_TELEFRAG", + "MOD_FALLING", + "MOD_SUICIDE", + "MOD_TARGET_LASER", + "MOD_TRIGGER_HURT", + "MOD_GRAPPLE", + "MOD_VENOM", + "MOD_HSPAWN", + "MOD_ASPAWN" +}; + + +/* +================== +CheckAlmostCapture +================== +*/ +void CheckAlmostCapture( gentity_t *self, gentity_t *attacker ) { + gentity_t *ent; + vec3_t dir; + char *classname; + + // if this player was carrying a flag + /*if ( self->client->ps.powerups[PW_REDFLAG] || + self->client->ps.powerups[PW_BLUEFLAG] || + self->client->ps.powerups[PW_NEUTRALFLAG] ) { + // get the goal flag this player should have been going for + if ( g_gametype.integer == GT_CTF ) { + if ( self->client->sess.sessionTeam == TEAM_DROIDS ) { + classname = "team_CTF_blueflag"; + } + else { + classname = "team_CTF_redflag"; + } + } + else { + if ( self->client->sess.sessionTeam == TEAM_DROIDS ) { + classname = "team_CTF_redflag"; + } + else { + classname = "team_CTF_blueflag"; + } + } + ent = NULL; + do + { + ent = G_Find(ent, FOFS(classname), classname); + } while (ent && (ent->flags & FL_DROPPED_ITEM)); + // if we found the destination flag and it's not picked up + if (ent && !(ent->r.svFlags & SVF_NOCLIENT) ) { + // if the player was *very* close + VectorSubtract( self->client->ps.origin, ent->s.origin, dir ); + if ( VectorLength(dir) < 200 ) { + self->client->ps.persistant[PERS_PLAYEREVENTS] ^= PLAYEREVENT_HOLYSHIT; + if ( attacker->client ) { + attacker->client->ps.persistant[PERS_PLAYEREVENTS] ^= PLAYEREVENT_HOLYSHIT; + } + } + } + }*/ +} + +/* +================== +CheckAlmostScored +================== +*/ +void CheckAlmostScored( gentity_t *self, gentity_t *attacker ) { + /*gentity_t *ent; + vec3_t dir; + char *classname; + + // if the player was carrying cubes + if ( self->client->ps.generic1 ) { + if ( self->client->sess.sessionTeam == TEAM_DROIDS ) { + classname = "team_redobelisk"; + } + else { + classname = "team_blueobelisk"; + } + ent = G_Find(NULL, FOFS(classname), classname); + // if we found the destination obelisk + if ( ent ) { + // if the player was *very* close + VectorSubtract( self->client->ps.origin, ent->s.origin, dir ); + if ( VectorLength(dir) < 200 ) { + self->client->ps.persistant[PERS_PLAYEREVENTS] ^= PLAYEREVENT_HOLYSHIT; + if ( attacker->client ) { + attacker->client->ps.persistant[PERS_PLAYEREVENTS] ^= PLAYEREVENT_HOLYSHIT; + } + } + } + }*/ +} + + +/* +================== +player_die +================== +*/ +void player_die( gentity_t *self, gentity_t *inflictor, gentity_t *attacker, int damage, int meansOfDeath ) { + gentity_t *ent; + int anim; + int contents; + int killer; + int i; + char *killerName, *obit; + + if ( self->client->ps.pm_type == PM_DEAD ) { + return; + } + + if ( level.intermissiontime ) { + return; + } + + //TA: prolly dont need this + // check for an almost capture + //CheckAlmostCapture( self, attacker ); + // check for a player that almost brought in cubes + //CheckAlmostScored( self, attacker ); + + if (self->client && self->client->hook) + Weapon_HookFree(self->client->hook); + + self->client->ps.pm_type = PM_DEAD; + + if ( attacker ) { + killer = attacker->s.number; + if ( attacker->client ) { + killerName = attacker->client->pers.netname; + } else { + killerName = "<non-client>"; + } + } else { + killer = ENTITYNUM_WORLD; + killerName = "<world>"; + } + + if ( killer < 0 || killer >= MAX_CLIENTS ) { + killer = ENTITYNUM_WORLD; + killerName = "<world>"; + } + + if ( meansOfDeath < 0 || meansOfDeath >= sizeof( modNames ) / sizeof( modNames[0] ) ) { + obit = "<bad obituary>"; + } else { + obit = modNames[ meansOfDeath ]; + } + + G_LogPrintf("Kill: %i %i %i: %s killed %s by %s\n", + killer, self->s.number, meansOfDeath, killerName, + self->client->pers.netname, obit ); + + // broadcast the death event to everyone + ent = G_TempEntity( self->r.currentOrigin, EV_OBITUARY ); + ent->s.eventParm = meansOfDeath; + ent->s.otherEntityNum = self->s.number; + ent->s.otherEntityNum2 = killer; + ent->r.svFlags = SVF_BROADCAST; // send to everyone + + self->enemy = attacker; + + self->client->ps.persistant[PERS_KILLED]++; + + if (attacker && attacker->client) { + attacker->client->lastkilled_client = self->s.number; + if ( attacker == self || OnSameTeam (self, attacker ) ) { + AddScore( attacker, -1 ); + AddPoints( attacker, -10 ); + } else { + AddScore( attacker, 1 ); + AddPoints( attacker, CalculatePoints( self, attacker ) ); + + //TA: disable rewards + /*if( meansOfDeath == MOD_GAUNTLET ) { + attacker->client->ps.persistant[PERS_GAUNTLET_FRAG_COUNT]++; + attacker->client->ps.persistant[PERS_REWARD] = REWARD_GAUNTLET; + attacker->client->ps.persistant[PERS_REWARD_COUNT]++; + + // add the sprite over the player's head + attacker->client->ps.eFlags &= ~(EF_AWARD_IMPRESSIVE | EF_AWARD_EXCELLENT | EF_AWARD_GAUNTLET ); + attacker->client->ps.eFlags |= EF_AWARD_GAUNTLET; + attacker->client->rewardTime = level.time + REWARD_SPRITE_TIME; + + // also play humiliation on target + self->client->ps.persistant[PERS_REWARD] = REWARD_GAUNTLET; + self->client->ps.persistant[PERS_REWARD_COUNT]++; + } + + // check for two kills in a short amount of time + // if this is close enough to the last kill, give a reward sound + if ( level.time - attacker->client->lastKillTime < CARNAGE_REWARD_TIME ) { + attacker->client->ps.persistant[PERS_REWARD_COUNT]++; + attacker->client->ps.persistant[PERS_REWARD] = REWARD_EXCELLENT; + attacker->client->ps.persistant[PERS_EXCELLENT_COUNT]++; + + // add the sprite over the player's head + attacker->client->ps.eFlags &= ~(EF_AWARD_IMPRESSIVE | EF_AWARD_EXCELLENT | EF_AWARD_GAUNTLET ); + attacker->client->ps.eFlags |= EF_AWARD_EXCELLENT; + attacker->client->rewardTime = level.time + REWARD_SPRITE_TIME; + }*/ + attacker->client->lastKillTime = level.time; + + } + } else { + AddScore( self, -1 ); + AddPoints( self, -10 ); + } + + // Add team bonuses + Team_FragBonuses(self, inflictor, attacker); + + // if client is in a nodrop area, don't drop anything (but return CTF flags!) + contents = trap_PointContents( self->r.currentOrigin, -1 ); + if ( !( contents & CONTENTS_NODROP ) ) + //TossClientItems( self ); + + Cmd_Score_f( self ); // show scores + // send updated scores to any clients that are following this one, + // or they would get stale scoreboards + for ( i = 0 ; i < level.maxclients ; i++ ) { + gclient_t *client; + + client = &level.clients[i]; + if ( client->pers.connected != CON_CONNECTED ) { + continue; + } + if ( client->sess.sessionTeam != TEAM_SPECTATOR ) { + continue; + } + if ( client->sess.spectatorClient == self->s.number ) { + Cmd_Score_f( g_entities + i ); + } + } + + self->client->pers.pclass = 0; //TA: reset the classtype + + self->takedamage = qtrue; // can still be gibbed + + self->s.weapon = WP_NONE; + self->s.powerups = 0; + self->r.contents = CONTENTS_CORPSE; + + self->s.angles[0] = 0; + self->s.angles[2] = 0; + LookAtKiller (self, inflictor, attacker); + + VectorCopy( self->s.angles, self->client->ps.viewangles ); + + self->s.loopSound = 0; + + self->r.maxs[2] = -8; + + // don't allow respawn until the death anim is done + // g_forcerespawn may force spawning at some later time + self->client->respawnTime = level.time + 1700; + + // remove powerups + memset( self->client->ps.powerups, 0, sizeof(self->client->ps.powerups) ); + + // never gib in a nodrop + if ( self->health <= GIB_HEALTH && !(contents & CONTENTS_NODROP) && g_blood.integer ) { + // gib death + GibEntity( self, killer ); + } else { + // normal death + static int i; + + switch ( i ) { + case 0: + anim = BOTH_DEATH1; + break; + case 1: + anim = BOTH_DEATH2; + break; + case 2: + default: + anim = BOTH_DEATH3; + break; + } + + // for the no-blood option, we need to prevent the health + // from going to gib level + if ( self->health <= GIB_HEALTH ) { + self->health = GIB_HEALTH+1; + } + + self->client->ps.legsAnim = + ( ( self->client->ps.legsAnim & ANIM_TOGGLEBIT ) ^ ANIM_TOGGLEBIT ) | anim; + self->client->ps.torsoAnim = + ( ( self->client->ps.torsoAnim & ANIM_TOGGLEBIT ) ^ ANIM_TOGGLEBIT ) | anim; + + G_AddEvent( self, EV_DEATH1 + i, killer ); + + // the body can still be gibbed + self->die = body_die; + + // globally cycle through the different death animations + i = ( i + 1 ) % 3; + } + + trap_LinkEntity (self); +} + + +/* +================ +CheckArmor +================ +*/ +int CheckArmor (gentity_t *ent, int damage, int dflags) +{ + gclient_t *client; + int save; + int count; + + if (!damage) + return 0; + + client = ent->client; + + if (!client) + return 0; + + if (dflags & DAMAGE_NO_ARMOR) + return 0; + + // armor + count = client->ps.stats[STAT_ARMOR]; + save = ceil( damage * ARMOR_PROTECTION ); + if (save >= count) + save = count; + + if (!save) + return 0; + + client->ps.stats[STAT_ARMOR] -= save; + + return save; +} + + +/* +================ +RaySphereIntersections +================ +*/ +int RaySphereIntersections( vec3_t origin, float radius, vec3_t point, vec3_t dir, vec3_t intersections[2] ) { + float b, c, d, t; + + // | origin - (point + t * dir) | = radius + // a = dir[0]^2 + dir[1]^2 + dir[2]^2; + // b = 2 * (dir[0] * (point[0] - origin[0]) + dir[1] * (point[1] - origin[1]) + dir[2] * (point[2] - origin[2])); + // c = (point[0] - origin[0])^2 + (point[1] - origin[1])^2 + (point[2] - origin[2])^2 - radius^2; + + // normalize dir so a = 1 + VectorNormalize(dir); + b = 2 * (dir[0] * (point[0] - origin[0]) + dir[1] * (point[1] - origin[1]) + dir[2] * (point[2] - origin[2])); + c = (point[0] - origin[0]) * (point[0] - origin[0]) + + (point[1] - origin[1]) * (point[1] - origin[1]) + + (point[2] - origin[2]) * (point[2] - origin[2]) - + radius * radius; + + d = b * b - 4 * c; + if (d > 0) { + t = (- b + sqrt(d)) / 2; + VectorMA(point, t, dir, intersections[0]); + t = (- b - sqrt(d)) / 2; + VectorMA(point, t, dir, intersections[1]); + return 2; + } + else if (d == 0) { + t = (- b ) / 2; + VectorMA(point, t, dir, intersections[0]); + return 1; + } + return 0; +} + + +/* +============ +T_Damage + +targ entity that is being damaged +inflictor entity that is causing the damage +attacker entity that caused the inflictor to damage targ + example: targ=monster, inflictor=rocket, attacker=player + +dir direction of the attack for knockback +point point at which the damage is being inflicted, used for headshots +damage amount of damage being inflicted +knockback force to be applied against targ as a result of the damage + +inflictor, attacker, dir, and point can be NULL for environmental effects + +dflags these flags are used to control how T_Damage works + DAMAGE_RADIUS damage was indirect (from a nearby explosion) + DAMAGE_NO_ARMOR armor does not protect from this damage + DAMAGE_NO_KNOCKBACK do not affect velocity, just view angles + DAMAGE_NO_PROTECTION kills godmode, armor, everything +============ +*/ + +//TA: team is the team that is immune to this damage +void G_SelectiveDamage( gentity_t *targ, gentity_t *inflictor, gentity_t *attacker, + vec3_t dir, vec3_t point, int damage, int dflags, int mod, int team ) +{ + if( targ->client && ( team != targ->client->pers.pteam ) ) + G_Damage( targ, inflictor, attacker, dir, point, damage, dflags, mod ); +} + +void G_Damage( gentity_t *targ, gentity_t *inflictor, gentity_t *attacker, + vec3_t dir, vec3_t point, int damage, int dflags, int mod ) { + gclient_t *client; + int take; + int save; + int asave; + int knockback; + int max; + + if (!targ->takedamage) { + return; + } + + // the intermission has allready been qualified for, so don't + // allow any extra scoring + if ( level.intermissionQueued ) { + return; + } + + if ( !inflictor ) { + inflictor = &g_entities[ENTITYNUM_WORLD]; + } + if ( !attacker ) { + attacker = &g_entities[ENTITYNUM_WORLD]; + } + + // shootable doors / buttons don't actually have any health + if ( targ->s.eType == ET_MOVER ) { + if ( targ->use && targ->moverState == MOVER_POS1 ) { + targ->use( targ, inflictor, attacker ); + } + return; + } + + //TA: handicaps.. WTF is all that about? If someone is shit they deserve to die. + // reduce damage by the attacker's handicap value + // unless they are rocket jumping + /*if ( attacker->client && attacker != targ ) { + damage = damage * attacker->client->ps.stats[STAT_MAX_HEALTH] / 100; + }*/ + + client = targ->client; + + if ( client ) { + if ( client->noclip ) { + return; + } + } + + if ( !dir ) { + dflags |= DAMAGE_NO_KNOCKBACK; + } else { + VectorNormalize(dir); + } + + knockback = damage; + if ( knockback > 200 ) { + knockback = 200; + } + if ( targ->flags & FL_NO_KNOCKBACK ) { + knockback = 0; + } + if ( dflags & DAMAGE_NO_KNOCKBACK ) { + knockback = 0; + } + + // figure momentum add, even if the damage won't be taken + if ( knockback && targ->client ) { + vec3_t kvel; + float mass; + + mass = 200; + + VectorScale (dir, g_knockback.value * (float)knockback / mass, kvel); + VectorAdd (targ->client->ps.velocity, kvel, targ->client->ps.velocity); + + // set the timer so that the other client can't cancel + // out the movement immediately + if ( !targ->client->ps.pm_time ) { + int t; + + t = knockback * 2; + if ( t < 50 ) { + t = 50; + } + if ( t > 200 ) { + t = 200; + } + targ->client->ps.pm_time = t; + targ->client->ps.pm_flags |= PMF_TIME_KNOCKBACK; + } + } + + // check for completely getting out of the damage + if ( !(dflags & DAMAGE_NO_PROTECTION) ) { + + // if TF_NO_FRIENDLY_FIRE is set, don't do damage to the target + // if the attacker was on the same team + if ( targ != attacker && OnSameTeam (targ, attacker) ) { + if ( !g_friendlyFire.integer ) { + return; + } + } + + // check for godmode + if ( targ->flags & FL_GODMODE ) { + return; + } + } + + // battlesuit protects from all radius damage (but takes knockback) + // and protects 50% against all damage + /*if ( client && client->ps.powerups[PW_BATTLESUIT] ) { + G_AddEvent( targ, EV_POWERUP_BATTLESUIT, 0 ); + if ( ( dflags & DAMAGE_RADIUS ) || ( mod == MOD_FALLING ) ) { + return; + } + damage *= 0.5; + }*/ + + // add to the attacker's hit counter + if ( attacker->client && targ != attacker && targ->health > 0 + && targ->s.eType != ET_MISSILE + && targ->s.eType != ET_GENERAL) { + if ( OnSameTeam( targ, attacker ) ) { + attacker->client->ps.persistant[PERS_HITS] --; + } else { + attacker->client->ps.persistant[PERS_HITS] ++; + } + } + + // always give half damage if hurting self + // calculated after knockback, so rocket jumping works + if ( targ == attacker) { + damage *= 0.5; + } + + if ( damage < 1 ) { + damage = 1; + } + take = damage; + save = 0; + + // save some from armor + asave = CheckArmor (targ, take, dflags); + take -= asave; + + if ( g_debugDamage.integer ) { + G_Printf( "%i: client:%i health:%i damage:%i armor:%i\n", level.time, targ->s.number, + targ->health, take, asave ); + } + + // add to the damage inflicted on a player this frame + // the total will be turned into screen blends and view angle kicks + // at the end of the frame + if ( client ) { + if ( attacker ) { + client->ps.persistant[PERS_ATTACKER] = attacker->s.number; + } else { + client->ps.persistant[PERS_ATTACKER] = ENTITYNUM_WORLD; + } + client->damage_armor += asave; + client->damage_blood += take; + client->damage_knockback += knockback; + if ( dir ) { + VectorCopy ( dir, client->damage_from ); + client->damage_fromWorld = qfalse; + } else { + VectorCopy ( targ->r.currentOrigin, client->damage_from ); + client->damage_fromWorld = qtrue; + } + } + + // See if it's the player hurting the emeny flag carrier + if( g_gametype.integer == GT_CTF) { + Team_CheckHurtCarrier(targ, attacker); + } + + if (targ->client) { + // set the last client who damaged the target + targ->client->lasthurt_client = attacker->s.number; + targ->client->lasthurt_mod = mod; + } + + // do the damage + if (take) { + targ->health = targ->health - take; + if ( targ->client ) { + targ->client->ps.stats[STAT_HEALTH] = targ->health; + } + + if ( targ->health <= 0 ) { + if ( client ) + targ->flags |= FL_NO_KNOCKBACK; + + if (targ->health < -999) + targ->health = -999; + + targ->enemy = attacker; + targ->die (targ, inflictor, attacker, take, mod); + return; + } else if ( targ->pain ) { + targ->pain (targ, attacker, take); + } + } + +} + + +/* +============ +CanDamage + +Returns qtrue if the inflictor can directly damage the target. Used for +explosions and melee attacks. +============ +*/ +qboolean CanDamage (gentity_t *targ, vec3_t origin) { + vec3_t dest; + trace_t tr; + vec3_t midpoint; + + // use the midpoint of the bounds instead of the origin, because + // bmodels may have their origin is 0,0,0 + VectorAdd (targ->r.absmin, targ->r.absmax, midpoint); + VectorScale (midpoint, 0.5, midpoint); + + VectorCopy (midpoint, dest); + trap_Trace ( &tr, origin, vec3_origin, vec3_origin, dest, ENTITYNUM_NONE, MASK_SOLID); + if (tr.fraction == 1.0 || tr.entityNum == targ->s.number) + return qtrue; + + // this should probably check in the plane of projection, + // rather than in world coordinate, and also include Z + VectorCopy (midpoint, dest); + dest[0] += 15.0; + dest[1] += 15.0; + trap_Trace ( &tr, origin, vec3_origin, vec3_origin, dest, ENTITYNUM_NONE, MASK_SOLID); + if (tr.fraction == 1.0) + return qtrue; + + VectorCopy (midpoint, dest); + dest[0] += 15.0; + dest[1] -= 15.0; + trap_Trace ( &tr, origin, vec3_origin, vec3_origin, dest, ENTITYNUM_NONE, MASK_SOLID); + if (tr.fraction == 1.0) + return qtrue; + + VectorCopy (midpoint, dest); + dest[0] -= 15.0; + dest[1] += 15.0; + trap_Trace ( &tr, origin, vec3_origin, vec3_origin, dest, ENTITYNUM_NONE, MASK_SOLID); + if (tr.fraction == 1.0) + return qtrue; + + VectorCopy (midpoint, dest); + dest[0] -= 15.0; + dest[1] -= 15.0; + trap_Trace ( &tr, origin, vec3_origin, vec3_origin, dest, ENTITYNUM_NONE, MASK_SOLID); + if (tr.fraction == 1.0) + return qtrue; + + + return qfalse; +} + + +//TA: +/* +============ +G_RadiusDamage +============ +*/ +qboolean G_SelectiveRadiusDamage ( vec3_t origin, gentity_t *attacker, float damage, float radius, + gentity_t *ignore, int mod, int team ) { + float points, dist; + gentity_t *ent; + int entityList[MAX_GENTITIES]; + int numListedEntities; + vec3_t mins, maxs; + vec3_t v; + vec3_t dir; + int i, e; + qboolean hitClient = qfalse; + + if ( radius < 1 ) { + radius = 1; + } + + for ( i = 0 ; i < 3 ; i++ ) { + mins[i] = origin[i] - radius; + maxs[i] = origin[i] + radius; + } + + numListedEntities = trap_EntitiesInBox( mins, maxs, entityList, MAX_GENTITIES ); + + for ( e = 0 ; e < numListedEntities ; e++ ) { + ent = &g_entities[entityList[ e ]]; + + if (ent == ignore) + continue; + if (!ent->takedamage) + continue; + + // find the distance from the edge of the bounding box + for ( i = 0 ; i < 3 ; i++ ) { + if ( origin[i] < ent->r.absmin[i] ) { + v[i] = ent->r.absmin[i] - origin[i]; + } else if ( origin[i] > ent->r.absmax[i] ) { + v[i] = origin[i] - ent->r.absmax[i]; + } else { + v[i] = 0; + } + } + + dist = VectorLength( v ); + if ( dist >= radius ) { + continue; + } + + points = damage * ( 1.0 - dist / radius ); + + if( CanDamage (ent, origin) ) { + if( LogAccuracyHit( ent, attacker ) ) { + hitClient = qtrue; + } + VectorSubtract (ent->r.currentOrigin, origin, dir); + // push the center of mass higher than the origin so players + // get knocked into the air more + dir[2] += 24; + G_SelectiveDamage (ent, NULL, attacker, dir, origin, (int)points, DAMAGE_RADIUS, mod, team ); + } + } + + return hitClient; +} + + +/* +============ +G_RadiusDamage +============ +*/ +qboolean G_RadiusDamage ( vec3_t origin, gentity_t *attacker, float damage, float radius, + gentity_t *ignore, int mod) { + float points, dist; + gentity_t *ent; + int entityList[MAX_GENTITIES]; + int numListedEntities; + vec3_t mins, maxs; + vec3_t v; + vec3_t dir; + int i, e; + qboolean hitClient = qfalse; + + if ( radius < 1 ) { + radius = 1; + } + + for ( i = 0 ; i < 3 ; i++ ) { + mins[i] = origin[i] - radius; + maxs[i] = origin[i] + radius; + } + + numListedEntities = trap_EntitiesInBox( mins, maxs, entityList, MAX_GENTITIES ); + + for ( e = 0 ; e < numListedEntities ; e++ ) { + ent = &g_entities[entityList[ e ]]; + + if (ent == ignore) + continue; + if (!ent->takedamage) + continue; + + // find the distance from the edge of the bounding box + for ( i = 0 ; i < 3 ; i++ ) { + if ( origin[i] < ent->r.absmin[i] ) { + v[i] = ent->r.absmin[i] - origin[i]; + } else if ( origin[i] > ent->r.absmax[i] ) { + v[i] = origin[i] - ent->r.absmax[i]; + } else { + v[i] = 0; + } + } + + dist = VectorLength( v ); + if ( dist >= radius ) { + continue; + } + + points = damage * ( 1.0 - dist / radius ); + + if( CanDamage (ent, origin) ) { + if( LogAccuracyHit( ent, attacker ) ) { + hitClient = qtrue; + } + VectorSubtract (ent->r.currentOrigin, origin, dir); + // push the center of mass higher than the origin so players + // get knocked into the air more + dir[2] += 24; + G_Damage (ent, NULL, attacker, dir, origin, (int)points, DAMAGE_RADIUS, mod); + } + } + + return hitClient; +} diff --git a/src/game/g_local.h b/src/game/g_local.h new file mode 100644 index 00000000..c6a41345 --- /dev/null +++ b/src/game/g_local.h @@ -0,0 +1,977 @@ +// Copyright (C) 1999-2000 Id Software, Inc. +// +// g_local.h -- local definitions for game module + +/* + * Portions Copyright (C) 2000-2001 Tim Angus + * + * This program 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, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ + +/* To assertain which portions are licensed under the GPL and which are + * licensed by Id Software, Inc. please run a diff between the equivalent + * versions of the "Tremulous" modification and the unmodified "Quake3" + * game source code. + */ + +#include "q_shared.h" +#include "bg_public.h" +#include "g_public.h" + +//================================================================== + +// the "gameversion" client command will print this plus compile date +#define GAMEVERSION "baseq3" + +#define BODY_QUEUE_SIZE 8 + +#define INFINITE 1000000 + +#define FRAMETIME 100 // msec +#define EVENT_VALID_MSEC 300 +#define CARNAGE_REWARD_TIME 3000 +#define REWARD_SPRITE_TIME 2000 + +#define INTERMISSION_DELAY_TIME 1000 +#define SP_INTERMISSION_DELAY_TIME 5000 + +// gentity->flags +#define FL_GODMODE 0x00000010 +#define FL_NOTARGET 0x00000020 +#define FL_TEAMSLAVE 0x00000400 // not the first on the team +#define FL_NO_KNOCKBACK 0x00000800 +#define FL_DROPPED_ITEM 0x00001000 +#define FL_NO_BOTS 0x00002000 // spawn point not for bot use +#define FL_NO_HUMANS 0x00004000 // spawn point just for bots +#define FL_FORCE_GESTURE 0x00008000 // spawn point just for bots + +// movers are things like doors, plats, buttons, etc +typedef enum { + MOVER_POS1, + MOVER_POS2, + MOVER_1TO2, + MOVER_2TO1 +} moverState_t; + +#define SP_PODIUM_MODEL "models/mapobjects/podium/podium4.md3" + +//============================================================================ + +typedef struct gentity_s gentity_t; +typedef struct gclient_s gclient_t; + +struct gentity_s { + entityState_t s; // communicated by server to clients + entityShared_t r; // shared by both the server system and game + + // DO NOT MODIFY ANYTHING ABOVE THIS, THE SERVER + // EXPECTS THE FIELDS IN THAT ORDER! + //================================ + + struct gclient_s *client; // NULL if not a client + + qboolean inuse; + + char *classname; // set in QuakeEd + int spawnflags; // set in QuakeEd + + qboolean neverFree; // if true, FreeEntity will only unlink + // bodyque uses this + + int flags; // FL_* variables + + char *model; + char *model2; + int freetime; // level.time when the object was freed + + int eventTime; // events will be cleared EVENT_VALID_MSEC after set + qboolean freeAfterEvent; + qboolean unlinkAfterEvent; + + qboolean physicsObject; // if true, it can be pushed by movers and fall off edges + // all game items are physicsObjects, + float physicsBounce; // 1.0 = continuous bounce, 0.0 = no bounce + int clipmask; // brushes with this content value will be collided against + // when moving. items and corpses do not collide against + // players, for instance + + // movers + moverState_t moverState; + int soundPos1; + int sound1to2; + int sound2to1; + int soundPos2; + int soundLoop; + gentity_t *parent; + gentity_t *nextTrain; + gentity_t *prevTrain; + vec3_t pos1, pos2; + + char *message; + + int timestamp; // body queue sinking, etc + + float angle; // set in editor, -1 = up, -2 = down + char *target; + char *targetname; + char *team; + char *targetShaderName; + char *targetShaderNewName; + gentity_t *target_ent; + + float speed; + vec3_t movedir; + + int nextthink; + void (*think)(gentity_t *self); + void (*reached)(gentity_t *self); // movers call this when hitting endpoint + void (*blocked)(gentity_t *self, gentity_t *other); + void (*touch)(gentity_t *self, gentity_t *other, trace_t *trace); + void (*use)(gentity_t *self, gentity_t *other, gentity_t *activator); + void (*pain)(gentity_t *self, gentity_t *attacker, int damage); + void (*die)(gentity_t *self, gentity_t *inflictor, gentity_t *attacker, int damage, int mod); + + int pain_debounce_time; + int fly_sound_debounce_time; // wind tunnel + int last_move_time; + + int health; + + qboolean takedamage; + + int damage; + int splashDamage; // quad will increase this without increasing radius + int splashRadius; + int methodOfDeath; + int splashMethodOfDeath; + + int count; + + gentity_t *chain; + gentity_t *enemy; + gentity_t *activator; + gentity_t *teamchain; // next entity in team + gentity_t *teammaster; // master of the team + + int watertype; + int waterlevel; + + int noise_index; + + // timing variables + float wait; + float random; + + gitem_t *item; // for bonus items + + int biteam; //TA: buildable item team + gentity_t *parentNode; //TA: for creep and defence/spawn dependencies + vec3_t turloc; //TA: direction human defense turrets are pointing +}; + +typedef enum { + CON_DISCONNECTED, + CON_CONNECTING, + CON_CONNECTED +} clientConnected_t; + +typedef enum { + SPECTATOR_NOT, + SPECTATOR_FREE, + SPECTATOR_LOCKED, + SPECTATOR_FOLLOW, + SPECTATOR_SCOREBOARD +} spectatorState_t; + +typedef enum { + TEAM_BEGIN, // Beginning a team game, spawn at base + TEAM_ACTIVE // Now actively playing +} playerTeamStateState_t; + +typedef struct { + playerTeamStateState_t state; + + int location; + + int captures; + int basedefense; + int carrierdefense; + int flagrecovery; + int fragcarrier; + int assists; + + float lasthurtcarrier; + float lastreturnedflag; + float flagsince; + float lastfraggedcarrier; +} playerTeamState_t; + +// the auto following clients don't follow a specific client +// number, but instead follow the first two active players +#define FOLLOW_ACTIVE1 -1 +#define FOLLOW_ACTIVE2 -2 + +// client data that stays across multiple levels or tournament restarts +// this is achieved by writing all the data to cvar strings at game shutdown +// time and reading them back at connection time. Anything added here +// MUST be dealt with in G_InitSessionData() / G_ReadSessionData() / G_WriteSessionData() +typedef struct { + team_t sessionTeam; + int spectatorTime; // for determining next-in-line to play + spectatorState_t spectatorState; + int spectatorClient; // for chasecam and follow mode + int wins, losses; // tournament stats + qboolean teamLeader; // true when this client is a team leader +} clientSession_t; + +#define MAX_NETNAME 36 +#define MAX_VOTE_COUNT 3 + +// client data that stays across multiple respawns, but is cleared +// on each level change or team change at ClientBegin() +typedef struct { + clientConnected_t connected; + usercmd_t cmd; // we would lose angles if not persistant + qboolean localClient; // true if "ip" info key is "localhost" + qboolean initialSpawn; // the first spawn should be at a cool location + qboolean predictItemPickup; // based on cg_predictItems userinfo + qboolean pmoveFixed; // + char netname[ MAX_NETNAME ]; + int maxHealth; // for handicapping + int enterTime; // level.time the client entered the game + playerTeamState_t teamState; // status in teamplay games + int voteCount; // to prevent people from constantly calling votes + int teamVoteCount; // to prevent people from constantly calling votes + qboolean teamInfo; // send team overlay updates? + + int pclass; //TA: player class (copied to ent->client->ps.stats[ STAT_PCLASS ] once spawned) + //not really persistant.. this is just a nice place to stick it :) + int pteam; //TA: player team (team deathmatch is too complex to alter) (copied to ps.stats[ STAT_PTEAM ]) +} clientPersistant_t; + +// this structure is cleared on each ClientSpawn(), +// except for 'client->pers' and 'client->sess' +struct gclient_s { + // ps MUST be the first element, because the server expects it + playerState_t ps; // communicated by server to clients + + // the rest of the structure is private to game + clientPersistant_t pers; + clientSession_t sess; + + qboolean readyToExit; // wishes to leave the intermission + + qboolean noclip; + + int lastCmdTime; // level.time of last usercmd_t, for EF_CONNECTION + // we can't just use pers.lastCommand.time, because + // of the g_sycronousclients case + int buttons; + int oldbuttons; + int latched_buttons; + + vec3_t oldOrigin; + + // sum up damage over an entire frame, so + // shotgun blasts give a single big kick + int damage_armor; // damage absorbed by armor + int damage_blood; // damage taken out of health + int damage_knockback; // impact damage + vec3_t damage_from; // origin for vector calculation + qboolean damage_fromWorld; // if true, don't use the damage_from vector + + int accurateCount; // for "impressive" reward sound + int accuracy_shots; // total number of shots + int accuracy_hits; // total number of hits + // + int lastkilled_client; // last client that this client killed + int lasthurt_client; // last client that damaged this client + int lasthurt_mod; // type of damage the client did + + // timers + int respawnTime; // can respawn when time > this, force after g_forcerespwan + int inactivityTime; // kick players when time > this + qboolean inactivityWarning; // qtrue if the five seoond warning has been given + int rewardTime; // clear the EF_AWARD_IMPRESSIVE, etc when time > this + + int airOutTime; + + int lastKillTime; // for multiple kill rewards + + qboolean fireHeld; // used for hook + gentity_t *hook; // grapple hook if out + + int switchTeamTime; // time the player switched teams + + gentity_t *torch; //TA: torch entity ( NULL if switched off ) + + // timeResidual is used to handle events that happen every second + // like health / armor countdowns and regeneration + int timeResidual; + + char *areabits; + + float classSpeed; //TA: here to save STAT_ space +}; + +// +// this structure is cleared as each map is entered +// +#define MAX_SPAWN_VARS 64 +#define MAX_SPAWN_VARS_CHARS 2048 + +typedef struct { + struct gclient_s *clients; // [maxclients] + + struct gentity_s *gentities; + int gentitySize; + int num_entities; // current number, <= MAX_GENTITIES + + int warmupTime; // restart match at this time + + fileHandle_t logFile; + + // store latched cvars here that we want to get at often + int maxclients; + + int framenum; + int time; // in msec + int previousTime; // so movers can back up when blocked + + int startTime; // level.time the map was started + + int teamScores[TEAM_NUM_TEAMS]; + int lastTeamLocationTime; // last time of client team location update + + qboolean newSession; // don't use any old session data, because + // we changed gametype + + qboolean restarted; // waiting for a map_restart to fire + + int numConnectedClients; + int numNonSpectatorClients; // includes connecting clients + int numPlayingClients; // connected, non-spectators + int sortedClients[MAX_CLIENTS]; // sorted by score + int follow1, follow2; // clientNums for auto-follow spectators + + int snd_fry; // sound index for standing in lava + + int warmupModificationCount; // for detecting if g_warmup is changed + + // voting state + char voteString[MAX_STRING_CHARS]; + char voteDisplayString[MAX_STRING_CHARS]; + int voteTime; // level.time vote was called + int voteExecuteTime; // time the vote is executed + int voteYes; + int voteNo; + int numVotingClients; // set by CalculateRanks + + // team voting state + char teamVoteString[2][MAX_STRING_CHARS]; + int teamVoteTime[2]; // level.time vote was called + int teamVoteYes[2]; + int teamVoteNo[2]; + int numteamVotingClients[2];// set by CalculateRanks + + // spawn variables + qboolean spawning; // the G_Spawn*() functions are valid + int numSpawnVars; + char *spawnVars[MAX_SPAWN_VARS][2]; // key / value pairs + int numSpawnVarChars; + char spawnVarChars[MAX_SPAWN_VARS_CHARS]; + + // intermission state + int intermissionQueued; // intermission was qualified, but + // wait INTERMISSION_DELAY_TIME before + // actually going there so the last + // frag can be watched. Disable future + // kills during this delay + int intermissiontime; // time the intermission was started + char *changemap; + qboolean readyToExit; // at least one client wants to exit + int exitTime; + vec3_t intermission_origin; // also used for spectator spawns + vec3_t intermission_angle; + + qboolean locationLinked; // target_locations get linked + gentity_t *locationHead; // head of the location list + int bodyQueIndex; // dead bodies + gentity_t *bodyQue[BODY_QUEUE_SIZE]; + + //TA: extra stuff: + int numDroidSpawns; + int numHumanSpawns; + int numDroidClients; + int numHumanClients; + int numLiveDroidClients; + int numLiveHumanClients; + int droidBuildPoints; + int humanBuildPoints; +} level_locals_t; + + +// +// g_spawn.c +// +qboolean G_SpawnString( const char *key, const char *defaultString, char **out ); +// spawn string returns a temporary reference, you must CopyString() if you want to keep it +qboolean G_SpawnFloat( const char *key, const char *defaultString, float *out ); +qboolean G_SpawnInt( const char *key, const char *defaultString, int *out ); +qboolean G_SpawnVector( const char *key, const char *defaultString, float *out ); +void G_SpawnEntitiesFromString( void ); +char *G_NewString( const char *string ); + +// +// g_cmds.c +// +void Cmd_Score_f (gentity_t *ent); +void StopFollowing( gentity_t *ent ); +void BroadcastTeamChange( gclient_t *client, int oldTeam ); +void SetTeam( gentity_t *ent, char *s ); +void Cmd_FollowCycle_f( gentity_t *ent, int dir ); + +// +// g_items.c +// +void G_CheckTeamItems( void ); +void G_RunItem( gentity_t *ent ); +void RespawnItem( gentity_t *ent ); + +void UseHoldableItem( gentity_t *ent ); +void PrecacheItem (gitem_t *it); +gentity_t *Drop_Item( gentity_t *ent, gitem_t *item, float angle ); +gentity_t *LaunchItem( gitem_t *item, vec3_t origin, vec3_t velocity ); +void SetRespawn (gentity_t *ent, float delay); +void G_SpawnItem (gentity_t *ent, gitem_t *item); +void FinishSpawningItem( gentity_t *ent ); +void Think_Weapon (gentity_t *ent); +int ArmorIndex (gentity_t *ent); +void Add_Ammo (gentity_t *ent, int weapon, int count); +void Touch_Item (gentity_t *ent, gentity_t *other, trace_t *trace); + +void ClearRegisteredItems( void ); +void RegisterItem( gitem_t *item ); +void SaveRegisteredItems( void ); + +// +// g_buildable.c +// +qboolean itemFits( gentity_t *ent, gitem_t *item, int distance ); +gentity_t *Build_Item( gentity_t *ent, gitem_t *item, int distance ); + +// +// g_creep.c +// +gentity_t *createCreepNode( vec3_t origin ); + +// +// g_utils.c +// +int G_ModelIndex( char *name ); +int G_SoundIndex( char *name ); +void G_TeamCommand( team_t team, char *cmd ); +void G_KillBox (gentity_t *ent); +gentity_t *G_Find (gentity_t *from, int fieldofs, const char *match); +gentity_t *G_PickTarget (char *targetname); +void G_UseTargets (gentity_t *ent, gentity_t *activator); +void G_SetMovedir ( vec3_t angles, vec3_t movedir); + +void G_InitGentity( gentity_t *e ); +gentity_t *G_Spawn (void); +gentity_t *G_TempEntity( vec3_t origin, int event ); +void G_Sound( gentity_t *ent, int channel, int soundIndex ); +void G_FreeEntity( gentity_t *e ); +qboolean G_EntitiesFree( void ); + +void G_TouchTriggers (gentity_t *ent); +void G_TouchSolids (gentity_t *ent); + +float *tv (float x, float y, float z); +char *vtos( const vec3_t v ); + +float vectoyaw( const vec3_t vec ); + +void G_AddPredictableEvent( gentity_t *ent, int event, int eventParm ); +void G_AddEvent( gentity_t *ent, int event, int eventParm ); +void G_SetOrigin( gentity_t *ent, vec3_t origin ); +void AddRemap(const char *oldShader, const char *newShader, float timeOffset); +const char *BuildShaderStateConfig(); + +// +// g_combat.c +// +qboolean CanDamage (gentity_t *targ, vec3_t origin); +void G_Damage (gentity_t *targ, gentity_t *inflictor, gentity_t *attacker, vec3_t dir, vec3_t point, int damage, int dflags, int mod); +void G_SelectiveDamage (gentity_t *targ, gentity_t *inflictor, gentity_t *attacker, vec3_t dir, vec3_t point, int damage, int dflags, int mod, int team); +qboolean G_RadiusDamage (vec3_t origin, gentity_t *attacker, float damage, float radius, gentity_t *ignore, int mod); +qboolean G_SelectiveRadiusDamage (vec3_t origin, gentity_t *attacker, float damage, float radius, gentity_t *ignore, int mod, int team ); +void body_die( gentity_t *self, gentity_t *inflictor, gentity_t *attacker, int damage, int meansOfDeath ); +void TossClientItems( gentity_t *self ); + +// damage flags +#define DAMAGE_RADIUS 0x00000001 // damage was indirect +#define DAMAGE_NO_ARMOR 0x00000002 // armour does not protect from this damage +#define DAMAGE_NO_KNOCKBACK 0x00000004 // do not affect velocity, just view angles +#define DAMAGE_NO_PROTECTION 0x00000008 // armor, shields, invulnerability, and godmode have no effect + +// +// g_missile.c +// +void G_RunMissile( gentity_t *ent ); + +gentity_t *fire_blaster (gentity_t *self, vec3_t start, vec3_t aimdir); +gentity_t *fire_flamer( gentity_t *self, vec3_t start, vec3_t aimdir ); +gentity_t *fire_plasma( gentity_t *self, vec3_t start, vec3_t aimdir ); +gentity_t *fire_grenade (gentity_t *self, vec3_t start, vec3_t aimdir); +gentity_t *fire_rocket (gentity_t *self, vec3_t start, vec3_t dir); +gentity_t *fire_bfg (gentity_t *self, vec3_t start, vec3_t dir); +gentity_t *fire_grapple (gentity_t *self, vec3_t start, vec3_t dir); + + +// +// g_mover.c +// +void G_RunMover( gentity_t *ent ); +void Touch_DoorTrigger( gentity_t *ent, gentity_t *other, trace_t *trace ); + +// +// g_trigger.c +// +void trigger_teleporter_touch (gentity_t *self, gentity_t *other, trace_t *trace ); + + +// +// g_misc.c +// +void TeleportPlayer( gentity_t *player, vec3_t origin, vec3_t angles ); +void ShineTorch( gentity_t *self ); + +// +// g_weapon.c +// +qboolean LogAccuracyHit( gentity_t *target, gentity_t *attacker ); +void CalcMuzzlePoint ( gentity_t *ent, vec3_t forward, vec3_t right, vec3_t up, vec3_t muzzlePoint ); +void SnapVectorTowards( vec3_t v, vec3_t to ); +qboolean CheckGauntletAttack( gentity_t *ent ); +void Weapon_HookFree (gentity_t *ent); +void Weapon_HookThink (gentity_t *ent); + + +// +// g_client.c +// +team_t TeamCount( int ignoreClientNum, int team ); +int TeamLeader( int team ); +team_t PickTeam( int ignoreClientNum ); +void SetClientViewAngle( gentity_t *ent, vec3_t angle ); +gentity_t *SelectSpawnPoint ( vec3_t avoidPoint, vec3_t origin, vec3_t angles ); +void CopyToBodyQue( gentity_t *ent ); +void respawn (gentity_t *ent); +void BeginIntermission (void); +void InitClientPersistant (gclient_t *client); +void InitClientResp (gclient_t *client); +void InitBodyQue (void); +void ClientSpawn( gentity_t *ent ); +void player_die (gentity_t *self, gentity_t *inflictor, gentity_t *attacker, int damage, int mod); +void AddScore( gentity_t *ent, int score ); + +//TA: extra bits +void AddPoints( gentity_t *ent, int score ); +int CalculatePoints( gentity_t *victim, gentity_t *attacker ); + +void CalculateRanks( void ); +qboolean SpotWouldTelefrag( gentity_t *spot ); + +// +// g_svcmds.c +// +qboolean ConsoleCommand( void ); +void G_ProcessIPBans(void); +qboolean G_FilterPacket (char *from); + +// +// g_weapon.c +// +void FireWeapon( gentity_t *ent ); + +// +// p_hud.c +// +void MoveClientToIntermission (gentity_t *client); +void G_SetStats (gentity_t *ent); +void DeathmatchScoreboardMessage (gentity_t *client); + +// +// g_cmds.c +// + +// +// g_pweapon.c +// + + +// +// g_main.c +// +void FindIntermissionPoint( void ); +void SetLeader(int team, int client); +void CheckTeamLeader( int team ); +void G_RunThink (gentity_t *ent); +void QDECL G_LogPrintf( const char *fmt, ... ); +void SendScoreboardMessageToAllClients( void ); +void QDECL G_Printf( const char *fmt, ... ); +void QDECL G_Error( const char *fmt, ... ); + +// +// g_client.c +// +char *ClientConnect( int clientNum, qboolean firstTime, qboolean isBot ); +void ClientUserinfoChanged( int clientNum ); +void ClientDisconnect( int clientNum ); +void ClientBegin( int clientNum ); +void ClientCommand( int clientNum ); + +// +// g_active.c +// +void ClientThink( int clientNum ); +void ClientEndFrame( gentity_t *ent ); +void G_RunClient( gentity_t *ent ); + +// +// g_team.c +// +qboolean OnSameTeam( gentity_t *ent1, gentity_t *ent2 ); +void Team_CheckDroppedItem( gentity_t *dropped ); + + +// +// g_mem.c +// +void *G_Alloc( int size ); +void G_InitMemory( void ); +void Svcmd_GameMem_f( void ); + +// +// g_session.c +// +void G_ReadSessionData( gclient_t *client ); +void G_InitSessionData( gclient_t *client, char *userinfo ); + +void G_InitWorldSession( void ); +void G_WriteSessionData( void ); + +// +// g_arenas.c +// +void UpdateTournamentInfo( void ); +void SpawnModelsOnVictoryPads( void ); +void Svcmd_AbortPodium_f( void ); + +// +// g_bot.c +// +/*void G_InitBots( qboolean restart ); +char *G_GetBotInfoByNumber( int num ); +char *G_GetBotInfoByName( const char *name ); +void G_CheckBotSpawn( void ); +void G_QueueBotBegin( int clientNum ); +qboolean G_BotConnect( int clientNum, qboolean restart ); +void Svcmd_AddBot_f( void ); +void Svcmd_BotList_f( void ); +void BotInterbreedEndMatch( void );*/ + +// ai_main.c + +//some maxs +#define MAX_FILEPATH 144 + +//bot settings +/*typedef struct bot_settings_s +{ + char characterfile[MAX_FILEPATH]; + float skill; + char team[MAX_FILEPATH]; +} bot_settings_t; + +int BotAISetup( int restart ); +int BotAIShutdown( int restart ); +int BotAILoadMap( int restart ); +int BotAISetupClient( int client, bot_settings_t *settings ); +int BotAIShutdownClient( int client ); +int BotAIStartFrame( int time );*/ + + +#include "g_team.h" // teamplay specific stuff + + +extern level_locals_t level; +extern gentity_t g_entities[MAX_GENTITIES]; + +#define FOFS(x) ((int)&(((gentity_t *)0)->x)) + +extern vmCvar_t g_gametype; +extern vmCvar_t g_dedicated; +extern vmCvar_t g_cheats; +extern vmCvar_t g_maxclients; // allow this many total, including spectators +extern vmCvar_t g_maxGameClients; // allow this many active +extern vmCvar_t g_restarted; + +extern vmCvar_t g_dmflags; +extern vmCvar_t g_fraglimit; +extern vmCvar_t g_timelimit; +extern vmCvar_t g_capturelimit; +extern vmCvar_t g_friendlyFire; +extern vmCvar_t g_password; +extern vmCvar_t g_needpass; +extern vmCvar_t g_gravity; +extern vmCvar_t g_speed; +extern vmCvar_t g_knockback; +extern vmCvar_t g_quadfactor; +extern vmCvar_t g_forcerespawn; +extern vmCvar_t g_inactivity; +extern vmCvar_t g_debugMove; +extern vmCvar_t g_debugAlloc; +extern vmCvar_t g_debugDamage; +extern vmCvar_t g_weaponRespawn; +extern vmCvar_t g_weaponTeamRespawn; +extern vmCvar_t g_synchronousClients; +extern vmCvar_t g_motd; +extern vmCvar_t g_warmup; +extern vmCvar_t g_doWarmup; +extern vmCvar_t g_blood; +extern vmCvar_t g_allowVote; +extern vmCvar_t g_teamAutoJoin; +extern vmCvar_t g_teamForceBalance; +extern vmCvar_t g_banIPs; +extern vmCvar_t g_filterBan; +extern vmCvar_t g_smoothClients; +extern vmCvar_t pmove_fixed; +extern vmCvar_t pmove_msec; +extern vmCvar_t g_rankings; +extern vmCvar_t g_enableDust; +extern vmCvar_t g_enableBreath; +extern vmCvar_t g_singlePlayer; + +void trap_Printf( const char *fmt ); +void trap_Error( const char *fmt ); +int trap_Milliseconds( void ); +int trap_Argc( void ); +void trap_Argv( int n, char *buffer, int bufferLength ); +void trap_Args( char *buffer, int bufferLength ); +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 ); +int trap_FS_GetFileList( const char *path, const char *extension, char *listbuf, int bufsize ); +void trap_SendConsoleCommand( int exec_when, const char *text ); +void trap_Cvar_Register( vmCvar_t *cvar, const char *var_name, const char *value, int flags ); +void trap_Cvar_Update( vmCvar_t *cvar ); +void trap_Cvar_Set( const char *var_name, const char *value ); +int trap_Cvar_VariableIntegerValue( const char *var_name ); +float trap_Cvar_VariableValue( const char *var_name ); +void trap_Cvar_VariableStringBuffer( const char *var_name, char *buffer, int bufsize ); +void trap_LocateGameData( gentity_t *gEnts, int numGEntities, int sizeofGEntity_t, playerState_t *gameClients, int sizeofGameClient ); +void trap_DropClient( int clientNum, const char *reason ); +void trap_SendServerCommand( int clientNum, const char *text ); +void trap_SetConfigstring( int num, const char *string ); +void trap_GetConfigstring( int num, char *buffer, int bufferSize ); +void trap_GetUserinfo( int num, char *buffer, int bufferSize ); +void trap_SetUserinfo( int num, const char *buffer ); +void trap_GetServerinfo( char *buffer, int bufferSize ); +void trap_SetBrushModel( gentity_t *ent, const char *name ); +void trap_Trace( trace_t *results, const vec3_t start, const vec3_t mins, const vec3_t maxs, const vec3_t end, int passEntityNum, int contentmask ); +int trap_PointContents( const vec3_t point, int passEntityNum ); +qboolean trap_InPVS( const vec3_t p1, const vec3_t p2 ); +qboolean trap_InPVSIgnorePortals( const vec3_t p1, const vec3_t p2 ); +void trap_AdjustAreaPortalState( gentity_t *ent, qboolean open ); +qboolean trap_AreasConnected( int area1, int area2 ); +void trap_LinkEntity( gentity_t *ent ); +void trap_UnlinkEntity( gentity_t *ent ); +int trap_EntitiesInBox( const vec3_t mins, const vec3_t maxs, int *entityList, int maxcount ); +qboolean trap_EntityContact( const vec3_t mins, const vec3_t maxs, const gentity_t *ent ); +int trap_BotAllocateClient( void ); +void trap_BotFreeClient( int clientNum ); +void trap_GetUsercmd( int clientNum, usercmd_t *cmd ); +qboolean trap_GetEntityToken( char *buffer, int bufferSize ); + +int trap_DebugPolygonCreate(int color, int numPoints, vec3_t *points); +void trap_DebugPolygonDelete(int id); + +//TA: conceptually should live in q_shared.h +//void AxisToAngles( vec3_t axis[3], vec3_t angles); +//float arccos( float x ); + +int trap_BotLibSetup( void ); +int trap_BotLibShutdown( void ); +int trap_BotLibVarSet(char *var_name, char *value); +int trap_BotLibVarGet(char *var_name, char *value, int size); +int trap_BotLibDefine(char *string); +int trap_BotLibStartFrame(float time); +int trap_BotLibLoadMap(const char *mapname); +int trap_BotLibUpdateEntity(int ent, void /* struct bot_updateentity_s */ *bue); +int trap_BotLibTest(int parm0, char *parm1, vec3_t parm2, vec3_t parm3); + +int trap_BotGetSnapshotEntity( int clientNum, int sequence ); +int trap_BotGetServerCommand(int clientNum, char *message, int size); +void trap_BotUserCommand(int client, usercmd_t *ucmd); + +int trap_AAS_BBoxAreas(vec3_t absmins, vec3_t absmaxs, int *areas, int maxareas); +int trap_AAS_AreaInfo( int areanum, void /* struct aas_areainfo_s */ *info ); +void trap_AAS_EntityInfo(int entnum, void /* struct aas_entityinfo_s */ *info); + +int trap_AAS_Initialized(void); +void trap_AAS_PresenceTypeBoundingBox(int presencetype, vec3_t mins, vec3_t maxs); +float trap_AAS_Time(void); + +int trap_AAS_PointAreaNum(vec3_t point); +int trap_AAS_PointReachabilityAreaIndex(vec3_t point); +int trap_AAS_TraceAreas(vec3_t start, vec3_t end, int *areas, vec3_t *points, int maxareas); + +int trap_AAS_PointContents(vec3_t point); +int trap_AAS_NextBSPEntity(int ent); +int trap_AAS_ValueForBSPEpairKey(int ent, char *key, char *value, int size); +int trap_AAS_VectorForBSPEpairKey(int ent, char *key, vec3_t v); +int trap_AAS_FloatForBSPEpairKey(int ent, char *key, float *value); +int trap_AAS_IntForBSPEpairKey(int ent, char *key, int *value); + +int trap_AAS_AreaReachability(int areanum); + +int trap_AAS_AreaTravelTimeToGoalArea(int areanum, vec3_t origin, int goalareanum, int travelflags); +int trap_AAS_EnableRoutingArea( int areanum, int enable ); +int trap_AAS_PredictRoute(void /*struct aas_predictroute_s*/ *route, int areanum, vec3_t origin, + int goalareanum, int travelflags, int maxareas, int maxtime, + int stopevent, int stopcontents, int stoptfl, int stopareanum); + +int trap_AAS_AlternativeRouteGoals(vec3_t start, int startareanum, vec3_t goal, int goalareanum, int travelflags, + void /*struct aas_altroutegoal_s*/ *altroutegoals, int maxaltroutegoals, + int type); + +int trap_AAS_Swimming(vec3_t origin); +int trap_AAS_PredictClientMovement(void /* aas_clientmove_s */ *move, int entnum, vec3_t origin, int presencetype, int onground, vec3_t velocity, vec3_t cmdmove, int cmdframes, int maxframes, float frametime, int stopevent, int stopareanum, int visualize); + + +void trap_EA_Say(int client, char *str); +void trap_EA_SayTeam(int client, char *str); +void trap_EA_Command(int client, char *command); + +void trap_EA_Action(int client, int action); +void trap_EA_Gesture(int client); + +void trap_EA_Talk(int client); +void trap_EA_Attack(int client); +void trap_EA_Use(int client); +void trap_EA_Respawn(int client); +void trap_EA_Crouch(int client); +void trap_EA_MoveUp(int client); +void trap_EA_MoveDown(int client); +void trap_EA_MoveForward(int client); +void trap_EA_MoveBack(int client); +void trap_EA_MoveLeft(int client); +void trap_EA_MoveRight(int client); +void trap_EA_SelectWeapon(int client, int weapon); +void trap_EA_Jump(int client); +void trap_EA_DelayedJump(int client); +void trap_EA_Move(int client, vec3_t dir, float speed); +void trap_EA_View(int client, vec3_t viewangles); + +void trap_EA_EndRegular(int client, float thinktime); +void trap_EA_GetInput(int client, float thinktime, void /* struct bot_input_s */ *input); +void trap_EA_ResetInput(int client); + + +int trap_BotLoadCharacter(char *charfile, float skill); +void trap_BotFreeCharacter(int character); +float trap_Characteristic_Float(int character, int index); +float trap_Characteristic_BFloat(int character, int index, float min, float max); +int trap_Characteristic_Integer(int character, int index); +int trap_Characteristic_BInteger(int character, int index, int min, int max); +void trap_Characteristic_String(int character, int index, char *buf, int size); + +int trap_BotAllocChatState(void); +void trap_BotFreeChatState(int handle); +void trap_BotQueueConsoleMessage(int chatstate, int type, char *message); +void trap_BotRemoveConsoleMessage(int chatstate, int handle); +int trap_BotNextConsoleMessage(int chatstate, void /* struct bot_consolemessage_s */ *cm); +int trap_BotNumConsoleMessages(int chatstate); +void trap_BotInitialChat(int chatstate, char *type, int mcontext, char *var0, char *var1, char *var2, char *var3, char *var4, char *var5, char *var6, char *var7 ); +int trap_BotNumInitialChats(int chatstate, char *type); +int trap_BotReplyChat(int chatstate, char *message, int mcontext, int vcontext, char *var0, char *var1, char *var2, char *var3, char *var4, char *var5, char *var6, char *var7 ); +int trap_BotChatLength(int chatstate); +void trap_BotEnterChat(int chatstate, int client, int sendto); +void trap_BotGetChatMessage(int chatstate, char *buf, int size); +int trap_StringContains(char *str1, char *str2, int casesensitive); +int trap_BotFindMatch(char *str, void /* struct bot_match_s */ *match, unsigned long int context); +void trap_BotMatchVariable(void /* struct bot_match_s */ *match, int variable, char *buf, int size); +void trap_UnifyWhiteSpaces(char *string); +void trap_BotReplaceSynonyms(char *string, unsigned long int context); +int trap_BotLoadChatFile(int chatstate, char *chatfile, char *chatname); +void trap_BotSetChatGender(int chatstate, int gender); +void trap_BotSetChatName(int chatstate, char *name, int client); +void trap_BotResetGoalState(int goalstate); +void trap_BotRemoveFromAvoidGoals(int goalstate, int number); +void trap_BotResetAvoidGoals(int goalstate); +void trap_BotPushGoal(int goalstate, void /* struct bot_goal_s */ *goal); +void trap_BotPopGoal(int goalstate); +void trap_BotEmptyGoalStack(int goalstate); +void trap_BotDumpAvoidGoals(int goalstate); +void trap_BotDumpGoalStack(int goalstate); +void trap_BotGoalName(int number, char *name, int size); +int trap_BotGetTopGoal(int goalstate, void /* struct bot_goal_s */ *goal); +int trap_BotGetSecondGoal(int goalstate, void /* struct bot_goal_s */ *goal); +int trap_BotChooseLTGItem(int goalstate, vec3_t origin, int *inventory, int travelflags); +int trap_BotChooseNBGItem(int goalstate, vec3_t origin, int *inventory, int travelflags, void /* struct bot_goal_s */ *ltg, float maxtime); +int trap_BotTouchingGoal(vec3_t origin, void /* struct bot_goal_s */ *goal); +int trap_BotItemGoalInVisButNotVisible(int viewer, vec3_t eye, vec3_t viewangles, void /* struct bot_goal_s */ *goal); +int trap_BotGetNextCampSpotGoal(int num, void /* struct bot_goal_s */ *goal); +int trap_BotGetMapLocationGoal(char *name, void /* struct bot_goal_s */ *goal); +int trap_BotGetLevelItemGoal(int index, char *classname, void /* struct bot_goal_s */ *goal); +float trap_BotAvoidGoalTime(int goalstate, int number); +void trap_BotSetAvoidGoalTime(int goalstate, int number, float avoidtime); +void trap_BotInitLevelItems(void); +void trap_BotUpdateEntityItems(void); +int trap_BotLoadItemWeights(int goalstate, char *filename); +void trap_BotFreeItemWeights(int goalstate); +void trap_BotInterbreedGoalFuzzyLogic(int parent1, int parent2, int child); +void trap_BotSaveGoalFuzzyLogic(int goalstate, char *filename); +void trap_BotMutateGoalFuzzyLogic(int goalstate, float range); +int trap_BotAllocGoalState(int state); +void trap_BotFreeGoalState(int handle); + +void trap_BotResetMoveState(int movestate); +void trap_BotMoveToGoal(void /* struct bot_moveresult_s */ *result, int movestate, void /* struct bot_goal_s */ *goal, int travelflags); +int trap_BotMoveInDirection(int movestate, vec3_t dir, float speed, int type); +void trap_BotResetAvoidReach(int movestate); +void trap_BotResetLastAvoidReach(int movestate); +int trap_BotReachabilityArea(vec3_t origin, int testground); +int trap_BotMovementViewTarget(int movestate, void /* struct bot_goal_s */ *goal, int travelflags, float lookahead, vec3_t target); +int trap_BotPredictVisiblePosition(vec3_t origin, int areanum, void /* struct bot_goal_s */ *goal, int travelflags, vec3_t target); +int trap_BotAllocMoveState(void); +void trap_BotFreeMoveState(int handle); +void trap_BotInitMoveState(int handle, void /* struct bot_initmove_s */ *initmove); +void trap_BotAddAvoidSpot(int movestate, vec3_t origin, float radius, int type); + +int trap_BotChooseBestFightWeapon(int weaponstate, int *inventory); +void trap_BotGetWeaponInfo(int weaponstate, int weapon, void /* struct weaponinfo_s */ *weaponinfo); +int trap_BotLoadWeaponWeights(int weaponstate, char *filename); +int trap_BotAllocWeaponState(void); +void trap_BotFreeWeaponState(int weaponstate); +void trap_BotResetWeaponState(int weaponstate); + +int trap_GeneticParentsAndChildSelection(int numranks, float *ranks, int *parent1, int *parent2, int *child); + +void trap_SnapVector( float *v ); + diff --git a/src/game/g_main.c b/src/game/g_main.c new file mode 100644 index 00000000..fd922ad4 --- /dev/null +++ b/src/game/g_main.c @@ -0,0 +1,1881 @@ +// Copyright (C) 1999-2000 Id Software, Inc. +// + +/* + * Portions Copyright (C) 2000-2001 Tim Angus + * + * This program 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, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ + +/* To assertain which portions are licensed under the GPL and which are + * licensed by Id Software, Inc. please run a diff between the equivalent + * versions of the "Tremulous" modification and the unmodified "Quake3" + * game source code. + */ + +#include "g_local.h" + +level_locals_t level; + +typedef struct { + vmCvar_t *vmCvar; + char *cvarName; + char *defaultString; + int cvarFlags; + int modificationCount; // for tracking changes + qboolean trackChange; // track this variable, and announce if changed + qboolean teamShader; // track and if changed, update shader state +} cvarTable_t; + +gentity_t g_entities[MAX_GENTITIES]; +gclient_t g_clients[MAX_CLIENTS]; + +vmCvar_t g_gametype; +vmCvar_t g_dmflags; +vmCvar_t g_fraglimit; +vmCvar_t g_timelimit; +vmCvar_t g_capturelimit; +vmCvar_t g_friendlyFire; +vmCvar_t g_password; +vmCvar_t g_needpass; +vmCvar_t g_maxclients; +vmCvar_t g_maxGameClients; +vmCvar_t g_dedicated; +vmCvar_t g_speed; +vmCvar_t g_gravity; +vmCvar_t g_cheats; +vmCvar_t g_knockback; +vmCvar_t g_quadfactor; +vmCvar_t g_forcerespawn; +vmCvar_t g_inactivity; +vmCvar_t g_debugMove; +vmCvar_t g_debugDamage; +vmCvar_t g_debugAlloc; +vmCvar_t g_weaponRespawn; +vmCvar_t g_weaponTeamRespawn; +vmCvar_t g_motd; +vmCvar_t g_synchronousClients; +vmCvar_t g_warmup; +vmCvar_t g_doWarmup; +vmCvar_t g_restarted; +vmCvar_t g_log; +vmCvar_t g_logSync; +vmCvar_t g_blood; +vmCvar_t g_podiumDist; +vmCvar_t g_podiumDrop; +vmCvar_t g_allowVote; +vmCvar_t g_teamAutoJoin; +vmCvar_t g_teamForceBalance; +vmCvar_t g_banIPs; +vmCvar_t g_filterBan; +vmCvar_t g_smoothClients; +vmCvar_t pmove_fixed; +vmCvar_t pmove_msec; +vmCvar_t g_rankings; +vmCvar_t g_listEntity; + + +cvarTable_t gameCvarTable[] = { + // don't override the cheat state set by the system + { &g_cheats, "sv_cheats", "", 0, 0, qfalse }, + + // noset vars + { NULL, "gamename", GAMEVERSION , CVAR_SERVERINFO | CVAR_ROM, 0, qfalse }, + { NULL, "gamedate", __DATE__ , CVAR_ROM, 0, qfalse }, + { &g_restarted, "g_restarted", "0", CVAR_ROM, 0, qfalse }, + { NULL, "sv_mapname", "", CVAR_SERVERINFO | CVAR_ROM, 0, qfalse }, + + // latched vars + { &g_gametype, "g_gametype", "0", CVAR_SERVERINFO | CVAR_LATCH, 0, qfalse }, + + { &g_maxclients, "sv_maxclients", "8", CVAR_SERVERINFO | CVAR_LATCH | CVAR_ARCHIVE, 0, qfalse }, + { &g_maxGameClients, "g_maxGameClients", "0", CVAR_SERVERINFO | CVAR_LATCH | CVAR_ARCHIVE, 0, qfalse }, + + // change anytime vars + { &g_dmflags, "dmflags", "0", CVAR_SERVERINFO | CVAR_ARCHIVE, 0, qtrue }, + { &g_fraglimit, "fraglimit", "20", CVAR_SERVERINFO | CVAR_ARCHIVE | CVAR_NORESTART, 0, qtrue }, + { &g_timelimit, "timelimit", "0", CVAR_SERVERINFO | CVAR_ARCHIVE | CVAR_NORESTART, 0, qtrue }, + { &g_capturelimit, "capturelimit", "8", CVAR_SERVERINFO | CVAR_ARCHIVE | CVAR_NORESTART, 0, qtrue }, + + { &g_synchronousClients, "g_synchronousClients", "0", CVAR_SYSTEMINFO, 0, qfalse }, + + { &g_friendlyFire, "g_friendlyFire", "1", CVAR_ARCHIVE, 0, qtrue }, + + { &g_teamAutoJoin, "g_teamAutoJoin", "0", CVAR_ARCHIVE }, + { &g_teamForceBalance, "g_teamForceBalance", "0", CVAR_ARCHIVE }, + + { &g_warmup, "g_warmup", "20", CVAR_ARCHIVE, 0, qtrue }, + { &g_doWarmup, "g_doWarmup", "0", 0, 0, qtrue }, + { &g_log, "g_log", "games.log", CVAR_ARCHIVE, 0, qfalse }, + { &g_logSync, "g_logSync", "0", CVAR_ARCHIVE, 0, qfalse }, + + { &g_password, "g_password", "", CVAR_USERINFO, 0, qfalse }, + + { &g_banIPs, "g_banIPs", "", CVAR_ARCHIVE, 0, qfalse }, + { &g_filterBan, "g_filterBan", "1", CVAR_ARCHIVE, 0, qfalse }, + + { &g_needpass, "g_needpass", "0", CVAR_SERVERINFO | CVAR_ROM, 0, qfalse }, + + { &g_dedicated, "dedicated", "0", 0, 0, qfalse }, + + { &g_speed, "g_speed", "320", 0, 0, qtrue }, + { &g_gravity, "g_gravity", "800", 0, 0, qtrue }, + { &g_knockback, "g_knockback", "1000", 0, 0, qtrue }, + { &g_quadfactor, "g_quadfactor", "3", 0, 0, qtrue }, + { &g_weaponRespawn, "g_weaponrespawn", "5", 0, 0, qtrue }, + { &g_weaponTeamRespawn, "g_weaponTeamRespawn", "30", 0, 0, qtrue }, + { &g_forcerespawn, "g_forcerespawn", "20", 0, 0, qtrue }, + { &g_inactivity, "g_inactivity", "0", 0, 0, qtrue }, + { &g_debugMove, "g_debugMove", "0", 0, 0, qfalse }, + { &g_debugDamage, "g_debugDamage", "0", 0, 0, qfalse }, + { &g_debugAlloc, "g_debugAlloc", "0", 0, 0, qfalse }, + { &g_motd, "g_motd", "", 0, 0, qfalse }, + { &g_blood, "com_blood", "1", 0, 0, qfalse }, + + { &g_podiumDist, "g_podiumDist", "80", 0, 0, qfalse }, + { &g_podiumDrop, "g_podiumDrop", "70", 0, 0, qfalse }, + + { &g_allowVote, "g_allowVote", "1", CVAR_ARCHIVE, 0, qfalse }, + { &g_listEntity, "g_listEntity", "0", 0, 0, qfalse }, + + { &g_smoothClients, "g_smoothClients", "1", 0, 0, qfalse}, + { &pmove_fixed, "pmove_fixed", "0", CVAR_SYSTEMINFO, 0, qfalse}, + { &pmove_msec, "pmove_msec", "8", CVAR_SYSTEMINFO, 0, qfalse}, + + { &g_rankings, "g_rankings", "0", 0, 0, qfalse} +}; + +int gameCvarTableSize = sizeof( gameCvarTable ) / sizeof( gameCvarTable[0] ); + + +void G_InitGame( int levelTime, int randomSeed, int restart ); +void G_RunFrame( int levelTime ); +void G_ShutdownGame( int restart ); +void CheckExitRules( void ); + + +/* +================ +vmMain + +This is the only way control passes into the module. +This must be the very first function compiled into the .q3vm file +================ +*/ +int 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 GAME_INIT: + G_InitGame( arg0, arg1, arg2 ); + return 0; + case GAME_SHUTDOWN: + G_ShutdownGame( arg0 ); + return 0; + case GAME_CLIENT_CONNECT: + return (int)ClientConnect( arg0, arg1, arg2 ); + case GAME_CLIENT_THINK: + ClientThink( arg0 ); + return 0; + case GAME_CLIENT_USERINFO_CHANGED: + ClientUserinfoChanged( arg0 ); + return 0; + case GAME_CLIENT_DISCONNECT: + ClientDisconnect( arg0 ); + return 0; + case GAME_CLIENT_BEGIN: + ClientBegin( arg0 ); + return 0; + case GAME_CLIENT_COMMAND: + ClientCommand( arg0 ); + return 0; + case GAME_RUN_FRAME: + G_RunFrame( arg0 ); + return 0; + case GAME_CONSOLE_COMMAND: + return ConsoleCommand(); + //TA: rip bots + /*case BOTAI_START_FRAME: + return BotAIStartFrame( arg0 );*/ + } + + return -1; +} + + +void QDECL G_Printf( const char *fmt, ... ) { + va_list argptr; + char text[1024]; + + va_start (argptr, fmt); + vsprintf (text, fmt, argptr); + va_end (argptr); + + trap_Printf( text ); +} + +void QDECL G_Error( const char *fmt, ... ) { + va_list argptr; + char text[1024]; + + va_start (argptr, fmt); + vsprintf (text, fmt, argptr); + va_end (argptr); + + trap_Error( text ); +} + +/* +================ +G_FindTeams + +Chain together all entities with a matching team field. +Entity teams are used for item groups and multi-entity mover groups. + +All but the first will have the FL_TEAMSLAVE flag set and teammaster field set +All but the last will have the teamchain field set to the next one +================ +*/ +void G_FindTeams( void ) { + gentity_t *e, *e2; + int i, j; + int c, c2; + + c = 0; + c2 = 0; + for ( i=1, e=g_entities+i ; i < level.num_entities ; i++,e++ ){ + if (!e->inuse) + continue; + if (!e->team) + continue; + if (e->flags & FL_TEAMSLAVE) + continue; + e->teammaster = e; + c++; + c2++; + for (j=i+1, e2=e+1 ; j < level.num_entities ; j++,e2++) + { + if (!e2->inuse) + continue; + if (!e2->team) + continue; + if (e2->flags & FL_TEAMSLAVE) + continue; + if (!strcmp(e->team, e2->team)) + { + c2++; + e2->teamchain = e->teamchain; + e->teamchain = e2; + e2->teammaster = e; + e2->flags |= FL_TEAMSLAVE; + + // make sure that targets only point at the master + if ( e2->targetname ) { + e->targetname = e2->targetname; + e2->targetname = NULL; + } + } + } + } + + G_Printf ("%i teams with %i entities\n", c, c2); +} + +/* +================= +G_RegisterPlayerModels +================= +*/ +void G_RegisterPlayerModels( void ) +{ + char *precacheModels[ MAX_CLIENTS ] = { "klesk", "lucy", "sarge", NULL }; + char *s; + int i; + + for( i = 0; i < 3; i++ ) + { + s = va("n\\%s%d\\t\\%i\\model\\%s\\hmodel\\%s\\g_redteam\\%s\\g_blueteam\\%s\\c1\\%s\\hc\\%i\\w\\%i\\l\\%i\\tt\\%d\\tl\\%d", "precache", i, 0, precacheModels[ i ], precacheModels[ i ], 0, 0, "7", 100, 0, 0, 0, 0); + + trap_SetConfigstring( CS_PLAYERS + MAX_CLIENTS + i, s ); + } + +} + + +void G_RemapTeamShaders() { +} + + +/* +================= +G_RegisterCvars +================= +*/ +void G_RegisterCvars( void ) { + int i; + cvarTable_t *cv; + qboolean remapped = qfalse; + + for ( i = 0, cv = gameCvarTable ; i < gameCvarTableSize ; i++, cv++ ) { + trap_Cvar_Register( cv->vmCvar, cv->cvarName, + cv->defaultString, cv->cvarFlags ); + if ( cv->vmCvar ) + cv->modificationCount = cv->vmCvar->modificationCount; + + if (cv->teamShader) { + remapped = qtrue; + } + } + + if (remapped) { + G_RemapTeamShaders(); + } + + // check some things + + if ( g_gametype.integer < 0 || g_gametype.integer >= GT_MAX_GAME_TYPE ) { + G_Printf( "g_gametype %i is out of range, defaulting to 0\n", g_gametype.integer ); + trap_Cvar_Set( "g_gametype", "0" ); + } + + level.warmupModificationCount = g_warmup.modificationCount; +} + +/* +================= +G_UpdateCvars +================= +*/ +void G_UpdateCvars( void ) { + int i; + cvarTable_t *cv; + qboolean remapped = qfalse; + + for ( i = 0, cv = gameCvarTable ; i < gameCvarTableSize ; i++, cv++ ) { + if ( cv->vmCvar ) { + trap_Cvar_Update( cv->vmCvar ); + + if ( cv->modificationCount != cv->vmCvar->modificationCount ) { + cv->modificationCount = cv->vmCvar->modificationCount; + + if ( cv->trackChange ) { + trap_SendServerCommand( -1, va("print \"Server: %s changed to %s\n\"", + cv->cvarName, cv->vmCvar->string ) ); + } + + if (cv->teamShader) { + remapped = qtrue; + } + } + } + } + + if (remapped) { + G_RemapTeamShaders(); + } +} + + +/* +============ +G_InitGame + +============ +*/ +void G_InitGame( int levelTime, int randomSeed, int restart ) { + int i; + + G_Printf ("------- Game Initialization -------\n"); + G_Printf ("gamename: %s\n", GAMEVERSION); + G_Printf ("gamedate: %s\n", __DATE__); + + srand( randomSeed ); + + G_RegisterCvars(); + + G_ProcessIPBans(); + + G_InitMemory(); + + // set some level globals + memset( &level, 0, sizeof( level ) ); + level.time = levelTime; + level.startTime = levelTime; + + level.snd_fry = G_SoundIndex("sound/player/fry.wav"); // FIXME standing in lava / slime + + if ( g_gametype.integer != GT_SINGLE_PLAYER && g_log.string[0] ) { + if ( g_logSync.integer ) { + trap_FS_FOpenFile( g_log.string, &level.logFile, FS_APPEND_SYNC ); + } else { + trap_FS_FOpenFile( g_log.string, &level.logFile, FS_APPEND ); + } + if ( !level.logFile ) { + G_Printf( "WARNING: Couldn't open logfile: %s\n", g_log.string ); + } else { + char serverinfo[MAX_INFO_STRING]; + + trap_GetServerinfo( serverinfo, sizeof( serverinfo ) ); + + G_LogPrintf("------------------------------------------------------------\n" ); + G_LogPrintf("InitGame: %s\n", serverinfo ); + } + } else { + G_Printf( "Not logging to disk.\n" ); + } + + G_InitWorldSession(); + + // initialize all entities for this game + memset( g_entities, 0, MAX_GENTITIES * sizeof(g_entities[0]) ); + level.gentities = g_entities; + + // initialize all clients for this game + level.maxclients = g_maxclients.integer; + memset( g_clients, 0, MAX_CLIENTS * sizeof(g_clients[0]) ); + level.clients = g_clients; + + // set client fields on player ents + for ( i=0 ; i<level.maxclients ; i++ ) { + g_entities[i].client = level.clients + i; + } + + // always leave room for the max number of clients, + // even if they aren't all used, so numbers inside that + // range are NEVER anything but clients + level.num_entities = MAX_CLIENTS; + + // let the server system know where the entites are + trap_LocateGameData( level.gentities, level.num_entities, sizeof( gentity_t ), + &level.clients[0].ps, sizeof( level.clients[0] ) ); + + // reserve some spots for dead player bodies + InitBodyQue(); + + ClearRegisteredItems(); + + // parse the key/value pairs and spawn gentities + G_SpawnEntitiesFromString(); + + // general initialization + G_FindTeams(); + + // make sure we have flags for CTF, etc + if( g_gametype.integer >= GT_TEAM ) { + G_CheckTeamItems(); + } + + SaveRegisteredItems(); + G_RegisterPlayerModels(); + + G_Printf ("-----------------------------------\n"); + + if( g_gametype.integer == GT_SINGLE_PLAYER || trap_Cvar_VariableIntegerValue( "com_buildScript" ) ) { + G_ModelIndex( SP_PODIUM_MODEL ); + G_SoundIndex( "sound/player/gurp1.wav" ); + G_SoundIndex( "sound/player/gurp2.wav" ); + } + + //TA: rip bots + /*if ( trap_Cvar_VariableIntegerValue( "bot_enable" ) ) { + BotAISetup( restart ); + BotAILoadMap( restart ); + G_InitBots( restart ); + }*/ + + G_RemapTeamShaders(); + + //TA: so the server counts the spawns without a client attached + CalculateRanks( ); +} + + + +/* +================= +G_ShutdownGame +================= +*/ +void G_ShutdownGame( int restart ) { + G_Printf ("==== ShutdownGame ====\n"); + + if ( level.logFile ) { + G_LogPrintf("ShutdownGame:\n" ); + G_LogPrintf("------------------------------------------------------------\n" ); + trap_FS_FCloseFile( level.logFile ); + } + + // write all the client session data so we can get it back + G_WriteSessionData(); + + //TA: rip bots + /*if ( trap_Cvar_VariableIntegerValue( "bot_enable" ) ) { + BotAIShutdown( restart ); + }*/ +} + + + +//=================================================================== + +#ifndef GAME_HARD_LINKED +// this is only here so the functions in q_shared.c and bg_*.c can link + +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); + + G_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); + + G_Printf ("%s", text); +} + +#endif + +/* +======================================================================== + +PLAYER COUNTING / SCORE SORTING + +======================================================================== +*/ + +/* +============= +AddTournamentPlayer + +If there are less than two tournament players, put a +spectator in the game and restart +============= +*/ +void AddTournamentPlayer( void ) { + int i; + gclient_t *client; + gclient_t *nextInLine; + + if ( level.numPlayingClients >= 2 ) { + return; + } + + // never change during intermission + if ( level.intermissiontime ) { + return; + } + + nextInLine = NULL; + + for ( i = 0 ; i < level.maxclients ; i++ ) { + client = &level.clients[i]; + if ( client->pers.connected != CON_CONNECTED ) { + continue; + } + if ( client->sess.sessionTeam != TEAM_SPECTATOR ) { + continue; + } + // never select the dedicated follow or scoreboard clients + if ( client->sess.spectatorState == SPECTATOR_SCOREBOARD || + client->sess.spectatorClient < 0 ) { + continue; + } + + if ( !nextInLine || client->sess.spectatorTime < nextInLine->sess.spectatorTime ) { + nextInLine = client; + } + } + + if ( !nextInLine ) { + return; + } + + level.warmupTime = -1; + + // set them to free-for-all team + SetTeam( &g_entities[ nextInLine - level.clients ], "f" ); +} + + +/* +======================= +RemoveTournamentLoser + +Make the loser a spectator at the back of the line +======================= +*/ +void RemoveTournamentLoser( void ) { + int clientNum; + + if ( level.numPlayingClients != 2 ) { + return; + } + + clientNum = level.sortedClients[1]; + + if ( level.clients[ clientNum ].pers.connected != CON_CONNECTED ) { + return; + } + + // make them a spectator + SetTeam( &g_entities[ clientNum ], "s" ); +} + + +/* +======================= +RemoveTournamentWinner +======================= +*/ +void RemoveTournamentWinner( void ) { + int clientNum; + + if ( level.numPlayingClients != 2 ) { + return; + } + + clientNum = level.sortedClients[0]; + + if ( level.clients[ clientNum ].pers.connected != CON_CONNECTED ) { + return; + } + + // make them a spectator + SetTeam( &g_entities[ clientNum ], "s" ); +} + + +/* +======================= +AdjustTournamentScores + +======================= +*/ +void AdjustTournamentScores( void ) { + int clientNum; + + clientNum = level.sortedClients[0]; + if ( level.clients[ clientNum ].pers.connected == CON_CONNECTED ) { + level.clients[ clientNum ].sess.wins++; + ClientUserinfoChanged( clientNum ); + } + + clientNum = level.sortedClients[1]; + if ( level.clients[ clientNum ].pers.connected == CON_CONNECTED ) { + level.clients[ clientNum ].sess.losses++; + ClientUserinfoChanged( clientNum ); + } + +} + + + +/* +============= +SortRanks + +============= +*/ +int QDECL SortRanks( const void *a, const void *b ) { + gclient_t *ca, *cb; + + ca = &level.clients[*(int *)a]; + cb = &level.clients[*(int *)b]; + + // sort special clients last + if ( ca->sess.spectatorState == SPECTATOR_SCOREBOARD || ca->sess.spectatorClient < 0 ) { + return 1; + } + if ( cb->sess.spectatorState == SPECTATOR_SCOREBOARD || cb->sess.spectatorClient < 0 ) { + return -1; + } + + // then connecting clients + if ( ca->pers.connected == CON_CONNECTING ) { + return 1; + } + if ( cb->pers.connected == CON_CONNECTING ) { + return -1; + } + + + // then spectators + if ( ca->sess.sessionTeam == TEAM_SPECTATOR && cb->sess.sessionTeam == TEAM_SPECTATOR ) { + if ( ca->sess.spectatorTime < cb->sess.spectatorTime ) { + return -1; + } + if ( ca->sess.spectatorTime > cb->sess.spectatorTime ) { + return 1; + } + return 0; + } + if ( ca->sess.sessionTeam == TEAM_SPECTATOR ) { + return 1; + } + if ( cb->sess.sessionTeam == TEAM_SPECTATOR ) { + return -1; + } + + // then sort by score + if ( ca->ps.persistant[PERS_SCORE] + > cb->ps.persistant[PERS_SCORE] ) { + return -1; + } + if ( ca->ps.persistant[PERS_SCORE] + < cb->ps.persistant[PERS_SCORE] ) { + return 1; + } + return 0; +} + +/* +============ +countSpawns + +Counts the number of spawns for each team +============ +*/ +void countSpawns( void ) +{ + int i; + gentity_t *ent; + + level.numDroidSpawns = 0; + level.numHumanSpawns = 0; + + for ( i = 1, ent = g_entities + i ; i < level.num_entities ; i++, ent++ ) + { + if (!ent->inuse) + continue; + + if( !Q_stricmp( ent->classname, "team_droid_spawn" ) && ent->health > 0 ) + level.numDroidSpawns++; + + if( !Q_stricmp( ent->classname, "team_human_spawn" ) && ent->health > 0 ) + level.numHumanSpawns++; + } +} + + +/* +============ +CalculateRanks + +Recalculates the score ranks of all players +This will be called on every client connect, begin, disconnect, death, +and team change. +============ +*/ +void CalculateRanks( void ) { + int i; + int rank; + int score; + int newScore; + gclient_t *cl; + + level.follow1 = -1; + level.follow2 = -1; + level.numConnectedClients = 0; + level.numNonSpectatorClients = 0; + level.numPlayingClients = 0; + level.numVotingClients = 0; // don't count bots + level.numDroidClients = 0; + level.numHumanClients = 0; + level.numLiveDroidClients = 0; + level.numLiveHumanClients = 0; + + for ( i = 0; i < TEAM_NUM_TEAMS; i++ ) { + level.numteamVotingClients[i] = 0; + } + + countSpawns( ); + + for ( i = 0 ; i < level.maxclients ; i++ ) + { + if ( level.clients[i].pers.connected != CON_DISCONNECTED ) + { + level.sortedClients[level.numConnectedClients] = i; + level.numConnectedClients++; + + //TA: so we know when the game ends and for team leveling + if( level.clients[i].pers.pteam == PTE_DROIDS ) + { + level.numDroidClients++; + if ( level.clients[i].sess.sessionTeam != TEAM_SPECTATOR ) + level.numLiveDroidClients++; + } + + if( level.clients[i].pers.pteam == PTE_HUMANS ) + { + level.numHumanClients++; + if ( level.clients[i].sess.sessionTeam != TEAM_SPECTATOR ) + level.numLiveHumanClients++; + } + //////////////// + + if ( level.clients[i].sess.sessionTeam != TEAM_SPECTATOR ) + { + level.numNonSpectatorClients++; + + // decide if this should be auto-followed + if ( level.clients[i].pers.connected == CON_CONNECTED ) + { + level.numPlayingClients++; + if ( !(g_entities[i].r.svFlags & SVF_BOT) ) + level.numVotingClients++; + if ( level.clients[i].sess.sessionTeam == TEAM_HUMANS ) + level.numteamVotingClients[0]++; + else if ( level.clients[i].sess.sessionTeam == TEAM_DROIDS ) + level.numteamVotingClients[1]++; + + if ( level.follow1 == -1 ) + level.follow1 = i; + else if ( level.follow2 == -1 ) + level.follow2 = i; + } + + } + } + } + + qsort( level.sortedClients, level.numConnectedClients, + sizeof(level.sortedClients[0]), SortRanks ); + + // set the rank value for all clients that are connected and not spectators + if ( g_gametype.integer >= GT_TEAM ) { + // in team games, rank is just the order of the teams, 0=red, 1=blue, 2=tied + for ( i = 0; i < level.numConnectedClients; i++ ) { + cl = &level.clients[ level.sortedClients[i] ]; + if ( level.teamScores[TEAM_HUMANS] == level.teamScores[TEAM_DROIDS] ) { + cl->ps.persistant[PERS_RANK] = 2; + } else if ( level.teamScores[TEAM_HUMANS] > level.teamScores[TEAM_DROIDS] ) { + cl->ps.persistant[PERS_RANK] = 0; + } else { + cl->ps.persistant[PERS_RANK] = 1; + } + } + } else { + rank = -1; + score = 0; + for ( i = 0; i < level.numPlayingClients; i++ ) { + cl = &level.clients[ level.sortedClients[i] ]; + newScore = cl->ps.persistant[PERS_SCORE]; + if ( i == 0 || newScore != score ) { + rank = i; + // assume we aren't tied until the next client is checked + level.clients[ level.sortedClients[i] ].ps.persistant[PERS_RANK] = rank; + } else { + // we are tied with the previous client + level.clients[ level.sortedClients[i-1] ].ps.persistant[PERS_RANK] = rank | RANK_TIED_FLAG; + level.clients[ level.sortedClients[i] ].ps.persistant[PERS_RANK] = rank | RANK_TIED_FLAG; + } + score = newScore; + if ( g_gametype.integer == GT_SINGLE_PLAYER && level.numPlayingClients == 1 ) { + level.clients[ level.sortedClients[i] ].ps.persistant[PERS_RANK] = rank | RANK_TIED_FLAG; + } + } + } + + trap_SetConfigstring( CS_ABPOINTS, va("%i", level.droidBuildPoints ) ); + trap_SetConfigstring( CS_HBPOINTS, va("%i", level.humanBuildPoints ) ); + + // set the CS_SCORES1/2 configstrings, which will be visible to everyone + if ( g_gametype.integer >= GT_TEAM ) { + trap_SetConfigstring( CS_SCORES1, va("%i", level.teamScores[TEAM_HUMANS] ) ); + trap_SetConfigstring( CS_SCORES2, va("%i", level.teamScores[TEAM_DROIDS] ) ); + } else { + if ( level.numConnectedClients == 0 ) { + trap_SetConfigstring( CS_SCORES1, va("%i", SCORE_NOT_PRESENT) ); + trap_SetConfigstring( CS_SCORES2, va("%i", SCORE_NOT_PRESENT) ); + } else if ( level.numConnectedClients == 1 ) { + trap_SetConfigstring( CS_SCORES1, va("%i", level.clients[ level.sortedClients[0] ].ps.persistant[PERS_SCORE] ) ); + trap_SetConfigstring( CS_SCORES2, va("%i", SCORE_NOT_PRESENT) ); + } else { + trap_SetConfigstring( CS_SCORES1, va("%i", level.clients[ level.sortedClients[0] ].ps.persistant[PERS_SCORE] ) ); + trap_SetConfigstring( CS_SCORES2, va("%i", level.clients[ level.sortedClients[1] ].ps.persistant[PERS_SCORE] ) ); + } + } + + // see if it is time to end the level + CheckExitRules(); + + // if we are at the intermission, send the new info to everyone + if ( level.intermissiontime ) { + SendScoreboardMessageToAllClients(); + } +} + + +/* +======================================================================== + +MAP CHANGING + +======================================================================== +*/ + +/* +======================== +SendScoreboardMessageToAllClients + +Do this at BeginIntermission time and whenever ranks are recalculated +due to enters/exits/forced team changes +======================== +*/ +void SendScoreboardMessageToAllClients( void ) { + int i; + + for ( i = 0 ; i < level.maxclients ; i++ ) { + if ( level.clients[ i ].pers.connected == CON_CONNECTED ) { + DeathmatchScoreboardMessage( g_entities + i ); + } + } +} + +/* +======================== +MoveClientToIntermission + +When the intermission starts, this will be called for all players. +If a new client connects, this will be called after the spawn function. +======================== +*/ +void MoveClientToIntermission( gentity_t *ent ) { + // take out of follow mode if needed + if ( ent->client->sess.spectatorState == SPECTATOR_FOLLOW ) { + StopFollowing( ent ); + } + + + // move to the spot + VectorCopy( level.intermission_origin, ent->s.origin ); + VectorCopy( level.intermission_origin, ent->client->ps.origin ); + VectorCopy (level.intermission_angle, ent->client->ps.viewangles); + ent->client->ps.pm_type = PM_INTERMISSION; + + // clean up powerup info + memset( ent->client->ps.powerups, 0, sizeof(ent->client->ps.powerups) ); + + ent->client->ps.eFlags = 0; + ent->s.eFlags = 0; + ent->s.eType = ET_GENERAL; + ent->s.modelindex = 0; + ent->s.loopSound = 0; + ent->s.event = 0; + ent->r.contents = 0; +} + +/* +================== +FindIntermissionPoint + +This is also used for spectator spawns +================== +*/ +void FindIntermissionPoint( void ) { + gentity_t *ent, *target; + vec3_t dir; + + // find the intermission spot + ent = G_Find (NULL, FOFS(classname), "info_player_intermission"); + if ( !ent ) { // the map creator forgot to put in an intermission point... + SelectSpawnPoint ( vec3_origin, level.intermission_origin, level.intermission_angle ); + } else { + VectorCopy (ent->s.origin, level.intermission_origin); + VectorCopy (ent->s.angles, level.intermission_angle); + // if it has a target, look towards it + if ( ent->target ) { + target = G_PickTarget( ent->target ); + if ( target ) { + VectorSubtract( target->s.origin, level.intermission_origin, dir ); + vectoangles( dir, level.intermission_angle ); + } + } + } + +} + +/* +================== +BeginIntermission +================== +*/ +void BeginIntermission( void ) { + int i; + gentity_t *client; + + if ( level.intermissiontime ) { + return; // already active + } + + // if in tournement mode, change the wins / losses + if ( g_gametype.integer == GT_TOURNAMENT ) { + AdjustTournamentScores(); + } + + level.intermissiontime = level.time; + FindIntermissionPoint(); + + // if single player game + if ( g_gametype.integer == GT_SINGLE_PLAYER ) { + UpdateTournamentInfo(); + SpawnModelsOnVictoryPads(); + } + + // move all clients to the intermission point + for (i=0 ; i< level.maxclients ; i++) { + client = g_entities + i; + if (!client->inuse) + continue; + // respawn if dead + if (client->health <= 0) { + respawn(client); + } + MoveClientToIntermission( client ); + } + + // send the current scoring to all clients + SendScoreboardMessageToAllClients(); +} + + +/* +============= +ExitLevel + +When the intermission has been exited, the server is either killed +or moved to a new level based on the "nextmap" cvar + +============= +*/ +void ExitLevel (void) { + int i; + gclient_t *cl; + + //bot interbreeding + //TA: rip bots + //BotInterbreedEndMatch(); + + // if we are running a tournement map, kick the loser to spectator status, + // which will automatically grab the next spectator and restart + if ( g_gametype.integer == GT_TOURNAMENT ) { + if ( !level.restarted ) { + RemoveTournamentLoser(); + trap_SendConsoleCommand( EXEC_APPEND, "map_restart 0\n" ); + level.restarted = qtrue; + level.changemap = NULL; + level.intermissiontime = 0; + } + return; + } + + + trap_SendConsoleCommand( EXEC_APPEND, "vstr nextmap\n" ); + level.changemap = NULL; + level.intermissiontime = 0; + + // reset all the scores so we don't enter the intermission again + level.teamScores[TEAM_HUMANS] = 0; + level.teamScores[TEAM_DROIDS] = 0; + for ( i=0 ; i< g_maxclients.integer ; i++ ) { + cl = level.clients + i; + if ( cl->pers.connected != CON_CONNECTED ) { + continue; + } + cl->ps.persistant[PERS_SCORE] = 0; + } + + // we need to do this here before chaning to CON_CONNECTING + G_WriteSessionData(); + + // change all client states to connecting, so the early players into the + // next level will know the others aren't done reconnecting + for (i=0 ; i< g_maxclients.integer ; i++) { + if ( level.clients[i].pers.connected == CON_CONNECTED ) { + level.clients[i].pers.connected = CON_CONNECTING; + } + } + +} + +/* +================= +G_LogPrintf + +Print to the logfile with a time stamp if it is open +================= +*/ +void QDECL G_LogPrintf( const char *fmt, ... ) { + va_list argptr; + char string[1024]; + int min, tens, sec; + + sec = level.time / 1000; + + min = sec / 60; + sec -= min * 60; + tens = sec / 10; + sec -= tens * 10; + + Com_sprintf( string, sizeof(string), "%3i:%i%i ", min, tens, sec ); + + va_start( argptr, fmt ); + vsprintf( string +7 , fmt,argptr ); + va_end( argptr ); + + if ( g_dedicated.integer ) { + G_Printf( "%s", string + 7 ); + } + + if ( !level.logFile ) { + return; + } + + trap_FS_Write( string, strlen( string ), level.logFile ); +} + +/* +================ +LogExit + +Append information about this game to the log file +================ +*/ +void LogExit( const char *string ) { + int i, numSorted; + gclient_t *cl; + qboolean won = qtrue; + + G_LogPrintf( "Exit: %s\n", string ); + + level.intermissionQueued = level.time; + + // this will keep the clients from playing any voice sounds + // that will get cut off when the queued intermission starts + trap_SetConfigstring( CS_INTERMISSION, "1" ); + + // don't send more than 32 scores (FIXME?) + numSorted = level.numConnectedClients; + if ( numSorted > 32 ) { + numSorted = 32; + } + + if ( g_gametype.integer >= GT_TEAM ) { + G_LogPrintf( "red:%i blue:%i\n", + level.teamScores[TEAM_HUMANS], level.teamScores[TEAM_DROIDS] ); + } + + for (i=0 ; i < numSorted ; i++) { + int ping; + + cl = &level.clients[level.sortedClients[i]]; + + if ( cl->sess.sessionTeam == TEAM_SPECTATOR ) { + continue; + } + if ( cl->pers.connected == CON_CONNECTING ) { + continue; + } + + ping = cl->ps.ping < 999 ? cl->ps.ping : 999; + + G_LogPrintf( "score: %i ping: %i client: %i %s\n", + cl->ps.persistant[PERS_SCORE], ping, level.sortedClients[i], + cl->pers.netname ); + + } +} + + +/* +================= +CheckIntermissionExit + +The level will stay at the intermission for a minimum of 5 seconds +If all players wish to continue, the level will then exit. +If one or more players have not acknowledged the continue, the game will +wait 10 seconds before going on. +================= +*/ +void CheckIntermissionExit( void ) { + int ready, notReady; + int i; + gclient_t *cl; + int readyMask; + + if ( g_gametype.integer == GT_SINGLE_PLAYER ) { + return; + } + + // see which players are ready + ready = 0; + notReady = 0; + readyMask = 0; + for (i=0 ; i< g_maxclients.integer ; i++) { + cl = level.clients + i; + if ( cl->pers.connected != CON_CONNECTED ) { + continue; + } + if ( g_entities[cl->ps.clientNum].r.svFlags & SVF_BOT ) { + continue; + } + + if ( cl->readyToExit ) { + ready++; + if ( i < 16 ) { + readyMask |= 1 << i; + } + } else { + notReady++; + } + } + + // copy the readyMask to each player's stats so + // it can be displayed on the scoreboard + for (i=0 ; i< g_maxclients.integer ; i++) { + cl = level.clients + i; + if ( cl->pers.connected != CON_CONNECTED ) { + continue; + } + cl->ps.stats[STAT_CLIENTS_READY] = readyMask; + } + + // never exit in less than five seconds + if ( level.time < level.intermissiontime + 5000 ) { + return; + } + + // if nobody wants to go, clear timer + if ( !ready ) { + level.readyToExit = qfalse; + return; + } + + // if everyone wants to go, go now + if ( !notReady ) { + ExitLevel(); + return; + } + + // the first person to ready starts the ten second timeout + if ( !level.readyToExit ) { + level.readyToExit = qtrue; + level.exitTime = level.time; + } + + // if we have waited ten seconds since at least one player + // wanted to exit, go ahead + if ( level.time < level.exitTime + 10000 ) { + return; + } + + ExitLevel(); +} + +/* +============= +ScoreIsTied +============= +*/ +qboolean ScoreIsTied( void ) { + int a, b; + + if ( level.numPlayingClients < 2 ) { + return qfalse; + } + + if ( g_gametype.integer >= GT_TEAM ) { + return level.teamScores[TEAM_HUMANS] == level.teamScores[TEAM_DROIDS]; + } + + a = level.clients[level.sortedClients[0]].ps.persistant[PERS_SCORE]; + b = level.clients[level.sortedClients[1]].ps.persistant[PERS_SCORE]; + + return a == b; +} + +/* +================= +CheckExitRules + +There will be a delay between the time the exit is qualified for +and the time everyone is moved to the intermission spot, so you +can see the last frag. +================= +*/ +void CheckExitRules( void ) { + int i; + gclient_t *cl; + + // if at the intermission, wait for all non-bots to + // signal ready, then go to next level + if ( level.intermissiontime ) { + CheckIntermissionExit (); + return; + } + + if ( level.intermissionQueued ) { + if ( level.time - level.intermissionQueued >= INTERMISSION_DELAY_TIME ) { + level.intermissionQueued = 0; + BeginIntermission(); + } + return; + } + + // check for sudden death + if ( ScoreIsTied() ) { + // always wait for sudden death + return; + } + + if ( g_timelimit.integer && !level.warmupTime ) { + if ( level.time - level.startTime >= g_timelimit.integer*60000 ) { + trap_SendServerCommand( -1, "print \"Timelimit hit.\n\""); + LogExit( "Timelimit hit." ); + return; + } + } + + /*trap_SendServerCommand( -1, va("print \"%d %d %d %d %d %d\n\"", + level.numDroidClients, + level.numLiveDroidClients, + level.numHumanClients, + level.numLiveHumanClients, + level.numDroidSpawns, + level.numHumanSpawns ) );*/ + + //TA: end the game on these conditions + if( ( level.time > level.startTime + 1000 ) && ( level.numDroidSpawns == 0 ) && ( level.numLiveDroidClients == 0 ) ) + { + //droids lose + trap_SendServerCommand( -1, "print \"Humans win.\n\""); + LogExit( "Humans win." ); + return; + } + else if( ( level.time > level.startTime + 1000 ) && ( level.numHumanSpawns == 0 ) && ( level.numLiveHumanClients == 0 ) ) + { + //humans lose + trap_SendServerCommand( -1, "print \"Droids win.\n\""); + LogExit( "Droids win." ); + return; + } + + if ( level.numPlayingClients < 2 ) { + return; + } + + if ( g_gametype.integer < GT_CTF && g_fraglimit.integer ) { + if ( level.teamScores[TEAM_HUMANS] >= g_fraglimit.integer ) { + trap_SendServerCommand( -1, "print \"Red hit the fraglimit.\n\"" ); + LogExit( "Fraglimit hit." ); + return; + } + + if ( level.teamScores[TEAM_DROIDS] >= g_fraglimit.integer ) { + trap_SendServerCommand( -1, "print \"Blue hit the fraglimit.\n\"" ); + LogExit( "Fraglimit hit." ); + return; + } + + for ( i=0 ; i< g_maxclients.integer ; i++ ) { + cl = level.clients + i; + if ( cl->pers.connected != CON_CONNECTED ) { + continue; + } + if ( cl->sess.sessionTeam != TEAM_FREE ) { + continue; + } + + if ( cl->ps.persistant[PERS_SCORE] >= g_fraglimit.integer ) { + LogExit( "Fraglimit hit." ); + trap_SendServerCommand( -1, va("print \"%s" S_COLOR_WHITE " hit the fraglimit.\n\"", + cl->pers.netname ) ); + return; + } + } + } + + if ( g_gametype.integer >= GT_CTF && g_capturelimit.integer ) { + + if ( level.teamScores[TEAM_HUMANS] >= g_capturelimit.integer ) { + trap_SendServerCommand( -1, "print \"Red hit the capturelimit.\n\"" ); + LogExit( "Capturelimit hit." ); + return; + } + + if ( level.teamScores[TEAM_DROIDS] >= g_capturelimit.integer ) { + trap_SendServerCommand( -1, "print \"Blue hit the capturelimit.\n\"" ); + LogExit( "Capturelimit hit." ); + return; + } + } +} + + + +/* +======================================================================== + +FUNCTIONS CALLED EVERY FRAME + +======================================================================== +*/ + + +/* +============= +CheckTournament + +Once a frame, check for changes in tournement player state +============= +*/ +void CheckTournament( void ) { + if ( level.numPlayingClients == 0 ) { + return; + } + + if ( g_gametype.integer == GT_TOURNAMENT ) { + + // pull in a spectator if needed + if ( level.numPlayingClients < 2 ) { + AddTournamentPlayer(); + } + + // if we don't have two players, go back to "waiting for players" + if ( level.numPlayingClients != 2 ) { + if ( level.warmupTime != -1 ) { + level.warmupTime = -1; + trap_SetConfigstring( CS_WARMUP, va("%i", level.warmupTime) ); + G_LogPrintf( "Warmup:\n" ); + } + return; + } + + if ( level.warmupTime == 0 ) { + return; + } + + // if the warmup is changed at the console, restart it + if ( g_warmup.modificationCount != level.warmupModificationCount ) { + level.warmupModificationCount = g_warmup.modificationCount; + level.warmupTime = -1; + } + + // if all players have arrived, start the countdown + if ( level.warmupTime < 0 ) { + if ( level.numPlayingClients == 2 ) { + // fudge by -1 to account for extra delays + level.warmupTime = level.time + ( g_warmup.integer - 1 ) * 1000; + trap_SetConfigstring( CS_WARMUP, va("%i", level.warmupTime) ); + } + return; + } + + // if the warmup time has counted down, restart + if ( level.time > level.warmupTime ) { + level.warmupTime += 10000; + trap_Cvar_Set( "g_restarted", "1" ); + trap_SendConsoleCommand( EXEC_APPEND, "map_restart 0\n" ); + level.restarted = qtrue; + return; + } + } else if ( g_gametype.integer != GT_SINGLE_PLAYER && level.warmupTime != 0 ) { + int counts[TEAM_NUM_TEAMS]; + qboolean notEnough = qfalse; + + if ( g_gametype.integer > GT_TEAM ) { + counts[TEAM_DROIDS] = TeamCount( -1, TEAM_DROIDS ); + counts[TEAM_HUMANS] = TeamCount( -1, TEAM_HUMANS ); + + if (counts[TEAM_HUMANS] < 1 || counts[TEAM_DROIDS] < 1) { + notEnough = qtrue; + } + } else if ( level.numPlayingClients < 2 ) { + notEnough = qtrue; + } + + if ( notEnough ) { + if ( level.warmupTime != -1 ) { + level.warmupTime = -1; + trap_SetConfigstring( CS_WARMUP, va("%i", level.warmupTime) ); + G_LogPrintf( "Warmup:\n" ); + } + return; // still waiting for team members + } + + if ( level.warmupTime == 0 ) { + return; + } + + // if the warmup is changed at the console, restart it + if ( g_warmup.modificationCount != level.warmupModificationCount ) { + level.warmupModificationCount = g_warmup.modificationCount; + level.warmupTime = -1; + } + + // if all players have arrived, start the countdown + if ( level.warmupTime < 0 ) { + // fudge by -1 to account for extra delays + level.warmupTime = level.time + ( g_warmup.integer - 1 ) * 1000; + trap_SetConfigstring( CS_WARMUP, va("%i", level.warmupTime) ); + return; + } + + // if the warmup time has counted down, restart + if ( level.time > level.warmupTime ) { + level.warmupTime += 10000; + trap_Cvar_Set( "g_restarted", "1" ); + trap_SendConsoleCommand( EXEC_APPEND, "map_restart 0\n" ); + level.restarted = qtrue; + return; + } + } +} + + +/* +================== +CheckVote +================== +*/ +void CheckVote( void ) { + if ( level.voteExecuteTime && level.voteExecuteTime < level.time ) { + level.voteExecuteTime = 0; + trap_SendConsoleCommand( EXEC_APPEND, va("%s\n", level.voteString ) ); + } + if ( !level.voteTime ) { + return; + } + if ( level.time - level.voteTime >= VOTE_TIME ) { + trap_SendServerCommand( -1, "print \"Vote failed.\n\"" ); + } else { + if ( level.voteYes > level.numVotingClients/2 ) { + // execute the command, then remove the vote + trap_SendServerCommand( -1, "print \"Vote passed.\n\"" ); + level.voteExecuteTime = level.time + 3000; + } else if ( level.voteNo >= level.numVotingClients/2 ) { + // same behavior as a timeout + trap_SendServerCommand( -1, "print \"Vote failed.\n\"" ); + } else { + // still waiting for a majority + return; + } + } + level.voteTime = 0; + trap_SetConfigstring( CS_VOTE_TIME, "" ); + +} + + +/* +================== +PrintTeam +================== +*/ +void PrintTeam(int team, char *message) { + int i; + + for ( i = 0 ; i < level.maxclients ; i++ ) { + if (level.clients[i].sess.sessionTeam != team) + continue; + trap_SendServerCommand( i, message ); + } +} + +/* +================== +SetLeader +================== +*/ +void SetLeader(int team, int client) { + int i; + + if ( level.clients[client].pers.connected == CON_DISCONNECTED ) { + PrintTeam(team, va("print \"%s is not connected\n\"", level.clients[client].pers.netname) ); + return; + } + if (level.clients[client].sess.sessionTeam != team) { + PrintTeam(team, va("print \"%s is not on the team anymore\n\"", level.clients[client].pers.netname) ); + return; + } + for ( i = 0 ; i < level.maxclients ; i++ ) { + if (level.clients[i].sess.sessionTeam != team) + continue; + if (level.clients[i].sess.teamLeader) { + level.clients[i].sess.teamLeader = qfalse; + ClientUserinfoChanged(i); + } + } + level.clients[client].sess.teamLeader = qtrue; + ClientUserinfoChanged( client ); + PrintTeam(team, va("print \"%s is the new team leader\n\"", level.clients[client].pers.netname) ); +} + +/* +================== +CheckTeamLeader +================== +*/ +void CheckTeamLeader( int team ) { + int i; + + for ( i = 0 ; i < level.maxclients ; i++ ) { + if (level.clients[i].sess.sessionTeam != team) + continue; + if (level.clients[i].sess.teamLeader) + break; + } + if (i >= level.maxclients) { + for ( i = 0 ; i < level.maxclients ; i++ ) { + if (level.clients[i].sess.sessionTeam != team) + continue; + if (!(g_entities[i].r.svFlags & SVF_BOT)) { + level.clients[i].sess.teamLeader = qtrue; + break; + } + } + for ( i = 0 ; i < level.maxclients ; i++ ) { + if (level.clients[i].sess.sessionTeam != team) + continue; + level.clients[i].sess.teamLeader = qtrue; + break; + } + } +} + +/* +================== +CheckTeamVote +================== +*/ +void CheckTeamVote( int team ) { + int cs_offset; + + if ( team == TEAM_HUMANS ) + cs_offset = 0; + else if ( team == TEAM_DROIDS ) + cs_offset = 1; + else + return; + + if ( !level.teamVoteTime[cs_offset] ) { + return; + } + if ( level.time - level.teamVoteTime[cs_offset] >= VOTE_TIME ) { + trap_SendServerCommand( -1, "print \"Team vote failed.\n\"" ); + } else { + if ( level.teamVoteYes[cs_offset] > level.numteamVotingClients[cs_offset]/2 ) { + // execute the command, then remove the vote + trap_SendServerCommand( -1, "print \"Team vote passed.\n\"" ); + // + if ( !Q_strncmp( "leader", level.teamVoteString[cs_offset], 6) ) { + //set the team leader + SetLeader(team, atoi(level.teamVoteString[cs_offset] + 7)); + } + else { + trap_SendConsoleCommand( EXEC_APPEND, va("%s\n", level.teamVoteString[cs_offset] ) ); + } + } else if ( level.teamVoteNo[cs_offset] >= level.numteamVotingClients[cs_offset]/2 ) { + // same behavior as a timeout + trap_SendServerCommand( -1, "print \"Team vote failed.\n\"" ); + } else { + // still waiting for a majority + return; + } + } + level.teamVoteTime[cs_offset] = 0; + trap_SetConfigstring( CS_TEAMVOTE_TIME + cs_offset, "" ); + +} + + +/* +================== +CheckCvars +================== +*/ +void CheckCvars( void ) { + static int lastMod = -1; + + if ( g_password.modificationCount != lastMod ) { + lastMod = g_password.modificationCount; + if ( *g_password.string && Q_stricmp( g_password.string, "none" ) ) { + trap_Cvar_Set( "g_needpass", "1" ); + } else { + trap_Cvar_Set( "g_needpass", "0" ); + } + } +} + +/* +============= +G_RunThink + +Runs thinking code for this frame if necessary +============= +*/ +void G_RunThink (gentity_t *ent) { + float thinktime; + + thinktime = ent->nextthink; + if (thinktime <= 0) { + return; + } + if (thinktime > level.time) { + return; + } + + ent->nextthink = 0; + if (!ent->think) { + G_Error ( "NULL ent->think"); + } + ent->think (ent); +} + +/* +================ +G_RunFrame + +Advances the non-player objects in the world +================ +*/ +void G_RunFrame( int levelTime ) { + int i; + gentity_t *ent; + int msec; +int start, end; + + // if we are waiting for the level to restart, do nothing + if ( level.restarted ) { + return; + } + + level.framenum++; + level.previousTime = level.time; + level.time = levelTime; + msec = level.time - level.previousTime; + + // get any cvar changes + G_UpdateCvars(); + + // + // go through all allocated objects + // +start = trap_Milliseconds(); + ent = &g_entities[0]; + for (i=0 ; i<level.num_entities ; i++, ent++) { + if ( !ent->inuse ) { + continue; + } + + // clear events that are too old + if ( level.time - ent->eventTime > EVENT_VALID_MSEC ) { + if ( ent->s.event ) { + ent->s.event = 0; // &= EV_EVENT_BITS; + if ( ent->client ) { + ent->client->ps.externalEvent = 0; + //ent->client->ps.events[0] = 0; + //ent->client->ps.events[1] = 0; + } + } + if ( ent->freeAfterEvent ) { + // tempEntities or dropped items completely go away after their event + G_FreeEntity( ent ); + continue; + } else if ( ent->unlinkAfterEvent ) { + // items that will respawn will hide themselves after their pickup event + ent->unlinkAfterEvent = qfalse; + trap_UnlinkEntity( ent ); + } + } + + // temporary entities don't think + if ( ent->freeAfterEvent ) { + continue; + } + + if ( !ent->r.linked && ent->neverFree ) { + continue; + } + + if ( ent->s.eType == ET_MISSILE ) { + G_RunMissile( ent ); + continue; + } + + if ( ( ent->s.eType == ET_ITEM || ent->s.eType == ET_BUILDABLE ) || ent->physicsObject ) { + G_RunItem( ent ); + continue; + } + + if ( ent->s.eType == ET_MOVER ) { + G_RunMover( ent ); + continue; + } + + if ( i < MAX_CLIENTS ) { + G_RunClient( ent ); + continue; + } + + G_RunThink( ent ); + } +end = trap_Milliseconds(); + +start = trap_Milliseconds(); + // perform final fixups on the players + ent = &g_entities[0]; + for (i=0 ; i < level.maxclients ; i++, ent++ ) { + if ( ent->inuse ) { + ClientEndFrame( ent ); + } + } +end = trap_Milliseconds(); + + // see if it is time to do a tournement restart + CheckTournament(); + + // see if it is time to end the level + CheckExitRules(); + + // update to team status? + CheckTeamStatus(); + + // cancel vote if timed out + CheckVote(); + + // check team votes + CheckTeamVote( TEAM_HUMANS ); + CheckTeamVote( TEAM_DROIDS ); + + // for tracking changes + CheckCvars(); + + if (g_listEntity.integer) { + for (i = 0; i < MAX_GENTITIES; i++) { + G_Printf("%4i: %s\n", i, g_entities[i].classname); + } + trap_Cvar_Set("g_listEntity", "0"); + } +} + diff --git a/src/game/g_mem.c b/src/game/g_mem.c new file mode 100644 index 00000000..a9ea5363 --- /dev/null +++ b/src/game/g_mem.c @@ -0,0 +1,64 @@ +// Copyright (C) 1999-2000 Id Software, Inc. +// +// +// g_mem.c +// + +/* + * Portions Copyright (C) 2000-2001 Tim Angus + * + * This program 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, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ + +/* To assertain which portions are licensed under the GPL and which are + * licensed by Id Software, Inc. please run a diff between the equivalent + * versions of the "Tremulous" modification and the unmodified "Quake3" + * game source code. + */ + +#include "g_local.h" + + +#define POOLSIZE (256 * 1024) + +static char memoryPool[POOLSIZE]; +static int allocPoint; + +void *G_Alloc( int size ) { + char *p; + + if ( g_debugAlloc.integer ) { + G_Printf( "G_Alloc of %i bytes (%i left)\n", size, POOLSIZE - allocPoint - ( ( size + 31 ) & ~31 ) ); + } + + if ( allocPoint + size > POOLSIZE ) { + G_Error( "G_Alloc: failed on allocation of %u bytes\n", size ); + return NULL; + } + + p = &memoryPool[allocPoint]; + + allocPoint += ( size + 31 ) & ~31; + + return p; +} + +void G_InitMemory( void ) { + allocPoint = 0; +} + +void Svcmd_GameMem_f( void ) { + G_Printf( "Game memory status: %i out of %i bytes allocated\n", allocPoint, POOLSIZE ); +} diff --git a/src/game/g_misc.c b/src/game/g_misc.c new file mode 100644 index 00000000..56fe9f8f --- /dev/null +++ b/src/game/g_misc.c @@ -0,0 +1,384 @@ +// Copyright (C) 1999-2000 Id Software, Inc. +// +// g_misc.c + +/* + * Portions Copyright (C) 2000-2001 Tim Angus + * + * This program 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, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ + +/* To assertain which portions are licensed under the GPL and which are + * licensed by Id Software, Inc. please run a diff between the equivalent + * versions of the "Tremulous" modification and the unmodified "Quake3" + * game source code. + */ + +#include "g_local.h" + + +/*QUAKED func_group (0 0 0) ? +Used to group brushes together just for editor convenience. They are turned into normal brushes by the utilities. +*/ + + +/*QUAKED info_camp (0 0.5 0) (-4 -4 -4) (4 4 4) +Used as a positional target for calculations in the utilities (spotlights, etc), but removed during gameplay. +*/ +void SP_info_camp( gentity_t *self ) { + G_SetOrigin( self, self->s.origin ); +} + + +/*QUAKED info_null (0 0.5 0) (-4 -4 -4) (4 4 4) +Used as a positional target for calculations in the utilities (spotlights, etc), but removed during gameplay. +*/ +void SP_info_null( gentity_t *self ) { + G_FreeEntity( self ); +} + + +/*QUAKED info_notnull (0 0.5 0) (-4 -4 -4) (4 4 4) +Used as a positional target for in-game calculation, like jumppad targets. +target_position does the same thing +*/ +void SP_info_notnull( gentity_t *self ){ + G_SetOrigin( self, self->s.origin ); +} + + +/*QUAKED light (0 1 0) (-8 -8 -8) (8 8 8) linear +Non-displayed light. +"light" overrides the default 300 intensity. +Linear checbox gives linear falloff instead of inverse square +Lights pointed at a target will be spotlights. +"radius" overrides the default 64 unit radius of a spotlight at the target point. +*/ +void SP_light( gentity_t *self ) { + G_FreeEntity( self ); +} + +#define TORCHR 200 +#define TORCHG 200 +#define TORCHB 255 + +//TA: position/colour/intensity calculating function +void ShineTorch( gentity_t *self ) +{ + trace_t tr; + vec3_t from, to, length; + vec3_t angles, forward; + int r, g, b, i; + int veclength; + + VectorCopy( self->parent->s.pos.trBase, from ); + VectorCopy( self->parent->s.apos.trBase, angles ); + + from[2] += self->parent->client->ps.viewheight; + + AngleVectors( angles, forward, NULL, NULL ); + VectorMA( from, 4096, forward, to ); + + trap_Trace( &tr, from, NULL, NULL, to, self->parent->s.number, MASK_SOLID ); + + VectorSubtract( tr.endpos, from, length ); + veclength = VectorLength( length ); + + //hack to place the light source slightly in front of what the player is pointing at + VectorMA( tr.endpos, -(veclength / 5), forward, tr.endpos ); + + //adjust brightness and intensity based on how far away subject is + r = TORCHR * 450 / veclength; + g = TORCHG * 450 / veclength; + b = TORCHB * 450 / veclength; + if( r > TORCHR ) r = TORCHR; + if( g > TORCHG ) g = TORCHG; + if( b > TORCHB ) b = TORCHB; + + i = veclength / 2; + if( i > 255 ) i = 255; + if( i < 0 ) i = 0; + + self->s.constantLight = r | ( g << 8 ) | ( b << 16 ) | ( i << 24 ); + + if( tr.fraction < 1.0 ) + G_SetOrigin( self, tr.endpos ); + else + G_SetOrigin( self, to ); + + trap_LinkEntity( self ); +} + + + +/* +================================================================================= + +TELEPORTERS + +================================================================================= +*/ + +void TeleportPlayer( gentity_t *player, vec3_t origin, vec3_t angles ) { + gentity_t *tent; + + // use temp events at source and destination to prevent the effect + // from getting dropped by a second player event + if ( player->client->sess.sessionTeam != TEAM_SPECTATOR ) { + tent = G_TempEntity( player->client->ps.origin, EV_PLAYER_TELEPORT_OUT ); + tent->s.clientNum = player->s.clientNum; + + tent = G_TempEntity( origin, EV_PLAYER_TELEPORT_IN ); + tent->s.clientNum = player->s.clientNum; + } + + // unlink to make sure it can't possibly interfere with G_KillBox + trap_UnlinkEntity (player); + + VectorCopy ( origin, player->client->ps.origin ); + player->client->ps.origin[2] += 1; + + // spit the player out + AngleVectors( angles, player->client->ps.velocity, NULL, NULL ); + VectorScale( player->client->ps.velocity, 400, player->client->ps.velocity ); + player->client->ps.pm_time = 160; // hold time + player->client->ps.pm_flags |= PMF_TIME_KNOCKBACK; + + // toggle the teleport bit so the client knows to not lerp + player->client->ps.eFlags ^= EF_TELEPORT_BIT; + + // set angles + SetClientViewAngle( player, angles ); + + // kill anything at the destination + if ( player->client->sess.sessionTeam != TEAM_SPECTATOR ) { + G_KillBox (player); + } + + // save results of pmove + BG_PlayerStateToEntityState( &player->client->ps, &player->s, qtrue ); + + // use the precise origin for linking + VectorCopy( player->client->ps.origin, player->r.currentOrigin ); + + if ( player->client->sess.sessionTeam != TEAM_SPECTATOR ) { + trap_LinkEntity (player); + } +} + + +/*QUAKED misc_teleporter_dest (1 0 0) (-32 -32 -24) (32 32 -16) +Point teleporters at these. +Now that we don't have teleport destination pads, this is just +an info_notnull +*/ +void SP_misc_teleporter_dest( gentity_t *ent ) { +} + + +//=========================================================== + +/*QUAKED misc_model (1 0 0) (-16 -16 -16) (16 16 16) +"model" arbitrary .md3 file to display +*/ +void SP_misc_model( gentity_t *ent ) { + +#if 0 + ent->s.modelindex = G_ModelIndex( ent->model ); + VectorSet (ent->mins, -16, -16, -16); + VectorSet (ent->maxs, 16, 16, 16); + trap_LinkEntity (ent); + + G_SetOrigin( ent, ent->s.origin ); + VectorCopy( ent->s.angles, ent->s.apos.trBase ); +#else + G_FreeEntity( ent ); +#endif +} + +//=========================================================== + +void locateCamera( gentity_t *ent ) { + vec3_t dir; + gentity_t *target; + gentity_t *owner; + + owner = G_PickTarget( ent->target ); + if ( !owner ) { + G_Printf( "Couldn't find target for misc_partal_surface\n" ); + G_FreeEntity( ent ); + return; + } + ent->r.ownerNum = owner->s.number; + + // frame holds the rotate speed + if ( owner->spawnflags & 1 ) { + ent->s.frame = 25; + } else if ( owner->spawnflags & 2 ) { + ent->s.frame = 75; + } + + // set to 0 for no rotation at all + ent->s.powerups = 1; + + // clientNum holds the rotate offset + ent->s.clientNum = owner->s.clientNum; + + VectorCopy( owner->s.origin, ent->s.origin2 ); + + // see if the portal_camera has a target + target = G_PickTarget( owner->target ); + if ( target ) { + VectorSubtract( target->s.origin, owner->s.origin, dir ); + VectorNormalize( dir ); + } else { + G_SetMovedir( owner->s.angles, dir ); + } + + ent->s.eventParm = DirToByte( dir ); +} + +/*QUAKED misc_portal_surface (0 0 1) (-8 -8 -8) (8 8 8) +The portal surface nearest this entity will show a view from the targeted misc_portal_camera, or a mirror view if untargeted. +This must be within 64 world units of the surface! +*/ +void SP_misc_portal_surface(gentity_t *ent) { + VectorClear( ent->r.mins ); + VectorClear( ent->r.maxs ); + trap_LinkEntity (ent); + + ent->r.svFlags = SVF_PORTAL; + ent->s.eType = ET_PORTAL; + + if ( !ent->target ) { + VectorCopy( ent->s.origin, ent->s.origin2 ); + } else { + ent->think = locateCamera; + ent->nextthink = level.time + 100; + } +} + +/*QUAKED misc_portal_camera (0 0 1) (-8 -8 -8) (8 8 8) slowrotate fastrotate +The target for a misc_portal_director. You can set either angles or target another entity to determine the direction of view. +"roll" an angle modifier to orient the camera around the target vector; +*/ +void SP_misc_portal_camera(gentity_t *ent) { + float roll; + + VectorClear( ent->r.mins ); + VectorClear( ent->r.maxs ); + trap_LinkEntity (ent); + + G_SpawnFloat( "roll", "0", &roll ); + + ent->s.clientNum = roll/360.0 * 256; +} + +/* +====================================================================== + + SHOOTERS + +====================================================================== +*/ + +void Use_Shooter( gentity_t *ent, gentity_t *other, gentity_t *activator ) { + vec3_t dir; + float deg; + vec3_t up, right; + + // see if we have a target + if ( ent->enemy ) { + VectorSubtract( ent->enemy->r.currentOrigin, ent->s.origin, dir ); + VectorNormalize( dir ); + } else { + VectorCopy( ent->movedir, dir ); + } + + // randomize a bit + PerpendicularVector( up, dir ); + CrossProduct( up, dir, right ); + + deg = crandom() * ent->random; + VectorMA( dir, deg, up, dir ); + + deg = crandom() * ent->random; + VectorMA( dir, deg, right, dir ); + + VectorNormalize( dir ); + + switch ( ent->s.weapon ) { + case WP_GRENADE_LAUNCHER: + fire_grenade( ent, ent->s.origin, dir ); + break; + case WP_ROCKET_LAUNCHER: + fire_rocket( ent, ent->s.origin, dir ); + break; + } + + G_AddEvent( ent, EV_FIRE_WEAPON, 0 ); +} + + +static void InitShooter_Finish( gentity_t *ent ) { + ent->enemy = G_PickTarget( ent->target ); + ent->think = 0; + ent->nextthink = 0; +} + +void InitShooter( gentity_t *ent, int weapon ) { + ent->use = Use_Shooter; + ent->s.weapon = weapon; + + RegisterItem( BG_FindItemForWeapon( weapon ) ); + + G_SetMovedir( ent->s.angles, ent->movedir ); + + if ( !ent->random ) { + ent->random = 1.0; + } + ent->random = sin( M_PI * ent->random / 180 ); + // target might be a moving object, so we can't set movedir for it + if ( ent->target ) { + ent->think = InitShooter_Finish; + ent->nextthink = level.time + 500; + } + trap_LinkEntity( ent ); +} + +/*QUAKED shooter_rocket (1 0 0) (-16 -16 -16) (16 16 16) +Fires at either the target or the current direction. +"random" the number of degrees of deviance from the taget. (1.0 default) +*/ +void SP_shooter_rocket( gentity_t *ent ) { + //InitShooter( ent, WP_ROCKET_LAUNCHER ); +} + +/*QUAKED shooter_plasma (1 0 0) (-16 -16 -16) (16 16 16) +Fires at either the target or the current direction. +"random" is the number of degrees of deviance from the taget. (1.0 default) +*/ +void SP_shooter_plasma( gentity_t *ent ) { + //InitShooter( ent, WP_PLASMAGUN); +} + +/*QUAKED shooter_grenade (1 0 0) (-16 -16 -16) (16 16 16) +Fires at either the target or the current direction. +"random" is the number of degrees of deviance from the taget. (1.0 default) +*/ +void SP_shooter_grenade( gentity_t *ent ) { + //InitShooter( ent, WP_GRENADE_LAUNCHER); +} + diff --git a/src/game/g_missile.c b/src/game/g_missile.c new file mode 100644 index 00000000..20a76163 --- /dev/null +++ b/src/game/g_missile.c @@ -0,0 +1,519 @@ +// Copyright (C) 1999-2000 Id Software, Inc. +// + +/* + * Portions Copyright (C) 2000-2001 Tim Angus + * + * This program 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, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ + +/* To assertain which portions are licensed under the GPL and which are + * licensed by Id Software, Inc. please run a diff between the equivalent + * versions of the "Tremulous" modification and the unmodified "Quake3" + * game source code. + */ + +#include "g_local.h" + +#define MISSILE_PRESTEP_TIME 50 + +/* +================ +G_BounceMissile + +================ +*/ +void G_BounceMissile( gentity_t *ent, trace_t *trace ) { + vec3_t velocity; + float dot; + int hitTime; + + // reflect the velocity on the trace plane + hitTime = level.previousTime + ( level.time - level.previousTime ) * trace->fraction; + BG_EvaluateTrajectoryDelta( &ent->s.pos, hitTime, velocity ); + dot = DotProduct( velocity, trace->plane.normal ); + VectorMA( velocity, -2*dot, trace->plane.normal, ent->s.pos.trDelta ); + + if ( ent->s.eFlags & EF_BOUNCE_HALF ) { + VectorScale( ent->s.pos.trDelta, 0.65, ent->s.pos.trDelta ); + // check for stop + if ( trace->plane.normal[2] > 0.2 && VectorLength( ent->s.pos.trDelta ) < 40 ) { + G_SetOrigin( ent, trace->endpos ); + return; + } + } + + VectorAdd( ent->r.currentOrigin, trace->plane.normal, ent->r.currentOrigin); + VectorCopy( ent->r.currentOrigin, ent->s.pos.trBase ); + ent->s.pos.trTime = level.time; +} + + +/* +================ +G_ExplodeMissile + +Explode a missile without an impact +================ +*/ +void G_ExplodeMissile( gentity_t *ent ) { + vec3_t dir; + vec3_t origin; + + BG_EvaluateTrajectory( &ent->s.pos, level.time, origin ); + SnapVector( origin ); + G_SetOrigin( ent, origin ); + + // we don't have a valid direction, so just point straight up + dir[0] = dir[1] = 0; + dir[2] = 1; + + ent->s.eType = ET_GENERAL; + G_AddEvent( ent, EV_MISSILE_MISS, DirToByte( dir ) ); + + ent->freeAfterEvent = qtrue; + + // splash damage + if ( ent->splashDamage ) { + if( G_RadiusDamage( ent->r.currentOrigin, ent->parent, ent->splashDamage, ent->splashRadius, ent + , ent->splashMethodOfDeath ) ) { + g_entities[ent->r.ownerNum].client->accuracy_hits++; + } + } + + trap_LinkEntity( ent ); +} + + +/* +================ +G_MissileImpact + +================ +*/ +void G_MissileImpact( gentity_t *ent, trace_t *trace ) { + gentity_t *other; + qboolean hitClient = qfalse; + + other = &g_entities[trace->entityNum]; + + // check for bounce + if ( !other->takedamage && + ( ent->s.eFlags & ( EF_BOUNCE | EF_BOUNCE_HALF ) ) ) { + G_BounceMissile( ent, trace ); + G_AddEvent( ent, EV_GRENADE_BOUNCE, 0 ); + return; + } + + // impact damage + if (other->takedamage) { + // FIXME: wrong damage direction? + if ( ent->damage ) { + vec3_t velocity; + + if( LogAccuracyHit( other, &g_entities[ent->r.ownerNum] ) ) { + g_entities[ent->r.ownerNum].client->accuracy_hits++; + hitClient = qtrue; + } + BG_EvaluateTrajectoryDelta( &ent->s.pos, level.time, velocity ); + if ( VectorLength( velocity ) == 0 ) { + velocity[2] = 1; // stepped on a grenade + } + G_Damage (other, ent, &g_entities[ent->r.ownerNum], velocity, + ent->s.origin, ent->damage, + 0, ent->methodOfDeath); + } + } + + if (!strcmp(ent->classname, "hook")) { + gentity_t *nent; + vec3_t v; + + nent = G_Spawn(); + if ( other->takedamage && other->client ) { + + G_AddEvent( nent, EV_MISSILE_HIT, DirToByte( trace->plane.normal ) ); + nent->s.otherEntityNum = other->s.number; + + ent->enemy = other; + + v[0] = other->r.currentOrigin[0] + (other->r.mins[0] + other->r.maxs[0]) * 0.5; + v[1] = other->r.currentOrigin[1] + (other->r.mins[1] + other->r.maxs[1]) * 0.5; + v[2] = other->r.currentOrigin[2] + (other->r.mins[2] + other->r.maxs[2]) * 0.5; + + SnapVectorTowards( v, ent->s.pos.trBase ); // save net bandwidth + } else { + VectorCopy(trace->endpos, v); + G_AddEvent( nent, EV_MISSILE_MISS, DirToByte( trace->plane.normal ) ); + ent->enemy = NULL; + } + + SnapVectorTowards( v, ent->s.pos.trBase ); // save net bandwidth + + nent->freeAfterEvent = qtrue; + // change over to a normal entity right at the point of impact + nent->s.eType = ET_GENERAL; + ent->s.eType = ET_GRAPPLE; + + G_SetOrigin( ent, v ); + G_SetOrigin( nent, v ); + + ent->think = Weapon_HookThink; + ent->nextthink = level.time + FRAMETIME; + + ent->parent->client->ps.pm_flags |= PMF_GRAPPLE_PULL; + VectorCopy( ent->r.currentOrigin, ent->parent->client->ps.grapplePoint); + + trap_LinkEntity( ent ); + trap_LinkEntity( nent ); + + return; + } + + // is it cheaper in bandwidth to just remove this ent and create a new + // one, rather than changing the missile into the explosion? + + if ( other->takedamage && other->client ) { + G_AddEvent( ent, EV_MISSILE_HIT, DirToByte( trace->plane.normal ) ); + ent->s.otherEntityNum = other->s.number; + } else if( trace->surfaceFlags & SURF_METALSTEPS ) { + G_AddEvent( ent, EV_MISSILE_MISS_METAL, DirToByte( trace->plane.normal ) ); + } else { + G_AddEvent( ent, EV_MISSILE_MISS, DirToByte( trace->plane.normal ) ); + } + + ent->freeAfterEvent = qtrue; + + // change over to a normal entity right at the point of impact + ent->s.eType = ET_GENERAL; + + SnapVectorTowards( trace->endpos, ent->s.pos.trBase ); // save net bandwidth + + G_SetOrigin( ent, trace->endpos ); + + // splash damage (doesn't apply to person directly hit) + if ( ent->splashDamage ) { + if( G_RadiusDamage( trace->endpos, ent->parent, ent->splashDamage, ent->splashRadius, + other, ent->splashMethodOfDeath ) ) { + if( !hitClient ) { + g_entities[ent->r.ownerNum].client->accuracy_hits++; + } + } + } + + trap_LinkEntity( ent ); +} + + +/* +================ +G_RunMissile + +================ +*/ +void G_RunMissile( gentity_t *ent ) { + vec3_t origin; + trace_t tr; + int passent; + + // get current position + BG_EvaluateTrajectory( &ent->s.pos, level.time, origin ); + + // if this missile bounced off an invulnerability sphere + if ( ent->target_ent ) { + passent = ent->target_ent->s.number; + } + else { + // ignore interactions with the missile owner + passent = ent->r.ownerNum; + } + // trace a line from the previous position to the current position + trap_Trace( &tr, ent->r.currentOrigin, ent->r.mins, ent->r.maxs, origin, passent, ent->clipmask ); + + if ( tr.startsolid || tr.allsolid ) { + // make sure the tr.entityNum is set to the entity we're stuck in + trap_Trace( &tr, ent->r.currentOrigin, ent->r.mins, ent->r.maxs, ent->r.currentOrigin, passent, ent->clipmask ); + tr.fraction = 0; + } + else { + VectorCopy( tr.endpos, ent->r.currentOrigin ); + } + + trap_LinkEntity( ent ); + + if ( tr.fraction != 1 ) { + // never explode or bounce on sky + if ( tr.surfaceFlags & SURF_NOIMPACT ) { + // If grapple, reset owner + if (ent->parent && ent->parent->client && ent->parent->client->hook == ent) { + ent->parent->client->hook = NULL; + } + G_FreeEntity( ent ); + return; + } + + G_MissileImpact( ent, &tr ); + if ( ent->s.eType != ET_MISSILE ) { + return; // exploded + } + } + + // check think function after bouncing + G_RunThink( ent ); +} + + +//============================================================================= + +/* +================= +fire_flamer + +================= +*/ +gentity_t *fire_flamer (gentity_t *self, vec3_t start, vec3_t dir) { + gentity_t *bolt; + + VectorNormalize (dir); + + bolt = G_Spawn(); + bolt->classname = "flame"; + bolt->nextthink = level.time + 600; + bolt->think = G_ExplodeMissile; + bolt->s.eType = ET_MISSILE; + bolt->r.svFlags = SVF_USE_CURRENT_ORIGIN; + bolt->s.weapon = WP_FLAMER; + bolt->r.ownerNum = self->s.number; + bolt->parent = self; + bolt->damage = 60; + bolt->splashDamage = 65; + bolt->splashRadius = 45; + bolt->methodOfDeath = MOD_FLAMER; + bolt->splashMethodOfDeath = MOD_FLAMER_SPLASH; + bolt->clipmask = MASK_SHOT; + bolt->target_ent = NULL; + + bolt->s.pos.trType = TR_LINEAR; + bolt->s.pos.trTime = level.time - MISSILE_PRESTEP_TIME; // move a bit on the very first frame + VectorCopy( start, bolt->s.pos.trBase ); + //VectorMA( self->client->ps.velocity, 300, dir, bolt->s.pos.trDelta ); + VectorScale( dir, 350, bolt->s.pos.trDelta ); + SnapVector( bolt->s.pos.trDelta ); // save net bandwidth + + VectorCopy (start, bolt->r.currentOrigin); + + return bolt; +} + +//============================================================================= + +/* +================= +fire_plasma + +================= +*/ +gentity_t *fire_plasma (gentity_t *self, vec3_t start, vec3_t dir) +{ + gentity_t *bolt; + + VectorNormalize (dir); + + bolt = G_Spawn(); + bolt->classname = "plasma"; + bolt->nextthink = level.time + 10000; + bolt->think = G_ExplodeMissile; + bolt->s.eType = ET_MISSILE; + bolt->r.svFlags = SVF_USE_CURRENT_ORIGIN; + bolt->s.weapon = WP_PLASMAGUN; + bolt->r.ownerNum = self->s.number; + bolt->parent = self; + bolt->damage = 20; + bolt->splashDamage = 15; + bolt->splashRadius = 20; + //bolt->methodOfDeath = MOD_PLASMA; + //bolt->splashMethodOfDeath = MOD_PLASMA_SPLASH; + bolt->clipmask = MASK_SHOT; + bolt->target_ent = NULL; + + bolt->s.pos.trType = TR_LINEAR; + bolt->s.pos.trTime = level.time - MISSILE_PRESTEP_TIME; // move a bit on the very first frame + VectorCopy( start, bolt->s.pos.trBase ); + VectorScale( dir, 2000, bolt->s.pos.trDelta ); + SnapVector( bolt->s.pos.trDelta ); // save net bandwidth + + VectorCopy (start, bolt->r.currentOrigin); + return bolt; +} + +//============================================================================= + +/* +================= +fire_grenade +================= +*/ +gentity_t *fire_grenade (gentity_t *self, vec3_t start, vec3_t dir) { + gentity_t *bolt; + + VectorNormalize (dir); + + bolt = G_Spawn(); + bolt->classname = "grenade"; + bolt->nextthink = level.time + 2500; + bolt->think = G_ExplodeMissile; + bolt->s.eType = ET_MISSILE; + bolt->r.svFlags = SVF_USE_CURRENT_ORIGIN; + bolt->s.weapon = WP_GRENADE_LAUNCHER; + bolt->s.eFlags = EF_BOUNCE_HALF; + bolt->r.ownerNum = self->s.number; + bolt->parent = self; + bolt->damage = 100; + bolt->splashDamage = 100; + bolt->splashRadius = 150; + bolt->methodOfDeath = MOD_GRENADE; + bolt->splashMethodOfDeath = MOD_GRENADE_SPLASH; + bolt->clipmask = MASK_SHOT; + bolt->target_ent = NULL; + + bolt->s.pos.trType = TR_GRAVITY; + bolt->s.pos.trTime = level.time - MISSILE_PRESTEP_TIME; // move a bit on the very first frame + VectorCopy( start, bolt->s.pos.trBase ); + VectorScale( dir, 700, bolt->s.pos.trDelta ); + SnapVector( bolt->s.pos.trDelta ); // save net bandwidth + + VectorCopy (start, bolt->r.currentOrigin); + + return bolt; +} + +//============================================================================= + + +/* +================= +fire_bfg +================= +*/ +gentity_t *fire_bfg (gentity_t *self, vec3_t start, vec3_t dir) { + gentity_t *bolt; + + VectorNormalize (dir); + + bolt = G_Spawn(); + bolt->classname = "bfg"; + bolt->nextthink = level.time + 10000; + bolt->think = G_ExplodeMissile; + bolt->s.eType = ET_MISSILE; + bolt->r.svFlags = SVF_USE_CURRENT_ORIGIN; + bolt->s.weapon = WP_BFG; + bolt->r.ownerNum = self->s.number; + bolt->parent = self; + bolt->damage = 100; + bolt->splashDamage = 100; + bolt->splashRadius = 120; + bolt->methodOfDeath = MOD_BFG; + bolt->splashMethodOfDeath = MOD_BFG_SPLASH; + bolt->clipmask = MASK_SHOT; + bolt->target_ent = NULL; + + bolt->s.pos.trType = TR_LINEAR; + bolt->s.pos.trTime = level.time - MISSILE_PRESTEP_TIME; // move a bit on the very first frame + VectorCopy( start, bolt->s.pos.trBase ); + VectorScale( dir, 2000, bolt->s.pos.trDelta ); + SnapVector( bolt->s.pos.trDelta ); // save net bandwidth + VectorCopy (start, bolt->r.currentOrigin); + + return bolt; +} + +//============================================================================= + + +/* +================= +fire_rocket +================= +*/ +gentity_t *fire_rocket (gentity_t *self, vec3_t start, vec3_t dir) { + gentity_t *bolt; + + VectorNormalize (dir); + + bolt = G_Spawn(); + bolt->classname = "rocket"; + bolt->nextthink = level.time + 15000; + bolt->think = G_ExplodeMissile; + bolt->s.eType = ET_MISSILE; + bolt->r.svFlags = SVF_USE_CURRENT_ORIGIN; + bolt->s.weapon = WP_ROCKET_LAUNCHER; + bolt->r.ownerNum = self->s.number; + bolt->parent = self; + bolt->damage = 100; + bolt->splashDamage = 100; + bolt->splashRadius = 120; + bolt->methodOfDeath = MOD_ROCKET; + bolt->splashMethodOfDeath = MOD_ROCKET_SPLASH; + bolt->clipmask = MASK_SHOT; + bolt->target_ent = NULL; + + bolt->s.pos.trType = TR_LINEAR; + bolt->s.pos.trTime = level.time - MISSILE_PRESTEP_TIME; // move a bit on the very first frame + VectorCopy( start, bolt->s.pos.trBase ); + VectorScale( dir, 900, bolt->s.pos.trDelta ); + SnapVector( bolt->s.pos.trDelta ); // save net bandwidth + VectorCopy (start, bolt->r.currentOrigin); + + return bolt; +} + +/* +================= +fire_grapple +================= +*/ +gentity_t *fire_grapple (gentity_t *self, vec3_t start, vec3_t dir) { + gentity_t *hook; + + VectorNormalize (dir); + + hook = G_Spawn(); + hook->classname = "hook"; + hook->nextthink = level.time + 10000; + hook->think = Weapon_HookFree; + hook->s.eType = ET_MISSILE; + hook->r.svFlags = SVF_USE_CURRENT_ORIGIN; + hook->s.weapon = WP_GRAPPLING_HOOK; + hook->r.ownerNum = self->s.number; + hook->methodOfDeath = MOD_GRAPPLE; + hook->clipmask = MASK_SHOT; + hook->parent = self; + hook->target_ent = NULL; + + hook->s.pos.trType = TR_LINEAR; + hook->s.pos.trTime = level.time - MISSILE_PRESTEP_TIME; // move a bit on the very first frame + hook->s.otherEntityNum = self->s.number; // use to match beam in client + VectorCopy( start, hook->s.pos.trBase ); + VectorScale( dir, 800, hook->s.pos.trDelta ); + SnapVector( hook->s.pos.trDelta ); // save net bandwidth + VectorCopy (start, hook->r.currentOrigin); + + self->client->hook = hook; + + return hook; +} + + + diff --git a/src/game/g_mover.c b/src/game/g_mover.c new file mode 100644 index 00000000..9f552700 --- /dev/null +++ b/src/game/g_mover.c @@ -0,0 +1,1490 @@ +// Copyright (C) 1999-2000 Id Software, Inc. +// + +/* + * Portions Copyright (C) 2000-2001 Tim Angus + * + * This program 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, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ + +/* To assertain which portions are licensed under the GPL and which are + * licensed by Id Software, Inc. please run a diff between the equivalent + * versions of the "Tremulous" modification and the unmodified "Quake3" + * game source code. + */ + +#include "g_local.h" + + + +/* +=============================================================================== + +PUSHMOVE + +=============================================================================== +*/ + +void MatchTeam( gentity_t *teamLeader, int moverState, int time ); + +typedef struct { + gentity_t *ent; + vec3_t origin; + vec3_t angles; + float deltayaw; +} pushed_t; +pushed_t pushed[MAX_GENTITIES], *pushed_p; + + +/* +============ +G_TestEntityPosition + +============ +*/ +gentity_t *G_TestEntityPosition( gentity_t *ent ) { + trace_t tr; + int mask; + + if ( ent->clipmask ) { + mask = ent->clipmask; + } else { + mask = MASK_SOLID; + } + if ( ent->client ) { + trap_Trace( &tr, ent->client->ps.origin, ent->r.mins, ent->r.maxs, ent->client->ps.origin, ent->s.number, mask ); + } else { + trap_Trace( &tr, ent->s.pos.trBase, ent->r.mins, ent->r.maxs, ent->s.pos.trBase, ent->s.number, mask ); + } + + if (tr.startsolid) + return &g_entities[ tr.entityNum ]; + + return NULL; +} + + +/* +================== +G_TryPushingEntity + +Returns qfalse if the move is blocked +================== +*/ +qboolean G_TryPushingEntity( gentity_t *check, gentity_t *pusher, vec3_t move, vec3_t amove ) { + vec3_t forward, right, up; + vec3_t org, org2, move2; + gentity_t *block; + + // EF_MOVER_STOP will just stop when contacting another entity + // instead of pushing it, but entities can still ride on top of it + if ( ( pusher->s.eFlags & EF_MOVER_STOP ) && + check->s.groundEntityNum != pusher->s.number ) { + return qfalse; + } + + // save off the old position + if (pushed_p > &pushed[MAX_GENTITIES]) { + G_Error( "pushed_p > &pushed[MAX_GENTITIES]" ); + } + pushed_p->ent = check; + VectorCopy (check->s.pos.trBase, pushed_p->origin); + VectorCopy (check->s.apos.trBase, pushed_p->angles); + if ( check->client ) { + pushed_p->deltayaw = check->client->ps.delta_angles[YAW]; + VectorCopy (check->client->ps.origin, pushed_p->origin); + } + pushed_p++; + + // we need this for pushing things later + VectorSubtract (vec3_origin, amove, org); + AngleVectors (org, forward, right, up); + + // try moving the contacted entity + VectorAdd (check->s.pos.trBase, move, check->s.pos.trBase); + if (check->client) { + // make sure the client's view rotates when on a rotating mover + check->client->ps.delta_angles[YAW] += ANGLE2SHORT(amove[YAW]); + } + + // figure movement due to the pusher's amove + VectorSubtract (check->s.pos.trBase, pusher->r.currentOrigin, org); + org2[0] = DotProduct (org, forward); + org2[1] = -DotProduct (org, right); + org2[2] = DotProduct (org, up); + VectorSubtract (org2, org, move2); + VectorAdd (check->s.pos.trBase, move2, check->s.pos.trBase); + if ( check->client ) { + VectorAdd (check->client->ps.origin, move, check->client->ps.origin); + VectorAdd (check->client->ps.origin, move2, check->client->ps.origin); + } + + // may have pushed them off an edge + if ( check->s.groundEntityNum != pusher->s.number ) { + check->s.groundEntityNum = -1; + } + + block = G_TestEntityPosition( check ); + if (!block) { + // pushed ok + if ( check->client ) { + VectorCopy( check->client->ps.origin, check->r.currentOrigin ); + } else { + VectorCopy( check->s.pos.trBase, check->r.currentOrigin ); + } + trap_LinkEntity (check); + return qtrue; + } + + // if it is ok to leave in the old position, do it + // this is only relevent for riding entities, not pushed + // Sliding trapdoors can cause this. + VectorCopy( (pushed_p-1)->origin, check->s.pos.trBase); + if ( check->client ) { + VectorCopy( (pushed_p-1)->origin, check->client->ps.origin); + } + VectorCopy( (pushed_p-1)->angles, check->s.apos.trBase ); + block = G_TestEntityPosition (check); + if ( !block ) { + check->s.groundEntityNum = -1; + pushed_p--; + return qtrue; + } + + // blocked + return qfalse; +} + + +/* +============ +G_MoverPush + +Objects need to be moved back on a failed push, +otherwise riders would continue to slide. +If qfalse is returned, *obstacle will be the blocking entity +============ +*/ +qboolean G_MoverPush( gentity_t *pusher, vec3_t move, vec3_t amove, gentity_t **obstacle ) { + int i, e; + gentity_t *check; + vec3_t mins, maxs; + pushed_t *p; + int entityList[MAX_GENTITIES]; + int listedEntities; + vec3_t totalMins, totalMaxs; + + *obstacle = NULL; + + + // mins/maxs are the bounds at the destination + // totalMins / totalMaxs are the bounds for the entire move + if ( pusher->r.currentAngles[0] || pusher->r.currentAngles[1] || pusher->r.currentAngles[2] + || amove[0] || amove[1] || amove[2] ) { + float radius; + + radius = RadiusFromBounds( pusher->r.mins, pusher->r.maxs ); + for ( i = 0 ; i < 3 ; i++ ) { + mins[i] = pusher->r.currentOrigin[i] + move[i] - radius; + maxs[i] = pusher->r.currentOrigin[i] + move[i] + radius; + totalMins[i] = mins[i] - move[i]; + totalMaxs[i] = maxs[i] - move[i]; + } + } else { + for (i=0 ; i<3 ; i++) { + mins[i] = pusher->r.absmin[i] + move[i]; + maxs[i] = pusher->r.absmax[i] + move[i]; + } + + VectorCopy( pusher->r.absmin, totalMins ); + VectorCopy( pusher->r.absmax, totalMaxs ); + for (i=0 ; i<3 ; i++) { + if ( move[i] > 0 ) { + totalMaxs[i] += move[i]; + } else { + totalMins[i] += move[i]; + } + } + } + + // unlink the pusher so we don't get it in the entityList + trap_UnlinkEntity( pusher ); + + listedEntities = trap_EntitiesInBox( totalMins, totalMaxs, entityList, MAX_GENTITIES ); + + // move the pusher to it's final position + VectorAdd( pusher->r.currentOrigin, move, pusher->r.currentOrigin ); + VectorAdd( pusher->r.currentAngles, amove, pusher->r.currentAngles ); + trap_LinkEntity( pusher ); + + // see if any solid entities are inside the final position + for ( e = 0 ; e < listedEntities ; e++ ) { + check = &g_entities[ entityList[ e ] ]; + + // only push items and players + if ( check->s.eType != ET_ITEM && check->s.eType != ET_BUILDABLE && check->s.eType != ET_PLAYER && !check->physicsObject ) { + continue; + } + + // if the entity is standing on the pusher, it will definitely be moved + if ( check->s.groundEntityNum != pusher->s.number ) { + // see if the ent needs to be tested + if ( check->r.absmin[0] >= maxs[0] + || check->r.absmin[1] >= maxs[1] + || check->r.absmin[2] >= maxs[2] + || check->r.absmax[0] <= mins[0] + || check->r.absmax[1] <= mins[1] + || check->r.absmax[2] <= mins[2] ) { + continue; + } + // see if the ent's bbox is inside the pusher's final position + // this does allow a fast moving object to pass through a thin entity... + if (!G_TestEntityPosition (check)) { + continue; + } + } + + // the entity needs to be pushed + if ( G_TryPushingEntity( check, pusher, move, amove ) ) { + continue; + } + + // the move was blocked an entity + + // bobbing entities are instant-kill and never get blocked + if ( pusher->s.pos.trType == TR_SINE || pusher->s.apos.trType == TR_SINE ) { + G_Damage( check, pusher, pusher, NULL, NULL, 99999, 0, MOD_CRUSH ); + continue; + } + + + // save off the obstacle so we can call the block function (crush, etc) + *obstacle = check; + + // move back any entities we already moved + // go backwards, so if the same entity was pushed + // twice, it goes back to the original position + for ( p=pushed_p-1 ; p>=pushed ; p-- ) { + VectorCopy (p->origin, p->ent->s.pos.trBase); + VectorCopy (p->angles, p->ent->s.apos.trBase); + if ( p->ent->client ) { + p->ent->client->ps.delta_angles[YAW] = p->deltayaw; + VectorCopy (p->origin, p->ent->client->ps.origin); + } + trap_LinkEntity (p->ent); + } + return qfalse; + } + + return qtrue; +} + + +/* +================= +G_MoverTeam +================= +*/ +void G_MoverTeam( gentity_t *ent ) { + vec3_t move, amove; + gentity_t *part, *obstacle; + vec3_t origin, angles; + + obstacle = NULL; + + // make sure all team slaves can move before commiting + // any moves or calling any think functions + // if the move is blocked, all moved objects will be backed out + pushed_p = pushed; + for (part = ent ; part ; part=part->teamchain) { + // get current position + BG_EvaluateTrajectory( &part->s.pos, level.time, origin ); + BG_EvaluateTrajectory( &part->s.apos, level.time, angles ); + VectorSubtract( origin, part->r.currentOrigin, move ); + VectorSubtract( angles, part->r.currentAngles, amove ); + if ( !G_MoverPush( part, move, amove, &obstacle ) ) { + break; // move was blocked + } + } + + if (part) { + // go back to the previous position + for ( part = ent ; part ; part = part->teamchain ) { + part->s.pos.trTime += level.time - level.previousTime; + part->s.apos.trTime += level.time - level.previousTime; + BG_EvaluateTrajectory( &part->s.pos, level.time, part->r.currentOrigin ); + BG_EvaluateTrajectory( &part->s.apos, level.time, part->r.currentAngles ); + trap_LinkEntity( part ); + } + + // if the pusher has a "blocked" function, call it + if (ent->blocked) { + ent->blocked( ent, obstacle ); + } + return; + } + + // the move succeeded + for ( part = ent ; part ; part = part->teamchain ) { + // call the reached function if time is at or past end point + if ( part->s.pos.trType == TR_LINEAR_STOP ) { + if ( level.time >= part->s.pos.trTime + part->s.pos.trDuration ) { + if ( part->reached ) { + part->reached( part ); + } + } + } + } +} + +/* +================ +G_RunMover + +================ +*/ +void G_RunMover( gentity_t *ent ) { + // if not a team captain, don't do anything, because + // the captain will handle everything + if ( ent->flags & FL_TEAMSLAVE ) { + return; + } + + // if stationary at one of the positions, don't move anything + if ( ent->s.pos.trType != TR_STATIONARY || ent->s.apos.trType != TR_STATIONARY ) { + G_MoverTeam( ent ); + } + + // check think function + G_RunThink( ent ); +} + +/* +============================================================================ + +GENERAL MOVERS + +Doors, plats, and buttons are all binary (two position) movers +Pos1 is "at rest", pos2 is "activated" +============================================================================ +*/ + +/* +=============== +SetMoverState +=============== +*/ +void SetMoverState( gentity_t *ent, moverState_t moverState, int time ) { + vec3_t delta; + float f; + + ent->moverState = moverState; + + ent->s.pos.trTime = time; + switch( moverState ) { + case MOVER_POS1: + VectorCopy( ent->pos1, ent->s.pos.trBase ); + ent->s.pos.trType = TR_STATIONARY; + break; + case MOVER_POS2: + VectorCopy( ent->pos2, ent->s.pos.trBase ); + ent->s.pos.trType = TR_STATIONARY; + break; + case MOVER_1TO2: + VectorCopy( ent->pos1, ent->s.pos.trBase ); + VectorSubtract( ent->pos2, ent->pos1, delta ); + f = 1000.0 / ent->s.pos.trDuration; + VectorScale( delta, f, ent->s.pos.trDelta ); + ent->s.pos.trType = TR_LINEAR_STOP; + break; + case MOVER_2TO1: + VectorCopy( ent->pos2, ent->s.pos.trBase ); + VectorSubtract( ent->pos1, ent->pos2, delta ); + f = 1000.0 / ent->s.pos.trDuration; + VectorScale( delta, f, ent->s.pos.trDelta ); + ent->s.pos.trType = TR_LINEAR_STOP; + break; + } + BG_EvaluateTrajectory( &ent->s.pos, level.time, ent->r.currentOrigin ); + trap_LinkEntity( ent ); +} + +/* +================ +MatchTeam + +All entities in a mover team will move from pos1 to pos2 +in the same amount of time +================ +*/ +void MatchTeam( gentity_t *teamLeader, int moverState, int time ) { + gentity_t *slave; + + for ( slave = teamLeader ; slave ; slave = slave->teamchain ) { + SetMoverState( slave, moverState, time ); + } +} + + + +/* +================ +ReturnToPos1 +================ +*/ +void ReturnToPos1( gentity_t *ent ) { + MatchTeam( ent, MOVER_2TO1, level.time ); + + // looping sound + ent->s.loopSound = ent->soundLoop; + + // starting sound + if ( ent->sound2to1 ) { + G_AddEvent( ent, EV_GENERAL_SOUND, ent->sound2to1 ); + } +} + + +/* +================ +Reached_BinaryMover +================ +*/ +void Reached_BinaryMover( gentity_t *ent ) { + + // stop the looping sound + ent->s.loopSound = ent->soundLoop; + + if ( ent->moverState == MOVER_1TO2 ) { + // reached pos2 + SetMoverState( ent, MOVER_POS2, level.time ); + + // play sound + if ( ent->soundPos2 ) { + G_AddEvent( ent, EV_GENERAL_SOUND, ent->soundPos2 ); + } + + // return to pos1 after a delay + ent->think = ReturnToPos1; + ent->nextthink = level.time + ent->wait; + + // fire targets + if ( !ent->activator ) { + ent->activator = ent; + } + G_UseTargets( ent, ent->activator ); + } else if ( ent->moverState == MOVER_2TO1 ) { + // reached pos1 + SetMoverState( ent, MOVER_POS1, level.time ); + + // play sound + if ( ent->soundPos1 ) { + G_AddEvent( ent, EV_GENERAL_SOUND, ent->soundPos1 ); + } + + // close areaportals + if ( ent->teammaster == ent || !ent->teammaster ) { + trap_AdjustAreaPortalState( ent, qfalse ); + } + } else { + G_Error( "Reached_BinaryMover: bad moverState" ); + } +} + + +/* +================ +Use_BinaryMover +================ +*/ +void Use_BinaryMover( gentity_t *ent, gentity_t *other, gentity_t *activator ) { + int total; + int partial; + + // only the master should be used + if ( ent->flags & FL_TEAMSLAVE ) { + Use_BinaryMover( ent->teammaster, other, activator ); + return; + } + + ent->activator = activator; + + if ( ent->moverState == MOVER_POS1 ) { + // start moving 50 msec later, becase if this was player + // triggered, level.time hasn't been advanced yet + MatchTeam( ent, MOVER_1TO2, level.time + 50 ); + + // starting sound + if ( ent->sound1to2 ) { + G_AddEvent( ent, EV_GENERAL_SOUND, ent->sound1to2 ); + } + + // looping sound + ent->s.loopSound = ent->soundLoop; + + // open areaportal + if ( ent->teammaster == ent || !ent->teammaster ) { + trap_AdjustAreaPortalState( ent, qtrue ); + } + return; + } + + // if all the way up, just delay before coming down + if ( ent->moverState == MOVER_POS2 ) { + ent->nextthink = level.time + ent->wait; + return; + } + + // only partway down before reversing + if ( ent->moverState == MOVER_2TO1 ) { + total = ent->s.pos.trDuration; + partial = level.time - ent->s.pos.trTime; + if ( partial > total ) { + partial = total; + } + + MatchTeam( ent, MOVER_1TO2, level.time - ( total - partial ) ); + + if ( ent->sound1to2 ) { + G_AddEvent( ent, EV_GENERAL_SOUND, ent->sound1to2 ); + } + return; + } + + // only partway up before reversing + if ( ent->moverState == MOVER_1TO2 ) { + total = ent->s.pos.trDuration; + partial = level.time - ent->s.pos.trTime; + if ( partial > total ) { + partial = total; + } + + MatchTeam( ent, MOVER_2TO1, level.time - ( total - partial ) ); + + if ( ent->sound2to1 ) { + G_AddEvent( ent, EV_GENERAL_SOUND, ent->sound2to1 ); + } + return; + } +} + + + +/* +================ +InitMover + +"pos1", "pos2", and "speed" should be set before calling, +so the movement delta can be calculated +================ +*/ +void InitMover( gentity_t *ent ) { + vec3_t move; + float distance; + float light; + vec3_t color; + qboolean lightSet, colorSet; + char *sound; + + // if the "model2" key is set, use a seperate model + // for drawing, but clip against the brushes + if ( ent->model2 ) { + ent->s.modelindex2 = G_ModelIndex( ent->model2 ); + } + + // if the "loopsound" key is set, use a constant looping sound when moving + if ( G_SpawnString( "noise", "100", &sound ) ) { + ent->s.loopSound = G_SoundIndex( sound ); + } + + // if the "color" or "light" keys are set, setup constantLight + lightSet = G_SpawnFloat( "light", "100", &light ); + colorSet = G_SpawnVector( "color", "1 1 1", color ); + if ( lightSet || colorSet ) { + int r, g, b, i; + + r = color[0] * 255; + if ( r > 255 ) { + r = 255; + } + g = color[1] * 255; + if ( g > 255 ) { + g = 255; + } + b = color[2] * 255; + if ( b > 255 ) { + b = 255; + } + i = light / 4; + if ( i > 255 ) { + i = 255; + } + ent->s.constantLight = r | ( g << 8 ) | ( b << 16 ) | ( i << 24 ); + } + + + ent->use = Use_BinaryMover; + ent->reached = Reached_BinaryMover; + + ent->moverState = MOVER_POS1; + ent->r.svFlags = SVF_USE_CURRENT_ORIGIN; + ent->s.eType = ET_MOVER; + VectorCopy (ent->pos1, ent->r.currentOrigin); + trap_LinkEntity (ent); + + ent->s.pos.trType = TR_STATIONARY; + VectorCopy( ent->pos1, ent->s.pos.trBase ); + + // calculate time to reach second position from speed + VectorSubtract( ent->pos2, ent->pos1, move ); + distance = VectorLength( move ); + if ( ! ent->speed ) { + ent->speed = 100; + } + VectorScale( move, ent->speed, ent->s.pos.trDelta ); + ent->s.pos.trDuration = distance * 1000 / ent->speed; + if ( ent->s.pos.trDuration <= 0 ) { + ent->s.pos.trDuration = 1; + } +} + + +/* +=============================================================================== + +DOOR + +A use can be triggered either by a touch function, by being shot, or by being +targeted by another entity. + +=============================================================================== +*/ + +/* +================ +Blocked_Door +================ +*/ +void Blocked_Door( gentity_t *ent, gentity_t *other ) { + // remove anything other than a client + if ( !other->client ) { + // except CTF flags!!!! + if( other->s.eType == ET_ITEM && other->item->giType == IT_TEAM ) { + Team_DroppedFlagThink( other ); + return; + } + G_TempEntity( other->s.origin, EV_ITEM_POP ); + G_FreeEntity( other ); + return; + } + + if ( ent->damage ) { + G_Damage( other, ent, ent, NULL, NULL, ent->damage, 0, MOD_CRUSH ); + } + if ( ent->spawnflags & 4 ) { + return; // crushers don't reverse + } + + // reverse direction + Use_BinaryMover( ent, ent, other ); +} + +/* +================ +Touch_DoorTriggerSpectator +================ +*/ +static void Touch_DoorTriggerSpectator( gentity_t *ent, gentity_t *other, trace_t *trace ) { + int i, axis; + vec3_t origin, dir, angles; + + axis = ent->count; + VectorClear(dir); + if (fabs(other->s.origin[axis] - ent->r.absmax[axis]) < + fabs(other->s.origin[axis] - ent->r.absmin[axis])) { + origin[axis] = ent->r.absmin[axis] - 10; + dir[axis] = -1; + } + else { + origin[axis] = ent->r.absmax[axis] + 10; + dir[axis] = 1; + } + for (i = 0; i < 3; i++) { + if (i == axis) continue; + origin[i] = (ent->r.absmin[i] + ent->r.absmax[i]) * 0.5; + } + vectoangles(dir, angles); + TeleportPlayer(other, origin, angles ); +} + +/* +================ +Touch_DoorTrigger +================ +*/ +void Touch_DoorTrigger( gentity_t *ent, gentity_t *other, trace_t *trace ) { + if ( other->client && other->client->sess.sessionTeam == TEAM_SPECTATOR ) { + // if the door is not open and not opening + if ( ent->parent->moverState != MOVER_1TO2 && + ent->parent->moverState != MOVER_POS2) { + Touch_DoorTriggerSpectator( ent, other, trace ); + } + } + else if ( ent->parent->moverState != MOVER_1TO2 ) { + Use_BinaryMover( ent->parent, ent, other ); + } +} + + +/* +====================== +Think_SpawnNewDoorTrigger + +All of the parts of a door have been spawned, so create +a trigger that encloses all of them +====================== +*/ +void Think_SpawnNewDoorTrigger( gentity_t *ent ) { + gentity_t *other; + vec3_t mins, maxs; + int i, best; + + // set all of the slaves as shootable + for ( other = ent ; other ; other = other->teamchain ) { + other->takedamage = qtrue; + } + + // find the bounds of everything on the team + VectorCopy (ent->r.absmin, mins); + VectorCopy (ent->r.absmax, maxs); + + for (other = ent->teamchain ; other ; other=other->teamchain) { + AddPointToBounds (other->r.absmin, mins, maxs); + AddPointToBounds (other->r.absmax, mins, maxs); + } + + // find the thinnest axis, which will be the one we expand + best = 0; + for ( i = 1 ; i < 3 ; i++ ) { + if ( maxs[i] - mins[i] < maxs[best] - mins[best] ) { + best = i; + } + } + maxs[best] += 120; + mins[best] -= 120; + + // create a trigger with this size + other = G_Spawn (); + other->classname = "door_trigger"; + VectorCopy (mins, other->r.mins); + VectorCopy (maxs, other->r.maxs); + other->parent = ent; + other->r.contents = CONTENTS_TRIGGER; + other->touch = Touch_DoorTrigger; + // remember the thinnest axis + other->count = best; + trap_LinkEntity (other); + + MatchTeam( ent, ent->moverState, level.time ); +} + +void Think_MatchTeam( gentity_t *ent ) { + MatchTeam( ent, ent->moverState, level.time ); +} + + +/*QUAKED func_door (0 .5 .8) ? START_OPEN x CRUSHER +TOGGLE wait in both the start and end states for a trigger event. +START_OPEN the door to moves to its destination when spawned, and operate in reverse. It is used to temporarily or permanently close off an area when triggered (not useful for touch or takedamage doors). +NOMONSTER monsters will not trigger this door + +"model2" .md3 model to also draw +"angle" determines the opening direction +"targetname" if set, no touch field will be spawned and a remote button or trigger field activates the door. +"speed" movement speed (100 default) +"wait" wait before returning (3 default, -1 = never return) +"lip" lip remaining at end of move (8 default) +"dmg" damage to inflict when blocked (2 default) +"color" constantLight color +"light" constantLight radius +"health" if set, the door must be shot open +*/ +void SP_func_door (gentity_t *ent) { + vec3_t abs_movedir; + float distance; + vec3_t size; + float lip; + + ent->sound1to2 = ent->sound2to1 = G_SoundIndex("sound/movers/doors/dr1_strt.wav"); + ent->soundPos1 = ent->soundPos2 = G_SoundIndex("sound/movers/doors/dr1_end.wav"); + + ent->blocked = Blocked_Door; + + // default speed of 400 + if (!ent->speed) + ent->speed = 400; + + // default wait of 2 seconds + if (!ent->wait) + ent->wait = 2; + ent->wait *= 1000; + + // default lip of 8 units + G_SpawnFloat( "lip", "8", &lip ); + + // default damage of 2 points + G_SpawnInt( "dmg", "2", &ent->damage ); + + // first position at start + VectorCopy( ent->s.origin, ent->pos1 ); + + // calculate second position + trap_SetBrushModel( ent, ent->model ); + G_SetMovedir (ent->s.angles, ent->movedir); + abs_movedir[0] = fabs(ent->movedir[0]); + abs_movedir[1] = fabs(ent->movedir[1]); + abs_movedir[2] = fabs(ent->movedir[2]); + VectorSubtract( ent->r.maxs, ent->r.mins, size ); + distance = DotProduct( abs_movedir, size ) - lip; + VectorMA( ent->pos1, distance, ent->movedir, ent->pos2 ); + + // if "start_open", reverse position 1 and 2 + if ( ent->spawnflags & 1 ) { + vec3_t temp; + + VectorCopy( ent->pos2, temp ); + VectorCopy( ent->s.origin, ent->pos2 ); + VectorCopy( temp, ent->pos1 ); + } + + InitMover( ent ); + + ent->nextthink = level.time + FRAMETIME; + + if ( ! (ent->flags & FL_TEAMSLAVE ) ) { + int health; + + G_SpawnInt( "health", "0", &health ); + if ( health ) { + ent->takedamage = qtrue; + } + if ( ent->targetname || health ) { + // non touch/shoot doors + ent->think = Think_MatchTeam; + } else { + ent->think = Think_SpawnNewDoorTrigger; + } + } + + +} + +/* +=============================================================================== + +PLAT + +=============================================================================== +*/ + +/* +============== +Touch_Plat + +Don't allow decent if a living player is on it +=============== +*/ +void Touch_Plat( gentity_t *ent, gentity_t *other, trace_t *trace ) { + if ( !other->client || other->client->ps.stats[STAT_HEALTH] <= 0 ) { + return; + } + + // delay return-to-pos1 by one second + if ( ent->moverState == MOVER_POS2 ) { + ent->nextthink = level.time + 1000; + } +} + +/* +============== +Touch_PlatCenterTrigger + +If the plat is at the bottom position, start it going up +=============== +*/ +void Touch_PlatCenterTrigger(gentity_t *ent, gentity_t *other, trace_t *trace ) { + if ( !other->client ) { + return; + } + + if ( ent->parent->moverState == MOVER_POS1 ) { + Use_BinaryMover( ent->parent, ent, other ); + } +} + + +/* +================ +SpawnPlatTrigger + +Spawn a trigger in the middle of the plat's low position +Elevator cars require that the trigger extend through the entire low position, +not just sit on top of it. +================ +*/ +void SpawnPlatTrigger( gentity_t *ent ) { + gentity_t *trigger; + vec3_t tmin, tmax; + + // the middle trigger will be a thin trigger just + // above the starting position + trigger = G_Spawn(); + trigger->classname = "plat_trigger"; + trigger->touch = Touch_PlatCenterTrigger; + trigger->r.contents = CONTENTS_TRIGGER; + trigger->parent = ent; + + tmin[0] = ent->pos1[0] + ent->r.mins[0] + 33; + tmin[1] = ent->pos1[1] + ent->r.mins[1] + 33; + tmin[2] = ent->pos1[2] + ent->r.mins[2]; + + tmax[0] = ent->pos1[0] + ent->r.maxs[0] - 33; + tmax[1] = ent->pos1[1] + ent->r.maxs[1] - 33; + tmax[2] = ent->pos1[2] + ent->r.maxs[2] + 8; + + if ( tmax[0] <= tmin[0] ) { + tmin[0] = ent->pos1[0] + (ent->r.mins[0] + ent->r.maxs[0]) *0.5; + tmax[0] = tmin[0] + 1; + } + if ( tmax[1] <= tmin[1] ) { + tmin[1] = ent->pos1[1] + (ent->r.mins[1] + ent->r.maxs[1]) *0.5; + tmax[1] = tmin[1] + 1; + } + + VectorCopy (tmin, trigger->r.mins); + VectorCopy (tmax, trigger->r.maxs); + + trap_LinkEntity (trigger); +} + + +/*QUAKED func_plat (0 .5 .8) ? +Plats are always drawn in the extended position so they will light correctly. + +"lip" default 8, protrusion above rest position +"height" total height of movement, defaults to model height +"speed" overrides default 200. +"dmg" overrides default 2 +"model2" .md3 model to also draw +"color" constantLight color +"light" constantLight radius +*/ +void SP_func_plat (gentity_t *ent) { + float lip, height; + + ent->sound1to2 = ent->sound2to1 = G_SoundIndex("sound/movers/plats/pt1_strt.wav"); + ent->soundPos1 = ent->soundPos2 = G_SoundIndex("sound/movers/plats/pt1_end.wav"); + + VectorClear (ent->s.angles); + + G_SpawnFloat( "speed", "200", &ent->speed ); + G_SpawnInt( "dmg", "2", &ent->damage ); + G_SpawnFloat( "wait", "1", &ent->wait ); + G_SpawnFloat( "lip", "8", &lip ); + + ent->wait = 1000; + + // create second position + trap_SetBrushModel( ent, ent->model ); + + if ( !G_SpawnFloat( "height", "0", &height ) ) { + height = (ent->r.maxs[2] - ent->r.mins[2]) - lip; + } + + // pos1 is the rest (bottom) position, pos2 is the top + VectorCopy( ent->s.origin, ent->pos2 ); + VectorCopy( ent->pos2, ent->pos1 ); + ent->pos1[2] -= height; + + InitMover( ent ); + + // touch function keeps the plat from returning while + // a live player is standing on it + ent->touch = Touch_Plat; + + ent->blocked = Blocked_Door; + + ent->parent = ent; // so it can be treated as a door + + // spawn the trigger if one hasn't been custom made + if ( !ent->targetname ) { + SpawnPlatTrigger(ent); + } +} + + +/* +=============================================================================== + +BUTTON + +=============================================================================== +*/ + +/* +============== +Touch_Button + +=============== +*/ +void Touch_Button(gentity_t *ent, gentity_t *other, trace_t *trace ) { + if ( !other->client ) { + return; + } + + if ( ent->moverState == MOVER_POS1 ) { + Use_BinaryMover( ent, other, other ); + } +} + + +/*QUAKED func_button (0 .5 .8) ? +When a button is touched, it moves some distance in the direction of it's angle, triggers all of it's targets, waits some time, then returns to it's original position where it can be triggered again. + +"model2" .md3 model to also draw +"angle" determines the opening direction +"target" all entities with a matching targetname will be used +"speed" override the default 40 speed +"wait" override the default 1 second wait (-1 = never return) +"lip" override the default 4 pixel lip remaining at end of move +"health" if set, the button must be killed instead of touched +"color" constantLight color +"light" constantLight radius +*/ +void SP_func_button( gentity_t *ent ) { + vec3_t abs_movedir; + float distance; + vec3_t size; + float lip; + + ent->sound1to2 = G_SoundIndex("sound/movers/switches/butn2.wav"); + + if ( !ent->speed ) { + ent->speed = 40; + } + + if ( !ent->wait ) { + ent->wait = 1; + } + ent->wait *= 1000; + + // first position + VectorCopy( ent->s.origin, ent->pos1 ); + + // calculate second position + trap_SetBrushModel( ent, ent->model ); + + G_SpawnFloat( "lip", "4", &lip ); + + G_SetMovedir( ent->s.angles, ent->movedir ); + abs_movedir[0] = fabs(ent->movedir[0]); + abs_movedir[1] = fabs(ent->movedir[1]); + abs_movedir[2] = fabs(ent->movedir[2]); + VectorSubtract( ent->r.maxs, ent->r.mins, size ); + distance = abs_movedir[0] * size[0] + abs_movedir[1] * size[1] + abs_movedir[2] * size[2] - lip; + VectorMA (ent->pos1, distance, ent->movedir, ent->pos2); + + if (ent->health) { + // shootable button + ent->takedamage = qtrue; + } else { + // touchable button + ent->touch = Touch_Button; + } + + InitMover( ent ); +} + + + +/* +=============================================================================== + +TRAIN + +=============================================================================== +*/ + + +#define TRAIN_START_ON 1 +#define TRAIN_TOGGLE 2 +#define TRAIN_BLOCK_STOPS 4 + +/* +=============== +Think_BeginMoving + +The wait time at a corner has completed, so start moving again +=============== +*/ +void Think_BeginMoving( gentity_t *ent ) { + ent->s.pos.trTime = level.time; + ent->s.pos.trType = TR_LINEAR_STOP; +} + +/* +=============== +Reached_Train +=============== +*/ +void Reached_Train( gentity_t *ent ) { + gentity_t *next; + float speed; + vec3_t move; + float length; + + // copy the apropriate values + next = ent->nextTrain; + if ( !next || !next->nextTrain ) { + return; // just stop + } + + // fire all other targets + G_UseTargets( next, NULL ); + + // set the new trajectory + ent->nextTrain = next->nextTrain; + VectorCopy( next->s.origin, ent->pos1 ); + VectorCopy( next->nextTrain->s.origin, ent->pos2 ); + + // if the path_corner has a speed, use that + if ( next->speed ) { + speed = next->speed; + } else { + // otherwise use the train's speed + speed = ent->speed; + } + if ( speed < 1 ) { + speed = 1; + } + + // calculate duration + VectorSubtract( ent->pos2, ent->pos1, move ); + length = VectorLength( move ); + + ent->s.pos.trDuration = length * 1000 / speed; + + // looping sound + ent->s.loopSound = next->soundLoop; + + // start it going + SetMoverState( ent, MOVER_1TO2, level.time ); + + // if there is a "wait" value on the target, don't start moving yet + if ( next->wait ) { + ent->nextthink = level.time + next->wait * 1000; + ent->think = Think_BeginMoving; + ent->s.pos.trType = TR_STATIONARY; + } +} + + +/* +=============== +Think_SetupTrainTargets + +Link all the corners together +=============== +*/ +void Think_SetupTrainTargets( gentity_t *ent ) { + gentity_t *path, *next, *start; + + ent->nextTrain = G_Find( NULL, FOFS(targetname), ent->target ); + if ( !ent->nextTrain ) { + G_Printf( "func_train at %s with an unfound target\n", + vtos(ent->r.absmin) ); + return; + } + + start = NULL; + for ( path = ent->nextTrain ; path != start ; path = next ) { + if ( !start ) { + start = path; + } + + if ( !path->target ) { + G_Printf( "Train corner at %s without a target\n", + vtos(path->s.origin) ); + return; + } + + // find a path_corner among the targets + // there may also be other targets that get fired when the corner + // is reached + next = NULL; + do { + next = G_Find( next, FOFS(targetname), path->target ); + if ( !next ) { + G_Printf( "Train corner at %s without a target path_corner\n", + vtos(path->s.origin) ); + return; + } + } while ( strcmp( next->classname, "path_corner" ) ); + + path->nextTrain = next; + } + + // start the train moving from the first corner + Reached_Train( ent ); +} + + + +/*QUAKED path_corner (.5 .3 0) (-8 -8 -8) (8 8 8) +Train path corners. +Target: next path corner and other targets to fire +"speed" speed to move to the next corner +"wait" seconds to wait before behining move to next corner +*/ +void SP_path_corner( gentity_t *self ) { + if ( !self->targetname ) { + G_Printf ("path_corner with no targetname at %s\n", vtos(self->s.origin)); + G_FreeEntity( self ); + return; + } + // path corners don't need to be linked in +} + + + +/*QUAKED func_train (0 .5 .8) ? START_ON TOGGLE BLOCK_STOPS +A train is a mover that moves between path_corner target points. +Trains MUST HAVE AN ORIGIN BRUSH. +The train spawns at the first target it is pointing at. +"model2" .md3 model to also draw +"speed" default 100 +"dmg" default 2 +"noise" looping sound to play when the train is in motion +"target" next path corner +"color" constantLight color +"light" constantLight radius +*/ +void SP_func_train (gentity_t *self) { + VectorClear (self->s.angles); + + if (self->spawnflags & TRAIN_BLOCK_STOPS) { + self->damage = 0; + } else { + if (!self->damage) { + self->damage = 2; + } + } + + if ( !self->speed ) { + self->speed = 100; + } + + if ( !self->target ) { + G_Printf ("func_train without a target at %s\n", vtos(self->r.absmin)); + G_FreeEntity( self ); + return; + } + + trap_SetBrushModel( self, self->model ); + InitMover( self ); + + self->reached = Reached_Train; + + // start trains on the second frame, to make sure their targets have had + // a chance to spawn + self->nextthink = level.time + FRAMETIME; + self->think = Think_SetupTrainTargets; +} + +/* +=============================================================================== + +STATIC + +=============================================================================== +*/ + + +/*QUAKED func_static (0 .5 .8) ? +A bmodel that just sits there, doing nothing. Can be used for conditional walls and models. +"model2" .md3 model to also draw +"color" constantLight color +"light" constantLight radius +*/ +void SP_func_static( gentity_t *ent ) { + trap_SetBrushModel( ent, ent->model ); + InitMover( ent ); + VectorCopy( ent->s.origin, ent->s.pos.trBase ); + VectorCopy( ent->s.origin, ent->r.currentOrigin ); +} + + +/* +=============================================================================== + +ROTATING + +=============================================================================== +*/ + + +/*QUAKED func_rotating (0 .5 .8) ? START_ON - X_AXIS Y_AXIS +You need to have an origin brush as part of this entity. The center of that brush will be +the point around which it is rotated. It will rotate around the Z axis by default. You can +check either the X_AXIS or Y_AXIS box to change that. + +"model2" .md3 model to also draw +"speed" determines how fast it moves; default value is 100. +"dmg" damage to inflict when blocked (2 default) +"color" constantLight color +"light" constantLight radius +*/ +void SP_func_rotating (gentity_t *ent) { + if ( !ent->speed ) { + ent->speed = 100; + } + + // set the axis of rotation + ent->s.apos.trType = TR_LINEAR; + if ( ent->spawnflags & 4 ) { + ent->s.apos.trDelta[2] = ent->speed; + } else if ( ent->spawnflags & 8 ) { + ent->s.apos.trDelta[0] = ent->speed; + } else { + ent->s.apos.trDelta[1] = ent->speed; + } + + if (!ent->damage) { + ent->damage = 2; + } + + trap_SetBrushModel( ent, ent->model ); + InitMover( ent ); + + VectorCopy( ent->s.origin, ent->s.pos.trBase ); + VectorCopy( ent->s.pos.trBase, ent->r.currentOrigin ); + VectorCopy( ent->s.apos.trBase, ent->r.currentAngles ); + + trap_LinkEntity( ent ); +} + + +/* +=============================================================================== + +BOBBING + +=============================================================================== +*/ + + +/*QUAKED func_bobbing (0 .5 .8) ? X_AXIS Y_AXIS +Normally bobs on the Z axis +"model2" .md3 model to also draw +"height" amplitude of bob (32 default) +"speed" seconds to complete a bob cycle (4 default) +"phase" the 0.0 to 1.0 offset in the cycle to start at +"dmg" damage to inflict when blocked (2 default) +"color" constantLight color +"light" constantLight radius +*/ +void SP_func_bobbing (gentity_t *ent) { + float height; + float phase; + + G_SpawnFloat( "speed", "4", &ent->speed ); + G_SpawnFloat( "height", "32", &height ); + G_SpawnInt( "dmg", "2", &ent->damage ); + G_SpawnFloat( "phase", "0", &phase ); + + trap_SetBrushModel( ent, ent->model ); + InitMover( ent ); + + VectorCopy( ent->s.origin, ent->s.pos.trBase ); + VectorCopy( ent->s.origin, ent->r.currentOrigin ); + + ent->s.pos.trDuration = ent->speed * 1000; + ent->s.pos.trTime = ent->s.pos.trDuration * phase; + ent->s.pos.trType = TR_SINE; + + // set the axis of bobbing + if ( ent->spawnflags & 1 ) { + ent->s.pos.trDelta[0] = height; + } else if ( ent->spawnflags & 2 ) { + ent->s.pos.trDelta[1] = height; + } else { + ent->s.pos.trDelta[2] = height; + } +} + +/* +=============================================================================== + +PENDULUM + +=============================================================================== +*/ + + +/*QUAKED func_pendulum (0 .5 .8) ? +You need to have an origin brush as part of this entity. +Pendulums always swing north / south on unrotated models. Add an angles field to the model to allow rotation in other directions. +Pendulum frequency is a physical constant based on the length of the beam and gravity. +"model2" .md3 model to also draw +"speed" the number of degrees each way the pendulum swings, (30 default) +"phase" the 0.0 to 1.0 offset in the cycle to start at +"dmg" damage to inflict when blocked (2 default) +"color" constantLight color +"light" constantLight radius +*/ +void SP_func_pendulum(gentity_t *ent) { + float freq; + float length; + float phase; + float speed; + + G_SpawnFloat( "speed", "30", &speed ); + G_SpawnInt( "dmg", "2", &ent->damage ); + G_SpawnFloat( "phase", "0", &phase ); + + trap_SetBrushModel( ent, ent->model ); + + // find pendulum length + length = fabs( ent->r.mins[2] ); + if ( length < 8 ) { + length = 8; + } + + freq = 1 / ( M_PI * 2 ) * sqrt( g_gravity.value / ( 3 * length ) ); + + ent->s.pos.trDuration = ( 1000 / freq ); + + InitMover( ent ); + + VectorCopy( ent->s.origin, ent->s.pos.trBase ); + VectorCopy( ent->s.origin, ent->r.currentOrigin ); + + VectorCopy( ent->s.angles, ent->s.apos.trBase ); + + ent->s.apos.trDuration = 1000 / freq; + ent->s.apos.trTime = ent->s.apos.trDuration * phase; + ent->s.apos.trType = TR_SINE; + ent->s.apos.trDelta[2] = speed; +} diff --git a/src/game/g_public.h b/src/game/g_public.h new file mode 100644 index 00000000..dd534f65 --- /dev/null +++ b/src/game/g_public.h @@ -0,0 +1,413 @@ +// Copyright (C) 1999-2000 Id Software, Inc. +// + +/* + * Portions Copyright (C) 2000-2001 Tim Angus + * + * This program 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, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ + +/* To assertain which portions are licensed under the GPL and which are + * licensed by Id Software, Inc. please run a diff between the equivalent + * versions of the "Tremulous" modification and the unmodified "Quake3" + * game source code. + */ + +// g_public.h -- game module information visible to server + +#define GAME_API_VERSION 8 + +// entity->svFlags +// the server does not know how to interpret most of the values +// in entityStates (level eType), so the game must explicitly flag +// special server behaviors +#define SVF_NOCLIENT 0x00000001 // don't send entity to clients, even if it has effects +#define SVF_BOT 0x00000008 +#define SVF_BROADCAST 0x00000020 // send to all connected clients +#define SVF_PORTAL 0x00000040 // merge a second pvs at origin2 into snapshots +#define SVF_USE_CURRENT_ORIGIN 0x00000080 // entity->r.currentOrigin instead of entity->s.origin + // for link position (missiles and movers) +#define SVF_SINGLECLIENT 0x00000100 // only send to a single client + +//=============================================================== + + +typedef struct { + entityState_t s; // communicated by server to clients + + qboolean linked; // qfalse if not in any good cluster + int linkcount; + + int svFlags; // SVF_NOCLIENT, SVF_BROADCAST, etc + int singleClient; // only send to this client when SVF_SINGLECLIENT is set + + qboolean bmodel; // if false, assume an explicit mins / maxs bounding box + // only set by trap_SetBrushModel + vec3_t mins, maxs; + int contents; // CONTENTS_TRIGGER, CONTENTS_SOLID, CONTENTS_BODY, etc + // a non-solid entity should set to 0 + + vec3_t absmin, absmax; // derived from mins/maxs and origin + rotation + + // currentOrigin will be used for all collision detection and world linking. + // it will not necessarily be the same as the trajectory evaluation for the current + // time, because each entity must be moved one at a time after time is advanced + // to avoid simultanious collision issues + vec3_t currentOrigin; + vec3_t currentAngles; + + // when a trace call is made and passEntityNum != ENTITYNUM_NONE, + // an ent will be excluded from testing if: + // ent->s.number == passEntityNum (don't interact with self) + // ent->s.ownerNum = passEntityNum (don't interact with your own missiles) + // entity[ent->s.ownerNum].ownerNum = passEntityNum (don't interact with other missiles from owner) + int ownerNum; +} entityShared_t; + + + +// the server looks at a sharedEntity, which is the start of the game's gentity_t structure +typedef struct { + entityState_t s; // communicated by server to clients + entityShared_t r; // shared by both the server system and game +} sharedEntity_t; + + + +//=============================================================== + +// +// system traps provided by the main engine +// +typedef enum { + //============== general Quake services ================== + + G_PRINT, // ( const char *string ); + // print message on the local console + + G_ERROR, // ( const char *string ); + // abort the game + + G_MILLISECONDS, // ( void ); + // get current time for profiling reasons + // this should NOT be used for any game related tasks, + // because it is not journaled + + // console variable interaction + G_CVAR_REGISTER, // ( vmCvar_t *vmCvar, const char *varName, const char *defaultValue, int flags ); + G_CVAR_UPDATE, // ( vmCvar_t *vmCvar ); + G_CVAR_SET, // ( const char *var_name, const char *value ); + G_CVAR_VARIABLE_INTEGER_VALUE, // ( const char *var_name ); + + G_CVAR_VARIABLE_STRING_BUFFER, // ( const char *var_name, char *buffer, int bufsize ); + + G_ARGC, // ( void ); + // ClientCommand and ServerCommand parameter access + + G_ARGV, // ( int n, char *buffer, int bufferLength ); + + G_FS_FOPEN_FILE, // ( const char *qpath, fileHandle_t *file, fsMode_t mode ); + G_FS_READ, // ( void *buffer, int len, fileHandle_t f ); + G_FS_WRITE, // ( const void *buffer, int len, fileHandle_t f ); + G_FS_FCLOSE_FILE, // ( fileHandle_t f ); + + G_SEND_CONSOLE_COMMAND, // ( const char *text ); + // add commands to the console as if they were typed in + // for map changing, etc + + + //=========== server specific functionality ============= + + G_LOCATE_GAME_DATA, // ( gentity_t *gEnts, int numGEntities, int sizeofGEntity_t, + // playerState_t *clients, int sizeofGameClient ); + // the game needs to let the server system know where and how big the gentities + // are, so it can look at them directly without going through an interface + + G_DROP_CLIENT, // ( int clientNum, const char *reason ); + // kick a client off the server with a message + + G_SEND_SERVER_COMMAND, // ( int clientNum, const char *fmt, ... ); + // reliably sends a command string to be interpreted by the given + // client. If clientNum is -1, it will be sent to all clients + + G_SET_CONFIGSTRING, // ( int num, const char *string ); + // config strings hold all the index strings, and various other information + // that is reliably communicated to all clients + // All of the current configstrings are sent to clients when + // they connect, and changes are sent to all connected clients. + // All confgstrings are cleared at each level start. + + G_GET_CONFIGSTRING, // ( int num, char *buffer, int bufferSize ); + + G_GET_USERINFO, // ( int num, char *buffer, int bufferSize ); + // userinfo strings are maintained by the server system, so they + // are persistant across level loads, while all other game visible + // data is completely reset + + G_SET_USERINFO, // ( int num, const char *buffer ); + + G_GET_SERVERINFO, // ( char *buffer, int bufferSize ); + // the serverinfo info string has all the cvars visible to server browsers + + G_SET_BRUSH_MODEL, // ( gentity_t *ent, const char *name ); + // sets mins and maxs based on the brushmodel name + + G_TRACE, // ( trace_t *results, const vec3_t start, const vec3_t mins, const vec3_t maxs, const vec3_t end, int passEntityNum, int contentmask ); + // collision detection against all linked entities + + G_POINT_CONTENTS, // ( const vec3_t point, int passEntityNum ); + // point contents against all linked entities + + G_IN_PVS, // ( const vec3_t p1, const vec3_t p2 ); + + G_IN_PVS_IGNORE_PORTALS, // ( const vec3_t p1, const vec3_t p2 ); + + G_ADJUST_AREA_PORTAL_STATE, // ( gentity_t *ent, qboolean open ); + + G_AREAS_CONNECTED, // ( int area1, int area2 ); + + G_LINKENTITY, // ( gentity_t *ent ); + // an entity will never be sent to a client or used for collision + // if it is not passed to linkentity. If the size, position, or + // solidity changes, it must be relinked. + + G_UNLINKENTITY, // ( gentity_t *ent ); + // call before removing an interactive entity + + G_ENTITIES_IN_BOX, // ( const vec3_t mins, const vec3_t maxs, gentity_t **list, int maxcount ); + // EntitiesInBox will return brush models based on their bounding box, + // so exact determination must still be done with EntityContact + + G_ENTITY_CONTACT, // ( const vec3_t mins, const vec3_t maxs, const gentity_t *ent ); + // perform an exact check against inline brush models of non-square shape + + // access for bots to get and free a server client (FIXME?) + G_BOT_ALLOCATE_CLIENT, // ( void ); + + G_BOT_FREE_CLIENT, // ( int clientNum ); + + G_GET_USERCMD, // ( int clientNum, usercmd_t *cmd ) + + G_GET_ENTITY_TOKEN, // qboolean ( char *buffer, int bufferSize ) + // Retrieves the next string token from the entity spawn text, returning + // false when all tokens have been parsed. + // This should only be done at GAME_INIT time. + + G_FS_GETFILELIST, + G_DEBUG_POLYGON_CREATE, + G_DEBUG_POLYGON_DELETE, + G_REAL_TIME, + G_SNAPVECTOR, + + BOTLIB_SETUP = 200, // ( void ); + BOTLIB_SHUTDOWN, // ( void ); + BOTLIB_LIBVAR_SET, + BOTLIB_LIBVAR_GET, + BOTLIB_PC_ADD_GLOBAL_DEFINE, + BOTLIB_START_FRAME, + BOTLIB_LOAD_MAP, + BOTLIB_UPDATENTITY, + BOTLIB_TEST, + + BOTLIB_GET_SNAPSHOT_ENTITY, // ( int client, int ent ); + BOTLIB_GET_CONSOLE_MESSAGE, // ( int client, char *message, int size ); + BOTLIB_USER_COMMAND, // ( int client, usercmd_t *ucmd ); + + BOTLIB_AAS_ENABLE_ROUTING_AREA = 300, + BOTLIB_AAS_BBOX_AREAS, + BOTLIB_AAS_AREA_INFO, + BOTLIB_AAS_ENTITY_INFO, + + BOTLIB_AAS_INITIALIZED, + BOTLIB_AAS_PRESENCE_TYPE_BOUNDING_BOX, + BOTLIB_AAS_TIME, + + BOTLIB_AAS_POINT_AREA_NUM, + BOTLIB_AAS_TRACE_AREAS, + + BOTLIB_AAS_POINT_CONTENTS, + BOTLIB_AAS_NEXT_BSP_ENTITY, + BOTLIB_AAS_VALUE_FOR_BSP_EPAIR_KEY, + BOTLIB_AAS_VECTOR_FOR_BSP_EPAIR_KEY, + BOTLIB_AAS_FLOAT_FOR_BSP_EPAIR_KEY, + BOTLIB_AAS_INT_FOR_BSP_EPAIR_KEY, + + BOTLIB_AAS_AREA_REACHABILITY, + + BOTLIB_AAS_AREA_TRAVEL_TIME_TO_GOAL_AREA, + + BOTLIB_AAS_SWIMMING, + BOTLIB_AAS_PREDICT_CLIENT_MOVEMENT, + + + + BOTLIB_EA_SAY = 400, + BOTLIB_EA_SAY_TEAM, + BOTLIB_EA_COMMAND, + + BOTLIB_EA_ACTION, + BOTLIB_EA_GESTURE, + BOTLIB_EA_TALK, + BOTLIB_EA_ATTACK, + BOTLIB_EA_USE, + BOTLIB_EA_RESPAWN, + BOTLIB_EA_CROUCH, + BOTLIB_EA_MOVE_UP, + BOTLIB_EA_MOVE_DOWN, + BOTLIB_EA_MOVE_FORWARD, + BOTLIB_EA_MOVE_BACK, + BOTLIB_EA_MOVE_LEFT, + BOTLIB_EA_MOVE_RIGHT, + + BOTLIB_EA_SELECT_WEAPON, + BOTLIB_EA_JUMP, + BOTLIB_EA_DELAYED_JUMP, + BOTLIB_EA_MOVE, + BOTLIB_EA_VIEW, + + BOTLIB_EA_END_REGULAR, + BOTLIB_EA_GET_INPUT, + BOTLIB_EA_RESET_INPUT, + + + + BOTLIB_AI_LOAD_CHARACTER = 500, + BOTLIB_AI_FREE_CHARACTER, + BOTLIB_AI_CHARACTERISTIC_FLOAT, + BOTLIB_AI_CHARACTERISTIC_BFLOAT, + BOTLIB_AI_CHARACTERISTIC_INTEGER, + BOTLIB_AI_CHARACTERISTIC_BINTEGER, + BOTLIB_AI_CHARACTERISTIC_STRING, + + BOTLIB_AI_ALLOC_CHAT_STATE, + BOTLIB_AI_FREE_CHAT_STATE, + BOTLIB_AI_QUEUE_CONSOLE_MESSAGE, + BOTLIB_AI_REMOVE_CONSOLE_MESSAGE, + BOTLIB_AI_NEXT_CONSOLE_MESSAGE, + BOTLIB_AI_NUM_CONSOLE_MESSAGE, + BOTLIB_AI_INITIAL_CHAT, + BOTLIB_AI_REPLY_CHAT, + BOTLIB_AI_CHAT_LENGTH, + BOTLIB_AI_ENTER_CHAT, + BOTLIB_AI_STRING_CONTAINS, + BOTLIB_AI_FIND_MATCH, + BOTLIB_AI_MATCH_VARIABLE, + BOTLIB_AI_UNIFY_WHITE_SPACES, + BOTLIB_AI_REPLACE_SYNONYMS, + BOTLIB_AI_LOAD_CHAT_FILE, + BOTLIB_AI_SET_CHAT_GENDER, + BOTLIB_AI_SET_CHAT_NAME, + + BOTLIB_AI_RESET_GOAL_STATE, + BOTLIB_AI_RESET_AVOID_GOALS, + BOTLIB_AI_PUSH_GOAL, + BOTLIB_AI_POP_GOAL, + BOTLIB_AI_EMPTY_GOAL_STACK, + BOTLIB_AI_DUMP_AVOID_GOALS, + BOTLIB_AI_DUMP_GOAL_STACK, + BOTLIB_AI_GOAL_NAME, + BOTLIB_AI_GET_TOP_GOAL, + BOTLIB_AI_GET_SECOND_GOAL, + BOTLIB_AI_CHOOSE_LTG_ITEM, + BOTLIB_AI_CHOOSE_NBG_ITEM, + BOTLIB_AI_TOUCHING_GOAL, + BOTLIB_AI_ITEM_GOAL_IN_VIS_BUT_NOT_VISIBLE, + BOTLIB_AI_GET_LEVEL_ITEM_GOAL, + BOTLIB_AI_AVOID_GOAL_TIME, + BOTLIB_AI_INIT_LEVEL_ITEMS, + BOTLIB_AI_UPDATE_ENTITY_ITEMS, + BOTLIB_AI_LOAD_ITEM_WEIGHTS, + BOTLIB_AI_FREE_ITEM_WEIGHTS, + BOTLIB_AI_SAVE_GOAL_FUZZY_LOGIC, + BOTLIB_AI_ALLOC_GOAL_STATE, + BOTLIB_AI_FREE_GOAL_STATE, + + BOTLIB_AI_RESET_MOVE_STATE, + BOTLIB_AI_MOVE_TO_GOAL, + BOTLIB_AI_MOVE_IN_DIRECTION, + BOTLIB_AI_RESET_AVOID_REACH, + BOTLIB_AI_RESET_LAST_AVOID_REACH, + BOTLIB_AI_REACHABILITY_AREA, + BOTLIB_AI_MOVEMENT_VIEW_TARGET, + BOTLIB_AI_ALLOC_MOVE_STATE, + BOTLIB_AI_FREE_MOVE_STATE, + BOTLIB_AI_INIT_MOVE_STATE, + + BOTLIB_AI_CHOOSE_BEST_FIGHT_WEAPON, + BOTLIB_AI_GET_WEAPON_INFO, + BOTLIB_AI_LOAD_WEAPON_WEIGHTS, + BOTLIB_AI_ALLOC_WEAPON_STATE, + BOTLIB_AI_FREE_WEAPON_STATE, + BOTLIB_AI_RESET_WEAPON_STATE, + + BOTLIB_AI_GENETIC_PARENTS_AND_CHILD_SELECTION, + BOTLIB_AI_INTERBREED_GOAL_FUZZY_LOGIC, + BOTLIB_AI_MUTATE_GOAL_FUZZY_LOGIC, + BOTLIB_AI_GET_NEXT_CAMP_SPOT_GOAL, + BOTLIB_AI_GET_MAP_LOCATION_GOAL, + BOTLIB_AI_NUM_INITIAL_CHATS, + BOTLIB_AI_GET_CHAT_MESSAGE, + BOTLIB_AI_REMOVE_FROM_AVOID_GOALS, + BOTLIB_AI_PREDICT_VISIBLE_POSITION, + + BOTLIB_AI_SET_AVOID_GOAL_TIME, + BOTLIB_AI_ADD_AVOID_SPOT, + BOTLIB_AAS_ALTERNATIVE_ROUTE_GOAL, + BOTLIB_AAS_PREDICT_ROUTE, + BOTLIB_AAS_POINT_REACHABILITY_AREA_INDEX, + + BOTLIB_PC_LOAD_SOURCE, + BOTLIB_PC_FREE_SOURCE, + BOTLIB_PC_READ_TOKEN, + BOTLIB_PC_SOURCE_FILE_AND_LINE +} gameImport_t; + + +// +// functions exported by the game subsystem +// +typedef enum { + GAME_INIT, // ( int levelTime, int randomSeed, int restart ); + // init and shutdown will be called every single level + // The game should call G_GET_ENTITY_TOKEN to parse through all the + // entity configuration text and spawn gentities. + + GAME_SHUTDOWN, // (void); + + GAME_CLIENT_CONNECT, // ( int clientNum, qboolean firstTime, qboolean isBot ); + // return NULL if the client is allowed to connect, otherwise return + // a text string with the reason for denial + + GAME_CLIENT_BEGIN, // ( int clientNum ); + + GAME_CLIENT_USERINFO_CHANGED, // ( int clientNum ); + + GAME_CLIENT_DISCONNECT, // ( int clientNum ); + + GAME_CLIENT_COMMAND, // ( int clientNum ); + + GAME_CLIENT_THINK, // ( int clientNum ); + + GAME_RUN_FRAME, // ( int levelTime ); + + GAME_CONSOLE_COMMAND, // ( void ); + // ConsoleCommand will be called when a command has been issued + // that is not recognized as a builtin function. + // The game can issue trap_argc() / trap_argv() commands to get the command + // and parameters. Return qfalse if the game doesn't recognize it as a command. + + BOTAI_START_FRAME // ( int time ); +} gameExport_t; + diff --git a/src/game/g_session.c b/src/game/g_session.c new file mode 100644 index 00000000..06f2b1ba --- /dev/null +++ b/src/game/g_session.c @@ -0,0 +1,188 @@ +// Copyright (C) 1999-2000 Id Software, Inc. +// + +/* + * Portions Copyright (C) 2000-2001 Tim Angus + * + * This program 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, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ + +/* To assertain which portions are licensed under the GPL and which are + * licensed by Id Software, Inc. please run a diff between the equivalent + * versions of the "Tremulous" modification and the unmodified "Quake3" + * game source code. + */ + +#include "g_local.h" + + +/* +======================================================================= + + SESSION DATA + +Session data is the only data that stays persistant across level loads +and tournament restarts. +======================================================================= +*/ + +/* +================ +G_WriteClientSessionData + +Called on game shutdown +================ +*/ +void G_WriteClientSessionData( gclient_t *client ) { + const char *s; + const char *var; + + s = va("%i %i %i %i %i %i %i", + client->sess.sessionTeam, + client->sess.spectatorTime, + client->sess.spectatorState, + client->sess.spectatorClient, + client->sess.wins, + client->sess.losses, + client->sess.teamLeader + ); + + var = va( "session%i", client - level.clients ); + + trap_Cvar_Set( var, s ); +} + +/* +================ +G_ReadSessionData + +Called on a reconnect +================ +*/ +void G_ReadSessionData( gclient_t *client ) { + char s[MAX_STRING_CHARS]; + const char *var; + + var = va( "session%i", client - level.clients ); + trap_Cvar_VariableStringBuffer( var, s, sizeof(s) ); + + sscanf( s, "%i %i %i %i %i %i %i", + &client->sess.sessionTeam, + &client->sess.spectatorTime, + &client->sess.spectatorState, + &client->sess.spectatorClient, + &client->sess.wins, + &client->sess.losses, + &client->sess.teamLeader + ); +} + + +/* +================ +G_InitSessionData + +Called on a first-time connect +================ +*/ +void G_InitSessionData( gclient_t *client, char *userinfo ) { + clientSession_t *sess; + const char *value; + + sess = &client->sess; + + // initial team determination + if ( g_gametype.integer >= GT_TEAM ) { + if ( g_teamAutoJoin.integer ) { + sess->sessionTeam = PickTeam( -1 ); + BroadcastTeamChange( client, -1 ); + } else { + // always spawn as spectator in team games + sess->sessionTeam = TEAM_SPECTATOR; + } + } else { + value = Info_ValueForKey( userinfo, "team" ); + if ( value[0] == 's' ) { + // a willing spectator, not a waiting-in-line + sess->sessionTeam = TEAM_SPECTATOR; + } else { + switch ( g_gametype.integer ) { + default: + case GT_FFA: + case GT_SINGLE_PLAYER: + if ( g_maxGameClients.integer > 0 && + level.numNonSpectatorClients >= g_maxGameClients.integer ) { + sess->sessionTeam = TEAM_SPECTATOR; + } else { + sess->sessionTeam = TEAM_FREE; + } + break; + case GT_TOURNAMENT: + // if the game is full, go into a waiting mode + if ( level.numNonSpectatorClients >= 2 ) { + sess->sessionTeam = TEAM_SPECTATOR; + } else { + sess->sessionTeam = TEAM_FREE; + } + break; + } + } + } + + sess->spectatorState = SPECTATOR_FREE; + sess->spectatorTime = level.time; + + G_WriteClientSessionData( client ); +} + + +/* +================== +G_InitWorldSession + +================== +*/ +void G_InitWorldSession( void ) { + char s[MAX_STRING_CHARS]; + int gt; + + trap_Cvar_VariableStringBuffer( "session", s, sizeof(s) ); + gt = atoi( s ); + + // if the gametype changed since the last session, don't use any + // client sessions + if ( g_gametype.integer != gt ) { + level.newSession = qtrue; + G_Printf( "Gametype changed, clearing session data.\n" ); + } +} + +/* +================== +G_WriteSessionData + +================== +*/ +void G_WriteSessionData( void ) { + int i; + + trap_Cvar_Set( "session", va("%i", g_gametype.integer) ); + + for ( i = 0 ; i < level.maxclients ; i++ ) { + if ( level.clients[i].pers.connected == CON_CONNECTED ) { + G_WriteClientSessionData( &level.clients[i] ); + } + } +} diff --git a/src/game/g_spawn.c b/src/game/g_spawn.c new file mode 100644 index 00000000..c0e2a9be --- /dev/null +++ b/src/game/g_spawn.c @@ -0,0 +1,635 @@ +// Copyright (C) 1999-2000 Id Software, Inc. +// + +/* + * Portions Copyright (C) 2000-2001 Tim Angus + * + * This program 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, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ + +/* To assertain which portions are licensed under the GPL and which are + * licensed by Id Software, Inc. please run a diff between the equivalent + * versions of the "Tremulous" modification and the unmodified "Quake3" + * game source code. + */ + +#include "g_local.h" + +qboolean G_SpawnString( const char *key, const char *defaultString, char **out ) { + int i; + + if ( !level.spawning ) { + *out = (char *)defaultString; +// G_Error( "G_SpawnString() called while not spawning" ); + } + + for ( i = 0 ; i < level.numSpawnVars ; i++ ) { + if ( !Q_stricmp( key, level.spawnVars[i][0] ) ) { + *out = level.spawnVars[i][1]; + return qtrue; + } + } + + *out = (char *)defaultString; + return qfalse; +} + +qboolean G_SpawnFloat( const char *key, const char *defaultString, float *out ) { + char *s; + qboolean present; + + present = G_SpawnString( key, defaultString, &s ); + *out = atof( s ); + return present; +} + +qboolean G_SpawnInt( const char *key, const char *defaultString, int *out ) { + char *s; + qboolean present; + + present = G_SpawnString( key, defaultString, &s ); + *out = atoi( s ); + return present; +} + +qboolean G_SpawnVector( const char *key, const char *defaultString, float *out ) { + char *s; + qboolean present; + + present = G_SpawnString( key, defaultString, &s ); + sscanf( s, "%f %f %f", &out[0], &out[1], &out[2] ); + return present; +} + + + +// +// fields are needed for spawning from the entity string +// +typedef enum { + F_INT, + F_FLOAT, + F_LSTRING, // string on disk, pointer in memory, TAG_LEVEL + F_GSTRING, // string on disk, pointer in memory, TAG_GAME + F_VECTOR, + F_ANGLEHACK, + F_ENTITY, // index on disk, pointer in memory + F_ITEM, // index on disk, pointer in memory + F_CLIENT, // index on disk, pointer in memory + F_IGNORE +} fieldtype_t; + +typedef struct +{ + char *name; + int ofs; + fieldtype_t type; + int flags; +} field_t; + +field_t fields[] = { + {"classname", FOFS(classname), F_LSTRING}, + {"origin", FOFS(s.origin), F_VECTOR}, + {"model", FOFS(model), F_LSTRING}, + {"model2", FOFS(model2), F_LSTRING}, + {"spawnflags", FOFS(spawnflags), F_INT}, + {"speed", FOFS(speed), F_FLOAT}, + {"target", FOFS(target), F_LSTRING}, + {"targetname", FOFS(targetname), F_LSTRING}, + {"message", FOFS(message), F_LSTRING}, + {"team", FOFS(team), F_LSTRING}, + {"wait", FOFS(wait), F_FLOAT}, + {"random", FOFS(random), F_FLOAT}, + {"count", FOFS(count), F_INT}, + {"health", FOFS(health), F_INT}, + {"light", 0, F_IGNORE}, + {"dmg", FOFS(damage), F_INT}, + {"angles", FOFS(s.angles), F_VECTOR}, + {"angle", FOFS(s.angles), F_ANGLEHACK}, + {"targetShaderName", FOFS(targetShaderName), F_LSTRING}, + {"targetShaderNewName", FOFS(targetShaderNewName), F_LSTRING}, + + {NULL} +}; + + +typedef struct { + char *name; + void (*spawn)(gentity_t *ent); +} spawn_t; + +void SP_info_player_start (gentity_t *ent); +void SP_info_player_deathmatch (gentity_t *ent); +void SP_info_player_intermission (gentity_t *ent); + +//TA: extra bits +void SP_info_droid_intermission (gentity_t *ent); +void SP_info_human_intermission (gentity_t *ent); + +void SP_info_firstplace(gentity_t *ent); +void SP_info_secondplace(gentity_t *ent); +void SP_info_thirdplace(gentity_t *ent); +void SP_info_podium(gentity_t *ent); + +void SP_func_plat (gentity_t *ent); +void SP_func_static (gentity_t *ent); +void SP_func_rotating (gentity_t *ent); +void SP_func_bobbing (gentity_t *ent); +void SP_func_pendulum( gentity_t *ent ); +void SP_func_button (gentity_t *ent); +void SP_func_door (gentity_t *ent); +void SP_func_train (gentity_t *ent); +void SP_func_timer (gentity_t *self); + +void SP_trigger_always (gentity_t *ent); +void SP_trigger_multiple (gentity_t *ent); +void SP_trigger_push (gentity_t *ent); +void SP_trigger_teleport (gentity_t *ent); +void SP_trigger_hurt (gentity_t *ent); + +void SP_target_remove_powerups( gentity_t *ent ); +void SP_target_give (gentity_t *ent); +void SP_target_delay (gentity_t *ent); +void SP_target_speaker (gentity_t *ent); +void SP_target_print (gentity_t *ent); +void SP_target_laser (gentity_t *self); +void SP_target_character (gentity_t *ent); +void SP_target_score( gentity_t *ent ); +void SP_target_teleporter( gentity_t *ent ); +void SP_target_relay (gentity_t *ent); +void SP_target_kill (gentity_t *ent); +void SP_target_position (gentity_t *ent); +void SP_target_location (gentity_t *ent); +void SP_target_push (gentity_t *ent); + +void SP_light (gentity_t *self); +void SP_info_null (gentity_t *self); +void SP_info_notnull (gentity_t *self); +void SP_info_camp (gentity_t *self); +void SP_path_corner (gentity_t *self); + +void SP_misc_teleporter_dest (gentity_t *self); +void SP_misc_model(gentity_t *ent); +void SP_misc_portal_camera(gentity_t *ent); +void SP_misc_portal_surface(gentity_t *ent); + +void SP_shooter_rocket( gentity_t *ent ); +void SP_shooter_plasma( gentity_t *ent ); +void SP_shooter_grenade( gentity_t *ent ); + +void SP_team_CTF_redplayer( gentity_t *ent ); +void SP_team_CTF_blueplayer( gentity_t *ent ); + +void SP_team_CTF_redspawn( gentity_t *ent ); +void SP_team_CTF_bluespawn( gentity_t *ent ); + +void SP_item_botroam( gentity_t *ent ) {}; + +spawn_t spawns[] = { + // info entities don't do anything at all, but provide positional + // information for things controlled by other processes + {"info_player_start", SP_info_player_start}, + {"info_player_deathmatch", SP_info_player_deathmatch}, + {"info_player_intermission", SP_info_player_intermission}, + + //TA: extra bits + {"info_droid_intermission", SP_info_droid_intermission}, + {"info_human_intermission", SP_info_human_intermission}, + + {"info_null", SP_info_null}, + {"info_notnull", SP_info_notnull}, // use target_position instead + {"info_camp", SP_info_camp}, + + {"func_plat", SP_func_plat}, + {"func_button", SP_func_button}, + {"func_door", SP_func_door}, + {"func_static", SP_func_static}, + {"func_rotating", SP_func_rotating}, + {"func_bobbing", SP_func_bobbing}, + {"func_pendulum", SP_func_pendulum}, + {"func_train", SP_func_train}, + {"func_group", SP_info_null}, + {"func_timer", SP_func_timer}, // rename trigger_timer? + + // Triggers are brush objects that cause an effect when contacted + // by a living player, usually involving firing targets. + // While almost everything could be done with + // a single trigger class and different targets, triggered effects + // could not be client side predicted (push and teleport). + {"trigger_always", SP_trigger_always}, + {"trigger_multiple", SP_trigger_multiple}, + {"trigger_push", SP_trigger_push}, + {"trigger_teleport", SP_trigger_teleport}, + {"trigger_hurt", SP_trigger_hurt}, + + // targets perform no action by themselves, but must be triggered + // by another entity + {"target_give", SP_target_give}, + {"target_remove_powerups", SP_target_remove_powerups}, + {"target_delay", SP_target_delay}, + {"target_speaker", SP_target_speaker}, + {"target_print", SP_target_print}, + {"target_laser", SP_target_laser}, + {"target_score", SP_target_score}, + {"target_teleporter", SP_target_teleporter}, + {"target_relay", SP_target_relay}, + {"target_kill", SP_target_kill}, + {"target_position", SP_target_position}, + {"target_location", SP_target_location}, + {"target_push", SP_target_push}, + + {"light", SP_light}, + {"path_corner", SP_path_corner}, + + {"misc_teleporter_dest", SP_misc_teleporter_dest}, + {"misc_model", SP_misc_model}, + {"misc_portal_surface", SP_misc_portal_surface}, + {"misc_portal_camera", SP_misc_portal_camera}, + + {"shooter_rocket", SP_shooter_rocket}, + {"shooter_grenade", SP_shooter_grenade}, + {"shooter_plasma", SP_shooter_plasma}, + + {"team_CTF_redplayer", SP_team_CTF_redplayer}, + {"team_CTF_blueplayer", SP_team_CTF_blueplayer}, + + {"team_CTF_redspawn", SP_team_CTF_redspawn}, + {"team_CTF_bluespawn", SP_team_CTF_bluespawn}, + + {"item_botroam", SP_item_botroam}, + + {0, 0} +}; + +/* +=============== +G_CallSpawn + +Finds the spawn function for the entity and calls it, +returning qfalse if not found +=============== +*/ +qboolean G_CallSpawn( gentity_t *ent ) { + spawn_t *s; + gitem_t *item; + + if ( !ent->classname ) { + G_Printf ("G_CallSpawn: NULL classname\n"); + return qfalse; + } + + // check item spawn functions + for ( item=bg_itemlist+1 ; item->classname ; item++ ) { + if ( !strcmp(item->classname, ent->classname) ) { + //TA: don't allow items to spawn (atleast for the time being - might need to later?) + G_SpawnItem( ent, item ); + return qtrue; + //return qfalse; + } + } + + // check normal spawn functions + for ( s=spawns ; s->name ; s++ ) { + if ( !strcmp(s->name, ent->classname) ) { + // found it + s->spawn(ent); + return qtrue; + } + } + G_Printf ("%s doesn't have a spawn function\n", ent->classname); + return qfalse; +} + +/* +============= +G_NewString + +Builds a copy of the string, translating \n to real linefeeds +so message texts can be multi-line +============= +*/ +char *G_NewString( const char *string ) { + char *newb, *new_p; + int i,l; + + l = strlen(string) + 1; + + newb = G_Alloc( l ); + + new_p = newb; + + // turn \n into a real linefeed + for ( i=0 ; i< l ; i++ ) { + if (string[i] == '\\' && i < l-1) { + i++; + if (string[i] == 'n') { + *new_p++ = '\n'; + } else { + *new_p++ = '\\'; + } + } else { + *new_p++ = string[i]; + } + } + + return newb; +} + + + + +/* +=============== +G_ParseField + +Takes a key/value pair and sets the binary values +in a gentity +=============== +*/ +void G_ParseField( const char *key, const char *value, gentity_t *ent ) { + field_t *f; + byte *b; + float v; + vec3_t vec; + + for ( f=fields ; f->name ; f++ ) { + if ( !Q_stricmp(f->name, key) ) { + // found it + b = (byte *)ent; + + switch( f->type ) { + case F_LSTRING: + *(char **)(b+f->ofs) = G_NewString (value); + break; + case F_VECTOR: + sscanf (value, "%f %f %f", &vec[0], &vec[1], &vec[2]); + ((float *)(b+f->ofs))[0] = vec[0]; + ((float *)(b+f->ofs))[1] = vec[1]; + ((float *)(b+f->ofs))[2] = vec[2]; + break; + case F_INT: + *(int *)(b+f->ofs) = atoi(value); + break; + case F_FLOAT: + *(float *)(b+f->ofs) = atof(value); + break; + case F_ANGLEHACK: + v = atof(value); + ((float *)(b+f->ofs))[0] = 0; + ((float *)(b+f->ofs))[1] = v; + ((float *)(b+f->ofs))[2] = 0; + break; + default: + case F_IGNORE: + break; + } + return; + } + } +} + + + + +/* +=================== +G_SpawnGEntityFromSpawnVars + +Spawn an entity and fill in all of the level fields from +level.spawnVars[], then call the class specfic spawn function +=================== +*/ +void G_SpawnGEntityFromSpawnVars( void ) { + int i; + gentity_t *ent; + char *s, *value, *gametypeName; + static char *gametypeNames[] = {"ffa", "tournament", "single", "team", "ctf", "oneflag", "obelisk", "harvester", "teamtournament"}; + + + // get the next free entity + ent = G_Spawn(); + + for ( i = 0 ; i < level.numSpawnVars ; i++ ) { + G_ParseField( level.spawnVars[i][0], level.spawnVars[i][1], ent ); + } + + // check for "notteam" / "notfree" flags + if ( g_gametype.integer == GT_SINGLE_PLAYER ) { + G_SpawnInt( "notsingle", "0", &i ); + if ( i ) { + G_FreeEntity( ent ); + return; + } + } + if ( g_gametype.integer >= GT_TEAM ) { + G_SpawnInt( "notteam", "0", &i ); + if ( i ) { + G_FreeEntity( ent ); + return; + } + } else { + G_SpawnInt( "notfree", "0", &i ); + if ( i ) { + G_FreeEntity( ent ); + return; + } + } + + if( G_SpawnString( "gametype", NULL, &value ) ) { + if( g_gametype.integer >= GT_FFA && g_gametype.integer < GT_MAX_GAME_TYPE ) { + gametypeName = gametypeNames[g_gametype.integer]; + + s = strstr( value, gametypeName ); + if( !s ) { + G_FreeEntity( ent ); + return; + } + } + } + + // move editor origin to pos + VectorCopy( ent->s.origin, ent->s.pos.trBase ); + VectorCopy( ent->s.origin, ent->r.currentOrigin ); + + // if we didn't get a classname, don't bother spawning anything + if ( !G_CallSpawn( ent ) ) { + G_FreeEntity( ent ); + } +} + + + +/* +==================== +G_AddSpawnVarToken +==================== +*/ +char *G_AddSpawnVarToken( const char *string ) { + int l; + char *dest; + + l = strlen( string ); + if ( level.numSpawnVarChars + l + 1 > MAX_SPAWN_VARS_CHARS ) { + G_Error( "G_AddSpawnVarToken: MAX_SPAWN_VARS" ); + } + + dest = level.spawnVarChars + level.numSpawnVarChars; + memcpy( dest, string, l+1 ); + + level.numSpawnVarChars += l + 1; + + return dest; +} + +/* +==================== +G_ParseSpawnVars + +Parses a brace bounded set of key / value pairs out of the +level's entity strings into level.spawnVars[] + +This does not actually spawn an entity. +==================== +*/ +qboolean G_ParseSpawnVars( void ) { + char keyname[MAX_TOKEN_CHARS]; + char com_token[MAX_TOKEN_CHARS]; + + level.numSpawnVars = 0; + level.numSpawnVarChars = 0; + + // parse the opening brace + if ( !trap_GetEntityToken( com_token, sizeof( com_token ) ) ) { + // end of spawn string + return qfalse; + } + if ( com_token[0] != '{' ) { + G_Error( "G_ParseSpawnVars: found %s when expecting {",com_token ); + } + + // go through all the key / value pairs + while ( 1 ) { + // parse key + if ( !trap_GetEntityToken( keyname, sizeof( keyname ) ) ) { + G_Error( "G_ParseSpawnVars: EOF without closing brace" ); + } + + if ( keyname[0] == '}' ) { + break; + } + + // parse value + if ( !trap_GetEntityToken( com_token, sizeof( com_token ) ) ) { + G_Error( "G_ParseSpawnVars: EOF without closing brace" ); + } + + if ( com_token[0] == '}' ) { + G_Error( "G_ParseSpawnVars: closing brace without data" ); + } + if ( level.numSpawnVars == MAX_SPAWN_VARS ) { + G_Error( "G_ParseSpawnVars: MAX_SPAWN_VARS" ); + } + level.spawnVars[ level.numSpawnVars ][0] = G_AddSpawnVarToken( keyname ); + level.spawnVars[ level.numSpawnVars ][1] = G_AddSpawnVarToken( com_token ); + level.numSpawnVars++; + } + + return qtrue; +} + + + +/*QUAKED worldspawn (0 0 0) ? + +Every map should have exactly one worldspawn. +"music" music wav file +"gravity" 800 is default gravity +"message" Text to print during connection process +*/ +void SP_worldspawn( void ) { + char *s; + + G_SpawnString( "classname", "", &s ); + if ( Q_stricmp( s, "worldspawn" ) ) { + G_Error( "SP_worldspawn: The first entity isn't 'worldspawn'" ); + } + + // make some data visible to connecting client + trap_SetConfigstring( CS_GAME_VERSION, GAME_VERSION ); + + trap_SetConfigstring( CS_LEVEL_START_TIME, va("%i", level.startTime ) ); + + G_SpawnString( "music", "", &s ); + trap_SetConfigstring( CS_MUSIC, s ); + + G_SpawnString( "message", "", &s ); + trap_SetConfigstring( CS_MESSAGE, s ); // map specific message + + trap_SetConfigstring( CS_MOTD, g_motd.string ); // message of the day + + G_SpawnString( "gravity", "800", &s ); + trap_Cvar_Set( "g_gravity", s ); + + G_SpawnString( "enableDust", "0", &s ); + trap_Cvar_Set( "g_enableDust", s ); + + G_SpawnString( "enableBreath", "0", &s ); + trap_Cvar_Set( "g_enableBreath", s ); + + g_entities[ENTITYNUM_WORLD].s.number = ENTITYNUM_WORLD; + g_entities[ENTITYNUM_WORLD].classname = "worldspawn"; + + // see if we want a warmup time + trap_SetConfigstring( CS_WARMUP, "" ); + if ( g_restarted.integer ) { + trap_Cvar_Set( "g_restarted", "0" ); + level.warmupTime = 0; + } else if ( g_doWarmup.integer ) { // Turn it on + level.warmupTime = -1; + trap_SetConfigstring( CS_WARMUP, va("%i", level.warmupTime) ); + G_LogPrintf( "Warmup:\n" ); + } + +} + + +/* +============== +G_SpawnEntitiesFromString + +Parses textual entity definitions out of an entstring and spawns gentities. +============== +*/ +void G_SpawnEntitiesFromString( void ) { + // allow calls to G_Spawn*() + level.spawning = qtrue; + level.numSpawnVars = 0; + + // the worldspawn is not an actual entity, but it still + // has a "spawn" function to perform any global setup + // needed by a level (setting configstrings or cvars, etc) + if ( !G_ParseSpawnVars() ) { + G_Error( "SpawnEntities: no entities" ); + } + SP_worldspawn(); + + // parse ents + while( G_ParseSpawnVars() ) { + G_SpawnGEntityFromSpawnVars(); + } + + level.spawning = qfalse; // any future calls to G_Spawn*() will be errors +} + diff --git a/src/game/g_svcmds.c b/src/game/g_svcmds.c new file mode 100644 index 00000000..81af978f --- /dev/null +++ b/src/game/g_svcmds.c @@ -0,0 +1,488 @@ +// Copyright (C) 1999-2000 Id Software, Inc. +// + +/* + * Portions Copyright (C) 2000-2001 Tim Angus + * + * This program 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, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ + +/* To assertain which portions are licensed under the GPL and which are + * licensed by Id Software, Inc. please run a diff between the equivalent + * versions of the "Tremulous" modification and the unmodified "Quake3" + * game source code. + */ + +// this file holds commands that can be executed by the server console, but not remote clients + +#include "g_local.h" + + +/* +============================================================================== + +PACKET FILTERING + + +You can add or remove addresses from the filter list with: + +addip <ip> +removeip <ip> + +The ip address is specified in dot format, and any unspecified digits will match any value, so you can specify an entire class C network with "addip 192.246.40". + +Removeip will only remove an address specified exactly the same way. You cannot addip a subnet, then removeip a single host. + +listip +Prints the current list of filters. + +g_filterban <0 or 1> + +If 1 (the default), then ip addresses matching the current list will be prohibited from entering the game. This is the default setting. + +If 0, then only addresses matching the list will be allowed. This lets you easily set up a private game, or a game that only allows players from your local network. + + +============================================================================== +*/ + +// extern vmCvar_t g_banIPs; +// extern vmCvar_t g_filterBan; + + +typedef struct ipFilter_s +{ + unsigned mask; + unsigned compare; +} ipFilter_t; + +#define MAX_IPFILTERS 1024 + +static ipFilter_t ipFilters[MAX_IPFILTERS]; +static int numIPFilters; + +/* +================= +StringToFilter +================= +*/ +static qboolean StringToFilter (char *s, ipFilter_t *f) +{ + char num[128]; + int i, j; + byte b[4]; + byte m[4]; + + for (i=0 ; i<4 ; i++) + { + b[i] = 0; + m[i] = 0; + } + + for (i=0 ; i<4 ; i++) + { + if (*s < '0' || *s > '9') + { + G_Printf( "Bad filter address: %s\n", s ); + return qfalse; + } + + j = 0; + while (*s >= '0' && *s <= '9') + { + num[j++] = *s++; + } + num[j] = 0; + b[i] = atoi(num); + if (b[i] != 0) + m[i] = 255; + + if (!*s) + break; + s++; + } + + f->mask = *(unsigned *)m; + f->compare = *(unsigned *)b; + + return qtrue; +} + +/* +================= +UpdateIPBans +================= +*/ +static void UpdateIPBans (void) +{ + byte b[4]; + int i; + char iplist[MAX_INFO_STRING]; + + *iplist = 0; + for (i = 0 ; i < numIPFilters ; i++) + { + if (ipFilters[i].compare == 0xffffffff) + continue; + + *(unsigned *)b = ipFilters[i].compare; + Com_sprintf( iplist + strlen(iplist), sizeof(iplist) - strlen(iplist), + "%i.%i.%i.%i ", b[0], b[1], b[2], b[3]); + } + + trap_Cvar_Set( "g_banIPs", iplist ); +} + +/* +================= +G_FilterPacket +================= +*/ +qboolean G_FilterPacket (char *from) +{ + int i; + unsigned in; + byte m[4]; + char *p; + + i = 0; + p = from; + while (*p && i < 4) { + m[i] = 0; + while (*p >= '0' && *p <= '9') { + m[i] = m[i]*10 + (*p - '0'); + p++; + } + if (!*p || *p == ':') + break; + i++, p++; + } + + in = *(unsigned *)m; + + for (i=0 ; i<numIPFilters ; i++) + if ( (in & ipFilters[i].mask) == ipFilters[i].compare) + return g_filterBan.integer != 0; + + return g_filterBan.integer == 0; +} + +/* +================= +AddIP +================= +*/ +static void AddIP( char *str ) +{ + int i; + + for (i = 0 ; i < numIPFilters ; i++) + if (ipFilters[i].compare == 0xffffffff) + break; // free spot + if (i == numIPFilters) + { + if (numIPFilters == MAX_IPFILTERS) + { + G_Printf ("IP filter list is full\n"); + return; + } + numIPFilters++; + } + + if (!StringToFilter (str, &ipFilters[i])) + ipFilters[i].compare = 0xffffffffu; + + UpdateIPBans(); +} + +/* +================= +G_ProcessIPBans +================= +*/ +void G_ProcessIPBans(void) +{ + char *s, *t; + char str[MAX_TOKEN_CHARS]; + + Q_strncpyz( str, g_banIPs.string, sizeof(str) ); + + for (t = s = g_banIPs.string; *t; /* */ ) { + s = strchr(s, ' '); + if (!s) + break; + while (*s == ' ') + *s++ = 0; + if (*t) + AddIP( t ); + t = s; + } +} + + +/* +================= +Svcmd_AddIP_f +================= +*/ +void Svcmd_AddIP_f (void) +{ + char str[MAX_TOKEN_CHARS]; + + if ( trap_Argc() < 2 ) { + G_Printf("Usage: addip <ip-mask>\n"); + return; + } + + trap_Argv( 1, str, sizeof( str ) ); + + AddIP( str ); + +} + +/* +================= +Svcmd_RemoveIP_f +================= +*/ +void Svcmd_RemoveIP_f (void) +{ + ipFilter_t f; + int i; + char str[MAX_TOKEN_CHARS]; + + if ( trap_Argc() < 2 ) { + G_Printf("Usage: sv removeip <ip-mask>\n"); + return; + } + + trap_Argv( 1, str, sizeof( str ) ); + + if (!StringToFilter (str, &f)) + return; + + for (i=0 ; i<numIPFilters ; i++) { + if (ipFilters[i].mask == f.mask && + ipFilters[i].compare == f.compare) { + ipFilters[i].compare = 0xffffffffu; + G_Printf ("Removed.\n"); + + UpdateIPBans(); + return; + } + } + + G_Printf ( "Didn't find %s.\n", str ); +} + +/* +=================== +Svcmd_EntityList_f +=================== +*/ +void Svcmd_EntityList_f (void) { + int e; + gentity_t *check; + + check = g_entities+1; + for (e = 1; e < level.num_entities ; e++, check++) { + if ( !check->inuse ) { + continue; + } + G_Printf("%3i:", e); + switch ( check->s.eType ) { + case ET_GENERAL: + G_Printf("ET_GENERAL "); + break; + case ET_PLAYER: + G_Printf("ET_PLAYER "); + break; + case ET_ITEM: + G_Printf("ET_ITEM "); + break; + case ET_BUILDABLE: + G_Printf("ET_BUILDABLE "); + break; + case ET_MISSILE: + G_Printf("ET_MISSILE "); + break; + case ET_MOVER: + G_Printf("ET_MOVER "); + break; + case ET_BEAM: + G_Printf("ET_BEAM "); + break; + case ET_PORTAL: + G_Printf("ET_PORTAL "); + break; + case ET_SPEAKER: + G_Printf("ET_SPEAKER "); + break; + case ET_PUSH_TRIGGER: + G_Printf("ET_PUSH_TRIGGER "); + break; + case ET_TELEPORT_TRIGGER: + G_Printf("ET_TELEPORT_TRIGGER "); + break; + case ET_INVISIBLE: + G_Printf("ET_INVISIBLE "); + break; + case ET_GRAPPLE: + G_Printf("ET_GRAPPLE "); + break; + default: + G_Printf("%3i ", check->s.eType); + break; + } + + if ( check->classname ) { + G_Printf("%s", check->classname); + } + G_Printf("\n"); + } +} + +gclient_t *ClientForString( const char *s ) { + gclient_t *cl; + int i; + int idnum; + + // numeric values are just slot numbers + if ( s[0] >= '0' && s[0] <= '9' ) { + idnum = atoi( s ); + if ( idnum < 0 || idnum >= level.maxclients ) { + Com_Printf( "Bad client slot: %i\n", idnum ); + return NULL; + } + + cl = &level.clients[idnum]; + if ( cl->pers.connected == CON_DISCONNECTED ) { + G_Printf( "Client %i is not connected\n", idnum ); + return NULL; + } + return cl; + } + + // check for a name match + for ( i=0 ; i < level.maxclients ; i++ ) { + cl = &level.clients[i]; + if ( cl->pers.connected == CON_DISCONNECTED ) { + continue; + } + if ( !Q_stricmp( cl->pers.netname, s ) ) { + return cl; + } + } + + G_Printf( "User %s is not on the server\n", s ); + + return NULL; +} + +/* +=================== +Svcmd_ForceTeam_f + +forceteam <player> <team> +=================== +*/ +void Svcmd_ForceTeam_f( void ) { + gclient_t *cl; + char str[MAX_TOKEN_CHARS]; + + // find the player + trap_Argv( 1, str, sizeof( str ) ); + cl = ClientForString( str ); + if ( !cl ) { + return; + } + + // set the team + trap_Argv( 2, str, sizeof( str ) ); + SetTeam( &g_entities[cl - level.clients], str ); +} + +char *ConcatArgs( int start ); + +/* +================= +ConsoleCommand + +================= +*/ +qboolean ConsoleCommand( void ) { + char cmd[MAX_TOKEN_CHARS]; + + trap_Argv( 0, cmd, sizeof( cmd ) ); + + if ( Q_stricmp (cmd, "entitylist") == 0 ) { + Svcmd_EntityList_f(); + return qtrue; + } + + if ( Q_stricmp (cmd, "forceteam") == 0 ) { + Svcmd_ForceTeam_f(); + return qtrue; + } + + if (Q_stricmp (cmd, "game_memory") == 0) { + Svcmd_GameMem_f(); + return qtrue; + } + + //TA: rip bots + /*if (Q_stricmp (cmd, "addbot") == 0) { + Svcmd_AddBot_f(); + return qtrue; + } + + if (Q_stricmp (cmd, "botlist") == 0) { + Svcmd_BotList_f(); + return qtrue; + }*/ + + if (Q_stricmp (cmd, "abort_podium") == 0) { + Svcmd_AbortPodium_f(); + return qtrue; + } + + if (Q_stricmp (cmd, "addip") == 0) { + Svcmd_AddIP_f(); + return qtrue; + } + + if (Q_stricmp (cmd, "removeip") == 0) { + Svcmd_RemoveIP_f(); + return qtrue; + } + + if (Q_stricmp (cmd, "listip") == 0) { + trap_SendConsoleCommand( EXEC_INSERT, "g_banIPs\n" ); + return qtrue; + } + + if (g_dedicated.integer) { + if (Q_stricmp (cmd, "say") == 0) { + trap_SendServerCommand( -1, va("print \"server: %s\"", ConcatArgs(1) ) ); + return qtrue; + } + // everything else will also be printed as a say command + trap_SendServerCommand( -1, va("print \"server: %s\"", ConcatArgs(0) ) ); + return qtrue; + } + + return qfalse; +} + diff --git a/src/game/g_syscalls.asm b/src/game/g_syscalls.asm new file mode 100644 index 00000000..2d9c4f89 --- /dev/null +++ b/src/game/g_syscalls.asm @@ -0,0 +1,217 @@ +code + +equ trap_Printf -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_VariableIntegerValue -7 +equ trap_Cvar_VariableStringBuffer -8 +equ trap_Argc -9 +equ trap_Argv -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_LocateGameData -16 +equ trap_DropClient -17 +equ trap_SendServerCommand -18 +equ trap_SetConfigstring -19 +equ trap_GetConfigstring -20 +equ trap_GetUserinfo -21 +equ trap_SetUserinfo -22 +equ trap_GetServerinfo -23 +equ trap_SetBrushModel -24 +equ trap_Trace -25 +equ trap_PointContents -26 +equ trap_InPVS -27 +equ trap_InPVSIgnorePortals -28 +equ trap_AdjustAreaPortalState -29 +equ trap_AreasConnected -30 +equ trap_LinkEntity -31 +equ trap_UnlinkEntity -32 +equ trap_EntitiesInBox -33 +equ trap_EntityContact -34 +equ trap_BotAllocateClient -35 +equ trap_BotFreeClient -36 +equ trap_GetUsercmd -37 +equ trap_GetEntityToken -38 +equ trap_FS_GetFileList -39 +equ trap_DebugPolygonCreate -40 +equ trap_DebugPolygonDelete -41 +equ trap_RealTime -42 +equ trap_SnapVector -43 + + +equ memset -101 +equ memcpy -102 +equ strncpy -103 +equ sin -104 +equ cos -105 +equ atan2 -106 +equ sqrt -107 +equ floor -111 +equ ceil -112 +equ testPrintInt -113 +equ testPrintFloat -114 + + +equ trap_BotLibSetup -201 +equ trap_BotLibShutdown -202 +equ trap_BotLibVarSet -203 +equ trap_BotLibVarGet -204 +equ trap_BotLibDefine -205 +equ trap_BotLibStartFrame -206 +equ trap_BotLibLoadMap -207 +equ trap_BotLibUpdateEntity -208 +equ trap_BotLibTest -209 + +equ trap_BotGetSnapshotEntity -210 +equ trap_BotGetServerCommand -211 +equ trap_BotUserCommand -212 + + + +equ trap_AAS_EnableRoutingArea -301 +equ trap_AAS_BBoxAreas -302 +equ trap_AAS_AreaInfo -303 +equ trap_AAS_EntityInfo -304 + +equ trap_AAS_Initialized -305 +equ trap_AAS_PresenceTypeBoundingBox -306 +equ trap_AAS_Time -307 + +equ trap_AAS_PointAreaNum -308 +equ trap_AAS_TraceAreas -309 + +equ trap_AAS_PointContents -310 +equ trap_AAS_NextBSPEntity -311 +equ trap_AAS_ValueForBSPEpairKey -312 +equ trap_AAS_VectorForBSPEpairKey -313 +equ trap_AAS_FloatForBSPEpairKey -314 +equ trap_AAS_IntForBSPEpairKey -315 + +equ trap_AAS_AreaReachability -316 + +equ trap_AAS_AreaTravelTimeToGoalArea -317 + +equ trap_AAS_Swimming -318 +equ trap_AAS_PredictClientMovement -319 + + + +equ trap_EA_Say -401 +equ trap_EA_SayTeam -402 +equ trap_EA_Command -403 + +equ trap_EA_Action -404 +equ trap_EA_Gesture -405 +equ trap_EA_Talk -406 +equ trap_EA_Attack -407 +equ trap_EA_Use -408 +equ trap_EA_Respawn -409 +equ trap_EA_Crouch -410 +equ trap_EA_MoveUp -411 +equ trap_EA_MoveDown -412 +equ trap_EA_MoveForward -413 +equ trap_EA_MoveBack -414 +equ trap_EA_MoveLeft -415 +equ trap_EA_MoveRight -416 + +equ trap_EA_SelectWeapon -417 +equ trap_EA_Jump -418 +equ trap_EA_DelayedJump -419 +equ trap_EA_Move -420 +equ trap_EA_View -421 + +equ trap_EA_EndRegular -422 +equ trap_EA_GetInput -423 +equ trap_EA_ResetInput -424 + + + +equ trap_BotLoadCharacter -501 +equ trap_BotFreeCharacter -502 +equ trap_Characteristic_Float -503 +equ trap_Characteristic_BFloat -504 +equ trap_Characteristic_Integer -505 +equ trap_Characteristic_BInteger -506 +equ trap_Characteristic_String -507 + +equ trap_BotAllocChatState -508 +equ trap_BotFreeChatState -509 +equ trap_BotQueueConsoleMessage -510 +equ trap_BotRemoveConsoleMessage -511 +equ trap_BotNextConsoleMessage -512 +equ trap_BotNumConsoleMessages -513 +equ trap_BotInitialChat -514 +equ trap_BotReplyChat -515 +equ trap_BotChatLength -516 +equ trap_BotEnterChat -517 +equ trap_StringContains -518 +equ trap_BotFindMatch -519 +equ trap_BotMatchVariable -520 +equ trap_UnifyWhiteSpaces -521 +equ trap_BotReplaceSynonyms -522 +equ trap_BotLoadChatFile -523 +equ trap_BotSetChatGender -524 +equ trap_BotSetChatName -525 + +equ trap_BotResetGoalState -526 +equ trap_BotResetAvoidGoals -527 +equ trap_BotPushGoal -528 +equ trap_BotPopGoal -529 +equ trap_BotEmptyGoalStack -530 +equ trap_BotDumpAvoidGoals -531 +equ trap_BotDumpGoalStack -532 +equ trap_BotGoalName -533 +equ trap_BotGetTopGoal -534 +equ trap_BotGetSecondGoal -535 +equ trap_BotChooseLTGItem -536 +equ trap_BotChooseNBGItem -537 +equ trap_BotTouchingGoal -538 +equ trap_BotItemGoalInVisButNotVisible -539 +equ trap_BotGetLevelItemGoal -540 +equ trap_BotAvoidGoalTime -541 +equ trap_BotInitLevelItems -542 +equ trap_BotUpdateEntityItems -543 +equ trap_BotLoadItemWeights -544 +equ trap_BotFreeItemWeights -546 +equ trap_BotSaveGoalFuzzyLogic -546 +equ trap_BotAllocGoalState -547 +equ trap_BotFreeGoalState -548 + +equ trap_BotResetMoveState -549 +equ trap_BotMoveToGoal -550 +equ trap_BotMoveInDirection -551 +equ trap_BotResetAvoidReach -552 +equ trap_BotResetLastAvoidReach -553 +equ trap_BotReachabilityArea -554 +equ trap_BotMovementViewTarget -555 +equ trap_BotAllocMoveState -556 +equ trap_BotFreeMoveState -557 +equ trap_BotInitMoveState -558 + +equ trap_BotChooseBestFightWeapon -559 +equ trap_BotGetWeaponInfo -560 +equ trap_BotLoadWeaponWeights -561 +equ trap_BotAllocWeaponState -562 +equ trap_BotFreeWeaponState -563 +equ trap_BotResetWeaponState -564 +equ trap_GeneticParentsAndChildSelection -565 +equ trap_BotInterbreedGoalFuzzyLogic -566 +equ trap_BotMutateGoalFuzzyLogic -567 +equ trap_BotGetNextCampSpotGoal -568 +equ trap_BotGetMapLocationGoal -569 +equ trap_BotNumInitialChats -570 +equ trap_BotGetChatMessage -571 +equ trap_BotRemoveFromAvoidGoals -572 +equ trap_BotPredictVisiblePosition -573 +equ trap_BotSetAvoidGoalTime -574 +equ trap_BotAddAvoidSpot -575 +equ trap_AAS_AlternativeRouteGoals -576 +equ trap_AAS_PredictRoute -577 +equ trap_AAS_PointReachabilityAreaIndex -578 + diff --git a/src/game/g_syscalls.c b/src/game/g_syscalls.c new file mode 100644 index 00000000..9befdf87 --- /dev/null +++ b/src/game/g_syscalls.c @@ -0,0 +1,765 @@ +// Copyright (C) 1999-2000 Id Software, Inc. +// + +/* + * Portions Copyright (C) 2000-2001 Tim Angus + * + * This program 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, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ + +/* To assertain which portions are licensed under the GPL and which are + * licensed by Id Software, Inc. please run a diff between the equivalent + * versions of the "Tremulous" modification and the unmodified "Quake3" + * game source code. + */ + +#include "g_local.h" + +// this file is only included when building a dll +// g_syscalls.asm is included instead when building a qvm + +static int (QDECL *syscall)( int arg, ... ) = (int (QDECL *)( int, ...))-1; + + +void dllEntry( int (QDECL *syscallptr)( int arg,... ) ) { + syscall = syscallptr; +} + +int PASSFLOAT( float x ) { + float floatTemp; + floatTemp = x; + return *(int *)&floatTemp; +} + +void trap_Printf( const char *fmt ) { + syscall( G_PRINT, fmt ); +} + +void trap_Error( const char *fmt ) { + syscall( G_ERROR, fmt ); +} + +int trap_Milliseconds( void ) { + return syscall( G_MILLISECONDS ); +} +int trap_Argc( void ) { + return syscall( G_ARGC ); +} + +void trap_Argv( int n, char *buffer, int bufferLength ) { + syscall( G_ARGV, n, buffer, bufferLength ); +} + +int trap_FS_FOpenFile( const char *qpath, fileHandle_t *f, fsMode_t mode ) { + return syscall( G_FS_FOPEN_FILE, qpath, f, mode ); +} + +void trap_FS_Read( void *buffer, int len, fileHandle_t f ) { + syscall( G_FS_READ, buffer, len, f ); +} + +void trap_FS_Write( const void *buffer, int len, fileHandle_t f ) { + syscall( G_FS_WRITE, buffer, len, f ); +} + +void trap_FS_FCloseFile( fileHandle_t f ) { + syscall( G_FS_FCLOSE_FILE, f ); +} + +int trap_FS_GetFileList( const char *path, const char *extension, char *listbuf, int bufsize ) { + return syscall( G_FS_GETFILELIST, path, extension, listbuf, bufsize ); +} + +void trap_SendConsoleCommand( int exec_when, const char *text ) { + syscall( G_SEND_CONSOLE_COMMAND, exec_when, text ); +} + +void trap_Cvar_Register( vmCvar_t *cvar, const char *var_name, const char *value, int flags ) { + syscall( G_CVAR_REGISTER, cvar, var_name, value, flags ); +} + +void trap_Cvar_Update( vmCvar_t *cvar ) { + syscall( G_CVAR_UPDATE, cvar ); +} + +void trap_Cvar_Set( const char *var_name, const char *value ) { + syscall( G_CVAR_SET, var_name, value ); +} + +int trap_Cvar_VariableIntegerValue( const char *var_name ) { + return syscall( G_CVAR_VARIABLE_INTEGER_VALUE, var_name ); +} + +void trap_Cvar_VariableStringBuffer( const char *var_name, char *buffer, int bufsize ) { + syscall( G_CVAR_VARIABLE_STRING_BUFFER, var_name, buffer, bufsize ); +} + + +void trap_LocateGameData( gentity_t *gEnts, int numGEntities, int sizeofGEntity_t, + playerState_t *clients, int sizeofGClient ) { + syscall( G_LOCATE_GAME_DATA, gEnts, numGEntities, sizeofGEntity_t, clients, sizeofGClient ); +} + +void trap_DropClient( int clientNum, const char *reason ) { + syscall( G_DROP_CLIENT, clientNum, reason ); +} + +void trap_SendServerCommand( int clientNum, const char *text ) { + syscall( G_SEND_SERVER_COMMAND, clientNum, text ); +} + +void trap_SetConfigstring( int num, const char *string ) { + syscall( G_SET_CONFIGSTRING, num, string ); +} + +void trap_GetConfigstring( int num, char *buffer, int bufferSize ) { + syscall( G_GET_CONFIGSTRING, num, buffer, bufferSize ); +} + +void trap_GetUserinfo( int num, char *buffer, int bufferSize ) { + syscall( G_GET_USERINFO, num, buffer, bufferSize ); +} + +void trap_SetUserinfo( int num, const char *buffer ) { + syscall( G_SET_USERINFO, num, buffer ); +} + +void trap_GetServerinfo( char *buffer, int bufferSize ) { + syscall( G_GET_SERVERINFO, buffer, bufferSize ); +} + +void trap_SetBrushModel( gentity_t *ent, const char *name ) { + syscall( G_SET_BRUSH_MODEL, ent, name ); +} + +void trap_Trace( trace_t *results, const vec3_t start, const vec3_t mins, const vec3_t maxs, const vec3_t end, int passEntityNum, int contentmask ) { + syscall( G_TRACE, results, start, mins, maxs, end, passEntityNum, contentmask ); +} + +int trap_PointContents( const vec3_t point, int passEntityNum ) { + return syscall( G_POINT_CONTENTS, point, passEntityNum ); +} + + +qboolean trap_InPVS( const vec3_t p1, const vec3_t p2 ) { + return syscall( G_IN_PVS, p1, p2 ); +} + +qboolean trap_InPVSIgnorePortals( const vec3_t p1, const vec3_t p2 ) { + return syscall( G_IN_PVS_IGNORE_PORTALS, p1, p2 ); +} + +void trap_AdjustAreaPortalState( gentity_t *ent, qboolean open ) { + syscall( G_ADJUST_AREA_PORTAL_STATE, ent, open ); +} + +qboolean trap_AreasConnected( int area1, int area2 ) { + return syscall( G_AREAS_CONNECTED, area1, area2 ); +} + +void trap_LinkEntity( gentity_t *ent ) { + syscall( G_LINKENTITY, ent ); +} + +void trap_UnlinkEntity( gentity_t *ent ) { + syscall( G_UNLINKENTITY, ent ); +} + + +int trap_EntitiesInBox( const vec3_t mins, const vec3_t maxs, int *list, int maxcount ) { + return syscall( G_ENTITIES_IN_BOX, mins, maxs, list, maxcount ); +} + +qboolean trap_EntityContact( const vec3_t mins, const vec3_t maxs, const gentity_t *ent ) { + return syscall( G_ENTITY_CONTACT, mins, maxs, ent ); +} + +int trap_BotAllocateClient( void ) { + return syscall( G_BOT_ALLOCATE_CLIENT ); +} + +void trap_BotFreeClient( int clientNum ) { + syscall( G_BOT_FREE_CLIENT, clientNum ); +} + +void trap_GetUsercmd( int clientNum, usercmd_t *cmd ) { + syscall( G_GET_USERCMD, clientNum, cmd ); +} + +qboolean trap_GetEntityToken( char *buffer, int bufferSize ) { + return syscall( G_GET_ENTITY_TOKEN, buffer, bufferSize ); +} + +int trap_DebugPolygonCreate(int color, int numPoints, vec3_t *points) { + return syscall( G_DEBUG_POLYGON_CREATE, color, numPoints, points ); +} + +void trap_DebugPolygonDelete(int id) { + syscall( G_DEBUG_POLYGON_DELETE, id ); +} + +int trap_RealTime( qtime_t *qtime ) { + return syscall( G_REAL_TIME, qtime ); +} + +void trap_SnapVector( float *v ) { + syscall( G_SNAPVECTOR, v ); + return; +} + +// BotLib traps start here +int trap_BotLibSetup( void ) { + return syscall( BOTLIB_SETUP ); +} + +int trap_BotLibShutdown( void ) { + return syscall( BOTLIB_SHUTDOWN ); +} + +int trap_BotLibVarSet(char *var_name, char *value) { + return syscall( BOTLIB_LIBVAR_SET, var_name, value ); +} + +int trap_BotLibVarGet(char *var_name, char *value, int size) { + return syscall( BOTLIB_LIBVAR_GET, var_name, value, size ); +} + +int trap_BotLibDefine(char *string) { + return syscall( BOTLIB_PC_ADD_GLOBAL_DEFINE, string ); +} + +int trap_BotLibStartFrame(float time) { + return syscall( BOTLIB_START_FRAME, PASSFLOAT( time ) ); +} + +int trap_BotLibLoadMap(const char *mapname) { + return syscall( BOTLIB_LOAD_MAP, mapname ); +} + +int trap_BotLibUpdateEntity(int ent, void /* struct bot_updateentity_s */ *bue) { + return syscall( BOTLIB_UPDATENTITY, ent, bue ); +} + +int trap_BotLibTest(int parm0, char *parm1, vec3_t parm2, vec3_t parm3) { + return syscall( BOTLIB_TEST, parm0, parm1, parm2, parm3 ); +} + +int trap_BotGetSnapshotEntity( int clientNum, int sequence ) { + return syscall( BOTLIB_GET_SNAPSHOT_ENTITY, clientNum, sequence ); +} + +int trap_BotGetServerCommand(int clientNum, char *message, int size) { + return syscall( BOTLIB_GET_CONSOLE_MESSAGE, clientNum, message, size ); +} + +void trap_BotUserCommand(int clientNum, usercmd_t *ucmd) { + syscall( BOTLIB_USER_COMMAND, clientNum, ucmd ); +} + +void trap_AAS_EntityInfo(int entnum, void /* struct aas_entityinfo_s */ *info) { + syscall( BOTLIB_AAS_ENTITY_INFO, entnum, info ); +} + +int trap_AAS_Initialized(void) { + return syscall( BOTLIB_AAS_INITIALIZED ); +} + +void trap_AAS_PresenceTypeBoundingBox(int presencetype, vec3_t mins, vec3_t maxs) { + syscall( BOTLIB_AAS_PRESENCE_TYPE_BOUNDING_BOX, presencetype, mins, maxs ); +} + +float trap_AAS_Time(void) { + int temp; + temp = syscall( BOTLIB_AAS_TIME ); + return (*(float*)&temp); +} + +int trap_AAS_PointAreaNum(vec3_t point) { + return syscall( BOTLIB_AAS_POINT_AREA_NUM, point ); +} + +int trap_AAS_PointReachabilityAreaIndex(vec3_t point) { + return syscall( BOTLIB_AAS_POINT_REACHABILITY_AREA_INDEX, point ); +} + +int trap_AAS_TraceAreas(vec3_t start, vec3_t end, int *areas, vec3_t *points, int maxareas) { + return syscall( BOTLIB_AAS_TRACE_AREAS, start, end, areas, points, maxareas ); +} + +int trap_AAS_BBoxAreas(vec3_t absmins, vec3_t absmaxs, int *areas, int maxareas) { + return syscall( BOTLIB_AAS_BBOX_AREAS, absmins, absmaxs, areas, maxareas ); +} + +int trap_AAS_AreaInfo( int areanum, void /* struct aas_areainfo_s */ *info ) { + return syscall( BOTLIB_AAS_AREA_INFO, areanum, info ); +} + +int trap_AAS_PointContents(vec3_t point) { + return syscall( BOTLIB_AAS_POINT_CONTENTS, point ); +} + +int trap_AAS_NextBSPEntity(int ent) { + return syscall( BOTLIB_AAS_NEXT_BSP_ENTITY, ent ); +} + +int trap_AAS_ValueForBSPEpairKey(int ent, char *key, char *value, int size) { + return syscall( BOTLIB_AAS_VALUE_FOR_BSP_EPAIR_KEY, ent, key, value, size ); +} + +int trap_AAS_VectorForBSPEpairKey(int ent, char *key, vec3_t v) { + return syscall( BOTLIB_AAS_VECTOR_FOR_BSP_EPAIR_KEY, ent, key, v ); +} + +int trap_AAS_FloatForBSPEpairKey(int ent, char *key, float *value) { + return syscall( BOTLIB_AAS_FLOAT_FOR_BSP_EPAIR_KEY, ent, key, value ); +} + +int trap_AAS_IntForBSPEpairKey(int ent, char *key, int *value) { + return syscall( BOTLIB_AAS_INT_FOR_BSP_EPAIR_KEY, ent, key, value ); +} + +int trap_AAS_AreaReachability(int areanum) { + return syscall( BOTLIB_AAS_AREA_REACHABILITY, areanum ); +} + +int trap_AAS_AreaTravelTimeToGoalArea(int areanum, vec3_t origin, int goalareanum, int travelflags) { + return syscall( BOTLIB_AAS_AREA_TRAVEL_TIME_TO_GOAL_AREA, areanum, origin, goalareanum, travelflags ); +} + +int trap_AAS_EnableRoutingArea( int areanum, int enable ) { + return syscall( BOTLIB_AAS_ENABLE_ROUTING_AREA, areanum, enable ); +} + +int trap_AAS_PredictRoute(void /*struct aas_predictroute_s*/ *route, int areanum, vec3_t origin, + int goalareanum, int travelflags, int maxareas, int maxtime, + int stopevent, int stopcontents, int stoptfl, int stopareanum) { + return syscall( BOTLIB_AAS_PREDICT_ROUTE, route, areanum, origin, goalareanum, travelflags, maxareas, maxtime, stopevent, stopcontents, stoptfl, stopareanum ); +} + +int trap_AAS_AlternativeRouteGoals(vec3_t start, int startareanum, vec3_t goal, int goalareanum, int travelflags, + void /*struct aas_altroutegoal_s*/ *altroutegoals, int maxaltroutegoals, + int type) { + return syscall( BOTLIB_AAS_ALTERNATIVE_ROUTE_GOAL, start, startareanum, goal, goalareanum, travelflags, altroutegoals, maxaltroutegoals, type ); +} + +int trap_AAS_Swimming(vec3_t origin) { + return syscall( BOTLIB_AAS_SWIMMING, origin ); +} + +int trap_AAS_PredictClientMovement(void /* struct aas_clientmove_s */ *move, int entnum, vec3_t origin, int presencetype, int onground, vec3_t velocity, vec3_t cmdmove, int cmdframes, int maxframes, float frametime, int stopevent, int stopareanum, int visualize) { + return syscall( BOTLIB_AAS_PREDICT_CLIENT_MOVEMENT, move, entnum, origin, presencetype, onground, velocity, cmdmove, cmdframes, maxframes, PASSFLOAT(frametime), stopevent, stopareanum, visualize ); +} + +void trap_EA_Say(int client, char *str) { + syscall( BOTLIB_EA_SAY, client, str ); +} + +void trap_EA_SayTeam(int client, char *str) { + syscall( BOTLIB_EA_SAY_TEAM, client, str ); +} + +void trap_EA_Command(int client, char *command) { + syscall( BOTLIB_EA_COMMAND, client, command ); +} + +void trap_EA_Action(int client, int action) { + syscall( BOTLIB_EA_ACTION, client, action ); +} + +void trap_EA_Gesture(int client) { + syscall( BOTLIB_EA_GESTURE, client ); +} + +void trap_EA_Talk(int client) { + syscall( BOTLIB_EA_TALK, client ); +} + +void trap_EA_Attack(int client) { + syscall( BOTLIB_EA_ATTACK, client ); +} + +void trap_EA_Use(int client) { + syscall( BOTLIB_EA_USE, client ); +} + +void trap_EA_Respawn(int client) { + syscall( BOTLIB_EA_RESPAWN, client ); +} + +void trap_EA_Crouch(int client) { + syscall( BOTLIB_EA_CROUCH, client ); +} + +void trap_EA_MoveUp(int client) { + syscall( BOTLIB_EA_MOVE_UP, client ); +} + +void trap_EA_MoveDown(int client) { + syscall( BOTLIB_EA_MOVE_DOWN, client ); +} + +void trap_EA_MoveForward(int client) { + syscall( BOTLIB_EA_MOVE_FORWARD, client ); +} + +void trap_EA_MoveBack(int client) { + syscall( BOTLIB_EA_MOVE_BACK, client ); +} + +void trap_EA_MoveLeft(int client) { + syscall( BOTLIB_EA_MOVE_LEFT, client ); +} + +void trap_EA_MoveRight(int client) { + syscall( BOTLIB_EA_MOVE_RIGHT, client ); +} + +void trap_EA_SelectWeapon(int client, int weapon) { + syscall( BOTLIB_EA_SELECT_WEAPON, client, weapon ); +} + +void trap_EA_Jump(int client) { + syscall( BOTLIB_EA_JUMP, client ); +} + +void trap_EA_DelayedJump(int client) { + syscall( BOTLIB_EA_DELAYED_JUMP, client ); +} + +void trap_EA_Move(int client, vec3_t dir, float speed) { + syscall( BOTLIB_EA_MOVE, client, dir, PASSFLOAT(speed) ); +} + +void trap_EA_View(int client, vec3_t viewangles) { + syscall( BOTLIB_EA_VIEW, client, viewangles ); +} + +void trap_EA_EndRegular(int client, float thinktime) { + syscall( BOTLIB_EA_END_REGULAR, client, PASSFLOAT(thinktime) ); +} + +void trap_EA_GetInput(int client, float thinktime, void /* struct bot_input_s */ *input) { + syscall( BOTLIB_EA_GET_INPUT, client, PASSFLOAT(thinktime), input ); +} + +void trap_EA_ResetInput(int client) { + syscall( BOTLIB_EA_RESET_INPUT, client ); +} + +int trap_BotLoadCharacter(char *charfile, float skill) { + return syscall( BOTLIB_AI_LOAD_CHARACTER, charfile, PASSFLOAT(skill)); +} + +void trap_BotFreeCharacter(int character) { + syscall( BOTLIB_AI_FREE_CHARACTER, character ); +} + +float trap_Characteristic_Float(int character, int index) { + int temp; + temp = syscall( BOTLIB_AI_CHARACTERISTIC_FLOAT, character, index ); + return (*(float*)&temp); +} + +float trap_Characteristic_BFloat(int character, int index, float min, float max) { + int temp; + temp = syscall( BOTLIB_AI_CHARACTERISTIC_BFLOAT, character, index, PASSFLOAT(min), PASSFLOAT(max) ); + return (*(float*)&temp); +} + +int trap_Characteristic_Integer(int character, int index) { + return syscall( BOTLIB_AI_CHARACTERISTIC_INTEGER, character, index ); +} + +int trap_Characteristic_BInteger(int character, int index, int min, int max) { + return syscall( BOTLIB_AI_CHARACTERISTIC_BINTEGER, character, index, min, max ); +} + +void trap_Characteristic_String(int character, int index, char *buf, int size) { + syscall( BOTLIB_AI_CHARACTERISTIC_STRING, character, index, buf, size ); +} + +int trap_BotAllocChatState(void) { + return syscall( BOTLIB_AI_ALLOC_CHAT_STATE ); +} + +void trap_BotFreeChatState(int handle) { + syscall( BOTLIB_AI_FREE_CHAT_STATE, handle ); +} + +void trap_BotQueueConsoleMessage(int chatstate, int type, char *message) { + syscall( BOTLIB_AI_QUEUE_CONSOLE_MESSAGE, chatstate, type, message ); +} + +void trap_BotRemoveConsoleMessage(int chatstate, int handle) { + syscall( BOTLIB_AI_REMOVE_CONSOLE_MESSAGE, chatstate, handle ); +} + +int trap_BotNextConsoleMessage(int chatstate, void /* struct bot_consolemessage_s */ *cm) { + return syscall( BOTLIB_AI_NEXT_CONSOLE_MESSAGE, chatstate, cm ); +} + +int trap_BotNumConsoleMessages(int chatstate) { + return syscall( BOTLIB_AI_NUM_CONSOLE_MESSAGE, chatstate ); +} + +void trap_BotInitialChat(int chatstate, char *type, int mcontext, char *var0, char *var1, char *var2, char *var3, char *var4, char *var5, char *var6, char *var7 ) { + syscall( BOTLIB_AI_INITIAL_CHAT, chatstate, type, mcontext, var0, var1, var2, var3, var4, var5, var6, var7 ); +} + +int trap_BotNumInitialChats(int chatstate, char *type) { + return syscall( BOTLIB_AI_NUM_INITIAL_CHATS, chatstate, type ); +} + +int trap_BotReplyChat(int chatstate, char *message, int mcontext, int vcontext, char *var0, char *var1, char *var2, char *var3, char *var4, char *var5, char *var6, char *var7 ) { + return syscall( BOTLIB_AI_REPLY_CHAT, chatstate, message, mcontext, vcontext, var0, var1, var2, var3, var4, var5, var6, var7 ); +} + +int trap_BotChatLength(int chatstate) { + return syscall( BOTLIB_AI_CHAT_LENGTH, chatstate ); +} + +void trap_BotEnterChat(int chatstate, int client, int sendto) { + syscall( BOTLIB_AI_ENTER_CHAT, chatstate, client, sendto ); +} + +void trap_BotGetChatMessage(int chatstate, char *buf, int size) { + syscall( BOTLIB_AI_GET_CHAT_MESSAGE, chatstate, buf, size); +} + +int trap_StringContains(char *str1, char *str2, int casesensitive) { + return syscall( BOTLIB_AI_STRING_CONTAINS, str1, str2, casesensitive ); +} + +int trap_BotFindMatch(char *str, void /* struct bot_match_s */ *match, unsigned long int context) { + return syscall( BOTLIB_AI_FIND_MATCH, str, match, context ); +} + +void trap_BotMatchVariable(void /* struct bot_match_s */ *match, int variable, char *buf, int size) { + syscall( BOTLIB_AI_MATCH_VARIABLE, match, variable, buf, size ); +} + +void trap_UnifyWhiteSpaces(char *string) { + syscall( BOTLIB_AI_UNIFY_WHITE_SPACES, string ); +} + +void trap_BotReplaceSynonyms(char *string, unsigned long int context) { + syscall( BOTLIB_AI_REPLACE_SYNONYMS, string, context ); +} + +int trap_BotLoadChatFile(int chatstate, char *chatfile, char *chatname) { + return syscall( BOTLIB_AI_LOAD_CHAT_FILE, chatstate, chatfile, chatname ); +} + +void trap_BotSetChatGender(int chatstate, int gender) { + syscall( BOTLIB_AI_SET_CHAT_GENDER, chatstate, gender ); +} + +void trap_BotSetChatName(int chatstate, char *name, int client) { + syscall( BOTLIB_AI_SET_CHAT_NAME, chatstate, name, client ); +} + +void trap_BotResetGoalState(int goalstate) { + syscall( BOTLIB_AI_RESET_GOAL_STATE, goalstate ); +} + +void trap_BotResetAvoidGoals(int goalstate) { + syscall( BOTLIB_AI_RESET_AVOID_GOALS, goalstate ); +} + +void trap_BotRemoveFromAvoidGoals(int goalstate, int number) { + syscall( BOTLIB_AI_REMOVE_FROM_AVOID_GOALS, goalstate, number); +} + +void trap_BotPushGoal(int goalstate, void /* struct bot_goal_s */ *goal) { + syscall( BOTLIB_AI_PUSH_GOAL, goalstate, goal ); +} + +void trap_BotPopGoal(int goalstate) { + syscall( BOTLIB_AI_POP_GOAL, goalstate ); +} + +void trap_BotEmptyGoalStack(int goalstate) { + syscall( BOTLIB_AI_EMPTY_GOAL_STACK, goalstate ); +} + +void trap_BotDumpAvoidGoals(int goalstate) { + syscall( BOTLIB_AI_DUMP_AVOID_GOALS, goalstate ); +} + +void trap_BotDumpGoalStack(int goalstate) { + syscall( BOTLIB_AI_DUMP_GOAL_STACK, goalstate ); +} + +void trap_BotGoalName(int number, char *name, int size) { + syscall( BOTLIB_AI_GOAL_NAME, number, name, size ); +} + +int trap_BotGetTopGoal(int goalstate, void /* struct bot_goal_s */ *goal) { + return syscall( BOTLIB_AI_GET_TOP_GOAL, goalstate, goal ); +} + +int trap_BotGetSecondGoal(int goalstate, void /* struct bot_goal_s */ *goal) { + return syscall( BOTLIB_AI_GET_SECOND_GOAL, goalstate, goal ); +} + +int trap_BotChooseLTGItem(int goalstate, vec3_t origin, int *inventory, int travelflags) { + return syscall( BOTLIB_AI_CHOOSE_LTG_ITEM, goalstate, origin, inventory, travelflags ); +} + +int trap_BotChooseNBGItem(int goalstate, vec3_t origin, int *inventory, int travelflags, void /* struct bot_goal_s */ *ltg, float maxtime) { + return syscall( BOTLIB_AI_CHOOSE_NBG_ITEM, goalstate, origin, inventory, travelflags, ltg, PASSFLOAT(maxtime) ); +} + +int trap_BotTouchingGoal(vec3_t origin, void /* struct bot_goal_s */ *goal) { + return syscall( BOTLIB_AI_TOUCHING_GOAL, origin, goal ); +} + +int trap_BotItemGoalInVisButNotVisible(int viewer, vec3_t eye, vec3_t viewangles, void /* struct bot_goal_s */ *goal) { + return syscall( BOTLIB_AI_ITEM_GOAL_IN_VIS_BUT_NOT_VISIBLE, viewer, eye, viewangles, goal ); +} + +int trap_BotGetLevelItemGoal(int index, char *classname, void /* struct bot_goal_s */ *goal) { + return syscall( BOTLIB_AI_GET_LEVEL_ITEM_GOAL, index, classname, goal ); +} + +int trap_BotGetNextCampSpotGoal(int num, void /* struct bot_goal_s */ *goal) { + return syscall( BOTLIB_AI_GET_NEXT_CAMP_SPOT_GOAL, num, goal ); +} + +int trap_BotGetMapLocationGoal(char *name, void /* struct bot_goal_s */ *goal) { + return syscall( BOTLIB_AI_GET_MAP_LOCATION_GOAL, name, goal ); +} + +float trap_BotAvoidGoalTime(int goalstate, int number) { + int temp; + temp = syscall( BOTLIB_AI_AVOID_GOAL_TIME, goalstate, number ); + return (*(float*)&temp); +} + +void trap_BotSetAvoidGoalTime(int goalstate, int number, float avoidtime) { + syscall( BOTLIB_AI_SET_AVOID_GOAL_TIME, goalstate, number, PASSFLOAT(avoidtime)); +} + +void trap_BotInitLevelItems(void) { + syscall( BOTLIB_AI_INIT_LEVEL_ITEMS ); +} + +void trap_BotUpdateEntityItems(void) { + syscall( BOTLIB_AI_UPDATE_ENTITY_ITEMS ); +} + +int trap_BotLoadItemWeights(int goalstate, char *filename) { + return syscall( BOTLIB_AI_LOAD_ITEM_WEIGHTS, goalstate, filename ); +} + +void trap_BotFreeItemWeights(int goalstate) { + syscall( BOTLIB_AI_FREE_ITEM_WEIGHTS, goalstate ); +} + +void trap_BotInterbreedGoalFuzzyLogic(int parent1, int parent2, int child) { + syscall( BOTLIB_AI_INTERBREED_GOAL_FUZZY_LOGIC, parent1, parent2, child ); +} + +void trap_BotSaveGoalFuzzyLogic(int goalstate, char *filename) { + syscall( BOTLIB_AI_SAVE_GOAL_FUZZY_LOGIC, goalstate, filename ); +} + +void trap_BotMutateGoalFuzzyLogic(int goalstate, float range) { + syscall( BOTLIB_AI_MUTATE_GOAL_FUZZY_LOGIC, goalstate, range ); +} + +int trap_BotAllocGoalState(int state) { + return syscall( BOTLIB_AI_ALLOC_GOAL_STATE, state ); +} + +void trap_BotFreeGoalState(int handle) { + syscall( BOTLIB_AI_FREE_GOAL_STATE, handle ); +} + +void trap_BotResetMoveState(int movestate) { + syscall( BOTLIB_AI_RESET_MOVE_STATE, movestate ); +} + +void trap_BotAddAvoidSpot(int movestate, vec3_t origin, float radius, int type) { + syscall( BOTLIB_AI_ADD_AVOID_SPOT, movestate, origin, PASSFLOAT(radius), type); +} + +void trap_BotMoveToGoal(void /* struct bot_moveresult_s */ *result, int movestate, void /* struct bot_goal_s */ *goal, int travelflags) { + syscall( BOTLIB_AI_MOVE_TO_GOAL, result, movestate, goal, travelflags ); +} + +int trap_BotMoveInDirection(int movestate, vec3_t dir, float speed, int type) { + return syscall( BOTLIB_AI_MOVE_IN_DIRECTION, movestate, dir, PASSFLOAT(speed), type ); +} + +void trap_BotResetAvoidReach(int movestate) { + syscall( BOTLIB_AI_RESET_AVOID_REACH, movestate ); +} + +void trap_BotResetLastAvoidReach(int movestate) { + syscall( BOTLIB_AI_RESET_LAST_AVOID_REACH,movestate ); +} + +int trap_BotReachabilityArea(vec3_t origin, int testground) { + return syscall( BOTLIB_AI_REACHABILITY_AREA, origin, testground ); +} + +int trap_BotMovementViewTarget(int movestate, void /* struct bot_goal_s */ *goal, int travelflags, float lookahead, vec3_t target) { + return syscall( BOTLIB_AI_MOVEMENT_VIEW_TARGET, movestate, goal, travelflags, PASSFLOAT(lookahead), target ); +} + +int trap_BotPredictVisiblePosition(vec3_t origin, int areanum, void /* struct bot_goal_s */ *goal, int travelflags, vec3_t target) { + return syscall( BOTLIB_AI_PREDICT_VISIBLE_POSITION, origin, areanum, goal, travelflags, target ); +} + +int trap_BotAllocMoveState(void) { + return syscall( BOTLIB_AI_ALLOC_MOVE_STATE ); +} + +void trap_BotFreeMoveState(int handle) { + syscall( BOTLIB_AI_FREE_MOVE_STATE, handle ); +} + +void trap_BotInitMoveState(int handle, void /* struct bot_initmove_s */ *initmove) { + syscall( BOTLIB_AI_INIT_MOVE_STATE, handle, initmove ); +} + +int trap_BotChooseBestFightWeapon(int weaponstate, int *inventory) { + return syscall( BOTLIB_AI_CHOOSE_BEST_FIGHT_WEAPON, weaponstate, inventory ); +} + +void trap_BotGetWeaponInfo(int weaponstate, int weapon, void /* struct weaponinfo_s */ *weaponinfo) { + syscall( BOTLIB_AI_GET_WEAPON_INFO, weaponstate, weapon, weaponinfo ); +} + +int trap_BotLoadWeaponWeights(int weaponstate, char *filename) { + return syscall( BOTLIB_AI_LOAD_WEAPON_WEIGHTS, weaponstate, filename ); +} + +int trap_BotAllocWeaponState(void) { + return syscall( BOTLIB_AI_ALLOC_WEAPON_STATE ); +} + +void trap_BotFreeWeaponState(int weaponstate) { + syscall( BOTLIB_AI_FREE_WEAPON_STATE, weaponstate ); +} + +void trap_BotResetWeaponState(int weaponstate) { + syscall( BOTLIB_AI_RESET_WEAPON_STATE, weaponstate ); +} + +int trap_GeneticParentsAndChildSelection(int numranks, float *ranks, int *parent1, int *parent2, int *child) { + return syscall( BOTLIB_AI_GENETIC_PARENTS_AND_CHILD_SELECTION, numranks, ranks, parent1, parent2, child ); +} diff --git a/src/game/g_target.c b/src/game/g_target.c new file mode 100644 index 00000000..eac2e164 --- /dev/null +++ b/src/game/g_target.c @@ -0,0 +1,472 @@ +// Copyright (C) 1999-2000 Id Software, Inc. +// + +/* + * Portions Copyright (C) 2000-2001 Tim Angus + * + * This program 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, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ + +/* To assertain which portions are licensed under the GPL and which are + * licensed by Id Software, Inc. please run a diff between the equivalent + * versions of the "Tremulous" modification and the unmodified "Quake3" + * game source code. + */ + +#include "g_local.h" + +//========================================================== + +/*QUAKED target_give (1 0 0) (-8 -8 -8) (8 8 8) +Gives the activator all the items pointed to. +*/ +void Use_Target_Give( gentity_t *ent, gentity_t *other, gentity_t *activator ) { + gentity_t *t; + trace_t trace; + + if ( !activator->client ) { + return; + } + + if ( !ent->target ) { + return; + } + + memset( &trace, 0, sizeof( trace ) ); + t = NULL; + while ( (t = G_Find (t, FOFS(targetname), ent->target)) != NULL ) { + if ( !t->item ) { + continue; + } + Touch_Item( t, activator, &trace ); + + // make sure it isn't going to respawn or show any events + t->nextthink = 0; + trap_UnlinkEntity( t ); + } +} + +void SP_target_give( gentity_t *ent ) { + ent->use = Use_Target_Give; +} + + +//========================================================== + +/*QUAKED target_remove_powerups (1 0 0) (-8 -8 -8) (8 8 8) +takes away all the activators powerups. +Used to drop flight powerups into death puts. +*/ +void Use_target_remove_powerups( gentity_t *ent, gentity_t *other, gentity_t *activator ) { + if( !activator->client ) { + return; + } + + /*if( activator->client->ps.powerups[PW_REDFLAG] ) { + Team_ReturnFlag( TEAM_HUMANS ); + } else if( activator->client->ps.powerups[PW_BLUEFLAG] ) { + Team_ReturnFlag( TEAM_DROIDS ); + } else if( activator->client->ps.powerups[PW_NEUTRALFLAG] ) { + Team_ReturnFlag( TEAM_FREE ); + }*/ + + memset( activator->client->ps.powerups, 0, sizeof( activator->client->ps.powerups ) ); +} + +void SP_target_remove_powerups( gentity_t *ent ) { + ent->use = Use_target_remove_powerups; +} + + +//========================================================== + +/*QUAKED target_delay (1 0 0) (-8 -8 -8) (8 8 8) +"wait" seconds to pause before firing targets. +"random" delay variance, total delay = delay +/- random seconds +*/ +void Think_Target_Delay( gentity_t *ent ) { + G_UseTargets( ent, ent->activator ); +} + +void Use_Target_Delay( gentity_t *ent, gentity_t *other, gentity_t *activator ) { + ent->nextthink = level.time + ( ent->wait + ent->random * crandom() ) * 1000; + ent->think = Think_Target_Delay; + ent->activator = activator; +} + +void SP_target_delay( gentity_t *ent ) { + // check delay for backwards compatability + if ( !G_SpawnFloat( "delay", "0", &ent->wait ) ) { + G_SpawnFloat( "wait", "1", &ent->wait ); + } + + if ( !ent->wait ) { + ent->wait = 1; + } + ent->use = Use_Target_Delay; +} + + +//========================================================== + +/*QUAKED target_score (1 0 0) (-8 -8 -8) (8 8 8) +"count" number of points to add, default 1 + +The activator is given this many points. +*/ +void Use_Target_Score (gentity_t *ent, gentity_t *other, gentity_t *activator) { + AddScore( activator, ent->count ); +} + +void SP_target_score( gentity_t *ent ) { + if ( !ent->count ) { + ent->count = 1; + } + ent->use = Use_Target_Score; +} + + +//========================================================== + +/*QUAKED target_print (1 0 0) (-8 -8 -8) (8 8 8) redteam blueteam private +"message" text to print +If "private", only the activator gets the message. If no checks, all clients get the message. +*/ +void Use_Target_Print (gentity_t *ent, gentity_t *other, gentity_t *activator) { + if ( activator->client && ( ent->spawnflags & 4 ) ) { + trap_SendServerCommand( activator-g_entities, va("cp \"%s\"", ent->message )); + return; + } + + if ( ent->spawnflags & 3 ) { + if ( ent->spawnflags & 1 ) { + G_TeamCommand( TEAM_HUMANS, va("cp \"%s\"", ent->message) ); + } + if ( ent->spawnflags & 2 ) { + G_TeamCommand( TEAM_DROIDS, va("cp \"%s\"", ent->message) ); + } + return; + } + + trap_SendServerCommand( -1, va("cp \"%s\"", ent->message )); +} + +void SP_target_print( gentity_t *ent ) { + ent->use = Use_Target_Print; +} + + +//========================================================== + + +/*QUAKED target_speaker (1 0 0) (-8 -8 -8) (8 8 8) looped-on looped-off global activator +"noise" wav file to play + +A global sound will play full volume throughout the level. +Activator sounds will play on the player that activated the target. +Global and activator sounds can't be combined with looping. +Normal sounds play each time the target is used. +Looped sounds will be toggled by use functions. +Multiple identical looping sounds will just increase volume without any speed cost. +"wait" : Seconds between auto triggerings, 0 = don't auto trigger +"random" wait variance, default is 0 +*/ +void Use_Target_Speaker (gentity_t *ent, gentity_t *other, gentity_t *activator) { + if (ent->spawnflags & 3) { // looping sound toggles + if (ent->s.loopSound) + ent->s.loopSound = 0; // turn it off + else + ent->s.loopSound = ent->noise_index; // start it + }else { // normal sound + if ( ent->spawnflags & 8 ) { + G_AddEvent( activator, EV_GENERAL_SOUND, ent->noise_index ); + } else if (ent->spawnflags & 4) { + G_AddEvent( ent, EV_GLOBAL_SOUND, ent->noise_index ); + } else { + G_AddEvent( ent, EV_GENERAL_SOUND, ent->noise_index ); + } + } +} + +void SP_target_speaker( gentity_t *ent ) { + char buffer[MAX_QPATH]; + char *s; + + G_SpawnFloat( "wait", "0", &ent->wait ); + G_SpawnFloat( "random", "0", &ent->random ); + + if ( !G_SpawnString( "noise", "NOSOUND", &s ) ) { + G_Error( "target_speaker without a noise key at %s", vtos( ent->s.origin ) ); + } + + // force all client reletive sounds to be "activator" speakers that + // play on the entity that activates it + if ( s[0] == '*' ) { + ent->spawnflags |= 8; + } + + if (!strstr( s, ".wav" )) { + Com_sprintf (buffer, sizeof(buffer), "%s.wav", s ); + } else { + Q_strncpyz( buffer, s, sizeof(buffer) ); + } + ent->noise_index = G_SoundIndex(buffer); + + // a repeating speaker can be done completely client side + ent->s.eType = ET_SPEAKER; + ent->s.eventParm = ent->noise_index; + ent->s.frame = ent->wait * 10; + ent->s.clientNum = ent->random * 10; + + + // check for prestarted looping sound + if ( ent->spawnflags & 1 ) { + ent->s.loopSound = ent->noise_index; + } + + ent->use = Use_Target_Speaker; + + if (ent->spawnflags & 4) { + ent->r.svFlags |= SVF_BROADCAST; + } + + VectorCopy( ent->s.origin, ent->s.pos.trBase ); + + // must link the entity so we get areas and clusters so + // the server can determine who to send updates to + trap_LinkEntity( ent ); +} + + + +//========================================================== + +/*QUAKED target_laser (0 .5 .8) (-8 -8 -8) (8 8 8) START_ON +When triggered, fires a laser. You can either set a target or a direction. +*/ +void target_laser_think (gentity_t *self) { + vec3_t end; + trace_t tr; + vec3_t point; + + // if pointed at another entity, set movedir to point at it + if ( self->enemy ) { + VectorMA (self->enemy->s.origin, 0.5, self->enemy->r.mins, point); + VectorMA (point, 0.5, self->enemy->r.maxs, point); + VectorSubtract (point, self->s.origin, self->movedir); + VectorNormalize (self->movedir); + } + + // fire forward and see what we hit + VectorMA (self->s.origin, 2048, self->movedir, end); + + trap_Trace( &tr, self->s.origin, NULL, NULL, end, self->s.number, CONTENTS_SOLID|CONTENTS_BODY|CONTENTS_CORPSE); + + if ( tr.entityNum ) { + // hurt it if we can + G_Damage ( &g_entities[tr.entityNum], self, self->activator, self->movedir, + tr.endpos, self->damage, DAMAGE_NO_KNOCKBACK, MOD_TARGET_LASER); + } + + VectorCopy (tr.endpos, self->s.origin2); + + trap_LinkEntity( self ); + self->nextthink = level.time + FRAMETIME; +} + +void target_laser_on (gentity_t *self) +{ + if (!self->activator) + self->activator = self; + target_laser_think (self); +} + +void target_laser_off (gentity_t *self) +{ + trap_UnlinkEntity( self ); + self->nextthink = 0; +} + +void target_laser_use (gentity_t *self, gentity_t *other, gentity_t *activator) +{ + self->activator = activator; + if ( self->nextthink > 0 ) + target_laser_off (self); + else + target_laser_on (self); +} + +void target_laser_start (gentity_t *self) +{ + gentity_t *ent; + + self->s.eType = ET_BEAM; + + if (self->target) { + ent = G_Find (NULL, FOFS(targetname), self->target); + if (!ent) { + G_Printf ("%s at %s: %s is a bad target\n", self->classname, vtos(self->s.origin), self->target); + } + self->enemy = ent; + } else { + G_SetMovedir (self->s.angles, self->movedir); + } + + self->use = target_laser_use; + self->think = target_laser_think; + + if ( !self->damage ) { + self->damage = 1; + } + + if (self->spawnflags & 1) + target_laser_on (self); + else + target_laser_off (self); +} + +void SP_target_laser (gentity_t *self) +{ + // let everything else get spawned before we start firing + self->think = target_laser_start; + self->nextthink = level.time + FRAMETIME; +} + + +//========================================================== + +void target_teleporter_use( gentity_t *self, gentity_t *other, gentity_t *activator ) { + gentity_t *dest; + + if (!activator->client) + return; + dest = G_PickTarget( self->target ); + if (!dest) { + G_Printf ("Couldn't find teleporter destination\n"); + return; + } + + TeleportPlayer( activator, dest->s.origin, dest->s.angles ); +} + +/*QUAKED target_teleporter (1 0 0) (-8 -8 -8) (8 8 8) +The activator will be teleported away. +*/ +void SP_target_teleporter( gentity_t *self ) { + if (!self->targetname) + G_Printf("untargeted %s at %s\n", self->classname, vtos(self->s.origin)); + + self->use = target_teleporter_use; +} + +//========================================================== + + +/*QUAKED target_relay (.5 .5 .5) (-8 -8 -8) (8 8 8) RED_ONLY BLUE_ONLY RANDOM +This doesn't perform any actions except fire its targets. +The activator can be forced to be from a certain team. +if RANDOM is checked, only one of the targets will be fired, not all of them +*/ +void target_relay_use (gentity_t *self, gentity_t *other, gentity_t *activator) { + if ( ( self->spawnflags & 1 ) && activator->client + && activator->client->sess.sessionTeam != TEAM_HUMANS ) { + return; + } + if ( ( self->spawnflags & 2 ) && activator->client + && activator->client->sess.sessionTeam != TEAM_DROIDS ) { + return; + } + if ( self->spawnflags & 4 ) { + gentity_t *ent; + + ent = G_PickTarget( self->target ); + if ( ent && ent->use ) { + ent->use( ent, self, activator ); + } + return; + } + G_UseTargets (self, activator); +} + +void SP_target_relay (gentity_t *self) { + self->use = target_relay_use; +} + + +//========================================================== + +/*QUAKED target_kill (.5 .5 .5) (-8 -8 -8) (8 8 8) +Kills the activator. +*/ +void target_kill_use( gentity_t *self, gentity_t *other, gentity_t *activator ) { + G_Damage ( activator, NULL, NULL, NULL, NULL, 100000, DAMAGE_NO_PROTECTION, MOD_TELEFRAG); +} + +void SP_target_kill( gentity_t *self ) { + self->use = target_kill_use; +} + +/*QUAKED target_position (0 0.5 0) (-4 -4 -4) (4 4 4) +Used as a positional target for in-game calculation, like jumppad targets. +*/ +void SP_target_position( gentity_t *self ){ + G_SetOrigin( self, self->s.origin ); +} + +static void target_location_linkup(gentity_t *ent) +{ + int i; + int n; + + if (level.locationLinked) + return; + + level.locationLinked = qtrue; + + level.locationHead = NULL; + + trap_SetConfigstring( CS_LOCATIONS, "unknown" ); + + for (i = 0, ent = g_entities, n = 1; + i < level.num_entities; + i++, ent++) { + if (ent->classname && !Q_stricmp(ent->classname, "target_location")) { + // lets overload some variables! + ent->health = n; // use for location marking + trap_SetConfigstring( CS_LOCATIONS + n, ent->message ); + n++; + ent->nextTrain = level.locationHead; + level.locationHead = ent; + } + } + + // All linked together now +} + +/*QUAKED target_location (0 0.5 0) (-8 -8 -8) (8 8 8) +Set "message" to the name of this location. +Set "count" to 0-7 for color. +0:white 1:red 2:green 3:yellow 4:blue 5:cyan 6:magenta 7:white + +Closest target_location in sight used for the location, if none +in site, closest in distance +*/ +void SP_target_location( gentity_t *self ){ + self->think = target_location_linkup; + self->nextthink = level.time + 200; // Let them all spawn first + + G_SetOrigin( self, self->s.origin ); +} + diff --git a/src/game/g_team.c b/src/game/g_team.c new file mode 100644 index 00000000..97bfbd86 --- /dev/null +++ b/src/game/g_team.c @@ -0,0 +1,1046 @@ +// Copyright (C) 1999-2000 Id Software, Inc. +// + +/* + * Portions Copyright (C) 2000-2001 Tim Angus + * + * This program 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, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ + +/* To assertain which portions are licensed under the GPL and which are + * licensed by Id Software, Inc. please run a diff between the equivalent + * versions of the "Tremulous" modification and the unmodified "Quake3" + * game source code. + */ + +#include "g_local.h" + +typedef struct teamgame_s { + float last_flag_capture; + int last_capture_team; + flagStatus_t redStatus; // CTF + flagStatus_t blueStatus; // CTF + flagStatus_t flagStatus; // One Flag CTF + int redTakenTime; + int blueTakenTime; + int redObeliskAttackedTime; + int blueObeliskAttackedTime; +} teamgame_t; + +teamgame_t teamgame; + +void Team_SetFlagStatus( int team, flagStatus_t status ); + +void Team_InitGame(void) +{ + memset(&teamgame, 0, sizeof teamgame); + switch( g_gametype.integer ) { + case GT_CTF: + teamgame.redStatus = teamgame.blueStatus = -1; // Invalid to force update + Team_SetFlagStatus( TEAM_HUMANS, FLAG_ATBASE ); + Team_SetFlagStatus( TEAM_DROIDS, FLAG_ATBASE ); + break; + default: + break; + } +} + +int OtherTeam(int team) { + if (team==TEAM_HUMANS) + return TEAM_DROIDS; + else if (team==TEAM_DROIDS) + return TEAM_HUMANS; + return team; +} + +const char *TeamName(int team) { + if (team==TEAM_HUMANS) + return "RED"; + else if (team==TEAM_DROIDS) + return "BLUE"; + else if (team==TEAM_SPECTATOR) + return "SPECTATOR"; + return "FREE"; +} + +const char *OtherTeamName(int team) { + if (team==TEAM_HUMANS) + return "BLUE"; + else if (team==TEAM_DROIDS) + return "RED"; + else if (team==TEAM_SPECTATOR) + return "SPECTATOR"; + return "FREE"; +} + +const char *TeamColorString(int team) { + if (team==TEAM_HUMANS) + return S_COLOR_RED; + else if (team==TEAM_DROIDS) + return S_COLOR_BLUE; + else if (team==TEAM_SPECTATOR) + return S_COLOR_YELLOW; + return S_COLOR_WHITE; +} + +// NULL for everyone +void QDECL PrintMsg( gentity_t *ent, const char *fmt, ... ) { + char msg[1024]; + va_list argptr; + char *p; + + va_start (argptr,fmt); + if (vsprintf (msg, fmt, argptr) > sizeof(msg)) { + G_Error ( "PrintMsg overrun" ); + } + va_end (argptr); + + // double quotes are bad + while ((p = strchr(msg, '"')) != NULL) + *p = '\''; + + trap_SendServerCommand ( ( (ent == NULL) ? -1 : ent-g_entities ), va("print \"%s\"", msg )); +} + + +/* +============== +AddTeamScore + + used for gametype > GT_TEAM + for gametype GT_TEAM the level.teamScores is updated in AddScore in g_combat.c +============== +*/ +void AddTeamScore(vec3_t origin, int team, int score) { + gentity_t *te; + + //te = G_TempEntity(origin, EV_GLOBAL_TEAM_SOUND ); + te->r.svFlags |= SVF_BROADCAST; + + if ( team == TEAM_HUMANS ) { + if ( level.teamScores[ TEAM_HUMANS ] + score == level.teamScores[ TEAM_DROIDS ] ) { + //teams are tied sound + //te->s.eventParm = GTS_TEAMS_ARE_TIED; + } + else if ( level.teamScores[ TEAM_HUMANS ] <= level.teamScores[ TEAM_DROIDS ] && + level.teamScores[ TEAM_HUMANS ] + score > level.teamScores[ TEAM_DROIDS ]) { + // red took the lead sound + //te->s.eventParm = GTS_REDTEAM_TOOK_LEAD; + } + else { + // red scored sound + //te->s.eventParm = GTS_REDTEAM_SCORED; + } + } + else { + if ( level.teamScores[ TEAM_DROIDS ] + score == level.teamScores[ TEAM_HUMANS ] ) { + //teams are tied sound + //te->s.eventParm = GTS_TEAMS_ARE_TIED; + } + else if ( level.teamScores[ TEAM_DROIDS ] <= level.teamScores[ TEAM_HUMANS ] && + level.teamScores[ TEAM_DROIDS ] + score > level.teamScores[ TEAM_HUMANS ]) { + // blue took the lead sound + //te->s.eventParm = GTS_BLUETEAM_TOOK_LEAD; + } + else { + // blue scored sound + //te->s.eventParm = GTS_BLUETEAM_SCORED; + } + } + level.teamScores[ team ] += score; +} + + +/* +============== +OnSameTeam +============== +*/ +qboolean OnSameTeam( gentity_t *ent1, gentity_t *ent2 ) { + if ( !ent1->client || !ent2->client ) { + return qfalse; + } + + if ( g_gametype.integer < GT_TEAM ) { + return qfalse; + } + + if ( ent1->client->sess.sessionTeam == ent2->client->sess.sessionTeam ) { + return qtrue; + } + + return qfalse; +} + +static char ctfFlagStatusRemap[] = { '0', '1', '*', '*', '2' }; +static char oneFlagStatusRemap[] = { '0', '1', '2', '3', '4' }; + +void Team_SetFlagStatus( int team, flagStatus_t status ) { + qboolean modified = qfalse; + + switch( team ) { + case TEAM_HUMANS: // CTF + if( teamgame.redStatus != status ) { + teamgame.redStatus = status; + modified = qtrue; + } + break; + + case TEAM_DROIDS: // CTF + if( teamgame.blueStatus != status ) { + teamgame.blueStatus = status; + modified = qtrue; + } + break; + + case TEAM_FREE: // One Flag CTF + if( teamgame.flagStatus != status ) { + teamgame.flagStatus = status; + modified = qtrue; + } + break; + } + + if( modified ) { + char st[4]; + + if( g_gametype.integer == GT_CTF ) { + st[0] = ctfFlagStatusRemap[teamgame.redStatus]; + st[1] = ctfFlagStatusRemap[teamgame.blueStatus]; + st[2] = 0; + } + else { // GT_1FCTF + st[0] = oneFlagStatusRemap[teamgame.flagStatus]; + st[1] = 0; + } + + trap_SetConfigstring( CS_FLAGSTATUS, st ); + } +} + + +void Team_CheckDroppedItem( gentity_t *dropped ) +{ + /*if (dropped->item->giTag == PW_REDFLAG) + Team_SetFlagStatus( TEAM_HUMANS, FLAG_DROPPED ); + else if (dropped->item->giTag == PW_BLUEFLAG) + Team_SetFlagStatus( TEAM_DROIDS, FLAG_DROPPED );*/ +} + + +/* +================ +Team_ForceGesture +================ +*/ +void Team_ForceGesture(int team) { + int i; + gentity_t *ent; + + for (i = 0; i < MAX_CLIENTS; i++) { + ent = &g_entities[i]; + if (!ent->inuse) + continue; + if (!ent->client) + continue; + if (ent->client->sess.sessionTeam != team) + continue; + // + ent->flags |= FL_FORCE_GESTURE; + } +} + + +/* +================ +Team_FragBonuses + +Calculate the bonuses for flag defense, flag carrier defense, etc. +Note that bonuses are not cumlative. You get one, they are in importance +order. +================ +*/ +void Team_FragBonuses(gentity_t *targ, gentity_t *inflictor, gentity_t *attacker) +{ + int i; + gentity_t *ent; + int flag_pw, enemy_flag_pw; + int otherteam; + int tokens; + gentity_t *flag, *carrier = NULL; + char *c; + vec3_t v1, v2; + int team; + + // no bonus for fragging yourself + if (!targ->client || !attacker->client || targ == attacker || OnSameTeam(targ, attacker)) + return; + + team = targ->client->sess.sessionTeam; + otherteam = OtherTeam(targ->client->sess.sessionTeam); + if (otherteam < 0) + return; // whoever died isn't on a team + + // same team, if the flag at base, check to he has the enemy flag + /*if (team == TEAM_HUMANS) { + flag_pw = PW_REDFLAG; + enemy_flag_pw = PW_BLUEFLAG; + } else { + flag_pw = PW_BLUEFLAG; + enemy_flag_pw = PW_REDFLAG; + }*/ + + // did the attacker frag the flag carrier? + tokens = 0; + if (targ->client->ps.powerups[enemy_flag_pw]) { + attacker->client->pers.teamState.lastfraggedcarrier = level.time; + AddScore(attacker, CTF_FRAG_CARRIER_BONUS); + attacker->client->pers.teamState.fragcarrier++; + PrintMsg(NULL, "%s" S_COLOR_WHITE " fragged %s's flag carrier!\n", + attacker->client->pers.netname, TeamName(team)); + + // the target had the flag, clear the hurt carrier + // field on the other team + for (i = 0; i < g_maxclients.integer; i++) { + ent = g_entities + i; + if (ent->inuse && ent->client->sess.sessionTeam == otherteam) + ent->client->pers.teamState.lasthurtcarrier = 0; + } + return; + } + + // did the attacker frag a head carrier? other->client->ps.generic1 + if (tokens) { + attacker->client->pers.teamState.lastfraggedcarrier = level.time; + AddScore(attacker, CTF_FRAG_CARRIER_BONUS * tokens * tokens); + attacker->client->pers.teamState.fragcarrier++; + PrintMsg(NULL, "%s" S_COLOR_WHITE " fragged %s's skull carrier!\n", + attacker->client->pers.netname, TeamName(team)); + + // the target had the flag, clear the hurt carrier + // field on the other team + for (i = 0; i < g_maxclients.integer; i++) { + ent = g_entities + i; + if (ent->inuse && ent->client->sess.sessionTeam == otherteam) + ent->client->pers.teamState.lasthurtcarrier = 0; + } + return; + } + + if (targ->client->pers.teamState.lasthurtcarrier && + level.time - targ->client->pers.teamState.lasthurtcarrier < CTF_CARRIER_DANGER_PROTECT_TIMEOUT && + !attacker->client->ps.powerups[flag_pw]) { + // attacker is on the same team as the flag carrier and + // fragged a guy who hurt our flag carrier + AddScore(attacker, CTF_CARRIER_DANGER_PROTECT_BONUS); + + attacker->client->pers.teamState.carrierdefense++; + targ->client->pers.teamState.lasthurtcarrier = 0; + + //attacker->client->ps.persistant[PERS_DEFEND_COUNT]++; + team = attacker->client->sess.sessionTeam; + // add the sprite over the player's head + //attacker->client->ps.eFlags &= ~(EF_AWARD_IMPRESSIVE | EF_AWARD_EXCELLENT | EF_AWARD_GAUNTLET | EF_AWARD_ASSIST | EF_AWARD_DEFEND | EF_AWARD_CAP ); + //attacker->client->ps.eFlags |= EF_AWARD_DEFEND; + attacker->client->rewardTime = level.time + REWARD_SPRITE_TIME; + + return; + } + + if (targ->client->pers.teamState.lasthurtcarrier && + level.time - targ->client->pers.teamState.lasthurtcarrier < CTF_CARRIER_DANGER_PROTECT_TIMEOUT) { + // attacker is on the same team as the skull carrier and + AddScore(attacker, CTF_CARRIER_DANGER_PROTECT_BONUS); + + attacker->client->pers.teamState.carrierdefense++; + targ->client->pers.teamState.lasthurtcarrier = 0; + + //attacker->client->ps.persistant[PERS_DEFEND_COUNT]++; + team = attacker->client->sess.sessionTeam; + // add the sprite over the player's head + //attacker->client->ps.eFlags &= ~(EF_AWARD_IMPRESSIVE | EF_AWARD_EXCELLENT | EF_AWARD_GAUNTLET | EF_AWARD_ASSIST | EF_AWARD_DEFEND | EF_AWARD_CAP ); + //attacker->client->ps.eFlags |= EF_AWARD_DEFEND; + attacker->client->rewardTime = level.time + REWARD_SPRITE_TIME; + + return; + } + + // flag and flag carrier area defense bonuses + + // we have to find the flag and carrier entities + + // find the flag + switch (attacker->client->sess.sessionTeam) { + case TEAM_HUMANS: + c = "team_CTF_redflag"; + break; + case TEAM_DROIDS: + c = "team_CTF_blueflag"; + break; + default: + return; + } + + // find attacker's team's flag carrier + for (i = 0; i < g_maxclients.integer; i++) { + carrier = g_entities + i; + if (carrier->inuse && carrier->client->ps.powerups[flag_pw]) + break; + carrier = NULL; + } + + flag = NULL; + while ((flag = G_Find (flag, FOFS(classname), c)) != NULL) { + if (!(flag->flags & FL_DROPPED_ITEM)) + break; + } + + if (!flag) + return; // can't find attacker's flag + + // ok we have the attackers flag and a pointer to the carrier + + // check to see if we are defending the base's flag + VectorSubtract(targ->r.currentOrigin, flag->r.currentOrigin, v1); + VectorSubtract(attacker->r.currentOrigin, flag->r.currentOrigin, v2); + + if ( ( ( VectorLength(v1) < CTF_TARGET_PROTECT_RADIUS && + trap_InPVS(flag->r.currentOrigin, targ->r.currentOrigin ) ) || + ( VectorLength(v2) < CTF_TARGET_PROTECT_RADIUS && + trap_InPVS(flag->r.currentOrigin, attacker->r.currentOrigin ) ) ) && + attacker->client->sess.sessionTeam != targ->client->sess.sessionTeam) { + + // we defended the base flag + AddScore(attacker, CTF_FLAG_DEFENSE_BONUS); + attacker->client->pers.teamState.basedefense++; + //attacker->client->ps.persistant[PERS_DEFEND_COUNT]++; + // add the sprite over the player's head + //attacker->client->ps.eFlags &= ~(EF_AWARD_IMPRESSIVE | EF_AWARD_EXCELLENT | EF_AWARD_GAUNTLET | EF_AWARD_ASSIST | EF_AWARD_DEFEND | EF_AWARD_CAP ); + //attacker->client->ps.eFlags |= EF_AWARD_DEFEND; + attacker->client->rewardTime = level.time + REWARD_SPRITE_TIME; + return; + } + + if (carrier && carrier != attacker) { + VectorSubtract(targ->r.currentOrigin, carrier->r.currentOrigin, v1); + VectorSubtract(attacker->r.currentOrigin, carrier->r.currentOrigin, v1); + + if ( ( ( VectorLength(v1) < CTF_ATTACKER_PROTECT_RADIUS && + trap_InPVS(carrier->r.currentOrigin, targ->r.currentOrigin ) ) || + ( VectorLength(v2) < CTF_ATTACKER_PROTECT_RADIUS && + trap_InPVS(carrier->r.currentOrigin, attacker->r.currentOrigin ) ) ) && + attacker->client->sess.sessionTeam != targ->client->sess.sessionTeam) { + AddScore(attacker, CTF_CARRIER_PROTECT_BONUS); + attacker->client->pers.teamState.carrierdefense++; + //attacker->client->ps.persistant[PERS_DEFEND_COUNT]++; + // add the sprite over the player's head + //attacker->client->ps.eFlags &= ~(EF_AWARD_IMPRESSIVE | EF_AWARD_EXCELLENT | EF_AWARD_GAUNTLET | EF_AWARD_ASSIST | EF_AWARD_DEFEND | EF_AWARD_CAP ); + //attacker->client->ps.eFlags |= EF_AWARD_DEFEND; + attacker->client->rewardTime = level.time + REWARD_SPRITE_TIME; + return; + } + } +} + +/* +================ +Team_CheckHurtCarrier + +Check to see if attacker hurt the flag carrier. Needed when handing out bonuses for assistance to flag +carrier defense. +================ +*/ +void Team_CheckHurtCarrier(gentity_t *targ, gentity_t *attacker) +{ + int flag_pw; + + if (!targ->client || !attacker->client) + return; + + /*if (targ->client->sess.sessionTeam == TEAM_HUMANS) + flag_pw = PW_BLUEFLAG; + else + flag_pw = PW_REDFLAG;*/ + + if (targ->client->ps.powerups[flag_pw] && + targ->client->sess.sessionTeam != attacker->client->sess.sessionTeam) + attacker->client->pers.teamState.lasthurtcarrier = level.time; + + // skulls + if (targ->client->ps.generic1 && + targ->client->sess.sessionTeam != attacker->client->sess.sessionTeam) + attacker->client->pers.teamState.lasthurtcarrier = level.time; +} + + +gentity_t *Team_ResetFlag(int team) +{ + char *c; + gentity_t *ent, *rent = NULL; + + switch (team) { + case TEAM_HUMANS: + c = "team_CTF_redflag"; + break; + case TEAM_DROIDS: + c = "team_CTF_blueflag"; + break; + case TEAM_FREE: + c = "team_CTF_neutralflag"; + break; + default: + return NULL; + } + + ent = NULL; + while ((ent = G_Find (ent, FOFS(classname), c)) != NULL) { + if (ent->flags & FL_DROPPED_ITEM) + G_FreeEntity(ent); + else { + rent = ent; + RespawnItem(ent); + } + } + + Team_SetFlagStatus( team, FLAG_ATBASE ); + + return rent; +} + +void Team_ResetFlags( void ) { + if( g_gametype.integer == GT_CTF ) { + Team_ResetFlag( TEAM_HUMANS ); + Team_ResetFlag( TEAM_DROIDS ); + } +} + +void Team_ReturnFlagSound(gentity_t *ent, int team) +{ + // play powerup spawn sound to all clients + gentity_t *te; + + if (ent == NULL) { + G_Printf ("Warning: NULL passed to Team_ReturnFlagSound\n"); + return; + } + + //te = G_TempEntity( ent->s.pos.trBase, EV_GLOBAL_TEAM_SOUND ); + if( team == TEAM_DROIDS ) { + //te->s.eventParm = GTS_RED_RETURN; + } + else { + //te->s.eventParm = GTS_BLUE_RETURN; + } + te->r.svFlags |= SVF_BROADCAST; +} + +void Team_TakeFlagSound( gentity_t *ent, int team ) { + gentity_t *te; + + if (ent == NULL) { + G_Printf ("Warning: NULL passed to Team_TakeFlagSound\n"); + return; + } + + // only play sound when the flag was at the base + // or not picked up the last 10 seconds + switch(team) { + case TEAM_HUMANS: + if( teamgame.blueStatus != FLAG_ATBASE ) { + if (teamgame.blueTakenTime > level.time - 10000) + return; + } + teamgame.blueTakenTime = level.time; + break; + + case TEAM_DROIDS: // CTF + if( teamgame.redStatus != FLAG_ATBASE ) { + if (teamgame.redTakenTime > level.time - 10000) + return; + } + teamgame.redTakenTime = level.time; + break; + } + + //te = G_TempEntity( ent->s.pos.trBase, EV_GLOBAL_TEAM_SOUND ); + if( team == TEAM_DROIDS ) { + //te->s.eventParm = GTS_RED_TAKEN; + } + else { + //te->s.eventParm = GTS_BLUE_TAKEN; + } + te->r.svFlags |= SVF_BROADCAST; +} + +void Team_CaptureFlagSound( gentity_t *ent, int team ) { + gentity_t *te; + + if (ent == NULL) { + G_Printf ("Warning: NULL passed to Team_CaptureFlagSound\n"); + return; + } + + //te = G_TempEntity( ent->s.pos.trBase, EV_GLOBAL_TEAM_SOUND ); + if( team == TEAM_DROIDS ) { + //te->s.eventParm = GTS_BLUE_CAPTURE; + } + else { + //te->s.eventParm = GTS_RED_CAPTURE; + } + te->r.svFlags |= SVF_BROADCAST; +} + +void Team_ReturnFlag( int team ) { + Team_ReturnFlagSound(Team_ResetFlag(team), team); + if( team == TEAM_FREE ) { + PrintMsg(NULL, "The flag has returned!\n" ); + } + else { + PrintMsg(NULL, "The %s flag has returned!\n", TeamName(team)); + } +} + +void Team_FreeEntity(gentity_t *ent) +{ + /*if (ent->item->giTag == PW_REDFLAG) + Team_ReturnFlag(TEAM_HUMANS); + else if (ent->item->giTag == PW_BLUEFLAG) + Team_ReturnFlag(TEAM_DROIDS);*/ +} + +/* +============== +Team_DroppedFlagThink + +Automatically set in Launch_Item if the item is one of the flags + +Flags are unique in that if they are dropped, the base flag must be respawned when they time out +============== +*/ +void Team_DroppedFlagThink(gentity_t *ent) +{ + /*if (ent->item->giTag == PW_REDFLAG) + Team_ReturnFlagSound(Team_ResetFlag(TEAM_HUMANS), TEAM_HUMANS); + else if (ent->item->giTag == PW_BLUEFLAG) + Team_ReturnFlagSound(Team_ResetFlag(TEAM_DROIDS), TEAM_DROIDS);*/ + // Reset Flag will delete this entity +} + +int Team_TouchOurFlag( gentity_t *ent, gentity_t *other, int team ) { + int i; + gentity_t *player; + gclient_t *cl = other->client; + int our_flag, enemy_flag; + + /*if (cl->sess.sessionTeam == TEAM_HUMANS) { + our_flag = PW_REDFLAG; + enemy_flag = PW_BLUEFLAG; + } else { + our_flag = PW_BLUEFLAG; + enemy_flag = PW_REDFLAG; + }*/ + + if ( ent->flags & FL_DROPPED_ITEM ) { + // hey, its not home. return it by teleporting it back + PrintMsg( NULL, "%s" S_COLOR_WHITE " returned the %s flag!\n", + cl->pers.netname, TeamName(team)); + AddScore(other, CTF_RECOVERY_BONUS); + other->client->pers.teamState.flagrecovery++; + other->client->pers.teamState.lastreturnedflag = level.time; + //ResetFlag will remove this entity! We must return zero + Team_ReturnFlagSound(Team_ResetFlag(team), team); + return 0; + } + + // the flag is at home base. if the player has the enemy + // flag, he's just won! + if (!cl->ps.powerups[enemy_flag]) + return 0; // We don't have the flag + + PrintMsg( NULL, "%s" S_COLOR_WHITE " captured the %s flag!\n", cl->pers.netname, TeamName(OtherTeam(team))); + + cl->ps.powerups[enemy_flag] = 0; + + teamgame.last_flag_capture = level.time; + teamgame.last_capture_team = team; + + // Increase the team's score + AddTeamScore(ent->s.pos.trBase, other->client->sess.sessionTeam, 1); + Team_ForceGesture(other->client->sess.sessionTeam); + + other->client->pers.teamState.captures++; + + // other gets another 10 frag bonus + + // Ok, let's do the player loop, hand out the bonuses + for (i = 0; i < g_maxclients.integer; i++) { + player = &g_entities[i]; + if (!player->inuse) + continue; + + if (player->client->sess.sessionTeam != + cl->sess.sessionTeam) { + player->client->pers.teamState.lasthurtcarrier = -5; + } else if (player->client->sess.sessionTeam == + cl->sess.sessionTeam) { + if (player != other) + AddScore(player, CTF_TEAM_BONUS); + // award extra points for capture assists + if (player->client->pers.teamState.lastreturnedflag + + CTF_RETURN_FLAG_ASSIST_TIMEOUT > level.time) { + AddScore (player, CTF_RETURN_FLAG_ASSIST_BONUS); + other->client->pers.teamState.assists++; + } + if (player->client->pers.teamState.lastfraggedcarrier + + CTF_FRAG_CARRIER_ASSIST_TIMEOUT > level.time) { + AddScore(player, CTF_FRAG_CARRIER_ASSIST_BONUS); + other->client->pers.teamState.assists++; + } + } + } + Team_ResetFlags(); + + CalculateRanks(); + + return 0; // Do not respawn this automatically +} + +int Team_TouchEnemyFlag( gentity_t *ent, gentity_t *other, int team ) { + gclient_t *cl = other->client; + + // hey, its not our flag, pick it up + PrintMsg (NULL, "%s" S_COLOR_WHITE " got the %s flag!\n", + other->client->pers.netname, TeamName(team)); + AddScore(other, CTF_FLAG_BONUS); + + /*if (team == TEAM_HUMANS) + cl->ps.powerups[PW_REDFLAG] = INT_MAX; // flags never expire + else + cl->ps.powerups[PW_BLUEFLAG] = INT_MAX; // flags never expire*/ + + cl->pers.teamState.flagsince = level.time; + + Team_SetFlagStatus( team, FLAG_TAKEN ); + + return -1; // Do not respawn this automatically, but do delete it if it was FL_DROPPED +} + +int Pickup_Team( gentity_t *ent, gentity_t *other ) { + int team; + gclient_t *cl = other->client; + + // figure out what team this flag is + if (strcmp(ent->classname, "team_CTF_redflag") == 0) + team = TEAM_HUMANS; + else if (strcmp(ent->classname, "team_CTF_blueflag") == 0) + team = TEAM_DROIDS; + else { + PrintMsg ( other, "Don't know what team the flag is on.\n"); + return 0; + } + + return ((team == cl->sess.sessionTeam) ? + Team_TouchOurFlag : Team_TouchEnemyFlag) + (ent, other, team); +} + +/* +=========== +Team_GetLocation + +Report a location for the player. Uses placed nearby target_location entities +============ +*/ +gentity_t *Team_GetLocation(gentity_t *ent) +{ + gentity_t *eloc, *best; + float bestlen, len; + vec3_t origin; + + best = NULL; + bestlen = 3*8192.0*8192.0; + + VectorCopy( ent->r.currentOrigin, origin ); + + for (eloc = level.locationHead; eloc; eloc = eloc->nextTrain) { + len = ( origin[0] - eloc->r.currentOrigin[0] ) * ( origin[0] - eloc->r.currentOrigin[0] ) + + ( origin[1] - eloc->r.currentOrigin[1] ) * ( origin[1] - eloc->r.currentOrigin[1] ) + + ( origin[2] - eloc->r.currentOrigin[2] ) * ( origin[2] - eloc->r.currentOrigin[2] ); + + if ( len > bestlen ) { + continue; + } + + if ( !trap_InPVS( origin, eloc->r.currentOrigin ) ) { + continue; + } + + bestlen = len; + best = eloc; + } + + return best; +} + + +/* +=========== +Team_GetLocation + +Report a location for the player. Uses placed nearby target_location entities +============ +*/ +qboolean Team_GetLocationMsg(gentity_t *ent, char *loc, int loclen) +{ + gentity_t *best; + + best = Team_GetLocation( ent ); + + if (!best) + return qfalse; + + if (best->count) { + if (best->count < 0) + best->count = 0; + if (best->count > 7) + best->count = 7; + Com_sprintf(loc, loclen, "%c%c%s" S_COLOR_WHITE, Q_COLOR_ESCAPE, best->count + '0', best->message ); + } else + Com_sprintf(loc, loclen, "%s", best->message); + + return qtrue; +} + + +/*---------------------------------------------------------------------------*/ + +/* +================ +SelectRandomDeathmatchSpawnPoint + +go to a random point that doesn't telefrag +================ +*/ +#define MAX_TEAM_SPAWN_POINTS 32 +gentity_t *SelectRandomTeamSpawnPoint( int teamstate, team_t team ) { + gentity_t *spot; + int count; + int selection; + gentity_t *spots[MAX_TEAM_SPAWN_POINTS]; + char *classname; + + if (teamstate == TEAM_BEGIN) { + if (team == TEAM_HUMANS) + classname = "team_CTF_redplayer"; + else if (team == TEAM_DROIDS) + classname = "team_CTF_blueplayer"; + else + return NULL; + } else { + if (team == TEAM_HUMANS) + classname = "team_CTF_redspawn"; + else if (team == TEAM_DROIDS) + classname = "team_CTF_bluespawn"; + else + return NULL; + } + count = 0; + + spot = NULL; + + while ((spot = G_Find (spot, FOFS(classname), classname)) != NULL) { + if ( SpotWouldTelefrag( spot ) ) { + continue; + } + spots[ count ] = spot; + if (++count == MAX_TEAM_SPAWN_POINTS) + break; + } + + if ( !count ) { // no spots that won't telefrag + return G_Find( NULL, FOFS(classname), classname); + } + + selection = rand() % count; + return spots[ selection ]; +} + + +/* +=========== +SelectCTFSpawnPoint + +============ +*/ +gentity_t *SelectCTFSpawnPoint ( team_t team, int teamstate, vec3_t origin, vec3_t angles ) { + gentity_t *spot; + + spot = SelectRandomTeamSpawnPoint ( teamstate, team ); + + if (!spot) { + return SelectSpawnPoint( vec3_origin, origin, angles ); + } + + VectorCopy (spot->s.origin, origin); + origin[2] += 9; + VectorCopy (spot->s.angles, angles); + + return spot; +} + +/*---------------------------------------------------------------------------*/ + +static int QDECL SortClients( const void *a, const void *b ) { + return *(int *)a - *(int *)b; +} + + +/* +================== +TeamplayLocationsMessage + +Format: + clientNum location health armor weapon powerups + +================== +*/ +void TeamplayInfoMessage( gentity_t *ent ) { + char entry[1024]; + char string[8192]; + int stringlength; + int i, j; + gentity_t *player; + int cnt; + int h, a; + int clients[TEAM_MAXOVERLAY]; + + if ( ! ent->client->pers.teamInfo ) + return; + + // figure out what client should be on the display + // we are limited to 8, but we want to use the top eight players + // but in client order (so they don't keep changing position on the overlay) + for (i = 0, cnt = 0; i < g_maxclients.integer && cnt < TEAM_MAXOVERLAY; i++) { + player = g_entities + level.sortedClients[i]; + if (player->inuse && player->client->sess.sessionTeam == + ent->client->sess.sessionTeam ) { + clients[cnt++] = level.sortedClients[i]; + } + } + + // We have the top eight players, sort them by clientNum + qsort( clients, cnt, sizeof( clients[0] ), SortClients ); + + // send the latest information on all clients + string[0] = 0; + stringlength = 0; + + for (i = 0, cnt = 0; i < g_maxclients.integer && cnt < TEAM_MAXOVERLAY; i++) { + player = g_entities + i; + if (player->inuse && player->client->sess.sessionTeam == + ent->client->sess.sessionTeam ) { + + h = player->client->ps.stats[STAT_HEALTH]; + a = player->client->ps.stats[STAT_ARMOR]; + if (h < 0) h = 0; + if (a < 0) a = 0; + + Com_sprintf (entry, sizeof(entry), + " %i %i %i %i %i %i", +// level.sortedClients[i], player->client->pers.teamState.location, h, a, + i, player->client->pers.teamState.location, h, a, + player->client->ps.weapon, player->s.powerups); + j = strlen(entry); + if (stringlength + j > sizeof(string)) + break; + strcpy (string + stringlength, entry); + stringlength += j; + cnt++; + } + } + + trap_SendServerCommand( ent-g_entities, va("tinfo %i %s", cnt, string) ); +} + +void CheckTeamStatus(void) +{ + int i; + gentity_t *loc, *ent; + + if (level.time - level.lastTeamLocationTime > TEAM_LOCATION_UPDATE_TIME) { + + level.lastTeamLocationTime = level.time; + + for (i = 0; i < g_maxclients.integer; i++) { + ent = g_entities + i; + if ( ent->client->pers.connected != CON_CONNECTED ) { + continue; + } + + if (ent->inuse && (ent->client->sess.sessionTeam == TEAM_HUMANS || ent->client->sess.sessionTeam == TEAM_DROIDS)) { + + loc = Team_GetLocation( ent ); + if (loc) + ent->client->pers.teamState.location = loc->health; + else + ent->client->pers.teamState.location = 0; + } + } + + for (i = 0; i < g_maxclients.integer; i++) { + ent = g_entities + i; + if ( ent->client->pers.connected != CON_CONNECTED ) { + continue; + } + + if (ent->inuse && (ent->client->sess.sessionTeam == TEAM_HUMANS || ent->client->sess.sessionTeam == TEAM_DROIDS)) { + TeamplayInfoMessage( ent ); + } + } + } +} + +/*-----------------------------------------------------------------*/ + +/*QUAKED team_CTF_redplayer (1 0 0) (-16 -16 -16) (16 16 32) +Only in CTF games. Red players spawn here at game start. +*/ +void SP_team_CTF_redplayer( gentity_t *ent ) { +} + + +/*QUAKED team_CTF_blueplayer (0 0 1) (-16 -16 -16) (16 16 32) +Only in CTF games. Blue players spawn here at game start. +*/ +void SP_team_CTF_blueplayer( gentity_t *ent ) { +} + + +/*QUAKED team_CTF_redspawn (1 0 0) (-16 -16 -24) (16 16 32) +potential spawning position for red team in CTF games. +Targets will be fired when someone spawns in on them. +*/ +void SP_team_CTF_redspawn(gentity_t *ent) { +} + +/*QUAKED team_CTF_bluespawn (0 0 1) (-16 -16 -24) (16 16 32) +potential spawning position for blue team in CTF games. +Targets will be fired when someone spawns in on them. +*/ +void SP_team_CTF_bluespawn(gentity_t *ent) { +} + + + diff --git a/src/game/g_trigger.c b/src/game/g_trigger.c new file mode 100644 index 00000000..739bf440 --- /dev/null +++ b/src/game/g_trigger.c @@ -0,0 +1,469 @@ +// Copyright (C) 1999-2000 Id Software, Inc. +// + +/* + * Portions Copyright (C) 2000-2001 Tim Angus + * + * This program 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, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ + +/* To assertain which portions are licensed under the GPL and which are + * licensed by Id Software, Inc. please run a diff between the equivalent + * versions of the "Tremulous" modification and the unmodified "Quake3" + * game source code. + */ + +#include "g_local.h" + + +void InitTrigger( gentity_t *self ) { + if (!VectorCompare (self->s.angles, vec3_origin)) + G_SetMovedir (self->s.angles, self->movedir); + + trap_SetBrushModel( self, self->model ); + self->r.contents = CONTENTS_TRIGGER; // replaces the -1 from trap_SetBrushModel + self->r.svFlags = SVF_NOCLIENT; +} + + +// the wait time has passed, so set back up for another activation +void multi_wait( gentity_t *ent ) { + ent->nextthink = 0; +} + + +// the trigger was just activated +// ent->activator should be set to the activator so it can be held through a delay +// so wait for the delay time before firing +void multi_trigger( gentity_t *ent, gentity_t *activator ) { + ent->activator = activator; + if ( ent->nextthink ) { + return; // can't retrigger until the wait is over + } + + if ( activator->client ) { + if ( ( ent->spawnflags & 1 ) && + activator->client->sess.sessionTeam != TEAM_HUMANS ) { + return; + } + if ( ( ent->spawnflags & 2 ) && + activator->client->sess.sessionTeam != TEAM_DROIDS ) { + return; + } + } + + G_UseTargets (ent, ent->activator); + + if ( ent->wait > 0 ) { + ent->think = multi_wait; + ent->nextthink = level.time + ( ent->wait + ent->random * crandom() ) * 1000; + } else { + // we can't just remove (self) here, because this is a touch function + // called while looping through area links... + ent->touch = 0; + ent->nextthink = level.time + FRAMETIME; + ent->think = G_FreeEntity; + } +} + +void Use_Multi( gentity_t *ent, gentity_t *other, gentity_t *activator ) { + multi_trigger( ent, activator ); +} + +void Touch_Multi( gentity_t *self, gentity_t *other, trace_t *trace ) { + if( !other->client ) { + return; + } + multi_trigger( self, other ); +} + +/*QUAKED trigger_multiple (.5 .5 .5) ? +"wait" : Seconds between triggerings, 0.5 default, -1 = one time only. +"random" wait variance, default is 0 +Variable sized repeatable trigger. Must be targeted at one or more entities. +so, the basic time between firing is a random time between +(wait - random) and (wait + random) +*/ +void SP_trigger_multiple( gentity_t *ent ) { + G_SpawnFloat( "wait", "0.5", &ent->wait ); + G_SpawnFloat( "random", "0", &ent->random ); + + if ( ent->random >= ent->wait && ent->wait >= 0 ) { + ent->random = ent->wait - FRAMETIME; + G_Printf( "trigger_multiple has random >= wait\n" ); + } + + ent->touch = Touch_Multi; + ent->use = Use_Multi; + + InitTrigger( ent ); + trap_LinkEntity (ent); +} + + + +/* +============================================================================== + +trigger_always + +============================================================================== +*/ + +void trigger_always_think( gentity_t *ent ) { + G_UseTargets(ent, ent); + G_FreeEntity( ent ); +} + +/*QUAKED trigger_always (.5 .5 .5) (-8 -8 -8) (8 8 8) +This trigger will always fire. It is activated by the world. +*/ +void SP_trigger_always (gentity_t *ent) { + // we must have some delay to make sure our use targets are present + ent->nextthink = level.time + 300; + ent->think = trigger_always_think; +} + + +/* +============================================================================== + +trigger_push + +============================================================================== +*/ + +void trigger_push_touch (gentity_t *self, gentity_t *other, trace_t *trace ) { + if ( !other->client ) { + return; + } + + BG_TouchJumpPad( &other->client->ps, &self->s ); +} + + +/* +================= +AimAtTarget + +Calculate origin2 so the target apogee will be hit +================= +*/ +void AimAtTarget( gentity_t *self ) { + gentity_t *ent; + vec3_t origin; + float height, gravity, time, forward; + float dist; + + VectorAdd( self->r.absmin, self->r.absmax, origin ); + VectorScale ( origin, 0.5, origin ); + + ent = G_PickTarget( self->target ); + if ( !ent ) { + G_FreeEntity( self ); + return; + } + + height = ent->s.origin[2] - origin[2]; + gravity = g_gravity.value; + time = sqrt( height / ( .5 * gravity ) ); + if ( !time ) { + G_FreeEntity( self ); + return; + } + + // set s.origin2 to the push velocity + VectorSubtract ( ent->s.origin, origin, self->s.origin2 ); + self->s.origin2[2] = 0; + dist = VectorNormalize( self->s.origin2); + + forward = dist / time; + VectorScale( self->s.origin2, forward, self->s.origin2 ); + + self->s.origin2[2] = time * gravity; +} + + +/*QUAKED trigger_push (.5 .5 .5) ? +Must point at a target_position, which will be the apex of the leap. +This will be client side predicted, unlike target_push +*/ +void SP_trigger_push( gentity_t *self ) { + InitTrigger (self); + + // unlike other triggers, we need to send this one to the client + self->r.svFlags &= ~SVF_NOCLIENT; + + // make sure the client precaches this sound + G_SoundIndex("sound/world/jumppad.wav"); + + self->s.eType = ET_PUSH_TRIGGER; + self->touch = trigger_push_touch; + self->think = AimAtTarget; + self->nextthink = level.time + FRAMETIME; + trap_LinkEntity (self); +} + + +void Use_target_push( gentity_t *self, gentity_t *other, gentity_t *activator ) { + if ( !activator->client ) { + return; + } + + if ( activator->client->ps.pm_type != PM_NORMAL ) { + return; + } + /*if ( activator->client->ps.powerups[PW_FLIGHT] ) { + return; + }*/ + + VectorCopy (self->s.origin2, activator->client->ps.velocity); + + // play fly sound every 1.5 seconds + if ( activator->fly_sound_debounce_time < level.time ) { + activator->fly_sound_debounce_time = level.time + 1500; + G_Sound( activator, CHAN_AUTO, self->noise_index ); + } +} + +/*QUAKED target_push (.5 .5 .5) (-8 -8 -8) (8 8 8) bouncepad +Pushes the activator in the direction.of angle, or towards a target apex. +"speed" defaults to 1000 +if "bouncepad", play bounce noise instead of windfly +*/ +void SP_target_push( gentity_t *self ) { + if (!self->speed) { + self->speed = 1000; + } + G_SetMovedir (self->s.angles, self->s.origin2); + VectorScale (self->s.origin2, self->speed, self->s.origin2); + + if ( self->spawnflags & 1 ) { + self->noise_index = G_SoundIndex("sound/world/jumppad.wav"); + } else { + self->noise_index = G_SoundIndex("sound/misc/windfly.wav"); + } + if ( self->target ) { + VectorCopy( self->s.origin, self->r.absmin ); + VectorCopy( self->s.origin, self->r.absmax ); + self->think = AimAtTarget; + self->nextthink = level.time + FRAMETIME; + } + self->use = Use_target_push; +} + +/* +============================================================================== + +trigger_teleport + +============================================================================== +*/ + +void trigger_teleporter_touch (gentity_t *self, gentity_t *other, trace_t *trace ) { + gentity_t *dest; + + if ( !other->client ) { + return; + } + if ( other->client->ps.pm_type == PM_DEAD ) { + return; + } + // Spectators only? + if ( ( self->spawnflags & 1 ) && + other->client->sess.sessionTeam != TEAM_SPECTATOR ) { + return; + } + + + dest = G_PickTarget( self->target ); + if (!dest) { + G_Printf ("Couldn't find teleporter destination\n"); + return; + } + + TeleportPlayer( other, dest->s.origin, dest->s.angles ); +} + + +/*QUAKED trigger_teleport (.5 .5 .5) ? SPECTATOR +Allows client side prediction of teleportation events. +Must point at a target_position, which will be the teleport destination. + +If spectator is set, only spectators can use this teleport +Spectator teleporters are not normally placed in the editor, but are created +automatically near doors to allow spectators to move through them +*/ +void SP_trigger_teleport( gentity_t *self ) { + InitTrigger (self); + + // unlike other triggers, we need to send this one to the client + // unless is a spectator trigger + if ( self->spawnflags & 1 ) { + self->r.svFlags |= SVF_NOCLIENT; + } else { + self->r.svFlags &= ~SVF_NOCLIENT; + } + + // make sure the client precaches this sound + G_SoundIndex("sound/world/jumppad.wav"); + + self->s.eType = ET_TELEPORT_TRIGGER; + self->touch = trigger_teleporter_touch; + + trap_LinkEntity (self); +} + + +/* +============================================================================== + +trigger_hurt + +============================================================================== +*/ + +/*QUAKED trigger_hurt (.5 .5 .5) ? START_OFF - SILENT NO_PROTECTION SLOW +Any entity that touches this will be hurt. +It does dmg points of damage each server frame +Targeting the trigger will toggle its on / off state. + +SILENT supresses playing the sound +SLOW changes the damage rate to once per second +NO_PROTECTION *nothing* stops the damage + +"dmg" default 5 (whole numbers only) + +*/ +void hurt_use( gentity_t *self, gentity_t *other, gentity_t *activator ) { + if ( self->r.linked ) { + trap_UnlinkEntity( self ); + } else { + trap_LinkEntity( self ); + } +} + +void hurt_touch( gentity_t *self, gentity_t *other, trace_t *trace ) { + int dflags; + + if ( !other->takedamage ) { + return; + } + + if ( self->timestamp > level.time ) { + return; + } + + if ( self->spawnflags & 16 ) { + self->timestamp = level.time + 1000; + } else { + self->timestamp = level.time + FRAMETIME; + } + + // play sound + if ( !(self->spawnflags & 4) ) { + G_Sound( other, CHAN_AUTO, self->noise_index ); + } + + if (self->spawnflags & 8) + dflags = DAMAGE_NO_PROTECTION; + else + dflags = 0; + G_Damage (other, self, self, NULL, NULL, self->damage, dflags, MOD_TRIGGER_HURT); +} + +void SP_trigger_hurt( gentity_t *self ) { + InitTrigger (self); + + self->noise_index = G_SoundIndex( "sound/world/electro.wav" ); + self->touch = hurt_touch; + + if ( !self->damage ) { + self->damage = 5; + } + + self->r.contents = CONTENTS_TRIGGER; + + if ( self->spawnflags & 2 ) { + self->use = hurt_use; + } + + // link in to the world if starting active + if ( ! (self->spawnflags & 1) ) { + trap_LinkEntity (self); + } +} + + +/* +============================================================================== + +timer + +============================================================================== +*/ + + +/*QUAKED func_timer (0.3 0.1 0.6) (-8 -8 -8) (8 8 8) START_ON +This should be renamed trigger_timer... +Repeatedly fires its targets. +Can be turned on or off by using. + +"wait" base time between triggering all targets, default is 1 +"random" wait variance, default is 0 +so, the basic time between firing is a random time between +(wait - random) and (wait + random) + +*/ +void func_timer_think( gentity_t *self ) { + G_UseTargets (self, self->activator); + // set time before next firing + self->nextthink = level.time + 1000 * ( self->wait + crandom() * self->random ); +} + +void func_timer_use( gentity_t *self, gentity_t *other, gentity_t *activator ) { + self->activator = activator; + + // if on, turn it off + if ( self->nextthink ) { + self->nextthink = 0; + return; + } + + // turn it on + func_timer_think (self); +} + +void SP_func_timer( gentity_t *self ) { + G_SpawnFloat( "random", "1", &self->random); + G_SpawnFloat( "wait", "1", &self->wait ); + + self->use = func_timer_use; + self->think = func_timer_think; + + if ( self->random >= self->wait ) { + self->random = self->wait - FRAMETIME; + G_Printf( "func_timer at %s has random >= wait\n", vtos( self->s.origin ) ); + } + + if ( self->spawnflags & 1 ) { + self->nextthink = level.time + FRAMETIME; + self->activator = self; + } + + self->r.svFlags = SVF_NOCLIENT; +} + + diff --git a/src/game/g_utils.c b/src/game/g_utils.c new file mode 100644 index 00000000..b134a9df --- /dev/null +++ b/src/game/g_utils.c @@ -0,0 +1,718 @@ +// Copyright (C) 1999-2000 Id Software, Inc. +// +// g_utils.c -- misc utility functions for game module + +/* + * Portions Copyright (C) 2000-2001 Tim Angus + * + * This program 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, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ + +/* To assertain which portions are licensed under the GPL and which are + * licensed by Id Software, Inc. please run a diff between the equivalent + * versions of the "Tremulous" modification and the unmodified "Quake3" + * game source code. + */ + +#include "g_local.h" + +typedef struct { + char oldShader[MAX_QPATH]; + char newShader[MAX_QPATH]; + float timeOffset; +} shaderRemap_t; + +#define MAX_SHADER_REMAPS 128 + +int remapCount = 0; +shaderRemap_t remappedShaders[MAX_SHADER_REMAPS]; + +void AddRemap(const char *oldShader, const char *newShader, float timeOffset) { + int i; + + for (i = 0; i < remapCount; i++) { + if (Q_stricmp(oldShader, remappedShaders[i].oldShader) == 0) { + // found it, just update this one + strcpy(remappedShaders[i].newShader,newShader); + remappedShaders[i].timeOffset = timeOffset; + return; + } + } + if (remapCount < MAX_SHADER_REMAPS) { + strcpy(remappedShaders[remapCount].newShader,newShader); + strcpy(remappedShaders[remapCount].oldShader,oldShader); + remappedShaders[remapCount].timeOffset = timeOffset; + remapCount++; + } +} + +const char *BuildShaderStateConfig() { + static char buff[MAX_STRING_CHARS]; + char out[(MAX_QPATH * 2) + 5]; + int i; + + memset(buff, 0, MAX_STRING_CHARS); + for (i = 0; i < remapCount; i++) { + Com_sprintf(out, (MAX_QPATH * 2) + 5, "%s=%s:%5.2f@", remappedShaders[i].oldShader, remappedShaders[i].newShader, remappedShaders[i].timeOffset); + Q_strcat( buff, sizeof( buff ), out); + } + return buff; +} + + +/* +========================================================================= + +model / sound configstring indexes + +========================================================================= +*/ + +/* +================ +G_FindConfigstringIndex + +================ +*/ +int G_FindConfigstringIndex( char *name, int start, int max, qboolean create ) { + int i; + char s[MAX_STRING_CHARS]; + + if ( !name || !name[0] ) { + return 0; + } + + for ( i=1 ; i<max ; i++ ) { + trap_GetConfigstring( start + i, s, sizeof( s ) ); + if ( !s[0] ) { + break; + } + if ( !strcmp( s, name ) ) { + return i; + } + } + + if ( !create ) { + return 0; + } + + if ( i == max ) { + G_Error( "G_FindConfigstringIndex: overflow" ); + } + + trap_SetConfigstring( start + i, name ); + + return i; +} + + +int G_ModelIndex( char *name ) { + return G_FindConfigstringIndex (name, CS_MODELS, MAX_MODELS, qtrue); +} + +int G_SoundIndex( char *name ) { + return G_FindConfigstringIndex (name, CS_SOUNDS, MAX_SOUNDS, qtrue); +} + +//===================================================================== + + +/* +================ +G_TeamCommand + +Broadcasts a command to only a specific team +================ +*/ +void G_TeamCommand( team_t team, char *cmd ) { + int i; + + for ( i = 0 ; i < level.maxclients ; i++ ) { + if ( level.clients[i].pers.connected == CON_CONNECTED ) { + if ( level.clients[i].sess.sessionTeam == team ) { + trap_SendServerCommand( i, va("%s", cmd )); + } + } + } +} + + +/* +============= +G_Find + +Searches all active entities for the next one that holds +the matching string at fieldofs (use the FOFS() macro) in the structure. + +Searches beginning at the entity after from, or the beginning if NULL +NULL will be returned if the end of the list is reached. + +============= +*/ +gentity_t *G_Find (gentity_t *from, int fieldofs, const char *match) +{ + char *s; + + if (!from) + from = g_entities; + else + from++; + + for ( ; from < &g_entities[level.num_entities] ; from++) + { + if (!from->inuse) + continue; + s = *(char **) ((byte *)from + fieldofs); + if (!s) + continue; + if (!Q_stricmp (s, match)) + return from; + } + + return NULL; +} + + +/* +============= +G_PickTarget + +Selects a random entity from among the targets +============= +*/ +#define MAXCHOICES 32 + +gentity_t *G_PickTarget (char *targetname) +{ + gentity_t *ent = NULL; + int num_choices = 0; + gentity_t *choice[MAXCHOICES]; + + if (!targetname) + { + G_Printf("G_PickTarget called with NULL targetname\n"); + return NULL; + } + + while(1) + { + ent = G_Find (ent, FOFS(targetname), targetname); + if (!ent) + break; + choice[num_choices++] = ent; + if (num_choices == MAXCHOICES) + break; + } + + if (!num_choices) + { + G_Printf("G_PickTarget: target %s not found\n", targetname); + return NULL; + } + + return choice[rand() % num_choices]; +} + + +/* +============================== +G_UseTargets + +"activator" should be set to the entity that initiated the firing. + +Search for (string)targetname in all entities that +match (string)self.target and call their .use function + +============================== +*/ +void G_UseTargets( gentity_t *ent, gentity_t *activator ) { + gentity_t *t; + + if ( !ent ) { + return; + } + + if (ent->targetShaderName && ent->targetShaderNewName) { + float f = level.time * 0.001; + AddRemap(ent->targetShaderName, ent->targetShaderNewName, f); + trap_SetConfigstring(CS_SHADERSTATE, BuildShaderStateConfig()); + } + + if ( !ent->target ) { + return; + } + + t = NULL; + while ( (t = G_Find (t, FOFS(targetname), ent->target)) != NULL ) { + if ( t == ent ) { + G_Printf ("WARNING: Entity used itself.\n"); + } else { + if ( t->use ) { + t->use (t, ent, activator); + } + } + if ( !ent->inuse ) { + G_Printf("entity was removed while using targets\n"); + return; + } + } +} + + +/* +============= +TempVector + +This is just a convenience function +for making temporary vectors for function calls +============= +*/ +float *tv( float x, float y, float z ) { + static int index; + static vec3_t vecs[8]; + float *v; + + // use an array so that multiple tempvectors won't collide + // for a while + v = vecs[index]; + index = (index + 1)&7; + + v[0] = x; + v[1] = y; + v[2] = z; + + return v; +} + + +/* +============= +VectorToString + +This is just a convenience function +for printing vectors +============= +*/ +char *vtos( const vec3_t v ) { + static int index; + static char str[8][32]; + char *s; + + // use an array so that multiple vtos won't collide + s = str[index]; + index = (index + 1)&7; + + Com_sprintf (s, 32, "(%i %i %i)", (int)v[0], (int)v[1], (int)v[2]); + + return s; +} + + +/* +=============== +G_SetMovedir + +The editor only specifies a single value for angles (yaw), +but we have special constants to generate an up or down direction. +Angles will be cleared, because it is being used to represent a direction +instead of an orientation. +=============== +*/ +void G_SetMovedir( vec3_t angles, vec3_t movedir ) { + static vec3_t VEC_UP = {0, -1, 0}; + static vec3_t MOVEDIR_UP = {0, 0, 1}; + static vec3_t VEC_DOWN = {0, -2, 0}; + static vec3_t MOVEDIR_DOWN = {0, 0, -1}; + + if ( VectorCompare (angles, VEC_UP) ) { + VectorCopy (MOVEDIR_UP, movedir); + } else if ( VectorCompare (angles, VEC_DOWN) ) { + VectorCopy (MOVEDIR_DOWN, movedir); + } else { + AngleVectors (angles, movedir, NULL, NULL); + } + VectorClear( angles ); +} + + +float vectoyaw( const vec3_t vec ) { + float yaw; + + if (vec[YAW] == 0 && vec[PITCH] == 0) { + yaw = 0; + } else { + if (vec[PITCH]) { + yaw = ( atan2( vec[YAW], vec[PITCH]) * 180 / M_PI ); + } else if (vec[YAW] > 0) { + yaw = 90; + } else { + yaw = 270; + } + if (yaw < 0) { + yaw += 360; + } + } + + return yaw; +} + + +void G_InitGentity( gentity_t *e ) { + e->inuse = qtrue; + e->classname = "noclass"; + e->s.number = e - g_entities; + e->r.ownerNum = ENTITYNUM_NONE; +} + +/* +================= +G_Spawn + +Either finds a free entity, or allocates a new one. + + The slots from 0 to MAX_CLIENTS-1 are always reserved for clients, and will +never be used by anything else. + +Try to avoid reusing an entity that was recently freed, because it +can cause the client to think the entity morphed into something else +instead of being removed and recreated, which can cause interpolated +angles and bad trails. +================= +*/ +gentity_t *G_Spawn( void ) { + int i, force; + gentity_t *e; + + e = NULL; // shut up warning + i = 0; // shut up warning + for ( force = 0 ; force < 2 ; force++ ) { + // if we go through all entities and can't find one to free, + // override the normal minimum times before use + e = &g_entities[MAX_CLIENTS]; + for ( i = MAX_CLIENTS ; i<level.num_entities ; i++, e++) { + if ( e->inuse ) { + continue; + } + + // the first couple seconds of server time can involve a lot of + // freeing and allocating, so relax the replacement policy + if ( !force && e->freetime > level.startTime + 2000 && level.time - e->freetime < 1000 ) { + continue; + } + + // reuse this slot + G_InitGentity( e ); + return e; + } + if ( i != MAX_GENTITIES ) { + break; + } + } + if ( i == ENTITYNUM_MAX_NORMAL ) { + for (i = 0; i < MAX_GENTITIES; i++) { + G_Printf("%4i: %s\n", i, g_entities[i].classname); + } + G_Error( "G_Spawn: no free entities" ); + } + + // open up a new slot + level.num_entities++; + + // let the server system know that there are more entities + trap_LocateGameData( level.gentities, level.num_entities, sizeof( gentity_t ), + &level.clients[0].ps, sizeof( level.clients[0] ) ); + + G_InitGentity( e ); + return e; +} + + +/* +================= +G_EntitiesFree +================= +*/ +qboolean G_EntitiesFree( void ) { + int i; + gentity_t *e; + + e = &g_entities[MAX_CLIENTS]; + for ( i = MAX_CLIENTS; i < level.num_entities; i++, e++) { + if ( e->inuse ) { + continue; + } + // slot available + return qtrue; + } + return qfalse; +} + + +/* +================= +G_FreeEntity + +Marks the entity as free +================= +*/ +void G_FreeEntity( gentity_t *ed ) { + trap_UnlinkEntity (ed); // unlink from world + + if ( ed->neverFree ) { + return; + } + + memset (ed, 0, sizeof(*ed)); + ed->classname = "freed"; + ed->freetime = level.time; + ed->inuse = qfalse; +} + +/* +================= +G_TempEntity + +Spawns an event entity that will be auto-removed +The origin will be snapped to save net bandwidth, so care +must be taken if the origin is right on a surface (snap towards start vector first) +================= +*/ +gentity_t *G_TempEntity( vec3_t origin, int event ) { + gentity_t *e; + vec3_t snapped; + + e = G_Spawn(); + e->s.eType = ET_EVENTS + event; + + e->classname = "tempEntity"; + e->eventTime = level.time; + e->freeAfterEvent = qtrue; + + VectorCopy( origin, snapped ); + SnapVector( snapped ); // save network bandwidth + G_SetOrigin( e, snapped ); + + // find cluster for PVS + trap_LinkEntity( e ); + + return e; +} + + + +/* +============================================================================== + +Kill box + +============================================================================== +*/ + +/* +================= +G_KillBox + +Kills all entities that would touch the proposed new positioning +of ent. Ent should be unlinked before calling this! +================= +*/ +void G_KillBox (gentity_t *ent) { + int i, num; + int touch[MAX_GENTITIES]; + gentity_t *hit; + vec3_t mins, maxs; + + VectorAdd( ent->client->ps.origin, ent->r.mins, mins ); + VectorAdd( ent->client->ps.origin, ent->r.maxs, maxs ); + num = trap_EntitiesInBox( mins, maxs, touch, MAX_GENTITIES ); + + for (i=0 ; i<num ; i++) { + hit = &g_entities[touch[i]]; + if ( !hit->client ) { + continue; + } + + // nail it + G_Damage ( hit, ent, ent, NULL, NULL, + 100000, DAMAGE_NO_PROTECTION, MOD_TELEFRAG); + } + +} + +//============================================================================== + +/* +=============== +G_AddPredictableEvent + +Use for non-pmove events that would also be predicted on the +client side: jumppads and item pickups +Adds an event+parm and twiddles the event counter +=============== +*/ +void G_AddPredictableEvent( gentity_t *ent, int event, int eventParm ) { + if ( !ent->client ) { + return; + } + BG_AddPredictableEventToPlayerstate( event, eventParm, &ent->client->ps ); +} + + +/* +=============== +G_AddEvent + +Adds an event+parm and twiddles the event counter +=============== +*/ +void G_AddEvent( gentity_t *ent, int event, int eventParm ) { + int bits; + + if ( !event ) { + G_Printf( "G_AddEvent: zero event added for entity %i\n", ent->s.number ); + return; + } + + // clients need to add the event in playerState_t instead of entityState_t + if ( ent->client ) { + bits = ent->client->ps.externalEvent & EV_EVENT_BITS; + bits = ( bits + EV_EVENT_BIT1 ) & EV_EVENT_BITS; + ent->client->ps.externalEvent = event | bits; + ent->client->ps.externalEventParm = eventParm; + ent->client->ps.externalEventTime = level.time; + } else { + bits = ent->s.event & EV_EVENT_BITS; + bits = ( bits + EV_EVENT_BIT1 ) & EV_EVENT_BITS; + ent->s.event = event | bits; + ent->s.eventParm = eventParm; + } + ent->eventTime = level.time; +} + + +/* +============= +G_Sound +============= +*/ +void G_Sound( gentity_t *ent, int channel, int soundIndex ) { + gentity_t *te; + + te = G_TempEntity( ent->r.currentOrigin, EV_GENERAL_SOUND ); + te->s.eventParm = soundIndex; +} + + +//============================================================================== + + +/* +================ +G_SetOrigin + +Sets the pos trajectory for a fixed position +================ +*/ +void G_SetOrigin( gentity_t *ent, vec3_t origin ) { + VectorCopy( origin, ent->s.pos.trBase ); + ent->s.pos.trType = TR_STATIONARY; + ent->s.pos.trTime = 0; + ent->s.pos.trDuration = 0; + VectorClear( ent->s.pos.trDelta ); + + VectorCopy( origin, ent->r.currentOrigin ); +} + +//TA: from quakestyle.telefragged.com +// (NOBODY): Code helper function +// +gentity_t *G_FindRadius( gentity_t *from, vec3_t org, float rad ) +{ + vec3_t eorg; + int j; + + if( !from ) + from = g_entities; + else + from++; + + for( ; from < &g_entities[ level.num_entities ]; from++ ) + { + if( !from->inuse ) + continue; + + for( j = 0; j < 3; j++ ) + eorg[ j ] = org[ j ] - ( from->r.currentOrigin[ j ] + ( from->r.mins[ j ] + from->r.maxs[ j ] ) * 0.5 ); + + if( VectorLength( eorg ) > rad ) + continue; + + return from; + } + + return NULL; +} + +// (NOBODY): Code helper function +// +qboolean G_Visible( gentity_t *ent1, gentity_t *ent2 ) +{ + trace_t trace; + + trap_Trace( &trace, ent1->s.pos.trBase, NULL, NULL, ent2->s.pos.trBase, ent1->s.number, MASK_SHOT ); + + if( trace.contents & CONTENTS_SOLID ) + return qfalse; + + return qtrue; +} + + +/* +================ +DebugLine + + debug polygons only work when running a local game + with r_debugSurface set to 2 +================ +*/ +int DebugLine(vec3_t start, vec3_t end, int color) { + vec3_t points[4], dir, cross, up = {0, 0, 1}; + float dot; + + VectorCopy(start, points[0]); + VectorCopy(start, points[1]); + //points[1][2] -= 2; + VectorCopy(end, points[2]); + //points[2][2] -= 2; + VectorCopy(end, points[3]); + + + VectorSubtract(end, start, dir); + VectorNormalize(dir); + dot = DotProduct(dir, up); + if (dot > 0.99 || dot < -0.99) VectorSet(cross, 1, 0, 0); + else CrossProduct(dir, up, cross); + + VectorNormalize(cross); + + VectorMA(points[0], 2, cross, points[0]); + VectorMA(points[1], -2, cross, points[1]); + VectorMA(points[2], -2, cross, points[2]); + VectorMA(points[3], 2, cross, points[3]); + + return trap_DebugPolygonCreate(color, 4, points); +} + diff --git a/src/game/g_weapon.c b/src/game/g_weapon.c new file mode 100644 index 00000000..c5af5293 --- /dev/null +++ b/src/game/g_weapon.c @@ -0,0 +1,800 @@ +// Copyright (C) 1999-2000 Id Software, Inc. +// +// g_weapon.c +// perform the server side effects of a weapon firing + +/* + * Portions Copyright (C) 2000-2001 Tim Angus + * + * This program 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, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ + +/* To assertain which portions are licensed under the GPL and which are + * licensed by Id Software, Inc. please run a diff between the equivalent + * versions of the "Tremulous" modification and the unmodified "Quake3" + * game source code. + */ + +#include "g_local.h" + +static float s_quadFactor; +static vec3_t forward, right, up; +static vec3_t muzzle; + +//TA: for horizontal changes in muzzle point +static int hmuzzle = 10; + +#define NUM_NAILSHOTS 10 + +/* +================ +G_BounceProjectile +================ +*/ +void G_BounceProjectile( vec3_t start, vec3_t impact, vec3_t dir, vec3_t endout ) { + vec3_t v, newv; + float dot; + + VectorSubtract( impact, start, v ); + dot = DotProduct( v, dir ); + VectorMA( v, -2*dot, dir, newv ); + + VectorNormalize(newv); + VectorMA(impact, 8192, newv, endout); +} + +/* +====================================================================== + +GAUNTLET + +====================================================================== +*/ + +void Weapon_Gauntlet( gentity_t *ent ) { + +} + +/* +=============== +CheckGauntletAttack +=============== +*/ +qboolean CheckGauntletAttack( gentity_t *ent ) { + trace_t tr; + vec3_t end; + gentity_t *tent; + gentity_t *traceEnt; + int damage; + + // set aiming directions + AngleVectors (ent->client->ps.viewangles, forward, right, up); + + CalcMuzzlePoint ( ent, forward, right, up, muzzle ); + + VectorMA (muzzle, 32, forward, end); + + trap_Trace (&tr, muzzle, NULL, NULL, end, ent->s.number, MASK_SHOT); + if ( tr.surfaceFlags & SURF_NOIMPACT ) { + return qfalse; + } + + traceEnt = &g_entities[ tr.entityNum ]; + + // send blood impact + if ( traceEnt->takedamage && traceEnt->client ) { + tent = G_TempEntity( tr.endpos, EV_MISSILE_HIT ); + tent->s.otherEntityNum = traceEnt->s.number; + tent->s.eventParm = DirToByte( tr.plane.normal ); + tent->s.weapon = ent->s.weapon; + } + + if ( !traceEnt->takedamage) { + return qfalse; + } + + /*if (ent->client->ps.powerups[PW_QUAD] ) { + G_AddEvent( ent, EV_POWERUP_QUAD, 0 ); + s_quadFactor = g_quadfactor.value; + } else*/ { + s_quadFactor = 1; + } + + damage = 50 * s_quadFactor; + G_Damage( traceEnt, ent, ent, forward, tr.endpos, + damage, 0, MOD_GAUNTLET ); + + return qtrue; +} + + +/* +====================================================================== + +MACHINEGUN + +====================================================================== +*/ + +/* +====================== +SnapVectorTowards + +Round a vector to integers for more efficient network +transmission, but make sure that it rounds towards a given point +rather than blindly truncating. This prevents it from truncating +into a wall. +====================== +*/ +void SnapVectorTowards( vec3_t v, vec3_t to ) { + int i; + + for ( i = 0 ; i < 3 ; i++ ) { + if ( to[i] <= v[i] ) { + v[i] = (int)v[i]; + } else { + v[i] = (int)v[i] + 1; + } + } +} + +#define MACHINEGUN_SPREAD 200 +#define MACHINEGUN_DAMAGE 7 +#define MACHINEGUN_TEAM_DAMAGE 5 // wimpier MG in teamplay + +#define CHAINGUN_SPREAD 1200 +#define CHAINGUN_DAMAGE 14 + +void Bullet_Fire (gentity_t *ent, float spread, int damage, int mod ) { + trace_t tr; + vec3_t end; + float r; + float u; + gentity_t *tent; + gentity_t *traceEnt; + int i, passent; + + damage *= s_quadFactor; + + r = random() * M_PI * 2.0f; + u = sin(r) * crandom() * spread * 16; + r = cos(r) * crandom() * spread * 16; + VectorMA (muzzle, 8192*16, forward, end); + VectorMA (end, r, right, end); + VectorMA (end, u, up, end); + + passent = ent->s.number; + for (i = 0; i < 10; i++) { + + trap_Trace (&tr, muzzle, NULL, NULL, end, passent, MASK_SHOT); + if ( tr.surfaceFlags & SURF_NOIMPACT ) { + return; + } + + traceEnt = &g_entities[ tr.entityNum ]; + + // snap the endpos to integers, but nudged towards the line + SnapVectorTowards( tr.endpos, muzzle ); + + // send bullet impact + if ( traceEnt->takedamage && traceEnt->client ) { + tent = G_TempEntity( tr.endpos, EV_BULLET_HIT_FLESH ); + tent->s.eventParm = traceEnt->s.number; + if( LogAccuracyHit( traceEnt, ent ) ) { + ent->client->accuracy_hits++; + } + } else { + tent = G_TempEntity( tr.endpos, EV_BULLET_HIT_WALL ); + tent->s.eventParm = DirToByte( tr.plane.normal ); + } + tent->s.otherEntityNum = ent->s.number; + + if ( traceEnt->takedamage) { + G_Damage( traceEnt, ent, ent, forward, tr.endpos, + damage, 0, MOD_MACHINEGUN); + } + break; + } +} + + +/* +====================================================================== + +BFG + +====================================================================== +*/ + +void BFG_Fire ( gentity_t *ent ) { + gentity_t *m; + + m = fire_bfg (ent, muzzle, forward); + m->damage *= s_quadFactor; + m->splashDamage *= s_quadFactor; + +// VectorAdd( m->s.pos.trDelta, ent->client->ps.velocity, m->s.pos.trDelta ); // "real" physics +} + + +/* +====================================================================== + +SHOTGUN + +====================================================================== +*/ + +// DEFAULT_SHOTGUN_SPREAD and DEFAULT_SHOTGUN_COUNT are in bg_public.h, because +// client predicts same spreads +#define DEFAULT_SHOTGUN_DAMAGE 10 + +qboolean ShotgunPellet( vec3_t start, vec3_t end, gentity_t *ent ) { + trace_t tr; + int damage, i, passent; + gentity_t *traceEnt; + vec3_t tr_start, tr_end; + + passent = ent->s.number; + VectorCopy( start, tr_start ); + VectorCopy( end, tr_end ); + for (i = 0; i < 10; i++) { + trap_Trace (&tr, tr_start, NULL, NULL, tr_end, passent, MASK_SHOT); + traceEnt = &g_entities[ tr.entityNum ]; + + // send bullet impact + if ( tr.surfaceFlags & SURF_NOIMPACT ) { + return qfalse; + } + + if ( traceEnt->takedamage) { + damage = DEFAULT_SHOTGUN_DAMAGE * s_quadFactor; + G_Damage( traceEnt, ent, ent, forward, tr.endpos, damage, 0, MOD_SHOTGUN); + if( LogAccuracyHit( traceEnt, ent ) ) { + return qtrue; + } + } + return qfalse; + } + return qfalse; +} + +// this should match CG_ShotgunPattern +void ShotgunPattern( vec3_t origin, vec3_t origin2, int seed, gentity_t *ent ) { + int i; + float r, u; + vec3_t end; + vec3_t forward, right, up; + int oldScore; + qboolean hitClient = qfalse; + + // 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 ); + + oldScore = ent->client->ps.persistant[PERS_SCORE]; + + // generate the "random" spread pattern + for ( i = 0 ; i < DEFAULT_SHOTGUN_COUNT ; i++ ) { + r = Q_crandom( &seed ) * DEFAULT_SHOTGUN_SPREAD * 16; + u = Q_crandom( &seed ) * DEFAULT_SHOTGUN_SPREAD * 16; + VectorMA( origin, 8192 * 16, forward, end); + VectorMA (end, r, right, end); + VectorMA (end, u, up, end); + if( ShotgunPellet( origin, end, ent ) && !hitClient ) { + hitClient = qtrue; + ent->client->accuracy_hits++; + } + } +} + + +void weapon_supershotgun_fire (gentity_t *ent) { + gentity_t *tent; + + // send shotgun blast + tent = G_TempEntity( muzzle, EV_SHOTGUN ); + VectorScale( forward, 4096, tent->s.origin2 ); + SnapVector( tent->s.origin2 ); + tent->s.eventParm = rand() & 255; // seed for spread pattern + tent->s.otherEntityNum = ent->s.number; + + ShotgunPattern( tent->s.pos.trBase, tent->s.origin2, tent->s.eventParm, ent ); +} + + +/* +====================================================================== + +GRENADE LAUNCHER + +====================================================================== +*/ + +void weapon_grenadelauncher_fire (gentity_t *ent) { + gentity_t *m; + + // extra vertical velocity + forward[2] += 0.2f; + VectorNormalize( forward ); + + m = fire_grenade (ent, muzzle, forward); + m->damage *= s_quadFactor; + m->splashDamage *= s_quadFactor; + +// VectorAdd( m->s.pos.trDelta, ent->client->ps.velocity, m->s.pos.trDelta ); // "real" physics +} + +/* +====================================================================== + +ROCKET + +====================================================================== +*/ + +void Weapon_RocketLauncher_Fire (gentity_t *ent) { + gentity_t *m; + + m = fire_rocket (ent, muzzle, forward); + m->damage *= s_quadFactor; + m->splashDamage *= s_quadFactor; + +// VectorAdd( m->s.pos.trDelta, ent->client->ps.velocity, m->s.pos.trDelta ); // "real" physics +} + + +/* +====================================================================== + +FLAME THROWER + +====================================================================== +*/ + +void Weapon_Flamer_Fire (gentity_t *ent) { + gentity_t *m; + + m = fire_flamer (ent, muzzle, forward); + m->damage *= s_quadFactor; + m->splashDamage *= s_quadFactor; + +// VectorAdd( m->s.pos.trDelta, ent->client->ps.velocity, m->s.pos.trDelta ); // "real" physics +} + +/* +====================================================================== + +RAILGUN + +====================================================================== +*/ + + +/* +================= +weapon_railgun_fire +================= +*/ +#define MAX_RAIL_HITS 4 +void weapon_railgun_fire (gentity_t *ent) { + vec3_t end; + trace_t trace; + gentity_t *tent; + gentity_t *traceEnt; + int damage; + int i; + int hits; + int unlinked; + int passent; + gentity_t *unlinkedEntities[MAX_RAIL_HITS]; + + damage = 100 * s_quadFactor; + + VectorMA (muzzle, 8192, forward, end); + + // trace only against the solids, so the railgun will go through people + unlinked = 0; + hits = 0; + passent = ent->s.number; + do { + trap_Trace (&trace, muzzle, NULL, NULL, end, passent, MASK_SHOT ); + if ( trace.entityNum >= ENTITYNUM_MAX_NORMAL ) { + break; + } + traceEnt = &g_entities[ trace.entityNum ]; + if ( traceEnt->takedamage ) { + if( LogAccuracyHit( traceEnt, ent ) ) { + hits++; + } + G_Damage (traceEnt, ent, ent, forward, trace.endpos, damage, 0, MOD_RAILGUN); + } + if ( trace.contents & CONTENTS_SOLID ) { + break; // we hit something solid enough to stop the beam + } + // unlink this entity, so the next trace will go past it + trap_UnlinkEntity( traceEnt ); + unlinkedEntities[unlinked] = traceEnt; + unlinked++; + } while ( unlinked < MAX_RAIL_HITS ); + + // link back in any entities we unlinked + for ( i = 0 ; i < unlinked ; i++ ) { + trap_LinkEntity( unlinkedEntities[i] ); + } + + // the final trace endpos will be the terminal point of the rail trail + + // snap the endpos to integers to save net bandwidth, but nudged towards the line + SnapVectorTowards( trace.endpos, muzzle ); + + // send railgun beam effect + tent = G_TempEntity( trace.endpos, EV_RAILTRAIL ); + + // set player number for custom colors on the railtrail + tent->s.clientNum = ent->s.clientNum; + + VectorCopy( muzzle, tent->s.origin2 ); + // move origin a bit to come closer to the drawn gun muzzle + VectorMA( tent->s.origin2, 4, right, tent->s.origin2 ); + VectorMA( tent->s.origin2, -1, up, tent->s.origin2 ); + + // no explosion at end if SURF_NOIMPACT, but still make the trail + if ( trace.surfaceFlags & SURF_NOIMPACT ) { + tent->s.eventParm = 255; // don't make the explosion at the end + } else { + tent->s.eventParm = DirToByte( trace.plane.normal ); + } + tent->s.clientNum = ent->s.clientNum; + + // give the shooter a reward sound if they have made two railgun hits in a row + if ( hits == 0 ) { + // complete miss + ent->client->accurateCount = 0; + } else { + // check for "impressive" reward sound + ent->client->accurateCount += hits; + if ( ent->client->accurateCount >= 2 ) { + ent->client->accurateCount -= 2; + ent->client->ps.persistant[PERS_IMPRESSIVE_COUNT]++; + // add the sprite over the player's head + //ent->client->ps.eFlags &= ~(EF_AWARD_IMPRESSIVE | EF_AWARD_EXCELLENT | EF_AWARD_GAUNTLET | EF_AWARD_ASSIST | EF_AWARD_DEFEND | EF_AWARD_CAP ); + ent->client->ps.eFlags |= EF_AWARD_IMPRESSIVE; + ent->client->rewardTime = level.time + REWARD_SPRITE_TIME; + } + ent->client->accuracy_hits++; + } + +} + + +/* +====================================================================== + +GRAPPLING HOOK + +====================================================================== +*/ + +void Weapon_GrapplingHook_Fire (gentity_t *ent) +{ + if (!ent->client->fireHeld && !ent->client->hook) + fire_grapple (ent, muzzle, forward); + + ent->client->fireHeld = qtrue; +} + +void Weapon_HookFree (gentity_t *ent) +{ + ent->parent->client->hook = NULL; + ent->parent->client->ps.pm_flags &= ~PMF_GRAPPLE_PULL; + G_FreeEntity( ent ); +} + +void Weapon_HookThink (gentity_t *ent) +{ + if (ent->enemy) { + vec3_t v, oldorigin; + + VectorCopy(ent->r.currentOrigin, oldorigin); + v[0] = ent->enemy->r.currentOrigin[0] + (ent->enemy->r.mins[0] + ent->enemy->r.maxs[0]) * 0.5; + v[1] = ent->enemy->r.currentOrigin[1] + (ent->enemy->r.mins[1] + ent->enemy->r.maxs[1]) * 0.5; + v[2] = ent->enemy->r.currentOrigin[2] + (ent->enemy->r.mins[2] + ent->enemy->r.maxs[2]) * 0.5; + SnapVectorTowards( v, oldorigin ); // save net bandwidth + + G_SetOrigin( ent, v ); + } + + VectorCopy( ent->r.currentOrigin, ent->parent->client->ps.grapplePoint); +} + +/* +====================================================================== + +LIGHTNING GUN + +====================================================================== +*/ + + +void Weapon_LightningFire( gentity_t *ent ) { + trace_t tr; + vec3_t end; + gentity_t *traceEnt, *tent; + int damage, i, passent; + + damage = 8 * s_quadFactor; + + passent = ent->s.number; + for (i = 0; i < 10; i++) { + VectorMA( muzzle, LIGHTNING_RANGE, forward, end ); + + trap_Trace( &tr, muzzle, NULL, NULL, end, passent, MASK_SHOT ); + + if ( tr.entityNum == ENTITYNUM_NONE ) { + return; + } + + traceEnt = &g_entities[ tr.entityNum ]; + + if ( traceEnt->takedamage) { + G_Damage( traceEnt, ent, ent, forward, tr.endpos, + damage, 0, MOD_LIGHTNING); + } + + if ( traceEnt->takedamage && traceEnt->client ) { + tent = G_TempEntity( tr.endpos, EV_MISSILE_HIT ); + tent->s.otherEntityNum = traceEnt->s.number; + tent->s.eventParm = DirToByte( tr.plane.normal ); + tent->s.weapon = ent->s.weapon; + if( LogAccuracyHit( traceEnt, ent ) ) { + ent->client->accuracy_hits++; + } + } else if ( !( tr.surfaceFlags & SURF_NOIMPACT ) ) { + tent = G_TempEntity( tr.endpos, EV_MISSILE_MISS ); + tent->s.eventParm = DirToByte( tr.plane.normal ); + } + + break; + } +} + + +//====================================================================== + +/* +====================================================================== + +BUILD GUN + +====================================================================== +*/ + + +///////build weapons +/* +=============== +Weapon_Abuild_Fire +=============== +*/ +void Weapon_Abuild_Fire( gentity_t *ent ) +{ + G_AddPredictableEvent( ent, EV_MENU, MN_ABUILD ); +} + +/* +=============== +Weapon_Hbuild_Fire +=============== +*/ +void Weapon_Hbuild_Fire( gentity_t *ent ) +{ + G_AddPredictableEvent( ent, EV_MENU, MN_HBUILD ); +} +///////build weapons + +/* +=============== +Weapon_Venom_Fire +=============== +*/ +void Weapon_Venom_Fire( gentity_t *ent ) { + trace_t tr; + vec3_t end; + gentity_t *tent; + gentity_t *traceEnt; + + // set aiming directions + AngleVectors (ent->client->ps.viewangles, forward, right, up); + + CalcMuzzlePoint ( ent, forward, right, up, muzzle ); + + VectorMA (muzzle, 32, forward, end); + + trap_Trace (&tr, muzzle, NULL, NULL, end, ent->s.number, MASK_SHOT); + if ( tr.surfaceFlags & SURF_NOIMPACT ) { + return; + } + + traceEnt = &g_entities[ tr.entityNum ]; + + // send blood impact + if ( traceEnt->takedamage && traceEnt->client ) { + tent = G_TempEntity( tr.endpos, EV_MISSILE_HIT ); + tent->s.otherEntityNum = traceEnt->s.number; + tent->s.eventParm = DirToByte( tr.plane.normal ); + tent->s.weapon = ent->s.weapon; + } + + if ( traceEnt->takedamage) { + G_Damage( traceEnt, ent, ent, forward, tr.endpos, 50, 0, MOD_VENOM ); + } + +} + +//====================================================================== + +/* +=============== +LogAccuracyHit +=============== +*/ +qboolean LogAccuracyHit( gentity_t *target, gentity_t *attacker ) { + if( !target->takedamage ) { + return qfalse; + } + + if ( target == attacker ) { + return qfalse; + } + + if( !target->client ) { + return qfalse; + } + + if( !attacker->client ) { + return qfalse; + } + + if( target->client->ps.stats[STAT_HEALTH] <= 0 ) { + return qfalse; + } + + if ( OnSameTeam( target, attacker ) ) { + return qfalse; + } + + return qtrue; +} + + +/* +=============== +CalcMuzzlePoint + +set muzzle location relative to pivoting eye +=============== +*/ +void CalcMuzzlePoint ( gentity_t *ent, vec3_t forward, vec3_t right, vec3_t up, vec3_t muzzlePoint ) { + VectorCopy( ent->s.pos.trBase, muzzlePoint ); + muzzlePoint[2] += ent->client->ps.viewheight; + VectorMA( muzzlePoint, 8, forward, muzzlePoint ); + + //TA: move the muzzle a bit with BFG + if( ent->s.weapon == WP_BFG ) + { + VectorMA( muzzlePoint, hmuzzle, right, muzzlePoint ); + hmuzzle = -hmuzzle; + } + + // snap to integer coordinates for more efficient network bandwidth usage + SnapVector( muzzlePoint ); +} + +/* +=============== +CalcMuzzlePointOrigin + +set muzzle location relative to pivoting eye +=============== +*/ +void CalcMuzzlePointOrigin ( gentity_t *ent, vec3_t origin, vec3_t forward, vec3_t right, vec3_t up, vec3_t muzzlePoint ) { + VectorCopy( ent->s.pos.trBase, muzzlePoint ); + muzzlePoint[2] += ent->client->ps.viewheight; + VectorMA( muzzlePoint, 14, forward, muzzlePoint ); + // snap to integer coordinates for more efficient network bandwidth usage + SnapVector( muzzlePoint ); +} + + +/* +=============== +FireWeapon +=============== +*/ +void FireWeapon( gentity_t *ent ) { + /*if (ent->client->ps.powerups[PW_QUAD] ) { + s_quadFactor = g_quadfactor.value; + } else*/ { + s_quadFactor = 1; + } + + // track shots taken for accuracy tracking. Grapple is not a weapon and gauntet is just not tracked + if( ent->s.weapon != WP_GRAPPLING_HOOK && ent->s.weapon != WP_GAUNTLET ) { + ent->client->accuracy_shots++; + } + + // set aiming directions + AngleVectors (ent->client->ps.viewangles, forward, right, up); + + CalcMuzzlePointOrigin ( ent, ent->client->oldOrigin, forward, right, up, muzzle ); + + // fire the specific weapon + switch( ent->s.weapon ) { + case WP_GAUNTLET: + Weapon_Gauntlet( ent ); + break; + case WP_LIGHTNING: + Weapon_LightningFire( ent ); + break; + case WP_SHOTGUN: + weapon_supershotgun_fire( ent ); + break; + case WP_MACHINEGUN: + if ( g_gametype.integer != GT_TEAM ) { + Bullet_Fire( ent, MACHINEGUN_SPREAD, MACHINEGUN_DAMAGE, MOD_MACHINEGUN ); + } else { + Bullet_Fire( ent, MACHINEGUN_SPREAD, MACHINEGUN_TEAM_DAMAGE, MOD_MACHINEGUN ); + } + break; + case WP_CHAINGUN: + Bullet_Fire( ent, CHAINGUN_SPREAD, CHAINGUN_DAMAGE, MOD_CHAINGUN ); + break; + case WP_GRENADE_LAUNCHER: + weapon_grenadelauncher_fire( ent ); + break; + case WP_ROCKET_LAUNCHER: + Weapon_RocketLauncher_Fire( ent ); + break; + case WP_FLAMER: + Weapon_Flamer_Fire( ent ); + break; + case WP_RAILGUN: + weapon_railgun_fire( ent ); + break; + case WP_BFG: + BFG_Fire( ent ); + break; + case WP_GRAPPLING_HOOK: + Weapon_GrapplingHook_Fire( ent ); + break; + case WP_VENOM: + Weapon_Venom_Fire( ent ); + break; + case WP_ABUILD: + Weapon_Abuild_Fire( ent ); + break; + case WP_HBUILD: + Weapon_Hbuild_Fire( ent ); + break; + case WP_SCANNER: //scanner doesn't "fire" + default: +// FIXME G_Error( "Bad ent->s.weapon" ); + break; + } +} + diff --git a/src/game/surfaceflags.h b/src/game/surfaceflags.h new file mode 100644 index 00000000..a6356d10 --- /dev/null +++ b/src/game/surfaceflags.h @@ -0,0 +1,81 @@ +// Copyright (C) 1999-2000 Id Software, Inc. +// +// This file must be identical in the quake and utils directories + +/* + * Portions Copyright (C) 2000-2001 Tim Angus + * + * This program 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, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ + +/* To assertain which portions are licensed under the GPL and which are + * licensed by Id Software, Inc. please run a diff between the equivalent + * versions of the "Tremulous" modification and the unmodified "Quake3" + * game source code. + */ + +// contents flags are seperate bits +// a given brush can contribute multiple content bits + +// these definitions also need to be in q_shared.h! + +#define CONTENTS_SOLID 1 // an eye is never valid in a solid +#define CONTENTS_LAVA 8 +#define CONTENTS_SLIME 16 +#define CONTENTS_WATER 32 +#define CONTENTS_FOG 64 + +#define CONTENTS_AREAPORTAL 0x8000 + +#define CONTENTS_PLAYERCLIP 0x10000 +#define CONTENTS_MONSTERCLIP 0x20000 +//bot specific contents types +#define CONTENTS_TELEPORTER 0x40000 +#define CONTENTS_JUMPPAD 0x80000 +#define CONTENTS_CLUSTERPORTAL 0x100000 +#define CONTENTS_DONOTENTER 0x200000 +#define CONTENTS_BOTCLIP 0x400000 +#define CONTENTS_MOVER 0x800000 + +#define CONTENTS_ORIGIN 0x1000000 // removed before bsping an entity + +#define CONTENTS_BODY 0x2000000 // should never be on a brush, only in game +#define CONTENTS_CORPSE 0x4000000 +#define CONTENTS_DETAIL 0x8000000 // brushes not used for the bsp +#define CONTENTS_STRUCTURAL 0x10000000 // brushes used for the bsp +#define CONTENTS_TRANSLUCENT 0x20000000 // don't consume surface fragments inside +#define CONTENTS_TRIGGER 0x40000000 +#define CONTENTS_NODROP 0x80000000 // don't leave bodies or items (death fog, lava) + +#define SURF_NODAMAGE 0x1 // never give falling damage +#define SURF_SLICK 0x2 // effects game physics +#define SURF_SKY 0x4 // lighting from environment map +#define SURF_LADDER 0x8 +#define SURF_NOIMPACT 0x10 // don't make missile explosions +#define SURF_NOMARKS 0x20 // don't leave missile marks +#define SURF_FLESH 0x40 // make flesh sounds and effects +#define SURF_NODRAW 0x80 // don't generate a drawsurface at all +#define SURF_HINT 0x100 // make a primary bsp splitter +#define SURF_SKIP 0x200 // completely ignore, allowing non-closed brushes +#define SURF_NOLIGHTMAP 0x400 // surface doesn't need a lightmap +#define SURF_POINTLIGHT 0x800 // generate lighting info at vertexes +#define SURF_METALSTEPS 0x1000 // clanking footsteps +#define SURF_NOSTEPS 0x2000 // no footstep sounds +#define SURF_NONSOLID 0x4000 // don't collide against curves with this set +#define SURF_LIGHTFILTER 0x8000 // act as a light filter during q3map -light +#define SURF_ALPHASHADOW 0x10000 // do per-pixel light shadow casting in q3map +#define SURF_NODLIGHT 0x20000 // don't dlight even if solid (solid lava, skies) +#define SURF_DUST 0x40000 // leave a dust trail when walking on this surface + diff --git a/src/ui/ui_local.h b/src/ui/ui_local.h new file mode 100644 index 00000000..f42f3336 --- /dev/null +++ b/src/ui/ui_local.h @@ -0,0 +1,1111 @@ +// Copyright (C) 1999-2000 Id Software, Inc. +// +#ifndef __UI_LOCAL_H__ +#define __UI_LOCAL_H__ + +#include "../game/q_shared.h" +#include "../cgame/tr_types.h" +#include "ui_public.h" +#include "keycodes.h" +#include "../game/bg_public.h" +#include "ui_shared.h" + +// global display context + +extern vmCvar_t ui_ffa_fraglimit; +extern vmCvar_t ui_ffa_timelimit; + +extern vmCvar_t ui_tourney_fraglimit; +extern vmCvar_t ui_tourney_timelimit; + +extern vmCvar_t ui_team_fraglimit; +extern vmCvar_t ui_team_timelimit; +extern vmCvar_t ui_team_friendly; + +extern vmCvar_t ui_ctf_capturelimit; +extern vmCvar_t ui_ctf_timelimit; +extern vmCvar_t ui_ctf_friendly; + +extern vmCvar_t ui_arenasFile; +extern vmCvar_t ui_botsFile; +extern vmCvar_t ui_spScores1; +extern vmCvar_t ui_spScores2; +extern vmCvar_t ui_spScores3; +extern vmCvar_t ui_spScores4; +extern vmCvar_t ui_spScores5; +extern vmCvar_t ui_spAwards; +extern vmCvar_t ui_spVideos; +extern vmCvar_t ui_spSkill; + +extern vmCvar_t ui_spSelection; + +extern vmCvar_t ui_browserMaster; +extern vmCvar_t ui_browserGameType; +extern vmCvar_t ui_browserSortKey; +extern vmCvar_t ui_browserShowFull; +extern vmCvar_t ui_browserShowEmpty; + +extern vmCvar_t ui_brassTime; +extern vmCvar_t ui_drawCrosshair; +extern vmCvar_t ui_drawCrosshairNames; +extern vmCvar_t ui_marks; + +extern vmCvar_t ui_server1; +extern vmCvar_t ui_server2; +extern vmCvar_t ui_server3; +extern vmCvar_t ui_server4; +extern vmCvar_t ui_server5; +extern vmCvar_t ui_server6; +extern vmCvar_t ui_server7; +extern vmCvar_t ui_server8; +extern vmCvar_t ui_server9; +extern vmCvar_t ui_server10; +extern vmCvar_t ui_server11; +extern vmCvar_t ui_server12; +extern vmCvar_t ui_server13; +extern vmCvar_t ui_server14; +extern vmCvar_t ui_server15; +extern vmCvar_t ui_server16; + +extern vmCvar_t ui_cdkey; +extern vmCvar_t ui_cdkeychecked; + +extern vmCvar_t ui_captureLimit; +extern vmCvar_t ui_fragLimit; +extern vmCvar_t ui_gameType; +extern vmCvar_t ui_netGameType; +extern vmCvar_t ui_actualNetGameType; +extern vmCvar_t ui_joinGameType; +extern vmCvar_t ui_netSource; +extern vmCvar_t ui_serverFilterType; +extern vmCvar_t ui_dedicated; +extern vmCvar_t ui_opponentName; +extern vmCvar_t ui_menuFiles; +extern vmCvar_t ui_currentTier; +extern vmCvar_t ui_currentMap; +extern vmCvar_t ui_currentNetMap; +extern vmCvar_t ui_mapIndex; +extern vmCvar_t ui_currentOpponent; +extern vmCvar_t ui_selectedPlayer; +extern vmCvar_t ui_selectedPlayerName; +extern vmCvar_t ui_lastServerRefresh_0; +extern vmCvar_t ui_lastServerRefresh_1; +extern vmCvar_t ui_lastServerRefresh_2; +extern vmCvar_t ui_lastServerRefresh_3; +extern vmCvar_t ui_singlePlayerActive; +extern vmCvar_t ui_scoreAccuracy; +extern vmCvar_t ui_scoreImpressives; +extern vmCvar_t ui_scoreExcellents; +extern vmCvar_t ui_scoreDefends; +extern vmCvar_t ui_scoreAssists; +extern vmCvar_t ui_scoreGauntlets; +extern vmCvar_t ui_scoreScore; +extern vmCvar_t ui_scorePerfect; +extern vmCvar_t ui_scoreTeam; +extern vmCvar_t ui_scoreBase; +extern vmCvar_t ui_scoreTimeBonus; +extern vmCvar_t ui_scoreSkillBonus; +extern vmCvar_t ui_scoreShutoutBonus; +extern vmCvar_t ui_scoreTime; +extern vmCvar_t ui_smallFont; +extern vmCvar_t ui_bigFont; +extern vmCvar_t ui_serverStatusTimeOut; + + + +// +// ui_qmenu.c +// + +#define RCOLUMN_OFFSET ( BIGCHAR_WIDTH ) +#define LCOLUMN_OFFSET (-BIGCHAR_WIDTH ) + +#define SLIDER_RANGE 10 +#define MAX_EDIT_LINE 256 + +#define MAX_MENUDEPTH 8 +#define MAX_MENUITEMS 96 + +#define MTYPE_NULL 0 +#define MTYPE_SLIDER 1 +#define MTYPE_ACTION 2 +#define MTYPE_SPINCONTROL 3 +#define MTYPE_FIELD 4 +#define MTYPE_RADIOBUTTON 5 +#define MTYPE_BITMAP 6 +#define MTYPE_TEXT 7 +#define MTYPE_SCROLLLIST 8 +#define MTYPE_PTEXT 9 +#define MTYPE_BTEXT 10 + +#define QMF_BLINK 0x00000001 +#define QMF_SMALLFONT 0x00000002 +#define QMF_LEFT_JUSTIFY 0x00000004 +#define QMF_CENTER_JUSTIFY 0x00000008 +#define QMF_RIGHT_JUSTIFY 0x00000010 +#define QMF_NUMBERSONLY 0x00000020 // edit field is only numbers +#define QMF_HIGHLIGHT 0x00000040 +#define QMF_HIGHLIGHT_IF_FOCUS 0x00000080 // steady focus +#define QMF_PULSEIFFOCUS 0x00000100 // pulse if focus +#define QMF_HASMOUSEFOCUS 0x00000200 +#define QMF_NOONOFFTEXT 0x00000400 +#define QMF_MOUSEONLY 0x00000800 // only mouse input allowed +#define QMF_HIDDEN 0x00001000 // skips drawing +#define QMF_GRAYED 0x00002000 // grays and disables +#define QMF_INACTIVE 0x00004000 // disables any input +#define QMF_NODEFAULTINIT 0x00008000 // skip default initialization +#define QMF_OWNERDRAW 0x00010000 +#define QMF_PULSE 0x00020000 +#define QMF_LOWERCASE 0x00040000 // edit field is all lower case +#define QMF_UPPERCASE 0x00080000 // edit field is all upper case +#define QMF_SILENT 0x00100000 + +// callback notifications +#define QM_GOTFOCUS 1 +#define QM_LOSTFOCUS 2 +#define QM_ACTIVATED 3 + +typedef struct _tag_menuframework +{ + int cursor; + int cursor_prev; + + int nitems; + void *items[MAX_MENUITEMS]; + + void (*draw) (void); + sfxHandle_t (*key) (int key); + + qboolean wrapAround; + qboolean fullscreen; + qboolean showlogo; +} menuframework_s; + +typedef struct +{ + int type; + const char *name; + int id; + int x, y; + int left; + int top; + int right; + int bottom; + menuframework_s *parent; + int menuPosition; + unsigned flags; + + void (*callback)( void *self, int event ); + void (*statusbar)( void *self ); + void (*ownerdraw)( void *self ); +} menucommon_s; + +typedef struct { + int cursor; + int scroll; + int widthInChars; + char buffer[MAX_EDIT_LINE]; + int maxchars; +} mfield_t; + +typedef struct +{ + menucommon_s generic; + mfield_t field; +} menufield_s; + +typedef struct +{ + menucommon_s generic; + + float minvalue; + float maxvalue; + float curvalue; + + float range; +} menuslider_s; + +typedef struct +{ + menucommon_s generic; + + int oldvalue; + int curvalue; + int numitems; + int top; + + const char **itemnames; + + int width; + int height; + int columns; + int seperation; +} menulist_s; + +typedef struct +{ + menucommon_s generic; +} menuaction_s; + +typedef struct +{ + menucommon_s generic; + int curvalue; +} menuradiobutton_s; + +typedef struct +{ + menucommon_s generic; + char* focuspic; + char* errorpic; + qhandle_t shader; + qhandle_t focusshader; + int width; + int height; + float* focuscolor; +} menubitmap_s; + +typedef struct +{ + menucommon_s generic; + char* string; + int style; + float* color; +} menutext_s; + +extern void Menu_Cache( void ); +extern void Menu_Focus( menucommon_s *m ); +extern void Menu_AddItem( menuframework_s *menu, void *item ); +extern void Menu_AdjustCursor( menuframework_s *menu, int dir ); +extern void Menu_Draw( menuframework_s *menu ); +extern void *Menu_ItemAtCursor( menuframework_s *m ); +extern sfxHandle_t Menu_ActivateItem( menuframework_s *s, menucommon_s* item ); +extern void Menu_SetCursor( menuframework_s *s, int cursor ); +extern void Menu_SetCursorToItem( menuframework_s *m, void* ptr ); +extern sfxHandle_t Menu_DefaultKey( menuframework_s *s, int key ); +extern void Bitmap_Init( menubitmap_s *b ); +extern void Bitmap_Draw( menubitmap_s *b ); +extern void ScrollList_Draw( menulist_s *l ); +extern sfxHandle_t ScrollList_Key( menulist_s *l, int key ); +extern sfxHandle_t menu_in_sound; +extern sfxHandle_t menu_move_sound; +extern sfxHandle_t menu_out_sound; +extern sfxHandle_t menu_buzz_sound; +extern sfxHandle_t menu_null_sound; +extern sfxHandle_t weaponChangeSound; +extern vec4_t menu_text_color; +extern vec4_t menu_grayed_color; +extern vec4_t menu_dark_color; +extern vec4_t menu_highlight_color; +extern vec4_t menu_red_color; +extern vec4_t menu_black_color; +extern vec4_t menu_dim_color; +extern vec4_t color_black; +extern vec4_t color_white; +extern vec4_t color_yellow; +extern vec4_t color_blue; +extern vec4_t color_orange; +extern vec4_t color_red; +extern vec4_t color_dim; +extern vec4_t name_color; +extern vec4_t list_color; +extern vec4_t listbar_color; +extern vec4_t text_color_disabled; +extern vec4_t text_color_normal; +extern vec4_t text_color_highlight; + +extern char *ui_medalNames[]; +extern char *ui_medalPicNames[]; +extern char *ui_medalSounds[]; + +// +// ui_mfield.c +// +extern void MField_Clear( mfield_t *edit ); +extern void MField_KeyDownEvent( mfield_t *edit, int key ); +extern void MField_CharEvent( mfield_t *edit, int ch ); +extern void MField_Draw( mfield_t *edit, int x, int y, int style, vec4_t color ); +extern void MenuField_Init( menufield_s* m ); +extern void MenuField_Draw( menufield_s *f ); +extern sfxHandle_t MenuField_Key( menufield_s* m, int* key ); + +// +// ui_main.c +// +void UI_Report(); +void UI_Load(); +void UI_LoadMenus(const char *menuFile, qboolean reset); +void _UI_SetActiveMenu( uiMenuCommand_t menu ); +int UI_AdjustTimeByGame(int time); +void UI_ShowPostGame(qboolean newHigh); +void UI_ClearScores(); +void UI_LoadArenas(void); + +// +// ui_menu.c +// +extern void MainMenu_Cache( void ); +extern void UI_MainMenu(void); +extern void UI_RegisterCvars( void ); +extern void UI_UpdateCvars( void ); + +// +// ui_credits.c +// +extern void UI_CreditMenu( void ); + +// +// ui_ingame.c +// +extern void InGame_Cache( void ); +extern void UI_InGameMenu(void); + +// +// ui_confirm.c +// +extern void ConfirmMenu_Cache( void ); +extern void UI_ConfirmMenu( const char *question, void (*draw)( void ), void (*action)( qboolean result ) ); + +// +// ui_setup.c +// +extern void UI_SetupMenu_Cache( void ); +extern void UI_SetupMenu(void); + +// +// ui_team.c +// +extern void UI_TeamMainMenu( void ); +extern void TeamMain_Cache( void ); + +// +// ui_connect.c +// +extern void UI_DrawConnectScreen( qboolean overlay ); + +// +// ui_controls2.c +// +extern void UI_ControlsMenu( void ); +extern void Controls_Cache( void ); + +// +// ui_demo2.c +// +extern void UI_DemosMenu( void ); +extern void Demos_Cache( void ); + +// +// ui_cinematics.c +// +extern void UI_CinematicsMenu( void ); +extern void UI_CinematicsMenu_f( void ); +extern void UI_CinematicsMenu_Cache( void ); + +// +// ui_mods.c +// +extern void UI_ModsMenu( void ); +extern void UI_ModsMenu_Cache( void ); + +// +// ui_cdkey.c +// +extern void UI_CDKeyMenu( void ); +extern void UI_CDKeyMenu_Cache( void ); +extern void UI_CDKeyMenu_f( void ); + +// +// ui_playermodel.c +// +extern void UI_PlayerModelMenu( void ); +extern void PlayerModel_Cache( void ); + +// +// ui_playersettings.c +// +extern void UI_PlayerSettingsMenu( void ); +extern void PlayerSettings_Cache( void ); + +// +// ui_preferences.c +// +extern void UI_PreferencesMenu( void ); +extern void Preferences_Cache( void ); + +// +// ui_specifyleague.c +// +extern void UI_SpecifyLeagueMenu( void ); +extern void SpecifyLeague_Cache( void ); + +// +// ui_specifyserver.c +// +extern void UI_SpecifyServerMenu( void ); +extern void SpecifyServer_Cache( void ); + +// +// ui_servers2.c +// +#define MAX_FAVORITESERVERS 16 + +extern void UI_ArenaServersMenu( void ); +extern void ArenaServers_Cache( void ); + +// +// ui_startserver.c +// +extern void UI_StartServerMenu( qboolean multiplayer ); +extern void StartServer_Cache( void ); +extern void ServerOptions_Cache( void ); +extern void UI_BotSelectMenu( char *bot ); +extern void UI_BotSelectMenu_Cache( void ); + +// +// ui_serverinfo.c +// +extern void UI_ServerInfoMenu( void ); +extern void ServerInfo_Cache( void ); + +// +// ui_video.c +// +extern void UI_GraphicsOptionsMenu( void ); +extern void GraphicsOptions_Cache( void ); +extern void DriverInfo_Cache( void ); + +// +// ui_players.c +// + +//FIXME ripped from cg_local.h +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; + +typedef struct { + // model info + qhandle_t legsModel; + qhandle_t legsSkin; + lerpFrame_t legs; + + qhandle_t torsoModel; + qhandle_t torsoSkin; + lerpFrame_t torso; + + qhandle_t headModel; + qhandle_t headSkin; + + animation_t animations[MAX_TOTALANIMATIONS]; + + qhandle_t weaponModel; + qhandle_t barrelModel; + qhandle_t flashModel; + vec3_t flashDlightColor; + int muzzleFlashTime; + + // currently in use drawing parms + vec3_t viewAngles; + vec3_t moveAngles; + weapon_t currentWeapon; + int legsAnim; + int torsoAnim; + + // animation vars + weapon_t weapon; + weapon_t lastWeapon; + weapon_t pendingWeapon; + int weaponTimer; + int pendingLegsAnim; + int torsoAnimationTimer; + + int pendingTorsoAnim; + int legsAnimationTimer; + + qboolean chat; + qboolean newModel; + + qboolean barrelSpinning; + float barrelAngle; + int barrelTime; + + int realWeapon; +} playerInfo_t; + +void UI_DrawPlayer( float x, float y, float w, float h, playerInfo_t *pi, int time ); +void UI_PlayerInfo_SetModel( playerInfo_t *pi, const char *model, const char *headmodel, char *teamName ); +void UI_PlayerInfo_SetInfo( playerInfo_t *pi, int legsAnim, int torsoAnim, vec3_t viewAngles, vec3_t moveAngles, weapon_t weaponNum, qboolean chat ); +qboolean UI_RegisterClientModelname( playerInfo_t *pi, const char *modelSkinName , const char *headName, const char *teamName); + +// +// ui_atoms.c +// +// this is only used in the old ui, the new ui has it's own version +typedef struct { + int frametime; + int realtime; + int cursorx; + int cursory; + glconfig_t glconfig; + qboolean debug; + qhandle_t whiteShader; + qhandle_t menuBackShader; + qhandle_t menuBackShader2; + qhandle_t menuBackNoLogoShader; + qhandle_t charset; + qhandle_t charsetProp; + qhandle_t charsetPropGlow; + qhandle_t charsetPropB; + qhandle_t cursor; + qhandle_t rb_on; + qhandle_t rb_off; + float scale; + float bias; + qboolean demoversion; + qboolean firstdraw; +} uiStatic_t; + + +// new ui stuff +#define UI_NUMFX 7 +#define MAX_HEADS 64 +#define MAX_ALIASES 64 +#define MAX_HEADNAME 32 +#define MAX_TEAMS 64 +#define MAX_GAMETYPES 16 +#define MAX_MAPS 128 +#define MAX_SPMAPS 16 +#define PLAYERS_PER_TEAM 5 +#define MAX_PINGREQUESTS 16 +#define MAX_ADDRESSLENGTH 64 +#define MAX_HOSTNAMELENGTH 22 +#define MAX_MAPNAMELENGTH 16 +#define MAX_STATUSLENGTH 64 +#define MAX_LISTBOXWIDTH 59 +#define UI_FONT_THRESHOLD 0.1 +#define MAX_DISPLAY_SERVERS 2048 +#define MAX_SERVERSTATUS_LINES 128 +#define MAX_SERVERSTATUS_TEXT 1024 +#define MAX_FOUNDPLAYER_SERVERS 16 +#define TEAM_MEMBERS 5 +#define GAMES_ALL 0 +#define GAMES_FFA 1 +#define GAMES_TEAMPLAY 2 +#define GAMES_TOURNEY 3 +#define GAMES_CTF 4 +#define MAPS_PER_TIER 3 +#define MAX_TIERS 16 +#define MAX_MODS 64 +#define MAX_DEMOS 256 +#define MAX_MOVIES 256 +#define MAX_PLAYERMODELS 256 + + +typedef struct { + const char *name; + const char *imageName; + qhandle_t headImage; + qboolean female; +} characterInfo; + +typedef struct { + const char *name; + const char *ai; + const char *action; +} aliasInfo; + +typedef struct { + const char *teamName; + const char *imageName; + const char *teamMembers[TEAM_MEMBERS]; + qhandle_t teamIcon; + qhandle_t teamIcon_Metal; + qhandle_t teamIcon_Name; + int cinematic; +} teamInfo; + +typedef struct { + const char *gameType; + int gtEnum; +} gameTypeInfo; + +typedef struct { + const char *mapName; + const char *mapLoadName; + const char *imageName; + const char *opponentName; + int teamMembers; + int typeBits; + int cinematic; + int timeToBeat[MAX_GAMETYPES]; + qhandle_t levelShot; + qboolean active; +} mapInfo; + +typedef struct { + const char *tierName; + const char *maps[MAPS_PER_TIER]; + int gameTypes[MAPS_PER_TIER]; + qhandle_t mapHandles[MAPS_PER_TIER]; +} tierInfo; + +typedef struct serverFilter_s { + const char *description; + const char *basedir; +} serverFilter_t; + +typedef struct { + char adrstr[MAX_ADDRESSLENGTH]; + int start; +} pinglist_t; + + +typedef struct serverStatus_s { + pinglist_t pingList[MAX_PINGREQUESTS]; + int numqueriedservers; + int currentping; + int nextpingtime; + int maxservers; + int refreshtime; + int numServers; + int sortKey; + int sortDir; + int lastCount; + qboolean refreshActive; + int currentServer; + int displayServers[MAX_DISPLAY_SERVERS]; + int numDisplayServers; + int numPlayersOnServers; + int nextDisplayRefresh; + int nextSortTime; + qhandle_t currentServerPreview; + int currentServerCinematic; + int motdLen; + int motdWidth; + int motdPaintX; + int motdPaintX2; + int motdOffset; + int motdTime; + char motd[MAX_STRING_CHARS]; +} serverStatus_t; + + +typedef struct { + char adrstr[MAX_ADDRESSLENGTH]; + char name[MAX_ADDRESSLENGTH]; + int startTime; + int serverNum; + qboolean valid; +} pendingServer_t; + +typedef struct { + int num; + pendingServer_t server[MAX_SERVERSTATUSREQUESTS]; +} pendingServerStatus_t; + +typedef struct { + char address[MAX_ADDRESSLENGTH]; + char *lines[MAX_SERVERSTATUS_LINES][4]; + char text[MAX_SERVERSTATUS_TEXT]; + char pings[MAX_CLIENTS * 3]; + int numLines; +} serverStatusInfo_t; + +typedef struct { + const char *modName; + const char *modDescr; +} modInfo_t; + + +typedef struct { + displayContextDef_t uiDC; + int newHighScoreTime; + int newBestTime; + int showPostGameTime; + qboolean newHighScore; + qboolean demoAvailable; + qboolean soundHighScore; + + int characterCount; + int botIndex; + characterInfo characterList[MAX_HEADS]; + + int aliasCount; + aliasInfo aliasList[MAX_ALIASES]; + + int teamCount; + teamInfo teamList[MAX_TEAMS]; + + int numGameTypes; + gameTypeInfo gameTypes[MAX_GAMETYPES]; + + int numJoinGameTypes; + gameTypeInfo joinGameTypes[MAX_GAMETYPES]; + + int redBlue; + int playerCount; + int myTeamCount; + int teamIndex; + int playerRefresh; + int playerIndex; + int playerNumber; + qboolean teamLeader; + char playerNames[MAX_CLIENTS][MAX_NAME_LENGTH]; + char teamNames[MAX_CLIENTS][MAX_NAME_LENGTH]; + int teamClientNums[MAX_CLIENTS]; + + int mapCount; + mapInfo mapList[MAX_MAPS]; + + + int tierCount; + tierInfo tierList[MAX_TIERS]; + + int skillIndex; + + modInfo_t modList[MAX_MODS]; + int modCount; + int modIndex; + + const char *demoList[MAX_DEMOS]; + int demoCount; + int demoIndex; + + const char *movieList[MAX_MOVIES]; + int movieCount; + int movieIndex; + int previewMovie; + + serverStatus_t serverStatus; + + // for the showing the status of a server + char serverStatusAddress[MAX_ADDRESSLENGTH]; + serverStatusInfo_t serverStatusInfo; + int nextServerStatusRefresh; + + // to retrieve the status of server to find a player + pendingServerStatus_t pendingServerStatus; + char findPlayerName[MAX_STRING_CHARS]; + char foundPlayerServerAddresses[MAX_FOUNDPLAYER_SERVERS][MAX_ADDRESSLENGTH]; + char foundPlayerServerNames[MAX_FOUNDPLAYER_SERVERS][MAX_ADDRESSLENGTH]; + int currentFoundPlayerServer; + int numFoundPlayerServers; + int nextFindPlayerRefresh; + + int currentCrosshair; + int startPostGameTime; + sfxHandle_t newHighScoreSound; + + int q3HeadCount; + char q3HeadNames[MAX_PLAYERMODELS][64]; + qhandle_t q3HeadIcons[MAX_PLAYERMODELS]; + int q3SelectedHead; + + int effectsColor; + + qboolean inGameLoad; + +} uiInfo_t; + +extern uiInfo_t uiInfo; + + +extern void UI_Init( void ); +extern void UI_Shutdown( void ); +extern void UI_KeyEvent( int key ); +extern void UI_MouseEvent( int dx, int dy ); +extern void UI_Refresh( int realtime ); +extern qboolean UI_ConsoleCommand( int realTime ); +extern float UI_ClampCvar( float min, float max, float value ); +extern void UI_DrawNamedPic( float x, float y, float width, float height, const char *picname ); +extern void UI_DrawHandlePic( float x, float y, float w, float h, qhandle_t hShader ); +extern void UI_FillRect( float x, float y, float width, float height, const float *color ); +extern void UI_DrawRect( float x, float y, float width, float height, const float *color ); +extern void UI_DrawTopBottom(float x, float y, float w, float h); +extern void UI_DrawSides(float x, float y, float w, float h); +extern void UI_UpdateScreen( void ); +extern void UI_SetColor( const float *rgba ); +extern void UI_LerpColor(vec4_t a, vec4_t b, vec4_t c, float t); +extern void UI_DrawBannerString( int x, int y, const char* str, int style, vec4_t color ); +extern float UI_ProportionalSizeScale( int style ); +extern void UI_DrawProportionalString( int x, int y, const char* str, int style, vec4_t color ); +extern int UI_ProportionalStringWidth( const char* str ); +extern void UI_DrawString( int x, int y, const char* str, int style, vec4_t color ); +extern void UI_DrawChar( int x, int y, int ch, int style, vec4_t color ); +extern qboolean UI_CursorInRect (int x, int y, int width, int height); +extern void UI_AdjustFrom640( float *x, float *y, float *w, float *h ); +extern void UI_DrawTextBox (int x, int y, int width, int lines); +extern qboolean UI_IsFullscreen( void ); +extern void UI_SetActiveMenu( uiMenuCommand_t menu ); +extern void UI_PushMenu ( menuframework_s *menu ); +extern void UI_PopMenu (void); +extern void UI_ForceMenuOff (void); +extern char *UI_Argv( int arg ); +extern char *UI_Cvar_VariableString( const char *var_name ); +extern void UI_Refresh( int time ); +extern void UI_KeyEvent( int key ); +extern void UI_StartDemoLoop( void ); +extern qboolean m_entersound; +void UI_LoadBestScores(const char *map, int game); +extern uiStatic_t uis; + +// +// ui_spLevel.c +// +void UI_SPLevelMenu_Cache( void ); +void UI_SPLevelMenu( void ); +void UI_SPLevelMenu_f( void ); +void UI_SPLevelMenu_ReInit( void ); + +// +// ui_spArena.c +// +void UI_SPArena_Start( const char *arenaInfo ); + +// +// ui_spPostgame.c +// +void UI_SPPostgameMenu_Cache( void ); +void UI_SPPostgameMenu_f( void ); + +// +// ui_spSkill.c +// +void UI_SPSkillMenu( const char *arenaInfo ); +void UI_SPSkillMenu_Cache( void ); + +// +// ui_syscalls.c +// +void trap_Print( const char *string ); +void trap_Error( const char *string ); +int trap_Milliseconds( void ); +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 ); +float trap_Cvar_VariableValue( const char *var_name ); +void trap_Cvar_VariableStringBuffer( const char *var_name, char *buffer, int bufsize ); +void trap_Cvar_SetValue( const char *var_name, float value ); +void trap_Cvar_Reset( const char *name ); +void trap_Cvar_Create( const char *var_name, const char *var_value, int flags ); +void trap_Cvar_InfoStringBuffer( int bit, char *buffer, int bufsize ); +int trap_Argc( void ); +void trap_Argv( int n, char *buffer, int bufferLength ); +void trap_Cmd_ExecuteText( int exec_when, const char *text ); // don't use EXEC_NOW! +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 ); +int trap_FS_GetFileList( const char *path, const char *extension, char *listbuf, int bufsize ); +qhandle_t trap_R_RegisterModel( const char *name ); +qhandle_t trap_R_RegisterSkin( const char *name ); +qhandle_t trap_R_RegisterShaderNoMip( const char *name ); +void trap_R_ClearScene( void ); +void trap_R_AddRefEntityToScene( const refEntity_t *re ); +void trap_R_AddPolyToScene( qhandle_t hShader , int numVerts, const polyVert_t *verts ); +void trap_R_AddLightToScene( const vec3_t org, float intensity, float r, float g, float b ); +void trap_R_RenderScene( const refdef_t *fd ); +void trap_R_SetColor( const float *rgba ); +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 ); +void trap_UpdateScreen( void ); +int trap_CM_LerpTag( orientation_t *tag, clipHandle_t mod, int startFrame, int endFrame, float frac, const char *tagName ); +void trap_S_StartLocalSound( sfxHandle_t sfx, int channelNum ); +sfxHandle_t trap_S_RegisterSound( const char *sample, qboolean compressed ); +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 ); +qboolean trap_Key_IsDown( int keynum ); +qboolean trap_Key_GetOverstrikeMode( void ); +void trap_Key_SetOverstrikeMode( qboolean state ); +void trap_Key_ClearStates( void ); +int trap_Key_GetCatcher( void ); +void trap_Key_SetCatcher( int catcher ); +void trap_GetClipboardData( char *buf, int bufsize ); +void trap_GetClientState( uiClientState_t *state ); +void trap_GetGlconfig( glconfig_t *glconfig ); +int trap_GetConfigString( int index, char* buff, int buffsize ); +int trap_LAN_GetServerCount( int source ); +void trap_LAN_GetServerAddressString( int source, int n, char *buf, int buflen ); +void trap_LAN_GetServerInfo( int source, int n, char *buf, int buflen ); +int trap_LAN_GetServerPing( int source, int n ); +int trap_LAN_GetPingQueueCount( void ); +void trap_LAN_ClearPing( int n ); +void trap_LAN_GetPing( int n, char *buf, int buflen, int *pingtime ); +void trap_LAN_GetPingInfo( int n, char *buf, int buflen ); +void trap_LAN_LoadCachedServers(); +void trap_LAN_SaveCachedServers(); +void trap_LAN_MarkServerVisible(int source, int n, qboolean visible); +int trap_LAN_ServerIsVisible( int source, int n); +qboolean trap_LAN_UpdateVisiblePings( int source ); +int trap_LAN_AddServer(int source, const char *name, const char *addr); +void trap_LAN_RemoveServer(int source, const char *addr); +void trap_LAN_ResetPings(int n); +int trap_LAN_ServerStatus( const char *serverAddress, char *serverStatus, int maxLen ); +int trap_LAN_CompareServers( int source, int sortKey, int sortDir, int s1, int s2 ); +int trap_MemoryRemaining( void ); +void trap_GetCDKey( char *buf, int buflen ); +void trap_SetCDKey( char *buf ); +void trap_R_RegisterFont(const char *pFontname, int pointSize, fontInfo_t *font); +void trap_S_StopBackgroundTrack( void ); +void trap_S_StartBackgroundTrack( const char *intro, const char *loop); +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); +int trap_RealTime(qtime_t *qtime); +void trap_R_RemapShader( const char *oldShader, const char *newShader, const char *timeOffset ); +qboolean trap_VerifyCDKey( const char *key, const char *chksum); + +// +// ui_addbots.c +// +void UI_AddBots_Cache( void ); +void UI_AddBotsMenu( void ); + +// +// ui_removebots.c +// +void UI_RemoveBots_Cache( void ); +void UI_RemoveBotsMenu( void ); + +// +// ui_teamorders.c +// +extern void UI_TeamOrdersMenu( void ); +extern void UI_TeamOrdersMenu_f( void ); +extern void UI_TeamOrdersMenu_Cache( void ); + +// +// ui_loadconfig.c +// +void UI_LoadConfig_Cache( void ); +void UI_LoadConfigMenu( void ); + +// +// ui_saveconfig.c +// +void UI_SaveConfigMenu_Cache( void ); +void UI_SaveConfigMenu( void ); + +// +// ui_display.c +// +void UI_DisplayOptionsMenu_Cache( void ); +void UI_DisplayOptionsMenu( void ); + +// +// ui_sound.c +// +void UI_SoundOptionsMenu_Cache( void ); +void UI_SoundOptionsMenu( void ); + +// +// ui_network.c +// +void UI_NetworkOptionsMenu_Cache( void ); +void UI_NetworkOptionsMenu( void ); + +// +// ui_gameinfo.c +// +typedef enum { + AWARD_ACCURACY, + AWARD_IMPRESSIVE, + AWARD_EXCELLENT, + AWARD_GAUNTLET, + AWARD_FRAGS, + AWARD_PERFECT +} awardType_t; + +const char *UI_GetArenaInfoByNumber( int num ); +const char *UI_GetArenaInfoByMap( const char *map ); +const char *UI_GetSpecialArenaInfo( const char *tag ); +int UI_GetNumArenas( void ); +int UI_GetNumSPArenas( void ); +int UI_GetNumSPTiers( void ); + +char *UI_GetBotInfoByNumber( int num ); +char *UI_GetBotInfoByName( const char *name ); +int UI_GetNumBots( void ); +void UI_LoadBots( void ); +char *UI_GetBotNameByNumber( int num ); + +void UI_GetBestScore( int level, int *score, int *skill ); +void UI_SetBestScore( int level, int score ); +int UI_TierCompleted( int levelWon ); +qboolean UI_ShowTierVideo( int tier ); +qboolean UI_CanShowTierVideo( int tier ); +int UI_GetCurrentGame( void ); +void UI_NewGame( void ); +void UI_LogAwardData( int award, int data ); +int UI_GetAwardLevel( int award ); + +void UI_SPUnlock_f( void ); +void UI_SPUnlockMedals_f( void ); + +void UI_InitGameinfo( void ); + +// +// ui_login.c +// +void Login_Cache( void ); +void UI_LoginMenu( void ); + +// +// ui_signup.c +// +void Signup_Cache( void ); +void UI_SignupMenu( void ); + +// +// ui_rankstatus.c +// +void RankStatus_Cache( void ); +void UI_RankStatusMenu( void ); + + +// new ui + +#define ASSET_BACKGROUND "uiBackground" + +// for tracking sp game info in Team Arena +typedef struct postGameInfo_s { + int score; + int redScore; + int blueScore; + int perfects; + int accuracy; + int impressives; + int excellents; + int defends; + int assists; + int gauntlets; + int captures; + int time; + int timeBonus; + int shutoutBonus; + int skillBonus; + int baseScore; +} postGameInfo_t; + + + +#endif diff --git a/src/ui/ui_public.h b/src/ui/ui_public.h new file mode 100644 index 00000000..f3889603 --- /dev/null +++ b/src/ui/ui_public.h @@ -0,0 +1,168 @@ +// Copyright (C) 1999-2000 Id Software, Inc. +// + +#ifndef __UI_PUBLIC_H__ +#define __UI_PUBLIC_H__ + +#define UI_API_VERSION 6 + +typedef struct { + connstate_t connState; + int connectPacketCount; + int clientNum; + char servername[MAX_STRING_CHARS]; + char updateInfoString[MAX_STRING_CHARS]; + char messageString[MAX_STRING_CHARS]; +} uiClientState_t; + +typedef enum { + UI_ERROR, + UI_PRINT, + UI_MILLISECONDS, + UI_CVAR_SET, + UI_CVAR_VARIABLEVALUE, + UI_CVAR_VARIABLESTRINGBUFFER, + UI_CVAR_SETVALUE, + UI_CVAR_RESET, + UI_CVAR_CREATE, + UI_CVAR_INFOSTRINGBUFFER, + UI_ARGC, + UI_ARGV, + UI_CMD_EXECUTETEXT, + UI_FS_FOPENFILE, + UI_FS_READ, + UI_FS_WRITE, + UI_FS_FCLOSEFILE, + UI_FS_GETFILELIST, + UI_R_REGISTERMODEL, + UI_R_REGISTERSKIN, + UI_R_REGISTERSHADERNOMIP, + UI_R_CLEARSCENE, + UI_R_ADDREFENTITYTOSCENE, + UI_R_ADDPOLYTOSCENE, + UI_R_ADDLIGHTTOSCENE, + UI_R_RENDERSCENE, + UI_R_SETCOLOR, + UI_R_DRAWSTRETCHPIC, + UI_UPDATESCREEN, + UI_CM_LERPTAG, + UI_CM_LOADMODEL, + UI_S_REGISTERSOUND, + UI_S_STARTLOCALSOUND, + UI_KEY_KEYNUMTOSTRINGBUF, + UI_KEY_GETBINDINGBUF, + UI_KEY_SETBINDING, + UI_KEY_ISDOWN, + UI_KEY_GETOVERSTRIKEMODE, + UI_KEY_SETOVERSTRIKEMODE, + UI_KEY_CLEARSTATES, + UI_KEY_GETCATCHER, + UI_KEY_SETCATCHER, + UI_GETCLIPBOARDDATA, + UI_GETGLCONFIG, + UI_GETCLIENTSTATE, + UI_GETCONFIGSTRING, + UI_LAN_GETPINGQUEUECOUNT, + UI_LAN_CLEARPING, + UI_LAN_GETPING, + UI_LAN_GETPINGINFO, + UI_CVAR_REGISTER, + UI_CVAR_UPDATE, + UI_MEMORY_REMAINING, + UI_GET_CDKEY, + UI_SET_CDKEY, + UI_R_REGISTERFONT, + UI_R_MODELBOUNDS, + UI_PC_ADD_GLOBAL_DEFINE, + UI_PC_LOAD_SOURCE, + UI_PC_FREE_SOURCE, + UI_PC_READ_TOKEN, + UI_PC_SOURCE_FILE_AND_LINE, + UI_S_STOPBACKGROUNDTRACK, + UI_S_STARTBACKGROUNDTRACK, + UI_REAL_TIME, + UI_LAN_GETSERVERCOUNT, + UI_LAN_GETSERVERADDRESSSTRING, + UI_LAN_GETSERVERINFO, + UI_LAN_MARKSERVERVISIBLE, + UI_LAN_UPDATEVISIBLEPINGS, + UI_LAN_RESETPINGS, + UI_LAN_LOADCACHEDSERVERS, + UI_LAN_SAVECACHEDSERVERS, + UI_LAN_ADDSERVER, + UI_LAN_REMOVESERVER, + UI_CIN_PLAYCINEMATIC, + UI_CIN_STOPCINEMATIC, + UI_CIN_RUNCINEMATIC, + UI_CIN_DRAWCINEMATIC, + UI_CIN_SETEXTENTS, + UI_R_REMAP_SHADER, + UI_VERIFY_CDKEY, + UI_LAN_SERVERSTATUS, + UI_LAN_GETSERVERPING, + UI_LAN_SERVERISVISIBLE, + UI_LAN_COMPARESERVERS, + + UI_MEMSET = 100, + UI_MEMCPY, + UI_STRNCPY, + UI_SIN, + UI_COS, + UI_ATAN2, + UI_SQRT, + UI_FLOOR, + UI_CEIL +} uiImport_t; + +typedef enum { + UIMENU_NONE, + UIMENU_MAIN, + UIMENU_INGAME, + UIMENU_NEED_CD, + UIMENU_BAD_CD_KEY, + UIMENU_TEAM, + UIMENU_POSTGAME +} uiMenuCommand_t; + +#define SORT_HOST 0 +#define SORT_MAP 1 +#define SORT_CLIENTS 2 +#define SORT_GAME 3 +#define SORT_PING 4 + +typedef enum { + UI_GETAPIVERSION = 0, // system reserved + + UI_INIT, +// void UI_Init( void ); + + UI_SHUTDOWN, +// void UI_Shutdown( void ); + + UI_KEY_EVENT, +// void UI_KeyEvent( int key ); + + UI_MOUSE_EVENT, +// void UI_MouseEvent( int dx, int dy ); + + UI_REFRESH, +// void UI_Refresh( int time ); + + UI_IS_FULLSCREEN, +// qboolean UI_IsFullscreen( void ); + + UI_SET_ACTIVE_MENU, +// void UI_SetActiveMenu( uiMenuCommand_t menu ); + + UI_CONSOLE_COMMAND, +// qboolean UI_ConsoleCommand( int realTime ); + + UI_DRAW_CONNECT_SCREEN, +// void UI_DrawConnectScreen( qboolean overlay ); + UI_HASUNIQUECDKEY +// if !overlay, the background will be drawn, otherwise it will be +// overlayed over whatever the cgame has drawn. +// a GetClientState syscall will be made to get the current strings +} uiExport_t; + +#endif |