//============ Copyright (c) Valve Corporation, All rights reserved. ============ // // Definitions for the rule- and state-based level generation system. // //=============================================================================== #include "MapLayout.h" #include "Room.h" #include "tilegen_class_factories.h" #include "tilegen_mission_preprocessor.h" #include "tilegen_listeners.h" #include "tilegen_ranges.h" #include "tilegen_layout_system.h" #include "asw_npcs.h" ConVar tilegen_break_on_iteration( "tilegen_break_on_iteration", "-1", FCVAR_CHEAT, "If set to a non-negative value, tilegen will break at the start of iteration #N if a debugger is attached." ); DEFINE_LOGGING_CHANNEL_NO_TAGS( LOG_TilegenLayoutSystem, "TilegenLayoutSystem", 0, LS_MESSAGE, Color( 192, 255, 192, 255 ) ); void AddListeners( CLayoutSystem *pLayoutSystem ) { // @TODO: need a better mechanism to detect which of these listeners are required. For now, add them all. pLayoutSystem->AddListener( new CTilegenListener_NumTilesPlaced() ); } CTilegenState *CTilegenStateList::FindState( const char *pStateName ) { for ( int i = 0; i < m_States.Count(); ++ i ) { if ( Q_stricmp( pStateName, m_States[i]->GetStateName() ) == 0 ) { return m_States[i]; } CTilegenState *pState = m_States[i]->GetStateList()->FindState( pStateName ); if ( pState != NULL ) { return pState; } } return NULL; } CTilegenState *CTilegenStateList::GetNextState( CTilegenState *pState ) { int i; for ( i = 0; i < m_States.Count(); ++ i ) { if ( m_States[i] == pState ) break; } if ( i < ( m_States.Count() - 1 ) ) { return m_States[i + 1]; } else if ( i == m_States.Count() ) { // This should never happen unless there's a bug in the layout code Log_Warning( LOG_TilegenLayoutSystem, "State %s not found in state list.\n", pState->GetStateName() ); m_pLayoutSystem->OnError(); return NULL; } else if ( i == ( m_States.Count() - 1 ) ) { // We're on the last state CTilegenStateList *pParentStateList = GetParentStateList(); if ( pParentStateList == NULL ) { // No more states left in the layout system. return NULL; } else { Assert( pState->GetParentState() != NULL ); return pParentStateList->GetNextState( pState->GetParentState() ); } } UNREACHABLE(); return NULL; } CTilegenStateList *CTilegenStateList::GetParentStateList() { if ( m_pOwnerState == NULL ) { // No parent state implies that this state list is owned by the layout system, so there // is no parent state list. return NULL; } else if ( m_pOwnerState->GetParentState() == NULL ) { // A parent state with a NULL parent state implies that this list is owned by a top-level // state, so the parent state list belongs to the layout system. return m_pLayoutSystem->GetStateList(); } else { return m_pOwnerState->GetParentState()->GetStateList(); } } void CTilegenStateList::OnBeginGeneration() { for ( int i = 0; i < m_States.Count(); ++ i ) { m_States[i]->OnBeginGeneration( m_pLayoutSystem ); } } CTilegenState::~CTilegenState() { for ( int i = 0; i < m_Actions.Count(); ++ i ) { delete m_Actions[i].m_pAction; delete m_Actions[i].m_pCondition; } } bool CTilegenState::LoadFromKeyValues( KeyValues *pKeyValues ) { const char *pStateName = pKeyValues->GetString( "name", NULL ); if ( pStateName == NULL ) { Log_Warning( LOG_TilegenLayoutSystem, "No state name found in State key values.\n" ); return false; } m_StateName[0] = '\0'; // @TODO: support nested state names? // if ( pParentStateName != NULL ) // { // Q_strncat( m_StateName, pParentStateName, MAX_TILEGEN_IDENTIFIER_LENGTH ); // Q_strncat( m_StateName, ".", MAX_TILEGEN_IDENTIFIER_LENGTH ); // } Q_strncat( m_StateName, pStateName, MAX_TILEGEN_IDENTIFIER_LENGTH ); for ( KeyValues *pSubKey = pKeyValues->GetFirstSubKey(); pSubKey != NULL; pSubKey = pSubKey->GetNextKey() ) { if ( Q_stricmp( pSubKey->GetName(), "action" ) == 0 ) { if ( !ParseAction( pSubKey ) ) { return false; } } else if ( Q_stricmp( pSubKey->GetName(), "state") == 0 ) { // Nested state CTilegenState *pNewState = new CTilegenState( GetLayoutSystem(), this ); if ( !pNewState->LoadFromKeyValues( pSubKey ) ) { delete pNewState; return false; } m_ChildStates.AddState( pNewState ); } } return true; } void CTilegenState::ExecuteIteration( CLayoutSystem *pLayoutSystem ) { if ( m_Actions.Count() == 0 ) { // No actions in this state, go to the first nested state if one exists // or move on to the next sibling state. if ( m_ChildStates.GetStateCount() > 0 ) { Log_Msg( LOG_TilegenLayoutSystem, "No actions found in state %s, transitioning to first child state.\n", GetStateName() ); pLayoutSystem->TransitionToState( m_ChildStates.GetState( 0 ) ); } else { // Try to switch to the next sibling state. CTilegenState *pNextState = m_ChildStates.GetParentStateList()->GetNextState( this ); if ( pNextState != NULL ) { Log_Msg( LOG_TilegenLayoutSystem, "No actions or child states found in state %s, transitioning to next state.\n", GetStateName() ); pLayoutSystem->TransitionToState( pNextState ); } else { // This must be the last state in the entire layout system. Log_Msg( LOG_TilegenLayoutSystem, "No more states to which to transition." ); pLayoutSystem->OnFinished(); } } } else { pLayoutSystem->GetFreeVariables()->SetOrCreateFreeVariable( "CurrentState", this ); for ( int i = 0; i < m_Actions.Count(); ++ i ) { if ( pLayoutSystem->ShouldStopProcessingActions() ) { break; } pLayoutSystem->ExecuteAction( m_Actions[i].m_pAction, m_Actions[i].m_pCondition ); } pLayoutSystem->GetFreeVariables()->SetOrCreateFreeVariable( "CurrentState", NULL ); } } void CTilegenState::OnBeginGeneration( CLayoutSystem *pLayoutSystem ) { for ( int i = 0; i < m_Actions.Count(); ++ i ) { m_Actions[i].m_pAction->OnBeginGeneration( pLayoutSystem ); } m_ChildStates.OnBeginGeneration(); } void CTilegenState::OnStateChanged( CLayoutSystem *pLayoutSystem ) { for ( int i = 0; i < m_Actions.Count(); ++ i ) { m_Actions[i].m_pAction->OnStateChanged( pLayoutSystem ); } } bool CTilegenState::ParseAction( KeyValues *pKeyValues ) { ITilegenAction *pNewAction = NULL; ITilegenExpression< bool > *pCondition = NULL; if ( CreateActionAndCondition( pKeyValues, &pNewAction, &pCondition ) ) { AddAction( pNewAction, pCondition ); return true; } else { return false; } } CLayoutSystem::CLayoutSystem() : m_nRandomSeed( 0 ), m_pGlobalActionState( NULL ), m_pCurrentState( NULL ), m_pMapLayout( NULL ), m_ActionData( DefLessFunc( ITilegenAction *) ), m_bLayoutError( false ), m_bGenerating( false ), m_nIterations( 0 ) { m_States.SetLayoutSystem( this ); } CLayoutSystem::~CLayoutSystem() { m_TilegenListeners.PurgeAndDeleteElements(); delete m_pGlobalActionState; } bool CLayoutSystem::LoadFromKeyValues( KeyValues *pKeyValues ) { // Make sure all tilegen class factories have been registered. RegisterAllTilegenClasses(); for ( KeyValues *pSubKey = pKeyValues->GetFirstSubKey(); pSubKey != NULL; pSubKey = pSubKey->GetNextKey() ) { if ( Q_stricmp( pSubKey->GetName(), "state" ) == 0 ) { CTilegenState *pNewState = new CTilegenState( this, NULL ); if ( !pNewState->LoadFromKeyValues( pSubKey ) ) { delete pNewState; return false; } m_States.AddState( pNewState ); } else if ( Q_stricmp( pSubKey->GetName(), "action" ) == 0 ) { // Global actions, executed at the beginning of every state ITilegenAction *pNewAction = NULL; ITilegenExpression< bool > *pCondition = NULL; if ( CreateActionAndCondition( pSubKey, &pNewAction, &pCondition ) ) { if ( m_pGlobalActionState == NULL ) { m_pGlobalActionState = new CTilegenState( this, NULL ); } m_pGlobalActionState->AddAction( pNewAction, pCondition ); } else { return false; } } else if ( Q_stricmp( pSubKey->GetName(), "mission_settings" ) == 0 ) { m_nRandomSeed = pSubKey->GetInt( "RandomSeed", 0 ); } } return true; } // @TODO: add rotation/mirroring support here by adding rotation argument bool CLayoutSystem::TryPlaceRoom( const CRoomCandidate *pRoomCandidate ) { int nX = pRoomCandidate->m_iXPos; int nY = pRoomCandidate->m_iYPos; const CRoomTemplate *pRoomTemplate = pRoomCandidate->m_pRoomTemplate; CRoom *pSourceRoom = pRoomCandidate->m_pExit ? pRoomCandidate->m_pExit->pSourceRoom : NULL; if ( m_pMapLayout->TemplateFits( pRoomTemplate, nX, nY, false ) ) { // This has the side-effect of attaching itself to the layout; no need to keep track of it. CRoom *pRoom = new CRoom( m_pMapLayout, pRoomTemplate, nX, nY ); // Remove exits covered up by the room for ( int i = m_OpenExits.Count() - 1; i >= 0; -- i ) { CExit *pExit = &m_OpenExits[i]; if ( pExit->X >= nX && pExit->Y >= nY && pExit->X < nX + pRoomTemplate->GetTilesX() && pExit->Y < nY + pRoomTemplate->GetTilesY() ) { m_OpenExits.FastRemove( i ); } } if ( pSourceRoom != NULL ) { ++ pSourceRoom->m_iNumChildren; } AddOpenExitsFromRoom( pRoom ); OnRoomPlaced(); GetFreeVariables()->SetOrCreateFreeVariable( "LastPlacedRoomTemplate", ( void * )pRoomTemplate ); return true; } return false; } void CLayoutSystem::TransitionToState( const char *pStateName ) { CTilegenState *pState = m_States.FindState( pStateName ); if ( pState == NULL ) { Log_Warning( LOG_TilegenLayoutSystem, "Tilegen state %s not found.\n", pStateName ); OnError(); } else { TransitionToState( pState ); } } void CLayoutSystem::TransitionToState( CTilegenState *pState ) { CTilegenState *pOldState = m_pCurrentState; m_pCurrentState = pState; m_CurrentIterationState.m_bStopIteration = true; OnStateChanged( pOldState ); Log_Msg( LOG_TilegenLayoutSystem, "Transitioning to state %s.\n", pState->GetStateName() ); StopProcessingActions(); } void CLayoutSystem::OnError() { m_bLayoutError = true; m_bGenerating = false; Log_Warning( LOG_TilegenLayoutSystem, "An error occurred during level generation.\n" ); } void CLayoutSystem::OnFinished() { m_bGenerating = false; m_CurrentIterationState.m_bStopIteration = true; Log_Msg( LOG_TilegenLayoutSystem, "CLayoutSystem: finished layout generation.\n" ); // Temp hack to setup fixed alien spawns // TODO: Move this into a required rule CASWMissionChooserNPCs::InitFixedSpawns( this, m_pMapLayout ); } void CLayoutSystem::ExecuteAction( ITilegenAction *pAction, ITilegenExpression< bool > *pCondition ) { Assert( pAction != NULL ); // Since actions can be nested, only expose the inner-most nested action. ITilegenAction *pOldAction = ( ITilegenAction * )GetFreeVariables()->GetFreeVariableOrNULL( "Action" ); GetFreeVariables()->SetOrCreateFreeVariable( "Action", pAction ); // Get data associated with the current action and set it to free variables int nIndex = m_ActionData.Find( pAction ); if ( nIndex == m_ActionData.InvalidIndex() ) { ActionData_t actionData = { 0 }; actionData.m_nNumTimesExecuted = 0; nIndex = m_ActionData.Insert( pAction, actionData ); Assert( nIndex != m_ActionData.InvalidIndex() ); } GetFreeVariables()->SetOrCreateFreeVariable( "NumTimesExecuted", ( void * )m_ActionData[nIndex].m_nNumTimesExecuted ); // Execute the action if the condition is met if ( pCondition == NULL || pCondition->Evaluate( GetFreeVariables() ) ) { // Log_Msg( LOG_TilegenLayoutSystem, "Executing action %s.\n", pAction->GetTypeName() ); pAction->Execute( this ); ++ m_ActionData[nIndex].m_nNumTimesExecuted; OnActionExecuted( pAction ); } // Restore free variable state GetFreeVariables()->SetOrCreateFreeVariable( "Action", pOldAction ); nIndex = m_ActionData.Find( pOldAction ); Assert( nIndex != m_ActionData.InvalidIndex() ); GetFreeVariables()->SetOrCreateFreeVariable( "NumTimesExecuted", ( void * )m_ActionData[nIndex].m_nNumTimesExecuted ); } void CLayoutSystem::OnActionExecuted( const ITilegenAction *pAction ) { for ( int i = 0; i < m_TilegenListeners.Count(); ++ i ) { m_TilegenListeners[i]->OnActionExecuted( this, pAction, GetFreeVariables() ); } } void CLayoutSystem::OnRoomPlaced() { for ( int i = 0; i < m_TilegenListeners.Count(); ++ i ) { m_TilegenListeners[i]->OnRoomPlaced( this, GetFreeVariables() ); } } void CLayoutSystem::OnStateChanged( const CTilegenState *pOldState ) { for ( int i = 0; i < m_TilegenListeners.Count(); ++ i ) { m_TilegenListeners[i]->OnStateChanged( this, pOldState, GetFreeVariables() ); } m_pCurrentState->OnStateChanged( this ); } void CLayoutSystem::BeginGeneration( CMapLayout *pMapLayout ) { if ( m_States.GetStateCount() == 0 ) { Log_Warning( LOG_TilegenLayoutSystem, "No states in layout system!\n" ); OnError(); return; } // Reset random generator int nSeed; m_Random = CUniformRandomStream(); if ( m_nRandomSeed != 0 ) { nSeed = m_nRandomSeed; } else { // @TODO: this is rather unscientific, but it gets us desired randomness for now. nSeed = RandomInt( 1, 1000000000 ); } m_Random.SetSeed( nSeed ); Log_Msg( LOG_TilegenLayoutSystem, "Beginning generation with random seed " ); Log_Msg( LOG_TilegenLayoutSystem, Color( 255, 255, 0, 255 ), "%d.\n", nSeed ); m_bLayoutError = false; m_bGenerating = true; m_nIterations = 0; m_pMapLayout = pMapLayout; m_pCurrentState = m_States.GetState( 0 ); m_OpenExits.RemoveAll(); m_ActionData.RemoveAll(); m_FreeVariables.RemoveAll(); // Initialize with sentinel value ActionData_t nullActionData = { 0 }; m_ActionData.Insert( NULL, nullActionData ); for ( int i = 0; i < m_TilegenListeners.Count(); ++ i ) { m_TilegenListeners[i]->OnBeginGeneration( this, GetFreeVariables() ); } if ( m_pGlobalActionState != NULL ) { m_pGlobalActionState->OnBeginGeneration( this ); } m_States.OnBeginGeneration(); GetFreeVariables()->SetOrCreateFreeVariable( "LayoutSystem", this ); GetFreeVariables()->SetOrCreateFreeVariable( "MapLayout", GetMapLayout() ); } void CLayoutSystem::ExecuteIteration() { Assert( m_pCurrentState != NULL && m_pMapLayout != NULL ); const int nMaxIterations = 200; if ( m_nIterations > nMaxIterations ) { Log_Warning( LOG_TilegenLayoutSystem, "Exceeded %d iterations, may be in an infinite loop.\n", nMaxIterations ); OnError(); return; } // Debugging assistant int nBreakOnIteration = tilegen_break_on_iteration.GetInt(); if ( nBreakOnIteration >= 0 ) { Log_Msg( LOG_TilegenLayoutSystem, "Iteration #%d\n", m_nIterations ); } if ( m_nIterations == nBreakOnIteration ) { DebuggerBreakIfDebugging(); } // Initialize state scoped to the current iteration m_CurrentIterationState.m_bStopIteration = false; CUtlVector< CRoomCandidate > roomCandidateList; m_CurrentIterationState.m_pRoomCandidateList = &roomCandidateList; // Execute global actions first, if any are present. if ( m_pGlobalActionState != NULL ) { m_pGlobalActionState->ExecuteIteration( this ); } // Now process actions in the current state if ( !ShouldStopProcessingActions() ) { // Executing an iteration may have a number of side-effects on the layout system, // such as placing rooms, changing state, etc. m_pCurrentState->ExecuteIteration( this ); } ++ m_nIterations; m_CurrentIterationState.m_pRoomCandidateList = NULL; } // This function adds an open exit for every un-mated exit in the given room. // The exits are added to the grid tile where a new room would connect // (e.g., a north exit from a room tile at (x, y) is actually added to location (x, y+1) void CLayoutSystem::AddOpenExitsFromRoom( CRoom *pRoom ) { const CRoomTemplate *pTemplate = pRoom->m_pRoomTemplate; // Go through each exit in the room. for ( int i = 0; i < pTemplate->m_Exits.Count(); ++ i ) { const CRoomTemplateExit *pExit = pTemplate->m_Exits[i]; int nExitX, nExitY; if ( GetExitPosition( pTemplate, pRoom->m_iPosX, pRoom->m_iPosY, i, &nExitX, &nExitY ) ) { // If no room exists where the exit interface goes, then consider the exit open. // NOTE: this function assumes that the caller has already verified that the template fits, which means // that if a room exists where this exit goes, it must be a matching exit (and thus not open). if ( !m_pMapLayout->GetRoom( nExitX, nExitY ) ) { AddOpenExit( pRoom, nExitX, nExitY, pExit->m_ExitDirection, pExit->m_szExitTag, pExit->m_bChokepointGrowSource ); } } } } void CLayoutSystem::AddOpenExit( CRoom *pSourceRoom, int nX, int nY, ExitDirection_t exitDirection, const char *pExitTag, bool bChokepointGrowSource ) { // Check to make sure exit isn't already in the list. for ( int i = 0; i < m_OpenExits.Count(); ++ i ) { const CExit *pExit = &m_OpenExits[i]; if ( pExit->X == nX && pExit->Y == nY && pExit->ExitDirection == exitDirection ) { // Exit already exists. Assert( pExit->pSourceRoom == pSourceRoom && Q_stricmp( pExitTag, pExit->m_szExitTag ) == 0 ); return; } } m_OpenExits.AddToTail( CExit( nX, nY, exitDirection, pExitTag, pSourceRoom, bChokepointGrowSource ) ); }