476 lines
18 KiB
C++

/*
** Command & Conquer Generals(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.h
// Finite state machine encapsulation
// Author: Michael S. Booth, January 2002
#pragma once
#ifndef _STATE_MACHINE_H_
#define _STATE_MACHINE_H_
#include "Common/GameMemory.h"
#include "Common/GameType.h"
#include "Common/ModelState.h"
#include "Common/Snapshot.h"
#include "Common/Xfer.h"
//-----------------------------------------------------------------------------
//-----------------------------------------------------------------------------
//-----------------------------------------------------------------------------
class State;
class StateMachine;
class Object;
#undef STATE_MACHINE_DEBUG
#if defined(_DEBUG)
#define STATE_MACHINE_DEBUG
#endif
#if defined(_INTERNAL)
#define STATE_MACHINE_DEBUG //uncomment to debug state machines in internal. jba.
#endif
//-----------------------------------------------------------------------------
//-----------------------------------------------------------------------------
//-----------------------------------------------------------------------------
enum { MACHINE_DONE_STATE_ID = 999998, INVALID_STATE_ID = 999999 };
typedef UnsignedInt StateID; ///< used to denote individual states
typedef Bool (*StateTransFuncPtr)( State *state, void* userData );
/**
* State return codes
*/
enum StateReturnType
{
// note that all positive values are reserved for STATE_SLEEP!
STATE_CONTINUE = 0, ///< stay in this state (only for update method)
STATE_SUCCESS = -1, ///< state finished successfully, go to next state
STATE_FAILURE = -2, ///< state finished abnormally, go to next state
};
#define STATE_SLEEP(numFrames) ((StateReturnType)(numFrames))
#define IS_STATE_SLEEP(ret) ((Int)(ret) > 0)
#define GET_STATE_SLEEP_FRAMES(ret) ((UnsignedInt)(ret))
// (we use 0x3fffffff so that we can add offsets and not overflow...
// at 30fps that's around ~414 days!)
#define STATE_SLEEP_FOREVER STATE_SLEEP(0x3fffffff)
// this is mainly useful for states that enclose other state machines...
// where, even if the enclosed machine is sleeping, the encloser still needs
// to run every frame.
inline StateReturnType CONVERT_SLEEP_TO_CONTINUE(StateReturnType s)
{
return IS_STATE_SLEEP(s) ? STATE_CONTINUE : s;
}
// this is mainly useful for states that enclose other state machines...
// where the encloser and enclosee both might sleep. we need to choose the min
// sleep time that satisfies both.
inline StateReturnType MIN_SLEEP(UnsignedInt encloserSleep, StateReturnType encloseeResult)
{
if (IS_STATE_SLEEP(encloseeResult))
{
UnsignedInt encloseeSleep = GET_STATE_SLEEP_FRAMES(encloseeResult);
return STATE_SLEEP(min(encloserSleep, encloseeSleep));
}
else
{
// if enclosee needs to stay awake, we better do so, regardless
return encloseeResult;
}
}
/**
* Special argument for onCondition. It means when the given condition
* becomes true, the state machine will exit and return the given status.
*/
enum
{
EXIT_MACHINE_WITH_SUCCESS = 9998,
EXIT_MACHINE_WITH_FAILURE = 9999
};
/**
* Parameters for onExit().
*/
enum StateExitType
{
EXIT_NORMAL, ///< state exited due to normal state transitioning
EXIT_RESET ///< state exited due to state machine reset
};
struct StateConditionInfo
{
StateTransFuncPtr test;
StateID toStateID;
void* userData;
StateConditionInfo(StateTransFuncPtr t, StateID id, void* ud) : test(t), toStateID(id), userData(ud) { }
};
//-----------------------------------------------------------------------------
//-----------------------------------------------------------------------------
//-----------------------------------------------------------------------------
/**
* An abstraction of a machine's "state".
*/
class State : public MemoryPoolObject, public Snapshot
{
MEMORY_POOL_GLUE_ABC( State ) ///< this abstract class needs memory pool hooks
public:
State( StateMachine *machine, AsciiString name);
// already defined by MPO.
//virtual ~State() { }
virtual StateReturnType onEnter() { return STATE_CONTINUE; } ///< executed once when entering state
virtual void onExit( StateExitType status ) { } ///< executed once when leaving state
virtual StateReturnType update() = 0; ///< implements this state's behavior, decides when to change state
virtual Bool isIdle() const { return false; }
virtual Bool isAttack() const { return false; }
//Definition of busy -- when explicitly in the busy state. Moving or attacking is not considered busy!
virtual Bool isBusy() const { return false; }
inline StateMachine* getMachine() { return m_machine; } ///< return the machine this state is part of
inline StateID getID() const { return m_ID; } ///< get this state's id
Object* getMachineOwner();
const Object* getMachineOwner() const;
Object* getMachineGoalObject();
const Object* getMachineGoalObject() const;
const Coord3D* getMachineGoalPosition() const;
#ifdef STATE_MACHINE_DEBUG
virtual AsciiString getName() const {return m_name;}
#endif
// for internal use by the StateMachine class ---------------------------------------------------------
inline void friend_setID( StateID id ) { m_ID = id; } ///< define this state's id (for use only by StateMachine class)
void friend_onSuccess( StateID toStateID ) { m_successStateID = toStateID; } ///< define which state to move to after successful completion
void friend_onFailure( StateID toStateID ) { m_failureStateID = toStateID; } ///< define which state to move to after failure
void friend_onCondition( StateTransFuncPtr test, StateID toStateID, void* userData, const char* description = NULL ); ///< define when to change state
StateReturnType friend_checkForTransitions( StateReturnType status ); ///< given a return code, handle state transitions
StateReturnType friend_checkForSleepTransitions( StateReturnType status ); ///< given a return code, handle state transitions
protected:
#ifdef STATE_MACHINE_DEBUG
inline void setName(AsciiString n) { m_name = n; }
#endif
protected:
// snapshot interface - pure virtual here.
// Essentially all the member data gets set up on creation and shouldn't change.
// So none of it needs to be saved, and it nicely forces all user states to
// remember to implement crc, xfer & loadPostProcess. jba
virtual void crc( Xfer *xfer )=0;
virtual void xfer( Xfer *xfer )=0;
virtual void loadPostProcess()=0;
private:
struct TransitionInfo
{
StateTransFuncPtr test; ///< the condition evaluation function
StateID toStateID; ///< the state to transition to
void* userData; ///< data passed to transFuncPtr.
#ifdef STATE_MACHINE_DEBUG
const char* description; ///< description (for debugging purposes)
#endif
TransitionInfo(StateTransFuncPtr t, StateID id, void* ud, const char* desc) :
test(t),
toStateID(id),
userData(ud)
#ifdef STATE_MACHINE_DEBUG
, description(desc)
#endif
{ }
};
StateID m_ID; ///< this state's ID
StateID m_successStateID; ///< state to move to upon success
StateID m_failureStateID; ///< state to move to upon failure
std::vector<TransitionInfo> m_transitions; ///< possible transitions from this state
StateMachine *m_machine; ///< the state machine this state is part of
protected:
#ifdef STATE_MACHINE_DEBUG
AsciiString m_name; ///< Human readable name of this state - for debugging. jba.
#endif
};
inline State::~State() { }
//-----------------------------------------------------------------------------
//-----------------------------------------------------------------------------
//-----------------------------------------------------------------------------
/**
* A finite state machine.
*/
class StateMachine : public MemoryPoolObject, public Snapshot
{
MEMORY_POOL_GLUE_WITH_USERLOOKUP_CREATE( StateMachine, "StateMachinePool" );
public:
/**
* All of the states used by this machine should be
* instantiated and defined via defineState() in the
* machine's constructor.
*/
StateMachine( Object *owner, AsciiString name );
// virtual destructor defined by MemoryPool
virtual StateReturnType updateStateMachine(); ///< run one step of the machine
virtual void clear(); ///< clear the machine's internals to a known, initialized state
virtual StateReturnType resetToDefaultState(); ///< clear the machine's internals and set to the default state
/** must be called to jump the StateMachine into the default state... this may fail
(return a failure code), so it can't be done via the ctor. you MUST call this before
using the state machine. */
virtual StateReturnType initDefaultState();
virtual StateReturnType setState( StateID newStateID ); ///< change the current state of the machine (which may cause further state changes, due to onEnter)
StateID getCurrentStateID() const { return m_currentState ? m_currentState->getID() : INVALID_STATE_ID; } ///< return the id of the current state of the machine
Bool isInIdleState() const { return m_currentState ? m_currentState->isIdle() : true; } // stateless things are considered 'idle'
Bool isInAttackState() const { return m_currentState ? m_currentState->isAttack() : true; } // stateless things are considered 'idle'
Bool isInForceAttackState() const { return m_currentState ? m_currentState->isIdle() : true; } // stateless things are considered 'idle'
//Definition of busy -- when explicitly in the busy state. Moving or attacking is not considered busy!
Bool isInBusyState() const { return m_currentState ? m_currentState->isBusy() : false; } // stateless things are not considered 'busy'
// no, this is now deprecated. you should strive to avoid having to get the current state.
// try to make do with getCurrentStateID() or isInIdleState() instead. (srj)
// State *getCurrentState() { return m_currentState; } ///< return the current state of the machine
/**
* Lock/unlock this state machine.
* If a machine is locked, it cannot be reset, or given external setStates(), etc.
*/
void lock(const char* msg)
{
m_locked = true;
#ifdef STATE_MACHINE_DEBUG
m_lockedby = msg;
#endif
}
void unlock()
{
m_locked = false;
#ifdef STATE_MACHINE_DEBUG
m_lockedby = NULL;
#endif
}
Bool isLocked() const { return m_locked; }
/**
* Get the object that "owns" this machine.
* There need not be an object with a machine, but it is so common
* that it is useful to have this method in the generic state machine.
*/
Object *getOwner() { return m_owner; }
const Object *getOwner() const { return m_owner; }
// common parameters for state machines
void setGoalObject( const Object *obj );
Object *getGoalObject();
const Object *getGoalObject() const;
void setGoalPosition( const Coord3D *pos );
const Coord3D *getGoalPosition() const { return &m_goalPosition; }
Bool isGoalObjectDestroyed() const; ///< Returns true if we had a goal object, but it has been destroyed.
virtual void halt(void); ///< Stops the state machine & disables it in preparation for deleting it.
//
// The following methods are for internal use by the State class
//
StateReturnType internalSetState( StateID newStateID ); ///< for internal use only - change the current state of the machine
#if defined(_DEBUG) || defined(_INTERNAL)
UnsignedInt peekSleepTill() const { return m_sleepTill; }
#endif
#ifdef STATE_MACHINE_DEBUG
Bool getWantsDebugOutput() const;
void setDebugOutput( Bool output ) { m_debugOutput = output; }
void setName( AsciiString name) {m_name = name;}
inline AsciiString getName() const {return m_name;}
virtual AsciiString getCurrentStateName() const { return m_currentState ? m_currentState->getName() : AsciiString::TheEmptyString;}
#else
inline Bool getWantsDebugOutput() const { return false; }
inline AsciiString getCurrentStateName() const { return AsciiString::TheEmptyString;}
#endif
protected:
// snapshot interface
virtual void crc( Xfer *xfer );
virtual void xfer( Xfer *xfer );
virtual void loadPostProcess();
protected:
/**
* Given a unique (to this machine) integer ID representing a state, and an instance
* of that state, the machine records this as a possible state, and
* internally maps the given integer ID to the state instance.
* These state id's are used to change the machine's state via setState().
*/
void defineState( StateID id, State *state,
StateID successID,
StateID failureID,
const StateConditionInfo* conditions = NULL);
State* internalGetState( StateID id );
private:
void internalClear();
void internalSetGoalObject( const Object *obj );
void internalSetGoalPosition( const Coord3D *pos);
std::map<StateID, State *> m_stateMap; ///< the mapping of ids to states
Object* m_owner; ///< object that "owns" this machine
UnsignedInt m_sleepTill; ///< if nonzero, we are sleeping 'till this frame
StateID m_defaultStateID; ///< the default state of the machine
State* m_currentState;
ObjectID m_goalObjectID; ///< the object of interest for this state
Coord3D m_goalPosition; ///< the position of interest for this state
Bool m_locked; ///< whether this machine is locked or not
Bool m_defaultStateInited; ///< if initDefaultState has been called
#ifdef STATE_MACHINE_DEBUG
Bool m_debugOutput;
AsciiString m_name; ///< Human readable name of this state - for debugging. jba.
const char* m_lockedby;
#endif
};
//-----------------------------------------------------------------------------
inline Object* State::getMachineOwner() { return m_machine->getOwner(); }
inline const Object* State::getMachineOwner() const { return m_machine->getOwner(); }
inline Object* State::getMachineGoalObject() { return m_machine->getGoalObject(); } ///< return the machine this state is part of
inline const Object* State::getMachineGoalObject() const { return m_machine->getGoalObject(); } ///< return the machine this state is part of
inline const Coord3D* State::getMachineGoalPosition() const { return m_machine->getGoalPosition(); } ///< return the machine this state is part of
//-----------------------------------------------------------------------------------------------------------
/**
A utility state that immediately succeeds.
*/
class SuccessState : public State
{
MEMORY_POOL_GLUE_WITH_USERLOOKUP_CREATE(SuccessState, "SuccessState")
public:
SuccessState( StateMachine *machine ) : State( machine, "SuccessState") { }
virtual StateReturnType onEnter() { return STATE_SUCCESS; }
virtual StateReturnType update() { return STATE_SUCCESS; }
protected:
// snapshot interface STUBBED.
virtual void crc( Xfer *xfer ){};
virtual void xfer( Xfer *xfer ){XferVersion cv = 1; XferVersion v = cv; xfer->xferVersion( &v, cv );}
virtual void loadPostProcess(){};
};
EMPTY_DTOR(SuccessState)
//-----------------------------------------------------------------------------------------------------------
/**
A utility state that immediately fails.
*/
class FailureState : public State
{
MEMORY_POOL_GLUE_WITH_USERLOOKUP_CREATE(FailureState, "FailureState")
public:
FailureState( StateMachine *machine ) : State( machine, "FailureState") { }
virtual StateReturnType onEnter() { return STATE_FAILURE; }
virtual StateReturnType update() { return STATE_FAILURE; }
protected:
// snapshot interface STUBBED.
virtual void crc( Xfer *xfer ){};
virtual void xfer( Xfer *xfer ){XferVersion cv = 1; XferVersion v = cv; xfer->xferVersion( &v, cv );}
virtual void loadPostProcess(){};
};
EMPTY_DTOR(FailureState)
//-----------------------------------------------------------------------------------------------------------
/**
A utility state that never exits (except due to conditions).
*/
class ContinueState : public State
{
MEMORY_POOL_GLUE_WITH_USERLOOKUP_CREATE(ContinueState, "ContinueState")
public:
ContinueState( StateMachine *machine ) : State( machine, "ContinueState" ) { }
virtual StateReturnType onEnter() { return STATE_CONTINUE; }
virtual StateReturnType update() { return STATE_CONTINUE; }
protected:
// snapshot interface STUBBED.
virtual void crc( Xfer *xfer ){};
virtual void xfer( Xfer *xfer ){XferVersion cv = 1; XferVersion v = cv; xfer->xferVersion( &v, cv );}
virtual void loadPostProcess(){};
};
EMPTY_DTOR(ContinueState)
//-----------------------------------------------------------------------------------------------------------
/**
A utility state that sleeps forever.
*/
class SleepState : public State
{
MEMORY_POOL_GLUE_WITH_USERLOOKUP_CREATE(SleepState, "SleepState")
public:
SleepState( StateMachine *machine ) : State( machine, "SleepState" ) { }
virtual StateReturnType onEnter() { return STATE_SLEEP_FOREVER; }
virtual StateReturnType update() { return STATE_SLEEP_FOREVER; }
protected:
// snapshot interface STUBBED.
virtual void crc( Xfer *xfer ){};
virtual void xfer( Xfer *xfer ){XferVersion cv = 1; XferVersion v = cv; xfer->xferVersion( &v, cv );}
virtual void loadPostProcess(){};
};
EMPTY_DTOR(SleepState)
#endif // _STATE_MACHINE_H_