476 lines
18 KiB
C
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_
|