1803 lines
42 KiB
C
1803 lines
42 KiB
C
// Copyright (C) 2001-2002 Raven Software.
|
|
//
|
|
|
|
#include "g_local.h"
|
|
|
|
level_locals_t level;
|
|
|
|
typedef struct
|
|
{
|
|
vmCvar_t *vmCvar;
|
|
char *cvarName;
|
|
char *defaultString;
|
|
int cvarFlags;
|
|
float mMinValue, mMaxValue;
|
|
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_scorelimit;
|
|
vmCvar_t g_timelimit;
|
|
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_forcerespawn;
|
|
vmCvar_t g_inactivity;
|
|
vmCvar_t g_debugMove;
|
|
vmCvar_t g_debugDamage;
|
|
vmCvar_t g_debugAlloc;
|
|
vmCvar_t g_weaponRespawn;
|
|
vmCvar_t g_backpackRespawn;
|
|
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_logHits;
|
|
vmCvar_t g_allowVote;
|
|
vmCvar_t g_voteDuration;
|
|
vmCvar_t g_voteKickBanTime;
|
|
vmCvar_t g_failedVoteDelay;
|
|
vmCvar_t g_teamAutoJoin;
|
|
vmCvar_t g_teamForceBalance;
|
|
vmCvar_t g_smoothClients;
|
|
vmCvar_t pmove_fixed;
|
|
vmCvar_t pmove_msec;
|
|
vmCvar_t g_rankings;
|
|
vmCvar_t g_listEntity;
|
|
vmCvar_t g_fps;
|
|
vmCvar_t g_respawnInterval;
|
|
vmCvar_t g_respawnInvulnerability;
|
|
vmCvar_t g_roundtimelimit;
|
|
vmCvar_t g_roundjointime;
|
|
vmCvar_t g_timeextension;
|
|
vmCvar_t g_timeouttospec;
|
|
vmCvar_t g_roundstartdelay;
|
|
vmCvar_t g_availableWeapons;
|
|
vmCvar_t g_forceFollow;
|
|
vmCvar_t g_followEnemy;
|
|
vmCvar_t g_mapcycle;
|
|
vmCvar_t g_pickupsDisabled; // Whether or not pickups are available in a map (uses outfitting if not)
|
|
vmCvar_t g_suicidePenalty; // Amount of score added for killing yourself (typically negative)
|
|
vmCvar_t g_teamkillPenalty; // Amount of score added for killing a teammates (typically negative)
|
|
vmCvar_t g_teamkillDamageMax; // max damage one can do to teammates before being kicked
|
|
vmCvar_t g_teamkillDamageForgive; // amount of teamkill damage forgiven each minute
|
|
vmCvar_t g_teamkillBanTime; // number of minutes to ban someone for after being kicked
|
|
vmCvar_t g_voiceFloodCount; // Number of voice messages in one minute to be concidered flooding
|
|
vmCvar_t g_voiceFloodPenalty; // Amount of time a void flooder must wait before they can use voice again
|
|
vmCvar_t g_suddenDeath;
|
|
vmCvar_t g_voiceTalkingGhosts; // Allow ghosts to talk to alive players
|
|
|
|
vmCvar_t RMG;
|
|
vmCvar_t g_debugRMG;
|
|
|
|
static cvarTable_t gameCvarTable[] =
|
|
{
|
|
// don't override the cheat state set by the system
|
|
{ &g_cheats, "sv_cheats", "", 0, 0.0, 0.0, 0, qfalse },
|
|
|
|
// noset vars
|
|
{ NULL, "gamename", GAMEVERSION , CVAR_SERVERINFO | CVAR_ROM, 0.0, 0.0, 0, qfalse },
|
|
{ NULL, "gamedate", __DATE__ , CVAR_ROM, 0.0, 0.0, 0, qfalse },
|
|
{ &g_restarted, "g_restarted", "0", CVAR_ROM, 0.0, 0.0, 0, qfalse },
|
|
{ NULL, "sv_mapname", "", CVAR_SERVERINFO | CVAR_ROM, 0.0, 0.0, 0, qfalse },
|
|
|
|
{ &g_fps, "sv_fps", "", CVAR_ROM, 0.0, 0.0, 0, qfalse },
|
|
|
|
// latched vars
|
|
{ &g_gametype, "g_gametype", "dm", CVAR_SERVERINFO | CVAR_LATCH, 0.0, 0.0, 0, qfalse },
|
|
|
|
{ &g_maxclients, "sv_maxclients", "8", CVAR_SERVERINFO | CVAR_LATCH | CVAR_ARCHIVE, 0.0, 0.0, 0, qfalse },
|
|
{ &g_maxGameClients, "g_maxGameClients", "0", CVAR_SERVERINFO | CVAR_LATCH | CVAR_ARCHIVE, 0.0, 0.0, 0, qfalse },
|
|
|
|
{ &g_dmflags, "dmflags", "0", CVAR_SERVERINFO | CVAR_ARCHIVE, 0.0, 0.0, 0, qtrue },
|
|
{ &g_scorelimit, "scorelimit", "20", CVAR_SERVERINFO | CVAR_ARCHIVE | CVAR_NORESTART, 0.0, 0.0, 0, qtrue },
|
|
{ &g_timelimit, "timelimit", "0", CVAR_SERVERINFO | CVAR_ARCHIVE | CVAR_NORESTART, 0.0, 0.0, 0, qtrue },
|
|
|
|
{ &g_synchronousClients, "g_synchronousClients", "0", CVAR_SYSTEMINFO, 0.0, 0.0, 0, qfalse },
|
|
|
|
{ &g_friendlyFire, "g_friendlyFire", "0", CVAR_SERVERINFO|CVAR_ARCHIVE, 0.0, 0.0, 0, qtrue },
|
|
|
|
{ &g_teamAutoJoin, "g_teamAutoJoin", "0", CVAR_ARCHIVE, 0.0, 0.0, },
|
|
{ &g_teamForceBalance, "g_teamForceBalance", "0", CVAR_ARCHIVE, 0.0, 0.0, },
|
|
|
|
{ &g_warmup, "g_warmup", "20", CVAR_ARCHIVE, 0.0, 0.0, 0, qtrue },
|
|
{ &g_doWarmup, "g_doWarmup", "0", 0, 0.0, 0.0, 0, qtrue },
|
|
{ &g_log, "g_log", "games.log", CVAR_ARCHIVE, 0.0, 0.0, 0, qfalse },
|
|
{ &g_logSync, "g_logSync", "0", CVAR_ARCHIVE, 0.0, 0.0, 0, qfalse },
|
|
{ &g_logHits, "g_logHits", "0", CVAR_ARCHIVE, 0.0, 0.0, 0, qfalse },
|
|
|
|
{ &g_password, "g_password", "", CVAR_USERINFO, 0.0, 0.0, 0, qfalse },
|
|
|
|
{ &g_needpass, "g_needpass", "0", CVAR_SERVERINFO | CVAR_ROM, 0.0, 0.0, 0, qfalse },
|
|
|
|
{ &g_dedicated, "dedicated", "0", 0, 0.0, 0.0, 0, qfalse },
|
|
|
|
{ &g_speed, "g_speed", "280", 0, 0.0, 0.0, 0, qtrue },
|
|
{ &g_gravity, "g_gravity", "800", 0, 0.0, 0.0, 0, qtrue },
|
|
{ &g_knockback, "g_knockback", "700", 0, 0.0, 0.0, 0, qtrue },
|
|
{ &g_weaponRespawn, "g_weaponrespawn", "15", 0, 0.0, 0.0, 0, qtrue },
|
|
{ &g_backpackRespawn, "g_backpackrespawn", "40", 0, 0.0, 0.0, 0, qtrue },
|
|
{ &g_forcerespawn, "g_forcerespawn", "20", 0, 0.0, 0.0, 0, qtrue },
|
|
{ &g_inactivity, "g_inactivity", "180", CVAR_ARCHIVE, 0.0, 0.0, 0, qtrue },
|
|
{ &g_debugMove, "g_debugMove", "0", 0, 0.0, 0.0, 0, qfalse },
|
|
{ &g_debugDamage, "g_debugDamage", "0", 0, 0.0, 0.0, 0, qfalse },
|
|
{ &g_debugAlloc, "g_debugAlloc", "0", 0, 0.0, 0.0, 0, qfalse },
|
|
{ &g_motd, "g_motd", "", 0, 0.0, 0.0, 0, qfalse },
|
|
|
|
{ &g_allowVote, "g_allowVote", "1", CVAR_ARCHIVE, 0.0, 0.0, 0, qfalse },
|
|
{ &g_voteDuration, "g_voteDuration", "60", CVAR_ARCHIVE, 0.0, 0.0, 0, qfalse },
|
|
{ &g_voteKickBanTime, "g_voteKickBanTime", "0", CVAR_ARCHIVE, 0.0, 0.0, 0, qfalse },
|
|
{ &g_failedVoteDelay, "g_failedVoteDelay", "1", CVAR_ARCHIVE, 0.0, 0.0, 0, qfalse },
|
|
{ &g_listEntity, "g_listEntity", "0", 0, 0.0, 0.0, 0, qfalse },
|
|
|
|
{ &g_smoothClients, "g_smoothClients", "1", 0, 0.0, 0.0, 0, qfalse},
|
|
{ &pmove_fixed, "pmove_fixed", "0", CVAR_SYSTEMINFO, 0.0, 0.0, 0, qfalse},
|
|
{ &pmove_msec, "pmove_msec", "8", CVAR_SYSTEMINFO, 0.0, 0.0, 0, qfalse},
|
|
|
|
{ &g_rankings, "g_rankings", "0", 0, 0.0, 0.0, 0, qfalse},
|
|
|
|
{ &g_respawnInterval, "g_respawnInterval", "15", CVAR_ARCHIVE, 0.0, 0.0, 0, qfalse },
|
|
{ &g_respawnInvulnerability, "g_respawnInvulnerability", "5", CVAR_ARCHIVE, 0.0, 0.0, 0, qfalse },
|
|
|
|
{ &RMG, "RMG", "0", 0, 0.0, 0.0, },
|
|
{ &g_debugRMG, "g_debugRMG", "0", 0, 0.0f, 0.0f },
|
|
|
|
{ &g_timeouttospec, "g_timeouttospec", "15", CVAR_ARCHIVE, 0.0, 0.0, 0, qfalse },
|
|
{ &g_roundtimelimit, "g_roundtimelimit", "5", CVAR_ARCHIVE, 0.0, 0.0, 0, qfalse },
|
|
{ &g_roundjointime, "g_roundjointime", "5", CVAR_ARCHIVE, 0.0, 0.0, 0, qfalse },
|
|
{ &g_timeextension, "g_timeextension", "15", CVAR_ARCHIVE, 0.0, 0.0, 0, qfalse },
|
|
|
|
{ &g_roundstartdelay, "g_roundstartdelay", "5", CVAR_ARCHIVE, 0.0, 0.0, 0, qfalse },
|
|
|
|
{ &g_availableWeapons, "g_availableWeapons", "22222222222211", CVAR_ARCHIVE|CVAR_SERVERINFO|CVAR_LATCH, 0.0, 0.0, 0, qfalse },
|
|
|
|
{ &g_forceFollow, "g_forceFollow", "0", CVAR_ARCHIVE, 0.0, 0.0, 0, qfalse },
|
|
{ &g_followEnemy, "g_followEnemy", "1", CVAR_ARCHIVE, 0.0, 0.0, 0, qfalse },
|
|
|
|
{ &g_mapcycle, "sv_mapcycle", "none", CVAR_ARCHIVE, 0.0, 0.0, 0, qfalse },
|
|
|
|
{ &g_pickupsDisabled, "g_pickupsDisabled", "0", CVAR_ARCHIVE|CVAR_LATCH, 0.0, 0.0, 0, qfalse },
|
|
|
|
{ &g_suicidePenalty, "g_suicidePenalty", "-1", CVAR_ARCHIVE, 0.0f, 0.0f, 0, qfalse },
|
|
|
|
{ &g_voiceFloodCount, "g_voiceFloodCount", "6", CVAR_ARCHIVE, 0.0f, 0.0f, 0, qfalse },
|
|
{ &g_voiceFloodPenalty, "g_voiceFloodPenalty", "60", CVAR_ARCHIVE, 0.0f, 0.0f, 0, qfalse },
|
|
|
|
{ &g_teamkillPenalty, "g_teamkillPenalty", "-1", CVAR_ARCHIVE, 0.0f, 0.0f, 0, qfalse },
|
|
{ &g_teamkillDamageMax, "g_teamkillDamageMax", "300", CVAR_ARCHIVE, 0.0f, 0.0f, 0, qfalse },
|
|
{ &g_teamkillDamageForgive, "g_teamkillDamageForgive", "50", CVAR_ARCHIVE, 0.0f, 0.0f, 0, qfalse },
|
|
{ &g_teamkillBanTime, "g_teamkillBanTime", "5", CVAR_ARCHIVE, 0.0f, 0.0f, 0, qfalse },
|
|
|
|
{ &g_suddenDeath, "g_suddenDeath", "1", CVAR_ARCHIVE, 0.0f, 0.0f, 0, qfalse },
|
|
|
|
{ &g_voiceTalkingGhosts, "g_voiceTalkingGhosts", "1", CVAR_ARCHIVE, 0.0f, 0.0f, 0, qfalse },
|
|
};
|
|
|
|
// bk001129 - made static to avoid aliasing
|
|
static 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 );
|
|
void G_InitGhoul ( void );
|
|
void G_ShutdownGhoul ( 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_GHOUL_INIT:
|
|
G_InitGhoul ( );
|
|
return 0;
|
|
case GAME_GHOUL_SHUTDOWN:
|
|
G_ShutdownGhoul ( );
|
|
return 0;
|
|
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_GAMETYPE_COMMAND:
|
|
return G_GametypeCommand ( arg0, arg1, arg2, arg3, arg4, arg5 );
|
|
case GAME_RUN_FRAME:
|
|
G_RunFrame( arg0 );
|
|
return 0;
|
|
case GAME_CONSOLE_COMMAND:
|
|
return ConsoleCommand();
|
|
case BOTAI_START_FRAME:
|
|
return BotAIStartFrame( arg0 );
|
|
case GAME_SPAWN_RMG_ENTITY:
|
|
if (G_ParseSpawnVars(qfalse))
|
|
{
|
|
G_SpawnGEntityFromSpawnVars(qfalse);
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
return -1;
|
|
}
|
|
|
|
/*
|
|
================
|
|
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;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Com_Printf ("%i teams with %i entities\n", c, c2);
|
|
}
|
|
|
|
/*
|
|
=================
|
|
G_RemapTeamShaders
|
|
=================
|
|
*/
|
|
void G_RemapTeamShaders(void)
|
|
{
|
|
trap_SetConfigstring(CS_SHADERSTATE, BuildShaderStateConfig());
|
|
}
|
|
|
|
/*
|
|
=================
|
|
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, cv->mMinValue, cv->mMaxValue );
|
|
|
|
if ( cv->vmCvar )
|
|
{
|
|
cv->modificationCount = cv->vmCvar->modificationCount;
|
|
}
|
|
|
|
if (cv->teamShader)
|
|
{
|
|
remapped = qtrue;
|
|
}
|
|
}
|
|
|
|
if (remapped)
|
|
{
|
|
G_RemapTeamShaders();
|
|
}
|
|
|
|
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 )
|
|
{
|
|
// Handle any modified cvar checks
|
|
if ( !Q_stricmp ( cv->cvarName, "sv_fps" ) )
|
|
{
|
|
if ( cv->vmCvar->integer > 50 )
|
|
{
|
|
trap_Cvar_Set ( "sv_fps", "50" );
|
|
trap_Cvar_Update ( cv->vmCvar );
|
|
}
|
|
else if ( cv->vmCvar->integer < 10 )
|
|
{
|
|
trap_Cvar_Set ( "sv_fps", "10" );
|
|
trap_Cvar_Update ( cv->vmCvar );
|
|
}
|
|
}
|
|
|
|
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_SetGametype
|
|
|
|
Sets the current gametype to the given value, if the map doesnt support it then it will
|
|
use the first gametype that it does support
|
|
===============
|
|
*/
|
|
void G_SetGametype ( const char* gametype )
|
|
{
|
|
// Make sure the gametype is valid, if not default to deathmatch
|
|
level.gametype = BG_FindGametype ( gametype );
|
|
|
|
// First make sure its supported on this map
|
|
if ( !G_DoesMapSupportGametype ( gametype ) || level.gametype == -1 )
|
|
{
|
|
int i = 0;
|
|
|
|
// Find a gametype it does support
|
|
for ( i = 0; i < bg_gametypeCount; i ++ )
|
|
{
|
|
if ( G_DoesMapSupportGametype ( bg_gametypeData[i].name ) )
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
|
|
// This is bad, this means the map doesnt support any gametypes
|
|
if ( i >= bg_gametypeCount )
|
|
{
|
|
Com_Error ( ERR_FATAL, "map does not support any of the available gametypes" );
|
|
}
|
|
|
|
G_LogPrintf ( "gametype '%s' is not supported on this map and was defaulted to '%s'\n",
|
|
gametype,
|
|
bg_gametypeData[i].name );
|
|
|
|
gametype = bg_gametypeData[i].name;
|
|
trap_Cvar_Set( "g_gametype", gametype );
|
|
trap_Cvar_Set( "RMG_mission", gametype );
|
|
level.gametype = BG_FindGametype ( gametype );
|
|
|
|
trap_Cvar_Update( &g_gametype );
|
|
}
|
|
|
|
level.gametypeData = &bg_gametypeData[level.gametype];
|
|
|
|
// Copy the backpack percentage over
|
|
bg_itemlist[MODELINDEX_BACKPACK].quantity = level.gametypeData->backpack;
|
|
|
|
// Set the pickup state
|
|
if ( RMG.integer || g_pickupsDisabled.integer || level.gametypeData->pickupsDisabled )
|
|
{
|
|
level.pickupsDisabled = qtrue;
|
|
trap_SetConfigstring ( CS_PICKUPSDISABLED, "1" );
|
|
}
|
|
else
|
|
{
|
|
level.pickupsDisabled = qfalse;
|
|
trap_SetConfigstring ( CS_PICKUPSDISABLED, "0" );
|
|
}
|
|
}
|
|
|
|
/*
|
|
============
|
|
G_InitGame
|
|
============
|
|
*/
|
|
void G_InitGame( int levelTime, int randomSeed, int restart )
|
|
{
|
|
int i;
|
|
|
|
Com_Printf ("------- Game Initialization -------\n");
|
|
Com_Printf ("gamename: %s\n", GAMEVERSION);
|
|
Com_Printf ("gamedate: %s\n", __DATE__);
|
|
|
|
srand( randomSeed );
|
|
|
|
// set some level globals
|
|
memset( &level, 0, sizeof( level ) );
|
|
level.time = levelTime;
|
|
level.startTime = levelTime;
|
|
|
|
G_RegisterCvars();
|
|
|
|
|
|
// Load the list of arenas
|
|
G_LoadArenas ( );
|
|
|
|
// Build the gametype list so we can verify the given gametype
|
|
BG_BuildGametypeList ( );
|
|
|
|
// Set the current gametype
|
|
G_SetGametype(g_gametype.string);
|
|
|
|
// Set the available outfitting
|
|
BG_SetAvailableOutfitting ( g_availableWeapons.string );
|
|
|
|
// Give the game a uniqe id
|
|
trap_SetConfigstring ( CS_GAME_ID, va("%d", randomSeed ) );
|
|
|
|
if ( g_log.string[0] )
|
|
{
|
|
if ( g_logSync.integer )
|
|
{
|
|
trap_FS_FOpenFile( g_log.string, &level.logFile, FS_APPEND_SYNC_TEXT );
|
|
}
|
|
else
|
|
{
|
|
trap_FS_FOpenFile( g_log.string, &level.logFile, FS_APPEND_TEXT );
|
|
}
|
|
|
|
if ( !level.logFile )
|
|
{
|
|
Com_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
|
|
{
|
|
Com_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] ) );
|
|
|
|
// Get the boundaries of the world
|
|
trap_GetWorldBounds ( level.worldMins, level.worldMaxs );
|
|
|
|
// reserve some spots for dead player bodies
|
|
G_InitBodyQueue();
|
|
|
|
ClearRegisteredItems();
|
|
|
|
// parse the key/value pairs and spawn gentities
|
|
G_SpawnEntitiesFromString(qfalse);
|
|
|
|
// Now parse the gametype information that we need. This needs to be
|
|
// done after the entity spawn so that the items and triggers can be
|
|
// linked up properly
|
|
G_ParseGametypeFile ( );
|
|
|
|
BG_ParseInviewFile( level.pickupsDisabled );
|
|
|
|
// Load in the identities
|
|
BG_ParseNPCFiles ( );
|
|
|
|
// general initialization
|
|
G_FindTeams();
|
|
|
|
SaveRegisteredItems();
|
|
|
|
Com_Printf ("-----------------------------------\n");
|
|
|
|
if( trap_Cvar_VariableIntegerValue( "com_buildScript" ) )
|
|
{
|
|
G_SoundIndex( "sound/player/gurp1.wav" );
|
|
G_SoundIndex( "sound/player/gurp2.wav" );
|
|
}
|
|
|
|
#ifdef _SOF2_BOTS
|
|
if ( trap_Cvar_VariableIntegerValue( "bot_enable" ) )
|
|
{
|
|
BotAISetup( restart );
|
|
BotAILoadMap( restart );
|
|
G_InitBots( restart );
|
|
}
|
|
#endif
|
|
|
|
G_RemapTeamShaders();
|
|
|
|
// Initialize the gametype
|
|
trap_GT_Init ( g_gametype.string, restart );
|
|
|
|
// Music
|
|
if ( RMG.integer )
|
|
{
|
|
char temp[MAX_INFO_STRING];
|
|
|
|
// start the music
|
|
trap_Cvar_VariableStringBuffer("RMG_music", temp, MAX_QPATH);
|
|
trap_SetConfigstring( CS_MUSIC, temp );
|
|
}
|
|
|
|
trap_SetConfigstring( CS_VOTE_TIME, "" );
|
|
}
|
|
|
|
/*
|
|
=================
|
|
G_ShutdownGame
|
|
=================
|
|
*/
|
|
void G_ShutdownGame( int restart )
|
|
{
|
|
Com_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();
|
|
|
|
#ifdef _SOF2_BOTS
|
|
if ( trap_Cvar_VariableIntegerValue( "bot_enable" ) )
|
|
{
|
|
BotAIShutdown( restart );
|
|
}
|
|
#endif
|
|
}
|
|
|
|
|
|
#ifndef GAME_HARD_LINKED
|
|
|
|
void QDECL Com_Error ( int level, const char *fmt, ... )
|
|
{
|
|
va_list argptr;
|
|
char text[1024];
|
|
|
|
va_start (argptr, fmt);
|
|
vsprintf (text, fmt, argptr);
|
|
va_end (argptr);
|
|
|
|
trap_Error( 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);
|
|
|
|
trap_Printf( text );
|
|
}
|
|
|
|
#endif
|
|
|
|
/*
|
|
=============
|
|
SortRanks
|
|
=============
|
|
*/
|
|
int QDECL SortRanks( const void *a, const void *b )
|
|
{
|
|
gclient_t *ca;
|
|
gclient_t *cb;
|
|
|
|
ca = &level.clients[*(int *)a];
|
|
cb = &level.clients[*(int *)b];
|
|
|
|
// sort special clients last
|
|
if ( ca->sess.spectatorState == SPECTATOR_SCOREBOARD )
|
|
{
|
|
return 1;
|
|
}
|
|
|
|
if ( cb->sess.spectatorState == SPECTATOR_SCOREBOARD )
|
|
{
|
|
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.team == TEAM_SPECTATOR && cb->sess.team == 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.team == TEAM_SPECTATOR )
|
|
{
|
|
return 1;
|
|
}
|
|
|
|
if ( cb->sess.team == TEAM_SPECTATOR )
|
|
{
|
|
return -1;
|
|
}
|
|
|
|
// then sort by score
|
|
if ( ca->sess.score > cb->sess.score )
|
|
{
|
|
return -1;
|
|
}
|
|
|
|
if ( ca->sess.score < cb->sess.score )
|
|
{
|
|
return 1;
|
|
}
|
|
|
|
// then sort by kills
|
|
if ( ca->sess.kills > cb->sess.kills )
|
|
{
|
|
return -1;
|
|
}
|
|
|
|
if ( ca->sess.kills < cb->sess.kills )
|
|
{
|
|
return 1;
|
|
}
|
|
|
|
// then sort by deaths
|
|
if ( ca->sess.deaths > cb->sess.deaths )
|
|
{
|
|
return -1;
|
|
}
|
|
|
|
if ( ca->sess.deaths < cb->sess.deaths )
|
|
{
|
|
return 1;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
/*
|
|
============
|
|
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
|
|
|
|
for ( i = 0 ; i < level.maxclients ; i++ )
|
|
{
|
|
if ( level.clients[i].pers.connected != CON_DISCONNECTED )
|
|
{
|
|
level.sortedClients[level.numConnectedClients] = i;
|
|
level.numConnectedClients++;
|
|
|
|
if ( level.clients[i].sess.team != 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.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 ( level.gametypeData->teams )
|
|
{
|
|
int rank;
|
|
if ( level.teamScores[TEAM_RED] == level.teamScores[TEAM_BLUE] )
|
|
{
|
|
rank = 2;
|
|
}
|
|
else if ( level.teamScores[TEAM_RED] > level.teamScores[TEAM_BLUE] )
|
|
{
|
|
rank = 0;
|
|
}
|
|
else
|
|
{
|
|
rank = 1;
|
|
}
|
|
|
|
// 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] ];
|
|
cl->ps.persistant[PERS_RANK] = rank;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
rank = -1;
|
|
score = 0;
|
|
for ( i = 0; i < level.numPlayingClients; i++ )
|
|
{
|
|
cl = &level.clients[ level.sortedClients[i] ];
|
|
newScore = cl->sess.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;
|
|
}
|
|
}
|
|
|
|
|
|
// 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 )
|
|
{
|
|
G_StopFollowing( ent );
|
|
}
|
|
|
|
FindIntermissionPoint ( );
|
|
|
|
// move to the spot
|
|
VectorCopy( level.intermission_origin, ent->s.origin );
|
|
VectorCopy( level.intermission_origin, ent->r.currentOrigin );
|
|
VectorCopy( level.intermission_origin, ent->client->ps.pvsOrigin );
|
|
VectorCopy( level.intermission_origin, ent->client->ps.origin );
|
|
VectorCopy (level.intermission_angle, ent->client->ps.viewangles);
|
|
|
|
// Reset some client variables
|
|
ent->client->ps.pm_type = PM_INTERMISSION;
|
|
ent->client->ps.stats[STAT_GAMETYPE_ITEMS] = 0;
|
|
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 )
|
|
{
|
|
gspawn_t* spawn = G_SelectRandomSpawnPoint ( -1 );
|
|
if ( spawn )
|
|
{
|
|
VectorCopy (spawn->origin, level.intermission_origin);
|
|
VectorCopy (spawn->angles, level.intermission_angle);
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
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 *ent;
|
|
|
|
if ( level.intermissiontime )
|
|
{
|
|
// already active
|
|
return;
|
|
}
|
|
|
|
// Kill any votes
|
|
level.voteTime = 0;
|
|
trap_SetConfigstring( CS_VOTE_TIME, "" );
|
|
|
|
level.intermissiontime = level.time;
|
|
level.gametypeResetTime = level.time;
|
|
FindIntermissionPoint();
|
|
|
|
// move all clients to the intermission point
|
|
for (i=0 ; i< level.maxclients ; i++)
|
|
{
|
|
ent = g_entities + i;
|
|
if (!ent->inuse)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
// take out of follow mode
|
|
G_StopFollowing( ent );
|
|
|
|
// Get rid of ghost state
|
|
G_StopGhosting ( ent );
|
|
|
|
// respawn if dead
|
|
if ( G_IsClientDead ( ent->client ) )
|
|
{
|
|
respawn ( ent );
|
|
}
|
|
|
|
MoveClientToIntermission( ent );
|
|
}
|
|
|
|
// 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;
|
|
|
|
// Next map
|
|
trap_SendConsoleCommand( EXEC_APPEND, "mapcycle\n" );
|
|
level.changemap = NULL;
|
|
level.intermissiontime = 0;
|
|
|
|
// reset all the scores so we don't enter the intermission again
|
|
level.teamScores[TEAM_RED] = 0;
|
|
level.teamScores[TEAM_BLUE] = 0;
|
|
for ( i=0 ; i< g_maxclients.integer ; i++ )
|
|
{
|
|
cl = level.clients + i;
|
|
if ( cl->pers.connected != CON_CONNECTED )
|
|
{
|
|
continue;
|
|
}
|
|
|
|
cl->sess.score = 0;
|
|
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), "%4i:%i%i ", min, tens, sec );
|
|
|
|
va_start( argptr, fmt );
|
|
vsprintf( string +8 , fmt,argptr );
|
|
va_end( argptr );
|
|
|
|
#ifndef _DEBUG
|
|
if ( g_dedicated.integer ) {
|
|
#endif
|
|
Com_Printf( "%s", string + 8 );
|
|
#ifndef _DEBUG
|
|
}
|
|
#endif
|
|
|
|
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;
|
|
int numSorted;
|
|
gclient_t *cl;
|
|
|
|
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 ( level.gametypeData->teams )
|
|
{
|
|
G_LogPrintf( "red:%i blue:%i\n", level.teamScores[TEAM_RED], level.teamScores[TEAM_BLUE] );
|
|
}
|
|
|
|
for (i=0 ; i < numSorted ; i++)
|
|
{
|
|
int ping;
|
|
|
|
cl = &level.clients[level.sortedClients[i]];
|
|
|
|
if ( cl->sess.team == 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->sess.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;
|
|
|
|
// 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;
|
|
int b;
|
|
|
|
if ( level.numPlayingClients < 2 )
|
|
{
|
|
return qfalse;
|
|
}
|
|
|
|
if ( level.gametypeData->teams )
|
|
{
|
|
return level.teamScores[TEAM_RED] == level.teamScores[TEAM_BLUE];
|
|
}
|
|
|
|
a = level.clients[level.sortedClients[0]].sess.score;
|
|
b = level.clients[level.sortedClients[1]].sess.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 )
|
|
{
|
|
int time = INTERMISSION_DELAY_TIME;
|
|
if ( level.time - level.intermissionQueued >= time )
|
|
{
|
|
level.intermissionQueued = 0;
|
|
BeginIntermission();
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
// check for sudden death
|
|
if ( g_suddenDeath.integer && ScoreIsTied() )
|
|
{
|
|
// always wait for sudden death
|
|
return;
|
|
}
|
|
|
|
// Check to see if the timelimit was hit
|
|
if ( g_timelimit.integer && !level.warmupTime )
|
|
{
|
|
if ( level.time - level.startTime >= (g_timelimit.integer + level.timeExtension)*60000 )
|
|
{
|
|
gentity_t* tent;
|
|
tent = G_TempEntity( vec3_origin, EV_GAME_OVER );
|
|
tent->s.eventParm = GAME_OVER_TIMELIMIT;
|
|
tent->r.svFlags = SVF_BROADCAST;
|
|
|
|
LogExit( "Timelimit hit." );
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Check to see if the score was hit
|
|
if ( g_scorelimit.integer )
|
|
{
|
|
if ( level.gametypeData->teams )
|
|
{
|
|
if ( level.teamScores[TEAM_RED] >= g_scorelimit.integer )
|
|
{
|
|
gentity_t* tent;
|
|
tent = G_TempEntity( vec3_origin, EV_GAME_OVER );
|
|
tent->s.eventParm = GAME_OVER_SCORELIMIT;
|
|
tent->r.svFlags = SVF_BROADCAST;
|
|
tent->s.otherEntityNum = TEAM_RED;
|
|
|
|
LogExit( "Red team hit the score limit." );
|
|
return;
|
|
}
|
|
|
|
if ( level.teamScores[TEAM_BLUE] >= g_scorelimit.integer )
|
|
{
|
|
gentity_t* tent;
|
|
tent = G_TempEntity( vec3_origin, EV_GAME_OVER );
|
|
tent->s.eventParm = GAME_OVER_SCORELIMIT;
|
|
tent->r.svFlags = SVF_BROADCAST;
|
|
tent->s.otherEntityNum = TEAM_BLUE;
|
|
|
|
LogExit( "Blue team hit the score limit." );
|
|
return;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Check to see if any of the clients scores have crossed the scorelimit
|
|
for ( i = 0 ; i < level.numConnectedClients ; i++ )
|
|
{
|
|
cl = g_entities[level.sortedClients[i]].client;
|
|
|
|
if ( cl->pers.connected != CON_CONNECTED )
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if ( cl->sess.team != TEAM_FREE )
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if ( cl->sess.score >= g_scorelimit.integer )
|
|
{
|
|
gentity_t* tent;
|
|
tent = G_TempEntity( vec3_origin, EV_GAME_OVER );
|
|
tent->s.eventParm = GAME_OVER_SCORELIMIT;
|
|
tent->r.svFlags = SVF_BROADCAST;
|
|
tent->s.otherEntityNum = level.sortedClients[i];
|
|
|
|
LogExit( "Scorelimit hit." );
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/*
|
|
=============
|
|
CheckWarmup
|
|
=============
|
|
*/
|
|
void CheckWarmup ( void )
|
|
{
|
|
int counts[TEAM_NUM_TEAMS];
|
|
qboolean notEnough = qfalse;
|
|
|
|
// check because we run 3 game frames before calling Connect and/or ClientBegin
|
|
// for clients on a map_restart
|
|
if ( level.numPlayingClients == 0 )
|
|
{
|
|
return;
|
|
}
|
|
|
|
if ( !level.warmupTime )
|
|
{
|
|
return;
|
|
}
|
|
|
|
if ( level.gametypeData->teams )
|
|
{
|
|
counts[TEAM_BLUE] = TeamCount( -1, TEAM_BLUE, NULL );
|
|
counts[TEAM_RED] = TeamCount( -1, TEAM_RED, NULL );
|
|
|
|
if (counts[TEAM_RED] < 1 || counts[TEAM_BLUE] < 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;
|
|
}
|
|
|
|
// Update the needed clients
|
|
trap_SetConfigstring ( CS_VOTE_NEEDED, va("%i", (level.numVotingClients / 2) + 1 ) );
|
|
|
|
if ( level.time - level.voteTime >= g_voteDuration.integer*1000 )
|
|
{
|
|
trap_SendServerCommand( -1, "print \"Vote failed.\n\"" );
|
|
level.clients[level.voteClient].voteDelayTime = level.time + g_failedVoteDelay.integer * 60000;
|
|
}
|
|
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\"" );
|
|
level.clients[level.voteClient].voteDelayTime = level.time + g_failedVoteDelay.integer * 60000;
|
|
}
|
|
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.team != team)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
trap_SendServerCommand( i, message );
|
|
}
|
|
}
|
|
|
|
/*
|
|
==================
|
|
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)
|
|
{
|
|
Com_Error ( ERR_FATAL, "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;
|
|
|
|
//NT - store the time the frame started
|
|
level.frameStartTime = trap_Milliseconds();
|
|
|
|
// 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
|
|
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;
|
|
// predicted events should never be set to zero
|
|
//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->physicsObject )
|
|
{
|
|
G_RunItem( ent );
|
|
continue;
|
|
}
|
|
|
|
if ( ent->s.eType == ET_MOVER )
|
|
{
|
|
G_RunMover( ent );
|
|
continue;
|
|
}
|
|
|
|
if ( i < MAX_CLIENTS )
|
|
{
|
|
G_CheckClientTimeouts ( ent );
|
|
G_RunClient( ent );
|
|
G_CheckClientTeamkill ( ent );
|
|
continue;
|
|
}
|
|
|
|
G_RunThink( ent );
|
|
}
|
|
|
|
// perform final fixups on the players
|
|
ent = &g_entities[0];
|
|
for (i=0 ; i < level.maxclients ; i++, ent++ )
|
|
{
|
|
if ( ent->inuse )
|
|
{
|
|
ClientEndFrame( ent );
|
|
}
|
|
}
|
|
|
|
// Check warmup rules
|
|
CheckWarmup();
|
|
|
|
// see if it is time to end the level
|
|
CheckExitRules();
|
|
|
|
// Update gametype stuff
|
|
CheckGametype ();
|
|
|
|
trap_GT_RunFrame ( level.time );
|
|
|
|
// cancel vote if timed out
|
|
CheckVote();
|
|
|
|
// for tracking changes
|
|
CheckCvars();
|
|
|
|
if (g_listEntity.integer)
|
|
{
|
|
for (i = 0; i < MAX_GENTITIES; i++)
|
|
{
|
|
Com_Printf("%4i: %s\n", i, g_entities[i].classname);
|
|
}
|
|
trap_Cvar_Set("g_listEntity", "0");
|
|
}
|
|
}
|
|
|
|
void G_InitGhoul ( void )
|
|
{
|
|
G_InitHitModels ( );
|
|
}
|
|
|
|
void G_ShutdownGhoul ( void )
|
|
{
|
|
if ( !level.serverGhoul2 )
|
|
{
|
|
return;
|
|
}
|
|
|
|
trap_G2API_CleanGhoul2Models ( &level.serverGhoul2 );
|
|
level.serverGhoul2 = NULL;
|
|
}
|
|
|