/* ** Command & Conquer Generals Zero Hour(tm) ** Copyright 2025 Electronic Arts Inc. ** ** This program is free software: you can redistribute it and/or modify ** it under the terms of the GNU General Public License as published by ** the Free Software Foundation, either version 3 of the License, or ** (at your option) any later version. ** ** This program is distributed in the hope that it will be useful, ** but WITHOUT ANY WARRANTY; without even the implied warranty of ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ** GNU General Public License for more details. ** ** You should have received a copy of the GNU General Public License ** along with this program. If not, see <http://www.gnu.org/licenses/>. */ //////////////////////////////////////////////////////////////////////////////// // // // (c) 2001-2003 Electronic Arts Inc. // // // //////////////////////////////////////////////////////////////////////////////// // StateMachine.cpp // Implementation of basic state machine // Author: Michael S. Booth, January 2002 #include "PreRTS.h" // This must go first in EVERY cpp file int the GameEngine #include "Common/Errors.h" #include "Common/StateMachine.h" #include "Common/ThingTemplate.h" #include "Common/GameState.h" #include "Common/GlobalData.h" #include "Common/Xfer.h" #include "GameLogic/GameLogic.h" #include "GameLogic/Object.h" #ifdef _INTERNAL // for occasional debugging... //#pragma optimize("", off) //#pragma MESSAGE("************************************** WARNING, optimization disabled for debugging purposes") #endif //------------------------------------------------------------------------------ Performance Timers //#include "Common/PerfMetrics.h" //#include "Common/PerfTimer.h" //static PerfTimer s_stateMachineTimer("StateMachine::update", false, PERFMETRICS_LOGIC_STARTFRAME, PERFMETRICS_LOGIC_STOPFRAME); //------------------------------------------------------------------------------------------------- //----------------------------------------------------------------------------- /** * Constructor */ State::State( StateMachine *machine, AsciiString name ) #ifdef STATE_MACHINE_DEBUG : m_name(name) #endif { m_ID = INVALID_STATE_ID; m_successStateID = INVALID_STATE_ID; m_failureStateID = INVALID_STATE_ID; m_machine = machine; } //----------------------------------------------------------------------------- /** * Add another state transition condition for this state */ void State::friend_onCondition( StateTransFuncPtr test, StateID toStateID, void* userData, const char* description ) { m_transitions.push_back(TransitionInfo(test, toStateID, userData, description)); } //----------------------------------------------------------------------------- class StIncrementer { private: Int& num; public: StIncrementer(Int& n) : num(n) { ++num; } ~StIncrementer() { --num; } }; #ifdef STATE_MACHINE_DEBUG //----------------------------------------------------------------------------- std::vector<StateID> * State::getTransitions( void ) { std::vector<StateID> *ids = new std::vector<StateID>; ids->push_back(m_successStateID); ids->push_back(m_failureStateID); // check transition condition list if (!m_transitions.empty()) { for(std::vector<TransitionInfo>::const_iterator it = m_transitions.begin(); it != m_transitions.end(); ++it) { ids->push_back(it->toStateID); } } return ids; } #endif //----------------------------------------------------------------------------- /** * Given a return code, handle state transitions */ StateReturnType State::friend_checkForTransitions( StateReturnType status ) { static Int checkfortransitionsnum = 0; StIncrementer inc(checkfortransitionsnum); if (checkfortransitionsnum >= 20) { DEBUG_CRASH(("checkfortransitionsnum is > 20")); return STATE_FAILURE; } DEBUG_ASSERTCRASH(!IS_STATE_SLEEP(status), ("Please handle sleep states prior to this")); // handle transitions switch( status ) { case STATE_SUCCESS: // check if machine should exit if (m_successStateID == EXIT_MACHINE_WITH_SUCCESS) { getMachine()->internalSetState( MACHINE_DONE_STATE_ID ); return STATE_SUCCESS; } else if (m_successStateID == EXIT_MACHINE_WITH_FAILURE) { getMachine()->internalSetState( MACHINE_DONE_STATE_ID ); return STATE_FAILURE; } // move to new state return getMachine()->internalSetState( m_successStateID ); case STATE_FAILURE: // check if machine should exit if (m_failureStateID == EXIT_MACHINE_WITH_SUCCESS) { getMachine()->internalSetState( MACHINE_DONE_STATE_ID ); return STATE_SUCCESS; } else if (m_failureStateID == EXIT_MACHINE_WITH_FAILURE) { getMachine()->internalSetState( MACHINE_DONE_STATE_ID ); return STATE_FAILURE; } // move to new state return getMachine()->internalSetState( m_failureStateID ); case STATE_CONTINUE: // check transition condition list if (!m_transitions.empty()) { for(std::vector<TransitionInfo>::const_iterator it = m_transitions.begin(); it != m_transitions.end(); ++it) { if (it->test( this, it->userData )) { // test returned true, change to associated state #ifdef STATE_MACHINE_DEBUG if (getMachine()->getWantsDebugOutput()) { DEBUG_LOG(("%d '%s' -- '%s' condition '%s' returned true!\n", TheGameLogic->getFrame(), getMachineOwner()->getTemplate()->getName().str(), getMachine()->getName().str(), it->description ? it->description : "[no description]")); } #endif // check if machine should exit if (it->toStateID == EXIT_MACHINE_WITH_SUCCESS) { return STATE_SUCCESS; } else if (it->toStateID == EXIT_MACHINE_WITH_FAILURE) { return STATE_FAILURE;//Lorenzen wants to know why... } // move to new state return getMachine()->internalSetState( it->toStateID ); } } } break; } // the machine keeps running return STATE_CONTINUE; } //----------------------------------------------------------------------------- /** * Given a return code, handle state transitions */ StateReturnType State::friend_checkForSleepTransitions( StateReturnType status ) { static Int checkfortransitionsnum = 0; StIncrementer inc(checkfortransitionsnum); if (checkfortransitionsnum >= 20) { DEBUG_CRASH(("checkforsleeptransitionsnum is > 20")); return STATE_FAILURE; } DEBUG_ASSERTCRASH(IS_STATE_SLEEP(status), ("Please only pass sleep states here")); // check transition condition list if (m_transitions.empty()) return status; for(std::vector<TransitionInfo>::const_iterator it = m_transitions.begin(); it != m_transitions.end(); ++it) { if (!it->test( this, it->userData )) continue; // test returned true, change to associated state #ifdef STATE_MACHINE_DEBUG if (getMachine()->getWantsDebugOutput()) { DEBUG_LOG(("%d '%s' -- '%s' condition '%s' returned true!\n", TheGameLogic->getFrame(), getMachineOwner()->getTemplate()->getName().str(), getMachine()->getName().str(), it->description ? it->description : "[no description]")); } #endif // check if machine should exit if (it->toStateID == EXIT_MACHINE_WITH_SUCCESS) { return STATE_SUCCESS; } else if (it->toStateID == EXIT_MACHINE_WITH_FAILURE) { return STATE_FAILURE; } else { // move to new state return getMachine()->internalSetState( it->toStateID ); } } return status; } //----------------------------------------------------------------------------- //----------------------------------------------------------------------------- //----------------------------------------------------------------------------- //----------------------------------------------------------------------------- /** * Constructor */ StateMachine::StateMachine( Object *owner, AsciiString name ) { m_owner = owner; m_sleepTill = 0; m_defaultStateID = INVALID_STATE_ID; m_defaultStateInited = false; m_currentState = NULL; m_locked = false; #ifdef STATE_MACHINE_DEBUG m_name = name; m_debugOutput = false; m_lockedby = NULL; #endif internalClear(); } //----------------------------------------------------------------------------- /** * Destructor. Destroy any states attached to this machine. */ StateMachine::~StateMachine() { // do not allow current state to exit if (m_currentState) m_currentState->onExit( EXIT_RESET ); std::map<StateID, State *>::iterator i; // delete all states in the mapping for( i = m_stateMap.begin(); i != m_stateMap.end(); ++i ) { if ((*i).second) (*i).second->deleteInstance(); } } //----------------------------------------------------------------------------- #ifdef STATE_MACHINE_DEBUG Bool StateMachine::getWantsDebugOutput() const { if (m_debugOutput) { return true; } if (TheGlobalData->m_stateMachineDebug) { return true; } #ifdef DEBUG_OBJECT_ID_EXISTS if (TheObjectIDToDebug != 0 && getOwner() != NULL && getOwner()->getID() == TheObjectIDToDebug) { return true; } #endif return false; } #endif //----------------------------------------------------------------------------- /** * Clear the internal variables of state machine to known values. */ void StateMachine::internalClear() { m_goalObjectID = INVALID_ID; m_goalPosition.x = 0.0f; m_goalPosition.y = 0.0f; m_goalPosition.z = 0.0f; #ifdef STATE_MACHINE_DEBUG if (getWantsDebugOutput()) { DEBUG_LOG(("%d '%s'%x -- '%s' %x internalClear()\n", TheGameLogic->getFrame(), m_owner->getTemplate()->getName().str(), m_owner, m_name.str(), this)); } #endif } //----------------------------------------------------------------------------- /** * Clear the machine */ void StateMachine::clear() { // if the machine is locked, it cannot be cleared if (m_locked) { #ifdef STATE_MACHINE_DEBUG if (m_currentState) DEBUG_LOG((" cur state '%s'\n", m_currentState->getName().str())); DEBUG_LOG(("machine is locked (by %s), cannot be cleared (Please don't ignore; this generally indicates a potential logic flaw)\n",m_lockedby)); #endif return; } // invoke the old state's onExit() if (m_currentState) m_currentState->onExit( EXIT_RESET ); m_currentState = NULL; internalClear(); } //----------------------------------------------------------------------------- /** * Reset the machine to its default state */ StateReturnType StateMachine::resetToDefaultState() { // if the machine is locked, it cannot be reset if (m_locked) { #ifdef STATE_MACHINE_DEBUG if (m_currentState) DEBUG_LOG((" cur state '%s'\n", m_currentState->getName().str())); DEBUG_LOG(("machine is locked (by %s), cannot be cleared (Please don't ignore; this generally indicates a potential logic flaw)\n",m_lockedby)); #endif return STATE_FAILURE; } if (!m_defaultStateInited) { DEBUG_CRASH(("you may not call resetToDefaultState before initDefaultState")); return STATE_FAILURE; } // allow current state to exit with EXIT_RESET if present if (m_currentState) m_currentState->onExit( EXIT_RESET ); m_currentState = NULL; // // the current state has done an onExit, clear the internal guts before we set // the new state, to clear it after the new state is set might be overwriting // things the new state transition causes to happen // internalClear(); // change to the default state StateReturnType status = internalSetState( m_defaultStateID ); DEBUG_ASSERTCRASH( status != STATE_FAILURE, ( "StateMachine::resetToDefaultState() Error setting default state" ) ); return status; } //----------------------------------------------------------------------------- /** * Run one step of the machine */ StateReturnType StateMachine::updateStateMachine() { UnsignedInt now = TheGameLogic->getFrame(); if (m_sleepTill != 0 && now < m_sleepTill) { if( m_currentState == NULL ) { return STATE_FAILURE; } return m_currentState->friend_checkForSleepTransitions( STATE_SLEEP(m_sleepTill - now) ); } // not sleeping anymore m_sleepTill = 0; if (m_currentState) { // update() can change m_currentState, so save it for a moment... State* stateBeforeUpdate = m_currentState; // execute this state StateReturnType status = m_currentState->update(); // it is possible that the state's update() method may cause the state to be destroyed if (m_currentState == NULL) { return STATE_FAILURE; } // here's the scenario: // -- State A calls foo() and then says "sleep for 2000 frames". // -- however, foo() called setState() to State B. thus our current state is not the same. // -- thus, if the state changed, we must ignore any sleep result and pretend we got STATE_CONTINUE, // so that the new state will be called immediately. if (stateBeforeUpdate != m_currentState) { status = STATE_CONTINUE; } if (IS_STATE_SLEEP(status)) { // hey, we're sleepy! m_sleepTill = now + GET_STATE_SLEEP_FRAMES(status); return m_currentState->friend_checkForSleepTransitions( STATE_SLEEP(m_sleepTill - now) ); } else { // check for state transitions, possibly exiting this machine return m_currentState->friend_checkForTransitions( status ); } } else { DEBUG_CRASH(("State machine has no current state -- did you remember to call initDefaultState?")); return STATE_FAILURE; } } //----------------------------------------------------------------------------- /** * Given a unique (for this machine) id number, and an instance of the * State class, the machine records this as a possible state, and * retains the id mapping. * These state id's are used to change the machine's state via setState(). */ void StateMachine::defineState( StateID id, State *state, StateID successID, StateID failureID, const StateConditionInfo* conditions ) { #ifdef STATE_MACHINE_DEBUG DEBUG_ASSERTCRASH(m_stateMap.find( id ) == m_stateMap.end(), ("duplicate state ID in statemachine %s\n",m_name.str())); #endif // map the ID to the state m_stateMap.insert( std::map<StateID, State *>::value_type( id, state ) ); // store the ID in the state itself, as well state->friend_setID( id ); state->friend_onSuccess(successID); state->friend_onFailure(failureID); while (conditions && conditions->test != NULL) { state->friend_onCondition(conditions->test, conditions->toStateID, conditions->userData); ++conditions; } if (m_defaultStateID == INVALID_STATE_ID) m_defaultStateID = id; } //----------------------------------------------------------------------------- /** * Given a state ID, return the state instance */ State *StateMachine::internalGetState( StateID id ) { // locate the actual state associated with the given ID std::map<StateID, State *>::iterator i; i = m_stateMap.find( id ); if (i == m_stateMap.end()) { DEBUG_CRASH( ("StateMachine::internalGetState(): Invalid state for object %s using state %d", m_owner->getTemplate()->getName().str(), id) ); DEBUG_LOG(("Transisioning to state #d\n", (Int)id)); DEBUG_LOG(("Attempting to recover - locating default state...\n")); i = m_stateMap.find(m_defaultStateID); if (i == m_stateMap.end()) { DEBUG_LOG(("Failed to located default state. Aborting...\n")); throw ERROR_BAD_ARG; } else { DEBUG_LOG(("Located default state to recover.\n")); } } return (*i).second; } //----------------------------------------------------------------------------- /** * Change the current state of the machine. * This causes the old state's onExit() method to be invoked, * and the new state's onEnter() method to be invoked. */ StateReturnType StateMachine::setState( StateID newStateID ) { // if the machine is locked, it cannot change state via external events if (m_locked) { #ifdef STATE_MACHINE_DEBUG if (m_currentState) DEBUG_LOG((" cur state '%s'\n", m_currentState->getName().str())); DEBUG_LOG(("machine is locked (by %s), cannot be cleared (Please don't ignore; this generally indicates a potential logic flaw)\n",m_lockedby)); #endif return STATE_CONTINUE; } return internalSetState( newStateID ); } //----------------------------------------------------------------------------- /** * Change the current state of the machine. * This causes the old state's onExit() method to be invoked, * and the new state's onEnter() method to be invoked. */ StateReturnType StateMachine::internalSetState( StateID newStateID ) { State *newState = NULL; // anytime the state changes, stop sleeping m_sleepTill = 0; // if we're not setting the "done" state ID we will continue with the actual transition if( newStateID != MACHINE_DONE_STATE_ID ) { // if incoming state is invalid, go to the machine's default state if (newStateID == INVALID_STATE_ID) { newStateID = m_defaultStateID; if (newStateID == INVALID_STATE_ID) { DEBUG_CRASH(("you may NEVER set the current state to an invalid state id.")); return STATE_FAILURE; } } // extract the state associated with the given ID newState = internalGetState( newStateID ); #ifdef STATE_MACHINE_DEBUG if (getWantsDebugOutput()) { StateID curState = INVALID_STATE_ID; if (m_currentState) { curState = m_currentState->getID(); } DEBUG_LOG(("%d '%s'%x -- '%s' %x exit ", TheGameLogic->getFrame(), m_owner->getTemplate()->getName().str(), m_owner, m_name.str(), this)); if (m_currentState) { DEBUG_LOG((" '%s' ", m_currentState->getName().str())); } else { DEBUG_LOG((" INVALID_STATE_ID ")); } if (newState) { DEBUG_LOG(("enter '%s' \n", newState->getName().str())); } else { DEBUG_LOG(("to INVALID_STATE\n")); } } #endif } // invoke the old state's onExit() if (m_currentState) m_currentState->onExit( EXIT_NORMAL ); // set the new state m_currentState = newState; // invoke the new state's onEnter() /// @todo It might be useful to pass the old state in... (MSB) if( m_currentState ) { // onEnter() could conceivably change m_currentState, so save it for a moment... State* stateBeforeEnter = m_currentState; StateReturnType status = m_currentState->onEnter(); // it is possible that the state's onEnter() method may cause the state to be destroyed if (m_currentState == NULL) { return STATE_FAILURE; } // here's the scenario: // -- State A calls foo() and then says "sleep for 2000 frames". // -- however, foo() called setState() to State B. thus our current state is not the same. // -- thus, if the state changed, we must ignore any sleep result and pretend we got STATE_CONTINUE, // so that the new state will be called immediately. if (stateBeforeEnter != m_currentState) { status = STATE_CONTINUE; } if (IS_STATE_SLEEP(status)) { // hey, we're sleepy! UnsignedInt now = TheGameLogic->getFrame(); m_sleepTill = now + GET_STATE_SLEEP_FRAMES(status); return m_currentState->friend_checkForSleepTransitions( STATE_SLEEP(m_sleepTill - now) ); } else { // check for state transitions, possibly exiting this machine return m_currentState->friend_checkForTransitions( status ); } } else { return STATE_CONTINUE; // irrelevant return code, but we must return something } } //----------------------------------------------------------------------------- /** * Define the default state of the machine, and * set the machine's state to it. */ StateReturnType StateMachine::initDefaultState() { #if defined(_DEBUG) || defined(_INTERNAL) #ifdef STATE_MACHINE_DEBUG #define REALLY_VERBOSE_LOG(x) /* */ // Run through all the transitions and make sure there aren't any transitions to undefined states. jba. [8/18/2003] std::map<StateID, State *>::iterator i; REALLY_VERBOSE_LOG(("SM_BEGIN\n")); for( i = m_stateMap.begin(); i != m_stateMap.end(); ++i ) { State *state = (*i).second; StateID id = state->getID(); // Check transitions. [8/18/2003] std::vector<StateID> *ids = state->getTransitions(); // check transitions REALLY_VERBOSE_LOG(("State %s(%d) : ", state->getName().str(), id)); if (!ids->empty()) { for(std::vector<StateID>::const_iterator it = ids->begin(); it != ids->end(); ++it) { StateID curID = *it; REALLY_VERBOSE_LOG(("%d('", curID)); if (curID == INVALID_STATE_ID) { REALLY_VERBOSE_LOG(("INVALID_STATE_ID', ")); continue; } if (curID == EXIT_MACHINE_WITH_SUCCESS) { REALLY_VERBOSE_LOG(("EXIT_MACHINE_WITH_SUCCESS', ")); continue; } if (curID == EXIT_MACHINE_WITH_FAILURE) { REALLY_VERBOSE_LOG(("EXIT_MACHINE_WITH_FAILURE', ")); continue; } // locate the actual state associated with the given ID std::map<StateID, State *>::iterator i; i = m_stateMap.find( curID ); if (i == m_stateMap.end()) { DEBUG_LOG(("\nState %s(%d) : ", state->getName().str(), id)); DEBUG_LOG(("Transition %d not found\n", curID)); DEBUG_LOG(("This MUST BE FIXED!!!jba\n")); DEBUG_CRASH(("Invalid transition.")); } else { State *st = (*i).second; if (st->getName().isNotEmpty()) { REALLY_VERBOSE_LOG(("%s') ", st->getName().str())); } } } } REALLY_VERBOSE_LOG(("\n")); delete ids; ids = NULL; } REALLY_VERBOSE_LOG(("SM_END\n\n")); #endif #endif DEBUG_ASSERTCRASH(!m_locked, ("Machine is locked here, but probably should not be")); if (m_defaultStateInited) { DEBUG_CRASH(("you may not call initDefaultState twice for the same StateMachine")); return STATE_FAILURE; } else { m_defaultStateInited = true; return internalSetState( m_defaultStateID ); } } //----------------------------------------------------------------------------- void StateMachine::setGoalObject( const Object *obj ) { if (m_locked) return; internalSetGoalObject( obj ); } //----------------------------------------------------------------------------- Bool StateMachine::isGoalObjectDestroyed() const { if (m_goalObjectID == 0) { return false; // never had a goal object } return getGoalObject() == NULL; } //----------------------------------------------------------------------------- void StateMachine::halt() { m_locked = true; m_currentState = NULL; // don't exit current state, just clear it. #ifdef STATE_MACHINE_DEBUG if (getWantsDebugOutput()) { DEBUG_LOG(("%d '%s' -- '%s' %x halt()\n", TheGameLogic->getFrame(), m_owner->getTemplate()->getName().str(), m_name.str(), this)); } #endif } //----------------------------------------------------------------------------- void StateMachine::internalSetGoalObject( const Object *obj ) { if (obj) { m_goalObjectID = obj->getID(); internalSetGoalPosition(obj->getPosition()); } else { m_goalObjectID = INVALID_ID; } } //----------------------------------------------------------------------------- Object *StateMachine::getGoalObject() { return TheGameLogic->findObjectByID( m_goalObjectID ); } //----------------------------------------------------------------------------- const Object *StateMachine::getGoalObject() const { return TheGameLogic->findObjectByID( m_goalObjectID ); } //----------------------------------------------------------------------------- void StateMachine::setGoalPosition( const Coord3D *pos ) { if (m_locked) return; internalSetGoalPosition( pos ); } //----------------------------------------------------------------------------- void StateMachine::internalSetGoalPosition( const Coord3D *pos ) { if (pos) { m_goalPosition = *pos; // Don't clear the goal object, or everything breaks. Like construction of buildings. } } // ------------------------------------------------------------------------------------------------ /** CRC */ // ------------------------------------------------------------------------------------------------ void StateMachine::crc( Xfer *xfer ) { } // end crc // ------------------------------------------------------------------------------------------------ /** Xfer Method * Version Info * 1: Initial version */ // ------------------------------------------------------------------------------------------------ void StateMachine::xfer( Xfer *xfer ) { // version XferVersion currentVersion = 1; XferVersion version = currentVersion; xfer->xferVersion( &version, currentVersion ); xfer->xferUnsignedInt(&m_sleepTill); xfer->xferUnsignedInt(&m_defaultStateID); StateID curStateID = getCurrentStateID(); xfer->xferUnsignedInt(&curStateID); if (xfer->getXferMode() == XFER_LOAD) { // We are going to jump into the current state. We don't call onEnter or onExit, because the // state was already active when we saved. m_currentState = internalGetState( curStateID ); } Bool snapshotAllStates = false; #ifdef _DEBUG //snapshotAllStates = true; #endif xfer->xferBool(&snapshotAllStates); if (snapshotAllStates) { std::map<StateID, State *>::iterator i; // count all states in the mapping Int count = 0; for( i = m_stateMap.begin(); i != m_stateMap.end(); ++i ) count++; Int saveCount = count; xfer->xferInt(&saveCount); if (saveCount!=count) { DEBUG_CRASH(("State count mismatch - %d expected, %d read", count, saveCount)); throw SC_INVALID_DATA; } for( i = m_stateMap.begin(); i != m_stateMap.end(); ++i ) { State *state = (*i).second; StateID id = state->getID(); xfer->xferUnsignedInt(&id); if (id!=state->getID()) { DEBUG_CRASH(("State ID mismatch - %d expected, %d read", state->getID(), id)); throw SC_INVALID_DATA; } if( state == NULL ) { DEBUG_ASSERTCRASH(state != NULL, ("state was NULL on xfer, trying to heal...")); // Hmm... too late to find out why we are getting NULL in our state, but if we let it go, we will Throw in xferSnapshot. state = internalGetState(m_defaultStateID); } xfer->xferSnapshot(state); } } else { if( m_currentState == NULL ) { DEBUG_ASSERTCRASH(m_currentState != NULL, ("currentState was NULL on xfer, trying to heal...")); // Hmm... too late to find out why we are getting NULL in our state, but if we let it go, we will Throw in xferSnapshot. m_currentState = internalGetState(m_defaultStateID); } xfer->xferSnapshot(m_currentState); } xfer->xferObjectID(&m_goalObjectID); xfer->xferCoord3D(&m_goalPosition); xfer->xferBool(&m_locked); xfer->xferBool(&m_defaultStateInited); } // end xfer // ------------------------------------------------------------------------------------------------ /** Load post process */ // ------------------------------------------------------------------------------------------------ void StateMachine::loadPostProcess( void ) { } // end loadPostProcess