From f6b57d956d2f395d87e83f8097f94d9566308ae7 Mon Sep 17 00:00:00 2001 From: Aleric Inglewood Date: Thu, 5 May 2011 16:34:38 +0200 Subject: [PATCH] Added base class AIStateMachine. This is the skeleton needed to implement classes that can be reused and work together, which can perform asynchronous tasks (read: need to wait for certain events before they can continue). An example would be the task of waiting for a given inventory folder to be read. This could then be used to improve the builtin AO (automatically reading that folder when a notecard is dropped, and continuing when the whole folder is read). It's first use will be communication with a filepicker that runs in a plugin. --- doc/contributions.txt | 1 + indra/cwdebug/debug.cc | 1 + indra/cwdebug/debug.h | 1 + indra/llcommon/llfasttimer.h | 1 + indra/newview/CMakeLists.txt | 9 + indra/newview/app_settings/settings.xml | 11 + indra/newview/llcallbacklist.cpp | 40 +- indra/newview/llcallbacklist.h | 4 +- indra/newview/llfasttimerview.cpp | 1 + indra/newview/llviewercontrol.cpp | 7 + indra/newview/statemachine/aistatemachine.cpp | 343 +++++++++++++++++ indra/newview/statemachine/aistatemachine.h | 344 ++++++++++++++++++ 12 files changed, 757 insertions(+), 6 deletions(-) create mode 100644 indra/newview/statemachine/aistatemachine.cpp create mode 100644 indra/newview/statemachine/aistatemachine.h diff --git a/doc/contributions.txt b/doc/contributions.txt index f70336a94..ea148abd9 100644 --- a/doc/contributions.txt +++ b/doc/contributions.txt @@ -114,6 +114,7 @@ Aleric Inglewood IMP-670 IMP-701 IMP-734 + IMP-735 Alissa Sabre VWR-81 VWR-83 diff --git a/indra/cwdebug/debug.cc b/indra/cwdebug/debug.cc index 3066c2845..48392beae 100644 --- a/indra/cwdebug/debug.cc +++ b/indra/cwdebug/debug.cc @@ -171,6 +171,7 @@ void stop_recording_backtraces(void) channel_ct gtk DDCN("GTK"); //!< This debug channel is used for output related to gtk. channel_ct sdl DDCN("SDL"); //!< This debug channel is used for output related to sdl locking. channel_ct backtrace DDCN("BACKTRACE"); //!< This debug channel is used for backtraces. + channel_ct statemachine DDCN("STATEMACHINE"); //!< This debug channel is used class AIStateMachine. } // namespace dc } // namespace DEBUGCHANNELS diff --git a/indra/cwdebug/debug.h b/indra/cwdebug/debug.h index 66408ad72..e91eab92e 100644 --- a/indra/cwdebug/debug.h +++ b/indra/cwdebug/debug.h @@ -116,6 +116,7 @@ extern CWD_API channel_ct primbackup; extern CWD_API channel_ct gtk; extern CWD_API channel_ct sdl; extern CWD_API channel_ct backtrace; +extern CWD_API channel_ct statemachine; #endif diff --git a/indra/llcommon/llfasttimer.h b/indra/llcommon/llfasttimer.h index 898ed496b..726274c23 100644 --- a/indra/llcommon/llfasttimer.h +++ b/indra/llcommon/llfasttimer.h @@ -177,6 +177,7 @@ public: FTM_REFRESH, FTM_SORT, FTM_PICK, + FTM_STATEMACHINE, // Temp FTM_TEMP1, diff --git a/indra/newview/CMakeLists.txt b/indra/newview/CMakeLists.txt index 57a6163d7..628ee9ec4 100644 --- a/indra/newview/CMakeLists.txt +++ b/indra/newview/CMakeLists.txt @@ -1005,6 +1005,15 @@ set(viewer_HEADER_FILES source_group("CMake Rules" FILES ViewerInstall.cmake) +set(statemachine_SOURCE_FILES + statemachine/aistatemachine.cpp + ) +set(statemachine_HEADER_FILES + statemachine/aistatemachine.h + ) +list(APPEND viewer_SOURCE_FILES ${statemachine_SOURCE_FILES}) +list(APPEND viewer_HEADER_FILES ${statemachine_HEADER_FILES}) + if (DARWIN) LIST(APPEND viewer_SOURCE_FILES llappviewermacosx.cpp) diff --git a/indra/newview/app_settings/settings.xml b/indra/newview/app_settings/settings.xml index b0a04a3bd..1406c4e96 100644 --- a/indra/newview/app_settings/settings.xml +++ b/indra/newview/app_settings/settings.xml @@ -11382,6 +11382,17 @@ Value 0 + StateMachineMaxTime + + Comment + Maximum time per frame spent executing AIStateMachine objects, in miliseconds + Persist + 1 + Type + U32 + Value + 20 + StatsAutoRun Comment diff --git a/indra/newview/llcallbacklist.cpp b/indra/newview/llcallbacklist.cpp index bba4d021c..6063995cf 100644 --- a/indra/newview/llcallbacklist.cpp +++ b/indra/newview/llcallbacklist.cpp @@ -47,7 +47,7 @@ LLCallbackList gIdleCallbacks; // Member functions // -LLCallbackList::LLCallbackList() +LLCallbackList::LLCallbackList() : mLoopingOverCallbackList(false) { // nothing } @@ -96,7 +96,15 @@ BOOL LLCallbackList::deleteFunction( callback_t func, void *data) callback_list_t::iterator iter = std::find(mCallbackList.begin(), mCallbackList.end(), t); if (iter != mCallbackList.end()) { - mCallbackList.erase(iter); + if (mLoopingOverCallbackList) + { + iter->first = NULL; // Mark for removal later (when we return to LLCallbackList::callFunctions). + mNeedErase = true; + } + else + { + mCallbackList.erase(iter); + } return TRUE; } else @@ -108,16 +116,38 @@ BOOL LLCallbackList::deleteFunction( callback_t func, void *data) void LLCallbackList::deleteAllFunctions() { + llassert(!mLoopingOverCallbackList); // Only called from unit tests. mCallbackList.clear(); } void LLCallbackList::callFunctions() { - for (callback_list_t::iterator iter = mCallbackList.begin(); iter != mCallbackList.end(); ) + llassert(!mLoopingOverCallbackList); + mLoopingOverCallbackList = true; + mNeedErase = false; + for (callback_list_t::iterator iter = mCallbackList.begin(); iter != mCallbackList.end(); ++iter) { - callback_list_t::iterator curiter = iter++; - curiter->first(curiter->second); + if (iter->first) // Not pending removal? + { + iter->first(iter->second); // This can theorectically set any iter->first to NULL, which means the entry should be erased. + } + } + mLoopingOverCallbackList = false; + if (mNeedErase) + { + callback_list_t::iterator iter = mCallbackList.begin(); + while (iter != mCallbackList.end()) + { + if (!iter->first) + { + iter = mCallbackList.erase(iter); + } + else + { + ++iter; + } + } } } diff --git a/indra/newview/llcallbacklist.h b/indra/newview/llcallbacklist.h index be0fe9f5c..240346478 100644 --- a/indra/newview/llcallbacklist.h +++ b/indra/newview/llcallbacklist.h @@ -53,9 +53,11 @@ public: protected: // Use a list so that the callbacks are ordered in case that matters - typedef std::pair callback_pair_t; + typedef std::pair callback_pair_t; // callback_t is a (function) pointer. If it is NULL it means that the entry should be considered deleted. typedef std::list callback_list_t; callback_list_t mCallbackList; + bool mLoopingOverCallbackList; // True while looping over mCallbackList and calling the callback_t functions (see callFunctions). + bool mNeedErase; // True when deleteFunction was called while mLoopingOverCallbackList was true. }; extern LLCallbackList gIdleCallbacks; diff --git a/indra/newview/llfasttimerview.cpp b/indra/newview/llfasttimerview.cpp index 27661567e..418ea1bf0 100644 --- a/indra/newview/llfasttimerview.cpp +++ b/indra/newview/llfasttimerview.cpp @@ -83,6 +83,7 @@ static struct ft_display_info ft_display_table[] = { LLFastTimer::FTM_KEYHANDLER, " Keyboard", &LLColor4::grey1, 0 }, { LLFastTimer::FTM_SLEEP, " Sleep", &LLColor4::grey2, 0 }, { LLFastTimer::FTM_IDLE, " Idle", &blue0, 0 }, + { LLFastTimer::FTM_STATEMACHINE, " State Machines", &LLColor4::yellow1, 0 }, { LLFastTimer::FTM_PUMP, " Pump", &LLColor4::magenta2, 1 }, { LLFastTimer::FTM_CURL, " Curl", &LLColor4::magenta3, 0 }, { LLFastTimer::FTM_PUMPIO, " PumpIO", &LLColor4::magenta1, 0 }, diff --git a/indra/newview/llviewercontrol.cpp b/indra/newview/llviewercontrol.cpp index 23c9535fa..de65b97af 100644 --- a/indra/newview/llviewercontrol.cpp +++ b/indra/newview/llviewercontrol.cpp @@ -75,6 +75,7 @@ #include "llnetmap.h" #include "llrender.h" #include "llfloaterchat.h" +#include "statemachine/aistatemachine.h" #include "aithreadsafe.h" #include "llviewerobjectlist.h" #include "lldrawpoolbump.h" @@ -119,6 +120,11 @@ static bool handleTerrainDetailChanged(const LLSD& newvalue) return true; } +bool handleStateMachineMaxTimeChanged(const LLSD& newvalue) +{ + AIStateMachine::updateSettings(); + return true; +} static bool handleSetShaderChanged(const LLSD& newvalue) { @@ -707,6 +713,7 @@ void settings_setup_listeners() gSavedSettings.getControl("AudioLevelMic")->getSignal()->connect(boost::bind(&handleVoiceClientPrefsChanged, _1)); gSavedSettings.getControl("LipSyncEnabled")->getSignal()->connect(boost::bind(&handleVoiceClientPrefsChanged, _1)); gSavedSettings.getControl("TranslateChat")->getSignal()->connect(boost::bind(&handleTranslateChatPrefsChanged, _1)); + gSavedSettings.getControl("StateMachineMaxTime")->getSignal()->connect(boost::bind(&handleStateMachineMaxTimeChanged, _1)); gSavedSettings.getControl("CloudsEnabled")->getSignal()->connect(boost::bind(&handleCloudSettingsChanged, _1)); gSavedSettings.getControl("SkyUseClassicClouds")->getSignal()->connect(boost::bind(&handleCloudSettingsChanged, _1)); diff --git a/indra/newview/statemachine/aistatemachine.cpp b/indra/newview/statemachine/aistatemachine.cpp new file mode 100644 index 000000000..ca1b5c61e --- /dev/null +++ b/indra/newview/statemachine/aistatemachine.cpp @@ -0,0 +1,343 @@ +/** + * @file aistatemachine.cpp + * @brief Implementation of AIStateMachine + * + * Copyright (c) 2010, Aleric Inglewood. + * + * 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 2 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 . + * + * There are special exceptions to the terms and conditions of the GPL as + * it is applied to this Source Code. View the full text of the exception + * in the file doc/FLOSS-exception.txt in this software distribution. + * + * CHANGELOG + * and additional copyright holders. + * + * 01/03/2010 + * Initial version, written by Aleric Inglewood @ SL + */ + +#include "../llviewerprecompiledheaders.h" + +#include + +#include "../llcallbacklist.h" +#include "../llviewercontrol.h" + +#include "llfasttimer.h" +#include "aithreadsafe.h" +#include "aistatemachine.h" + +// Local variables. +namespace { + struct QueueElementComp; + + class QueueElement { + private: + AIStateMachine* mStateMachine; + U64 mRuntime; + + public: + QueueElement(AIStateMachine* statemachine) : mStateMachine(statemachine), mRuntime(0) { } + friend bool operator==(QueueElement const& e1, QueueElement const& e2) { return e1.mStateMachine == e2.mStateMachine; } + friend struct QueueElementComp; + + AIStateMachine& statemachine(void) const { return *mStateMachine; } + void add(U64 count) { mRuntime += count; } + }; + + struct QueueElementComp { + bool operator()(QueueElement const& e1, QueueElement const& e2) const { return e1.mRuntime < e2.mRuntime; } + }; + + typedef std::vector active_statemachines_type; + static active_statemachines_type active_statemachines; + typedef std::vector continued_statemachines_type; + struct cscm_type + { + continued_statemachines_type continued_statemachines; + bool calling_mainloop; + }; + static AITHREADSAFE(cscm_type, continued_statemachines_and_calling_mainloop, ); +} + +// static +AITHREADSAFESIMPLE(U64, AIStateMachine::sMaxCount, ); + +void AIStateMachine::updateSettings(void) +{ + Dout(dc::statemachine, "Initializing AIStateMachine::sMaxCount"); + *AIAccess(sMaxCount) = LLFastTimer::countsPerSecond() * gSavedSettings.getU32("StateMachineMaxTime") / 1000; +} + +//---------------------------------------------------------------------------- +// +// Public methods +// + +void AIStateMachine::run(AIStateMachine* parent, state_type new_parent_state, bool abort_parent) +{ + DoutEntering(dc::statemachine, "AIStateMachine::run(" << (void*)parent << ", " << (parent ? parent->state_str(new_parent_state) : "NA") << ", " << abort_parent << ") [" << (void*)this << "]"); + // Must be the first time we're being run. + llassert(!mParent); + llassert(!mCallback); + // Can only be run when in this state. + llassert(mState == bs_initialize); + // If a parent is provided, it must be running. + llassert(!parent || parent->mState == bs_run); + + mParent = parent; + mNewParentState = new_parent_state; + mAbortParent = abort_parent; + + cont(); +} + +void AIStateMachine::run(callback_type::signal_type::slot_type const& slot) +{ + DoutEntering(dc::statemachine, "AIStateMachine::run() [" << (void*)this << "]"); + // Must be the first time we're being run. + llassert(!mParent); + llassert(!mCallback); + // Can only be run when in this state. + llassert(mState == bs_initialize); + + mCallback = new callback_type(slot); + + cont(); +} + +void AIStateMachine::idle(void) +{ + DoutEntering(dc::statemachine, "AIStateMachine::idle() [" << (void*)this << "]"); + llassert(!mIdle); + mIdle = true; + mSleep = 0; +} + +void AIStateMachine::cont(void) +{ + DoutEntering(dc::statemachine, "AIStateMachine::cont() [" << (void*)this << "]"); + llassert(mIdle); + mIdle = false; + AIWriteAccess cscm_w(continued_statemachines_and_calling_mainloop); + cscm_w->continued_statemachines.push_back(this); + if (!cscm_w->calling_mainloop) + { + Dout(dc::statemachine, "Adding AIStateMachine::mainloop to gIdleCallbacks"); + cscm_w->calling_mainloop = true; + gIdleCallbacks.addFunction(&AIStateMachine::mainloop); + } +} + +void AIStateMachine::set_state(state_type state) +{ + DoutEntering(dc::statemachine, "AIStateMachine::set_state(" << state_str(state) << ") [" << (void*)this << "]"); + llassert(mState == bs_run); + if (mRunState != state) + { + mRunState = state; + Dout(dc::statemachine, "mRunState set to " << state_str(mRunState)); + } + if (mIdle) + cont(); +} + +void AIStateMachine::abort(void) +{ + DoutEntering(dc::statemachine, "AIStateMachine::abort() [" << (void*)this << "]"); + llassert(mState == bs_run); + mState = bs_abort; + abort_impl(); + mAborted = true; + finish(); +} + +void AIStateMachine::finish(void) +{ + DoutEntering(dc::statemachine, "AIStateMachine::finish() [" << (void*)this << "]"); + llassert(mState == bs_run || mState == bs_abort); + mState = bs_finish; + finish_impl(); + if (mParent) + { + // It is possible that the parent is not running when the parent is in fact aborting and called + // abort on this object from it's abort_impl function. It that case we don't want to recursively + // call abort again (or change it's state). + if (mParent->running()) + { + if (mAborted && mAbortParent) + { + mParent->abort(); + } + else + { + mParent->set_state(mNewParentState); + } + } + mParent = NULL; + } + // Set this already to bs_initialize now, so that (bool)*this evaluates to true. + mState = bs_initialize; + // It is possible that mIdle is false when abort or finish was called from + // outside multiplex_impl. However, that only may be done by the main thread. + llassert(!mIdle || is_main_thread()); + if (!mIdle) + idle(); + if (mCallback) + { + mCallback->callback(!mAborted); // This can/may call deleteMe(), in which case the whole AIStateMachine will be deleted from the mainloop. + delete mCallback; + mCallback = NULL; + } +} + +void AIStateMachine::deleteMe(void) +{ + llassert(mIdle && mState == bs_initialize); + mState = bs_deleted; +} + +// Return stringified 'state'. +char const* AIStateMachine::state_str(state_type state) +{ + if (state >= min_state && state < max_state) + { + switch (state) + { + AI_CASE_RETURN(bs_initialize); + AI_CASE_RETURN(bs_run); + AI_CASE_RETURN(bs_abort); + AI_CASE_RETURN(bs_finish); + AI_CASE_RETURN(bs_deleted); + } + } + return state_str_impl(state); +} + +//---------------------------------------------------------------------------- +// +// Private methods +// + +void AIStateMachine::multiplex(U64 current_time) +{ + // Return immediately when this state machine is sleeping. + // A negative value of mSleep means we're counting frames, + // a positive value means we're waiting till a certain + // amount of time has passed. + if (mSleep != 0) + { + if (mSleep < 0) + { + if (++mSleep) + return; + } + else + { + if (current_time < mSleep) + return; + mSleep = 0; + } + } + + DoutEntering(dc::statemachine, "AIStateMachine::multiplex() [" << (void*)this << "] [with state: " << state_str(mState == bs_run ? mRunState : mState) << "]"); + llassert(mState == bs_initialize || mState == bs_run); + + // Real state machine starts here. + if (mState == bs_initialize) + { + mAborted = false; + mState = bs_run; + initialize_impl(); + if (mAborted || mState != bs_run) + return; + } + multiplex_impl(); +} + +// static +void AIStateMachine::mainloop(void*) +{ + LLFastTimer t(LLFastTimer::FTM_STATEMACHINE); + // Add continued state machines. + { + AIReadAccess cscm_r(continued_statemachines_and_calling_mainloop); + bool nonempty = false; + for (continued_statemachines_type::const_iterator iter = cscm_r->continued_statemachines.begin(); iter != cscm_r->continued_statemachines.end(); ++iter) + { + nonempty = true; + active_statemachines.push_back(QueueElement(*iter)); + Dout(dc::statemachine, "Adding " << (void*)*iter << " to active_statemachines"); + } + if (nonempty) + AIWriteAccess(cscm_r)->continued_statemachines.clear(); + } + llassert(!active_statemachines.empty()); + // Run one or more state machines. + U64 total_clocks = 0; + U64 max_count = *AIAccess(sMaxCount); + for (active_statemachines_type::iterator iter = active_statemachines.begin(); iter != active_statemachines.end(); ++iter) + { + AIStateMachine& statemachine(iter->statemachine()); + if (!statemachine.mIdle) + { + U64 start = get_cpu_clock_count(); + iter->statemachine().multiplex(start); + U64 delta = get_cpu_clock_count() - start; + iter->add(delta); + total_clocks += delta; + if (total_clocks >= max_count) + { +#ifndef LL_RELEASE_FOR_DOWNLOAD + llwarns << "AIStateMachine::mainloop did run for " << (total_clocks * 1000 / LLFastTimer::countsPerSecond()) << " ms." << llendl; +#endif + std::sort(active_statemachines.begin(), active_statemachines.end(), QueueElementComp()); + break; + } + } + } + // Remove idle state machines from the loop. + active_statemachines_type::iterator iter = active_statemachines.begin(); + while (iter != active_statemachines.end()) + { + AIStateMachine& statemachine(iter->statemachine()); + if (statemachine.mIdle) + { + Dout(dc::statemachine, "Erasing " << (void*)&statemachine << " from active_statemachines"); + iter = active_statemachines.erase(iter); + if (statemachine.mState == bs_deleted) + { + Dout(dc::statemachine, "Deleting " << (void*)&statemachine); + delete &statemachine; + } + } + else + { + llassert(statemachine.mState == bs_run); + ++iter; + } + } + if (active_statemachines.empty()) + { + // If this was the last state machine, remove mainloop from the IdleCallbacks. + AIReadAccess cscm_r(continued_statemachines_and_calling_mainloop); + if (cscm_r->continued_statemachines.empty() && cscm_r->calling_mainloop) + { + Dout(dc::statemachine, "Removing AIStateMachine::mainloop from gIdleCallbacks"); + AIWriteAccess(cscm_r)->calling_mainloop = false; + gIdleCallbacks.deleteFunction(&AIStateMachine::mainloop); + } + } +} diff --git a/indra/newview/statemachine/aistatemachine.h b/indra/newview/statemachine/aistatemachine.h new file mode 100644 index 000000000..4b25b957d --- /dev/null +++ b/indra/newview/statemachine/aistatemachine.h @@ -0,0 +1,344 @@ +/** + * @file aistatemachine.h + * @brief State machine base class + * + * Copyright (c) 2010, Aleric Inglewood. + * + * 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 2 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 . + * + * There are special exceptions to the terms and conditions of the GPL as + * it is applied to this Source Code. View the full text of the exception + * in the file doc/FLOSS-exception.txt in this software distribution. + * + * CHANGELOG + * and additional copyright holders. + * + * 01/03/2010 + * Initial version, written by Aleric Inglewood @ SL + */ + +#ifndef AISTATEMACHINE_H +#define AISTATEMACHINE_H + +#include "aithreadsafe.h" +#include "llfasttimer.h" + +//! +// A AIStateMachine is a base class that allows derived classes to +// go through asynchronous states, while the code still appears to +// be more or less sequential. +// +// These state machine objects can be reused to build more complex +// objects. +// +// It is important to note that each state has a duality: the object +// can have a state that will cause a corresponding function to be +// called; and often that function will end with changing the state +// again, to signal that it was handled. It is easy to confuse the +// function of a state with the state at the end of the function. +// For example, the state "initialize" could cause the member +// function 'init()' to be called, and at the end one would be +// inclined to set the state to "initialized". However, this is the +// wrong approach: the correct use of state names does reflect the +// functions that will be called, never the function that just was +// called. +// +// Each (derived) class goes through a series of states as follows: +// +// Creation +// | +// v +// (idle) <----. Idle until run() is called. +// | | +// Initialize | Calls initialize_impl(). +// | | +// | (idle) | Idle until cont() or set_state() is called. +// | | ^ | +// v v | | +// .-------. | | +// | Run |_, | Call multiplex_impl() until idle(), abort() or finish() is called. +// '-------' | +// | | | +// v | | +// Abort | | Calls abort_impl(). +// | | | +// v v | +// Finish | Calls finish_impl(). +// | | +// `---------' +// +// Each state causes corresponding code to be called. +// Finish cleans up whatever is done by Initialize. +// Abort should clean up additional things done by Run. +// +// The Run state is entered by calling run(). +// +// While the base class is in the Run state, it is the derived class +// that goes through different states. The state variable of the derived +// class is only valid while the base class is in the state Run. +// +// A derived class can exit the Run state by calling one of two methods: +// abort() in case of failure, or finish() in case of success. +// Respectively these set the state to Abort and Finish. +// +// State machines are run from the "idle" part of the viewer main loop. +// Often a state machine has nothing to do however. In that case it can +// call the method idle(). This will stop the state machine until +// external code changes it's state (by calling set_state()), or calls +// cont() to continue with the last state. +// +// The methods of the derived class call set_state() to change their +// own state within the bs_run state, or by calling either abort() +// or finish(). +// +// Restarting a finished state machine can also be done by calling run(), +// which will cause a re-initialize. +// +// Derived classes should implement the following constants: +// +// static state_type const min_state = first_state; +// static state_type const max_state = last_state + 1; +// +// Where first_state should be equal to BaseClass::max_state. +// These should represent the minimum and (one past) the maximum +// values of mRunState. +// +// virtual void initialize_impl(void) +// +// Initializes the derived class. +// +// virtual void multiplex_impl(void); +// +// This method should handle mRunState in a switch. +// For example: +// +// switch(mRunState) +// { +// case foo: +// handle_foo(); +// break; +// case wait_state: +// if (still_waiting()) +// { +// idle(); +// break; +// } +// set_state(working); +// /*fall-through*/ +// case working: +// do_work(); +// if (failure()) +// abort(); +// break; +// case etc: +// etc(); +// finish(); +// break; +// } +// +// virtual void abort_impl(void); +// +// A call to this method should bring the object to a state +// where finish_impl() can be called. +// +// virtual void finish_impl(void); +// +// Should cleanup whatever init_impl() did, or any of the +// states of the object where multiplex_impl() calls finish(). +// +// virtual char const* state_str_impl(state_type run_state); +// +// Should return a stringified value of run_state. +// +class AIStateMachine { + //! The type of mState + enum base_state_type { + bs_initialize, + bs_run, + bs_abort, + bs_finish, + bs_deleted + }; + + public: + typedef U32 state_type; //!< The type of mRunState + + //! Integral value equal to the state with the lowest value. + static state_type const min_state = bs_initialize; + //! Integral value one more than the state with the highest value. + static state_type const max_state = bs_deleted + 1; + + private: + base_state_type mState; //!< State of the base class. + bool mIdle; //!< True if this state machine is not running. + bool mAborted; //!< True after calling abort() and before calling run(). + U64 mSleep; //!< Non-zero while the state machine is sleeping. + + // Callback facilities. + // From within an other state machine: + AIStateMachine* mParent; //!< The parent object that started this state machine, or NULL if there isn't any. + state_type mNewParentState; //!< The state at which the parent should continue upon a successful finish. + bool mAbortParent; //!< If true, abort parent on abort(). Otherwise continue as normal. + // From outside a state machine: + struct callback_type { + typedef boost::signal signal_type; + callback_type(signal_type::slot_type const& slot) { connection = signal.connect(slot); } + ~callback_type() { connection.disconnect(); } + void callback(bool success) const { signal(success); } + private: + boost::signals::connection connection; + signal_type signal; + }; + callback_type* mCallback; //!< Pointer to signal/connection, or NULL when not connected. + + static AIThreadSafeSimple sMaxCount; //!< Number of cpu clocks below which we start a new state machine within the same frame. + + protected: + //! State of the derived class. Only valid if mState == bs_run. Call set_state to change. + state_type mRunState; + + public: + //! Create a non-running state machine. + AIStateMachine(void) : mState(bs_initialize), mIdle(true), mAborted(true), mSleep(0), mParent(NULL), mCallback(NULL) { updateSettings(); } + + protected: + //! The user should call 'deleteMe()', not delete a AIStateMachine (derived) directly. + virtual ~AIStateMachine() { llassert(mState == bs_deleted); } + + public: + //! Halt the state machine until cont() is called. + void idle(void); + + //! Temporarily halt the state machine. + void yield_frame(unsigned int frames) { mSleep = -frames; } + + //! Temporarily halt the state machine. + void yield_ms(unsigned int ms) { mSleep = get_cpu_clock_count() + LLFastTimer::countsPerSecond() * ms / 1000; } + + //! Continue running after calling idle. + void cont(void); + + //--------------------------------------- + // Changing the state. + + //! Change state to bs_run. May only be called after creation or after returning from finish(). + // If parent is non-NULL, change the parent state machine's state to new_parent_state + // upon finish, or in the case of an abort and when abort_parent is true, call parent->abort() instead. + void run(AIStateMachine* parent, state_type new_parent_state, bool abort_parent = true); + + //! Change state to 'bs_run'. May only be called after creation or after returning from finish(). + // Does not cause a callback. + void run(void) { run(NULL, 0, false); } + + //! The same as above, but pass the result of a boost::bind with _1. + // + // Here _1, if present, will be replaced with a bool indicating success. + // + // For example: + // + // + // struct Foo { void callback(AIStateMachineDerived* ptr, bool success); }; + // ... + // AIStateMachineDerived* magic = new AIStateMachineDerived; // Deleted by callback + // // Call foo_ptr->callback(magic, _1) on finish. + // state_machine->run(boost::bind(&Foo::callback, foo_ptr, magic, _1)); + // + // + // or + // + // + // struct Foo { void callback(bool success, AIStateMachineDerived const& magic); }; + // ... + // AIStateMachineDerived magic; + // // Call foo_ptr->callback(_1, magic) on finish. + // magic.run(boost::bind(&Foo::callback, foo_ptr, _1, magic)); + // + // + // or + // + // + // static void callback(void* userdata); + // ... + // AIStateMachineDerived magic; + // // Call callback(userdata) on finish. + // magic.run(boost::bind(&callback, userdata)); + // + void run(callback_type::signal_type::slot_type const& slot); + + //! Change state to 'bs_abort'. May only be called while in the bs_run state. + void abort(void); + + //! Change state to 'bs_finish'. May only be called while in the bs_run state. + void finish(void); + + //! Refine state while in the bs_run state. May only be called while in the bs_run state. + void set_state(state_type run_state); + + //! Change state to 'bs_deleted'. May only be called while in the bs_finish state. + void deleteMe(void); + + //--------------------------------------- + // Other. + + //! Called whenever the StateMachineMaxTime setting is changed. + static void updateSettings(void); + + //--------------------------------------- + // Accessors. + + //! Return true if state machine was aborted (can be used in finish_impl). + bool aborted(void) const { return mAborted; } + + //! Return true if the derived class is running (also when we are idle). + bool running(void) const { return mState == bs_run; } + + //! Return true if the derived class is running but idle. + bool waiting(void) const { return mState == bs_run && mIdle; } + + // Use some safebool idiom (http://www.artima.com/cppsource/safebool.html) rather than operator bool. + typedef state_type AIStateMachine::* const bool_type; + //! Return true if state machine successfully finished. + operator bool_type() const { return (mState == bs_initialize && !mAborted) ? &AIStateMachine::mRunState : 0; } + + //! Return a stringified state, for debugging purposes. + char const* state_str(state_type state); + + private: + static void mainloop(void*); + void multiplex(U64 current_time); + + protected: + //--------------------------------------- + // Derived class implementations. + + // Handle initializing the object. + virtual void initialize_impl(void) = 0; + + // Handle mRunState. + virtual void multiplex_impl(void) = 0; + + // Handle aborting from current bs_run state. + virtual void abort_impl(void) = 0; + + // Handle cleaning up from initialization (or post abort) state. + virtual void finish_impl(void) = 0; + + // Implemenation of state_str for run states. + virtual char const* state_str_impl(state_type run_state) const = 0; +}; + +// This case be used in state_str_impl. +#define AI_CASE_RETURN(x) do { case x: return #x; } while(0) + +#endif