/* =========================================================================== Copyright (C) 1999-2005 Id Software, Inc. Copyright (C) 2000-2009 Darklegion Development This file is part of Tremulous. Tremulous is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. Tremulous is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Tremulous; if not, write to the Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA =========================================================================== */ // g_maprotation.c -- the map rotation system #include "g_local.h" #define MAX_MAP_ROTATIONS 64 #define MAX_MAP_ROTATION_MAPS 256 #define NOT_ROTATING -1 typedef enum { CV_ERR, CV_RANDOM, CV_NUMCLIENTS, CV_LASTWIN } conditionVariable_t; typedef enum { CO_LT, CO_EQ, CO_GT } conditionOp_t; typedef struct condition_s { struct node_s *target; conditionVariable_t lhs; conditionOp_t op; int numClients; team_t lastWin; } condition_t; typedef struct map_s { char name[ MAX_QPATH ]; char postCommand[ MAX_STRING_CHARS ]; char layouts[ MAX_CVAR_VALUE_STRING ]; } map_t; typedef struct label_s { char name[ MAX_QPATH ]; } label_t; typedef enum { NT_MAP, NT_CONDITION, NT_GOTO, NT_RESUME, NT_LABEL, NT_RETURN } nodeType_t; typedef struct node_s { nodeType_t type; union { map_t map; condition_t condition; label_t label; } u; } node_t; typedef struct mapRotation_s { char name[ MAX_QPATH ]; node_t *nodes[ MAX_MAP_ROTATION_MAPS ]; int numNodes; int currentNode; } mapRotation_t; typedef struct mapRotations_s { mapRotation_t rotations[ MAX_MAP_ROTATIONS ]; int numRotations; } mapRotations_t; static mapRotations_t mapRotations; static int G_NodeIndexAfter( int currentNode, int rotation ); /* =============== G_MapExists Check if a map exists =============== */ qboolean G_MapExists( char *name ) { return trap_FS_FOpenFile( va( "maps/%s.bsp", name ), NULL, FS_READ ); } /* =============== G_RotationExists Check if a rotation exists =============== */ static qboolean G_RotationExists( char *name ) { int i; for( i = 0; i < mapRotations.numRotations; i++ ) { if( Q_strncmp( mapRotations.rotations[ i ].name, name, MAX_QPATH ) == 0 ) return qtrue; } return qfalse; } /* =============== G_LabelExists Check if a label exists in a rotation =============== */ static qboolean G_LabelExists( int rotation, char *name ) { mapRotation_t *mr = &mapRotations.rotations[ rotation ]; int i; for( i = 0; i < mr->numNodes; i++ ) { node_t *node = mr->nodes[ i ]; if( node->type == NT_LABEL && !Q_stricmp( name, node->u.label.name ) ) return qtrue; if( node->type == NT_MAP && !Q_stricmp( name, node->u.map.name ) ) return qtrue; } return qfalse; } /* =============== G_AllocateNode Allocate memory for a node_t =============== */ static node_t *G_AllocateNode( void ) { node_t *node = BG_Alloc( sizeof( node_t ) ); return node; } /* =============== G_ParseMapCommandSection Parse a map rotation command section =============== */ static qboolean G_ParseMapCommandSection( node_t *node, char **text_p ) { char *token; map_t *map = &node->u.map; int commandLength = 0; // read optional parameters while( 1 ) { token = COM_Parse( text_p ); if( !*token ) break; if( !Q_stricmp( token, "" ) ) return qfalse; if( !Q_stricmp( token, "}" ) ) { if( commandLength > 0 ) { // Replace last ; with \n map->postCommand[ commandLength - 1 ] = '\n'; } return qtrue; //reached the end of this command section } if( !Q_stricmp( token, "layouts" ) ) { token = COM_ParseExt( text_p, qfalse ); map->layouts[ 0 ] = '\0'; while( token[ 0 ] != 0 ) { Q_strcat( map->layouts, sizeof( map->layouts ), token ); Q_strcat( map->layouts, sizeof( map->layouts ), " " ); token = COM_ParseExt( text_p, qfalse ); } continue; } // Parse the rest of the line into map->postCommand Q_strcat( map->postCommand, sizeof( map->postCommand ), token ); Q_strcat( map->postCommand, sizeof( map->postCommand ), " " ); token = COM_ParseExt( text_p, qfalse ); while( token[ 0 ] != 0 ) { Q_strcat( map->postCommand, sizeof( map->postCommand ), token ); Q_strcat( map->postCommand, sizeof( map->postCommand ), " " ); token = COM_ParseExt( text_p, qfalse ); } commandLength = strlen( map->postCommand ); map->postCommand[ commandLength - 1 ] = ';'; } return qfalse; } /* =============== G_ParseNode Parse a node =============== */ static qboolean G_ParseNode( node_t **node, char *token, char **text_p, qboolean conditional ) { if( !Q_stricmp( token, "if" ) ) { condition_t *condition; (*node)->type = NT_CONDITION; condition = &(*node)->u.condition; token = COM_Parse( text_p ); if( !*token ) return qfalse; if( !Q_stricmp( token, "numClients" ) ) { condition->lhs = CV_NUMCLIENTS; token = COM_Parse( text_p ); if( !*token ) return qfalse; if( !Q_stricmp( token, "<" ) ) condition->op = CO_LT; else if( !Q_stricmp( token, ">" ) ) condition->op = CO_GT; else if( !Q_stricmp( token, "=" ) ) condition->op = CO_EQ; else { G_Printf( S_COLOR_RED "ERROR: invalid operator in expression: %s\n", token ); return qfalse; } token = COM_Parse( text_p ); if( !*token ) return qfalse; condition->numClients = atoi( token ); } else if( !Q_stricmp( token, "lastWin" ) ) { condition->lhs = CV_LASTWIN; token = COM_Parse( text_p ); if( !*token ) return qfalse; if( !Q_stricmp( token, "aliens" ) ) condition->lastWin = TEAM_ALIENS; else if( !Q_stricmp( token, "humans" ) ) condition->lastWin = TEAM_HUMANS; else { G_Printf( S_COLOR_RED "ERROR: invalid right hand side in expression: %s\n", token ); return qfalse; } } else if( !Q_stricmp( token, "random" ) ) condition->lhs = CV_RANDOM; else { G_Printf( S_COLOR_RED "ERROR: invalid left hand side in expression: %s\n", token ); return qfalse; } token = COM_Parse( text_p ); if( !*token ) return qfalse; condition->target = G_AllocateNode( ); *node = condition->target; return G_ParseNode( node, token, text_p, qtrue ); } else if( !Q_stricmp( token, "return" ) ) { (*node)->type = NT_RETURN; } else if( !Q_stricmp( token, "goto" ) || !Q_stricmp( token, "resume" ) ) { label_t *label; if( !Q_stricmp( token, "goto" ) ) (*node)->type = NT_GOTO; else (*node)->type = NT_RESUME; label = &(*node)->u.label; token = COM_Parse( text_p ); if( !*token ) { G_Printf( S_COLOR_RED "ERROR: goto or resume without label\n" ); return qfalse; } Q_strncpyz( label->name, token, sizeof( label->name ) ); } else if( *token == '#' || conditional ) { label_t *label; (*node)->type = ( conditional ) ? NT_GOTO : NT_LABEL; label = &(*node)->u.label; Q_strncpyz( label->name, token, sizeof( label->name ) ); } else { map_t *map; (*node)->type = NT_MAP; map = &(*node)->u.map; Q_strncpyz( map->name, token, sizeof( map->name ) ); map->postCommand[ 0 ] = '\0'; } return qtrue; } /* =============== G_ParseMapRotation Parse a map rotation section =============== */ static qboolean G_ParseMapRotation( mapRotation_t *mr, char **text_p ) { char *token; node_t *node = NULL; // read optional parameters while( 1 ) { token = COM_Parse( text_p ); if( !*token ) break; if( !Q_stricmp( token, "" ) ) return qfalse; if( !Q_stricmp( token, "{" ) ) { if( node == NULL ) { G_Printf( S_COLOR_RED "ERROR: map command section with no associated map\n" ); return qfalse; } if( !G_ParseMapCommandSection( node, text_p ) ) { G_Printf( S_COLOR_RED "ERROR: failed to parse map command section\n" ); return qfalse; } continue; } else if( !Q_stricmp( token, "}" ) ) { // Reached the end of this map rotation return qtrue; } if( mr->numNodes == MAX_MAP_ROTATION_MAPS ) { G_Printf( S_COLOR_RED "ERROR: maximum number of maps in one rotation (%d) reached\n", MAX_MAP_ROTATION_MAPS ); return qfalse; } node = G_AllocateNode( ); mr->nodes[ mr->numNodes++ ] = node; if( !G_ParseNode( &node, token, text_p, qfalse ) ) return qfalse; } return qfalse; } /* =============== G_ParseMapRotationFile Load the map rotations from a map rotation file =============== */ static qboolean G_ParseMapRotationFile( const char *fileName ) { char *text_p; int i, j; int len; char *token; char text[ 20000 ]; char mrName[ MAX_QPATH ]; qboolean mrNameSet = qfalse; fileHandle_t f; // load the file len = trap_FS_FOpenFile( fileName, &f, FS_READ ); if( len < 0 ) return qfalse; if( len == 0 || len >= sizeof( text ) - 1 ) { trap_FS_FCloseFile( f ); G_Printf( S_COLOR_RED "ERROR: map rotation file %s is %s\n", fileName, len == 0 ? "empty" : "too long" ); return qfalse; } trap_FS_Read( text, len, f ); text[ len ] = 0; trap_FS_FCloseFile( f ); // parse the text text_p = text; // read optional parameters while( 1 ) { token = COM_Parse( &text_p ); if( !*token ) break; if( !Q_stricmp( token, "" ) ) break; if( !Q_stricmp( token, "{" ) ) { if( mrNameSet ) { //check for name space clashes if( G_RotationExists( mrName ) ) { G_Printf( S_COLOR_RED "ERROR: a map rotation is already named %s\n", mrName ); return qfalse; } if( mapRotations.numRotations == MAX_MAP_ROTATIONS ) { G_Printf( S_COLOR_RED "ERROR: maximum number of map rotations (%d) reached\n", MAX_MAP_ROTATIONS ); return qfalse; } Q_strncpyz( mapRotations.rotations[ mapRotations.numRotations ].name, mrName, MAX_QPATH ); if( !G_ParseMapRotation( &mapRotations.rotations[ mapRotations.numRotations ], &text_p ) ) { G_Printf( S_COLOR_RED "ERROR: %s: failed to parse map rotation %s\n", fileName, mrName ); return qfalse; } mapRotations.numRotations++; //start parsing map rotations again mrNameSet = qfalse; continue; } else { G_Printf( S_COLOR_RED "ERROR: unnamed map rotation\n" ); return qfalse; } } if( !mrNameSet ) { Q_strncpyz( mrName, token, sizeof( mrName ) ); mrNameSet = qtrue; } else { G_Printf( S_COLOR_RED "ERROR: map rotation already named\n" ); return qfalse; } } for( i = 0; i < mapRotations.numRotations; i++ ) { mapRotation_t *mr = &mapRotations.rotations[ i ]; int mapCount = 0; for( j = 0; j < mr->numNodes; j++ ) { node_t *node = mr->nodes[ j ]; if( node->type == NT_MAP ) { mapCount++; if( !G_MapExists( node->u.map.name ) ) { G_Printf( S_COLOR_RED "ERROR: rotation map \"%s\" doesn't exist\n", node->u.map.name ); return qfalse; } continue; } else if( node->type == NT_RETURN ) continue; else if( node->type == NT_LABEL ) continue; else while( node->type == NT_CONDITION ) node = node->u.condition.target; if( ( node->type == NT_GOTO || node->type == NT_RESUME ) && !G_LabelExists( i, node->u.label.name ) && !G_RotationExists( node->u.label.name ) ) { G_Printf( S_COLOR_RED "ERROR: goto destination named \"%s\" doesn't exist\n", node->u.label.name ); return qfalse; } } if( mapCount == 0 ) { G_Printf( S_COLOR_RED "ERROR: rotation \"%s\" needs at least one map entry\n", mr->name ); return qfalse; } } return qtrue; } /* =============== G_PrintSpaces =============== */ static void G_PrintSpaces( int spaces ) { int i; for( i = 0; i < spaces; i++ ) G_Printf( " " ); } /* =============== G_PrintRotations Print the parsed map rotations =============== */ void G_PrintRotations( void ) { int i, j; int size = sizeof( mapRotations ); G_Printf( "Map rotations as parsed:\n\n" ); for( i = 0; i < mapRotations.numRotations; i++ ) { mapRotation_t *mr = &mapRotations.rotations[ i ]; G_Printf( "rotation: %s\n{\n", mr->name ); size += mr->numNodes * sizeof( node_t ); for( j = 0; j < mr->numNodes; j++ ) { node_t *node = mr->nodes[ j ]; int indentation = 0; while( node->type == NT_CONDITION ) { G_PrintSpaces( indentation ); G_Printf( " condition\n" ); node = node->u.condition.target; size += sizeof( node_t ); indentation += 2; } G_PrintSpaces( indentation ); switch( node->type ) { case NT_MAP: G_Printf( " %s\n", node->u.map.name ); if( strlen( node->u.map.postCommand ) > 0 ) G_Printf( " command: %s", node->u.map.postCommand ); break; case NT_RETURN: G_Printf( " return\n" ); break; case NT_LABEL: G_Printf( " label: %s\n", node->u.label.name ); break; case NT_GOTO: G_Printf( " goto: %s\n", node->u.label.name ); break; case NT_RESUME: G_Printf( " resume: %s\n", node->u.label.name ); break; default: break; } } G_Printf( "}\n" ); } G_Printf( "Total memory used: %d bytes\n", size ); } /* =============== G_ClearRotationStack Clear the rotation stack =============== */ void G_ClearRotationStack( void ) { trap_Cvar_Set( "g_mapRotationStack", "" ); trap_Cvar_Update( &g_mapRotationStack ); } /* =============== G_PushRotationStack Push the rotation stack =============== */ static void G_PushRotationStack( int rotation ) { char text[ MAX_CVAR_VALUE_STRING ]; Com_sprintf( text, sizeof( text ), "%d %s", rotation, g_mapRotationStack.string ); trap_Cvar_Set( "g_mapRotationStack", text ); trap_Cvar_Update( &g_mapRotationStack ); } /* =============== G_PopRotationStack Pop the rotation stack =============== */ static int G_PopRotationStack( void ) { int value = -1; char text[ MAX_CVAR_VALUE_STRING ]; char *text_p, *token; Q_strncpyz( text, g_mapRotationStack.string, sizeof( text ) ); text_p = text; token = COM_Parse( &text_p ); if( *token ) value = atoi( token ); if( text_p ) { while ( *text_p == ' ' ) text_p++; trap_Cvar_Set( "g_mapRotationStack", text_p ); trap_Cvar_Update( &g_mapRotationStack ); } else G_ClearRotationStack( ); return value; } /* =============== G_RotationNameByIndex Returns the name of a rotation by its index =============== */ static char *G_RotationNameByIndex( int index ) { if( index >= 0 && index < mapRotations.numRotations ) return mapRotations.rotations[ index ].name; return NULL; } /* =============== G_CurrentNodeIndexArray Fill a static array with the current node of each rotation =============== */ static int *G_CurrentNodeIndexArray( void ) { static int currentNode[ MAX_MAP_ROTATIONS ]; int i = 0; char text[ MAX_MAP_ROTATIONS * 2 ]; char *text_p, *token; Q_strncpyz( text, g_mapRotationNodes.string, sizeof( text ) ); text_p = text; while( 1 ) { token = COM_Parse( &text_p ); if( !*token ) break; currentNode[ i++ ] = atoi( token ); } return currentNode; } /* =============== G_SetCurrentNodeByIndex Set the current map in some rotation =============== */ static void G_SetCurrentNodeByIndex( int currentNode, int rotation ) { char text[ MAX_MAP_ROTATIONS * 4 ] = { 0 }; int *p = G_CurrentNodeIndexArray( ); int i; p[ rotation ] = currentNode; for( i = 0; i < mapRotations.numRotations; i++ ) Q_strcat( text, sizeof( text ), va( "%d ", p[ i ] ) ); trap_Cvar_Set( "g_mapRotationNodes", text ); trap_Cvar_Update( &g_mapRotationNodes ); } /* =============== G_CurrentNodeIndex Return the current node index in some rotation =============== */ static int G_CurrentNodeIndex( int rotation ) { int *p = G_CurrentNodeIndexArray( ); return p[ rotation ]; } /* =============== G_NodeByIndex Return a node in a rotation by its index =============== */ static node_t *G_NodeByIndex( int index, int rotation ) { if( rotation >= 0 && rotation < mapRotations.numRotations && index >= 0 && index < mapRotations.rotations[ rotation ].numNodes ) return mapRotations.rotations[ rotation ].nodes[ index ]; return NULL; } /* =============== G_IssueMapChange Send commands to the server to actually change the map =============== */ static void G_IssueMapChange( int index, int rotation ) { node_t *node = mapRotations.rotations[ rotation ].nodes[ index ]; map_t *map = &node->u.map; // allow a manually defined g_layouts setting to override the maprotation if( !g_layouts.string[ 0 ] && map->layouts[ 0 ] ) { trap_Cvar_Set( "g_layouts", map->layouts ); } trap_SendConsoleCommand( EXEC_APPEND, va( "map %s\n", map->name ) ); // Load up map defaults if g_mapConfigs is set G_MapConfigs( map->name ); if( strlen( map->postCommand ) > 0 ) trap_SendConsoleCommand( EXEC_APPEND, map->postCommand ); } /* =============== G_GotoLabel Resolve the label of some condition =============== */ static qboolean G_GotoLabel( int rotation, int nodeIndex, char *name, qboolean reset_index, int depth ) { node_t *node; int i; // Search the rotation names... if( G_StartMapRotation( name, qtrue, qtrue, reset_index, depth ) ) return qtrue; // ...then try labels in the rotation for( i = 0; i < mapRotations.rotations[ rotation ].numNodes; i++ ) { node = mapRotations.rotations[ rotation ].nodes[ i ]; if( node->type == NT_LABEL && !Q_stricmp( node->u.label.name, name ) ) { G_SetCurrentNodeByIndex( G_NodeIndexAfter( i, rotation ), rotation ); G_AdvanceMapRotation( depth ); return qtrue; } } // finally check for a map by name for( i = 0; i < mapRotations.rotations[ rotation ].numNodes; i++ ) { nodeIndex = G_NodeIndexAfter( nodeIndex, rotation ); node = mapRotations.rotations[ rotation ].nodes[ nodeIndex ]; if( node->type == NT_MAP && !Q_stricmp( node->u.map.name, name ) ) { G_SetCurrentNodeByIndex( nodeIndex, rotation ); G_AdvanceMapRotation( depth ); return qtrue; } } return qfalse; } /* =============== G_EvaluateMapCondition Evaluate a map condition =============== */ static qboolean G_EvaluateMapCondition( condition_t **condition ) { qboolean result = qfalse; condition_t *localCondition = *condition; switch( localCondition->lhs ) { case CV_RANDOM: result = rand( ) / ( RAND_MAX / 2 + 1 ); break; case CV_NUMCLIENTS: switch( localCondition->op ) { case CO_LT: result = level.numConnectedClients < localCondition->numClients; break; case CO_GT: result = level.numConnectedClients > localCondition->numClients; break; case CO_EQ: result = level.numConnectedClients == localCondition->numClients; break; } break; case CV_LASTWIN: result = level.lastWin == localCondition->lastWin; break; default: case CV_ERR: G_Printf( S_COLOR_RED "ERROR: malformed map switch localCondition\n" ); break; } if( localCondition->target->type == NT_CONDITION ) { *condition = &localCondition->target->u.condition; return result && G_EvaluateMapCondition( condition ); } return result; } /* =============== G_NodeIndexAfter =============== */ static int G_NodeIndexAfter( int currentNode, int rotation ) { mapRotation_t *mr = &mapRotations.rotations[ rotation ]; return ( currentNode + 1 ) % mr->numNodes; } /* =============== G_StepMapRotation Run one node of a map rotation =============== */ qboolean G_StepMapRotation( int rotation, int nodeIndex, int depth ) { node_t *node; condition_t *condition; int returnRotation; qboolean step = qtrue; node = G_NodeByIndex( nodeIndex, rotation ); depth++; // guard against inifinite loop in conditional code if( depth > 32 && node->type != NT_MAP ) { if( depth > 64 ) { G_Printf( S_COLOR_RED "ERROR: infinite loop protection stopped at map rotation %s\n", G_RotationNameByIndex( rotation ) ); return qfalse; } G_Printf( S_COLOR_YELLOW "WARNING: possible infinite loop in map rotation %s\n", G_RotationNameByIndex( rotation ) ); return qtrue; } while( step ) { step = qfalse; switch( node->type ) { case NT_CONDITION: condition = &node->u.condition; if( G_EvaluateMapCondition( &condition ) ) { node = condition->target; step = qtrue; continue; } break; case NT_RETURN: returnRotation = G_PopRotationStack( ); if( returnRotation >= 0 ) { G_SetCurrentNodeByIndex( G_NodeIndexAfter( nodeIndex, rotation ), rotation ); if( G_StartMapRotation( G_RotationNameByIndex( returnRotation ), qtrue, qfalse, qfalse, depth ) ) { return qfalse; } } break; case NT_MAP: if( G_MapExists( node->u.map.name ) ) { G_SetCurrentNodeByIndex( G_NodeIndexAfter( nodeIndex, rotation ), rotation ); G_IssueMapChange( nodeIndex, rotation ); return qfalse; } G_Printf( S_COLOR_YELLOW "WARNING: skipped missing map %s in rotation %s\n", node->u.map.name, G_RotationNameByIndex( rotation ) ); break; case NT_LABEL: break; case NT_GOTO: case NT_RESUME: G_SetCurrentNodeByIndex( G_NodeIndexAfter( nodeIndex, rotation ), rotation ); if ( G_GotoLabel( rotation, nodeIndex, node->u.label.name, ( node->type == NT_GOTO ), depth ) ) return qfalse; G_Printf( S_COLOR_YELLOW "WARNING: label, map, or rotation %s not found in %s\n", node->u.label.name, G_RotationNameByIndex( rotation ) ); break; } } return qtrue; } /* =============== G_AdvanceMapRotation Increment the current map rotation =============== */ void G_AdvanceMapRotation( int depth ) { node_t *node; int rotation; int nodeIndex; rotation = g_currentMapRotation.integer; if( rotation < 0 || rotation >= MAX_MAP_ROTATIONS ) return; nodeIndex = G_CurrentNodeIndex( rotation ); node = G_NodeByIndex( nodeIndex, rotation ); if( !node ) { G_Printf( S_COLOR_YELLOW "WARNING: index incorrect for map rotation %s, trying 0\n", G_RotationNameByIndex( rotation) ); nodeIndex = 0; node = G_NodeByIndex( nodeIndex, rotation ); } while( node && G_StepMapRotation( rotation, nodeIndex, depth ) ) { nodeIndex = G_NodeIndexAfter( nodeIndex, rotation ); node = G_NodeByIndex( nodeIndex, rotation ); depth++; } if( !node ) G_Printf( S_COLOR_RED "ERROR: unexpected end of maprotation '%s'\n", G_RotationNameByIndex( rotation) ); } /* =============== G_StartMapRotation Switch to a new map rotation =============== */ qboolean G_StartMapRotation( char *name, qboolean advance, qboolean putOnStack, qboolean reset_index, int depth ) { int i; int currentRotation = g_currentMapRotation.integer; for( i = 0; i < mapRotations.numRotations; i++ ) { if( !Q_stricmp( mapRotations.rotations[ i ].name, name ) ) { if( putOnStack && currentRotation >= 0 ) G_PushRotationStack( currentRotation ); trap_Cvar_Set( "g_currentMapRotation", va( "%d", i ) ); trap_Cvar_Update( &g_currentMapRotation ); if( advance ) { if( reset_index ) G_SetCurrentNodeByIndex( 0, i ); G_AdvanceMapRotation( depth ); } break; } } if( i == mapRotations.numRotations ) return qfalse; else return qtrue; } /* =============== G_StopMapRotation Stop the current map rotation =============== */ void G_StopMapRotation( void ) { trap_Cvar_Set( "g_currentMapRotation", va( "%d", NOT_ROTATING ) ); trap_Cvar_Update( &g_currentMapRotation ); } /* =============== G_MapRotationActive Test if any map rotation is currently active =============== */ qboolean G_MapRotationActive( void ) { return ( g_currentMapRotation.integer != NOT_ROTATING ); } /* =============== G_InitMapRotations Load and initialise the map rotations =============== */ void G_InitMapRotations( void ) { const char *fileName = "maprotation.cfg"; // Load the file if it exists if( trap_FS_FOpenFile( fileName, NULL, FS_READ ) ) { if( !G_ParseMapRotationFile( fileName ) ) G_Printf( S_COLOR_RED "ERROR: failed to parse %s file\n", fileName ); } else G_Printf( "%s file not found.\n", fileName ); if( g_currentMapRotation.integer == NOT_ROTATING ) { if( g_initialMapRotation.string[ 0 ] != 0 ) { G_StartMapRotation( g_initialMapRotation.string, qfalse, qtrue, qfalse, 0 ); trap_Cvar_Set( "g_initialMapRotation", "" ); trap_Cvar_Update( &g_initialMapRotation ); } } } /* =============== G_FreeNode Free up memory used by a node =============== */ void G_FreeNode( node_t *node ) { if( node->type == NT_CONDITION ) G_FreeNode( node->u.condition.target ); BG_Free( node ); } /* =============== G_ShutdownMapRotations Free up memory used by map rotations =============== */ void G_ShutdownMapRotations( void ) { int i, j; for( i = 0; i < mapRotations.numRotations; i++ ) { mapRotation_t *mr = &mapRotations.rotations[ i ]; for( j = 0; j < mr->numNodes; j++ ) { node_t *node = mr->nodes[ j ]; G_FreeNode( node ); } } }