// 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_teamkillNoExcuseTime; // number of seconds into a round where tk damage is inexcusable vmCvar_t g_teamkillNoExcuseMultiplier; // multipier for tk damage that is inexcusable 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", "0", CVAR_SERVERINFO|CVAR_ROM|CVAR_LATCH, 0.0, 0.0, 0, qfalse }, { NULL, "disable_weapon_knife", "0", CVAR_ARCHIVE|CVAR_LATCH, 0.0, 0.0, 0, qfalse }, { NULL, "disable_pickup_weapon_US_SOCOM", "0", CVAR_ARCHIVE|CVAR_LATCH, 0.0, 0.0, 0, qfalse }, { NULL, "disable_pickup_weapon_M19", "0", CVAR_ARCHIVE|CVAR_LATCH, 0.0, 0.0, 0, qfalse }, { NULL, "disable_pickup_weapon_silvertalon", "0", CVAR_ARCHIVE|CVAR_LATCH, 0.0, 0.0, 0, qfalse }, { NULL, "disable_pickup_weapon_microuzi", "0", CVAR_ARCHIVE|CVAR_LATCH, 0.0, 0.0, 0, qfalse }, { NULL, "disable_pickup_weapon_M3A1", "0", CVAR_ARCHIVE|CVAR_LATCH, 0.0, 0.0, 0, qfalse }, { NULL, "disable_pickup_weapon_MP5", "0", CVAR_ARCHIVE|CVAR_LATCH, 0.0, 0.0, 0, qfalse }, { NULL, "disable_pickup_weapon_USAS_12", "0", CVAR_ARCHIVE|CVAR_LATCH, 0.0, 0.0, 0, qfalse }, { NULL, "disable_pickup_weapon_M590", "0", CVAR_ARCHIVE|CVAR_LATCH, 0.0, 0.0, 0, qfalse }, { NULL, "disable_pickup_weapon_MSG90A1", "0", CVAR_ARCHIVE|CVAR_LATCH, 0.0, 0.0, 0, qfalse }, { NULL, "disable_pickup_weapon_M4", "0", CVAR_ARCHIVE|CVAR_LATCH, 0.0, 0.0, 0, qfalse }, { NULL, "disable_pickup_weapon_AK_74", "0", CVAR_ARCHIVE|CVAR_LATCH, 0.0, 0.0, 0, qfalse }, { NULL, "disable_pickup_weapon_SIG551", "0", CVAR_ARCHIVE|CVAR_LATCH, 0.0, 0.0, 0, qfalse }, { NULL, "disable_pickup_weapon_M60", "0", CVAR_ARCHIVE|CVAR_LATCH, 0.0, 0.0, 0, qfalse }, { NULL, "disable_pickup_weapon_RPG_7", "2", CVAR_ARCHIVE|CVAR_LATCH, 0.0, 0.0, 0, qfalse }, { NULL, "disable_pickup_weapon_MM_1", "2", CVAR_ARCHIVE|CVAR_LATCH, 0.0, 0.0, 0, qfalse }, { NULL, "disable_pickup_weapon_M84", "0", CVAR_ARCHIVE|CVAR_LATCH, 0.0, 0.0, 0, qfalse }, { NULL, "disable_pickup_weapon_SMOHG92", "0", CVAR_ARCHIVE|CVAR_LATCH, 0.0, 0.0, 0, qfalse }, { NULL, "disable_pickup_weapon_AN_M14", "0", CVAR_ARCHIVE|CVAR_LATCH, 0.0, 0.0, 0, qfalse }, { NULL, "disable_pickup_weapon_M15", "0", CVAR_ARCHIVE|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_teamkillNoExcuseTime, "g_teamkillNoExcuseTime", "8", CVAR_ARCHIVE, 0.0f, 0.0f, 0, qfalse }, { &g_teamkillNoExcuseMultiplier, "g_teamkillNoExcuseMultiplier", "3", 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_UpdateAvailableWeapons Updates the g_availableWeapons cvar using the disable cvars. =============== */ void G_UpdateAvailableWeapons ( void ) { weapon_t weapon; char available[WP_NUM_WEAPONS+1]; memset ( available, 0, sizeof(available) ); for ( weapon = WP_KNIFE; weapon < WP_NUM_WEAPONS; weapon ++ ) { gitem_t* item = BG_FindWeaponItem ( weapon ); if ( !item ) { continue; } switch ( (int)trap_Cvar_VariableValue ( va("disable_%s", item->classname ) ) ) { case 0: available[weapon-1] = '2'; break; case 1: available[weapon-1] = '0'; break; case 2: available[weapon-1] = '1'; break; } } trap_Cvar_Set ( "g_availableWeapons", available ); } /* =============== 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); // Sets the available weapons cvar from the disable_ cvars. G_UpdateAvailableWeapons ( ); // 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 ; isess.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.gametypeData->respawnType != RT_NONE || level.gametypeResetTime ) { 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" ); G_ResetGametype ( qtrue ); // 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 ; iinuse ) { 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; }