diff --git a/apps/openmw/CMakeLists.txt b/apps/openmw/CMakeLists.txt index 9a5669c870..45fb5ecdaa 100644 --- a/apps/openmw/CMakeLists.txt +++ b/apps/openmw/CMakeLists.txt @@ -83,7 +83,7 @@ add_openmw_dir (mwworld store esmstore fallback actionrepair actionsoulgem livecellref actiondoor contentloader esmloader actiontrap cellreflist cellref weather projectilemanager cellpreloader datetimemanager groundcoverstore magiceffects cell ptrregistry - positioncellgrid + positioncellgrid asynctimer ) add_openmw_dir (mwphysics diff --git a/apps/openmw/engine.cpp b/apps/openmw/engine.cpp index 2ebfdeb8ae..a39ffd6bd0 100644 --- a/apps/openmw/engine.cpp +++ b/apps/openmw/engine.cpp @@ -69,6 +69,7 @@ #include "mwworld/class.hpp" #include "mwworld/datetimemanager.hpp" +#include "mwworld/asynctimer.hpp" #include "mwworld/worldimp.hpp" #include "mwrender/vismask.hpp" @@ -721,6 +722,9 @@ void OMW::Engine::prepareEngine() mStateManager = std::make_unique(mCfgMgr.getUserDataPath() / "saves", mContentFiles); mEnvironment.setStateManager(*mStateManager); + mAsyncTimer = std::make_unique(); + mEnvironment.setAsyncTimer(*mAsyncTimer); + const bool stereoEnabled = Settings::stereo().mStereoEnabled || osg::DisplaySettings::instance().get()->getStereo(); mStereoManager = std::make_unique( mViewer, stereoEnabled, Settings::camera().mNearClip, Settings::camera().mViewingDistance); @@ -1051,6 +1055,8 @@ void OMW::Engine::go() timeManager.setRenderingSimulationTime(timeManager.getRenderingSimulationTime() + dt); } + mAsyncTimer->updateTimer(); + if (stats) { // The delay is required because rendering happens in parallel to the main thread and stats from there is diff --git a/apps/openmw/engine.hpp b/apps/openmw/engine.hpp index 97b6a78ee9..467be8b5bc 100644 --- a/apps/openmw/engine.hpp +++ b/apps/openmw/engine.hpp @@ -91,6 +91,7 @@ namespace MWSound namespace MWWorld { class World; + class AsyncTimer; } namespace MWScript @@ -139,6 +140,7 @@ namespace OMW std::unique_ptr mJournal; std::unique_ptr mInputManager; std::unique_ptr mStateManager; + std::unique_ptr mAsyncTimer; std::unique_ptr mLuaManager; std::unique_ptr mLuaWorker; std::unique_ptr mL10nManager; diff --git a/apps/openmw/mwbase/environment.hpp b/apps/openmw/mwbase/environment.hpp index 94f918a60b..f2ec6f12fc 100644 --- a/apps/openmw/mwbase/environment.hpp +++ b/apps/openmw/mwbase/environment.hpp @@ -20,6 +20,7 @@ namespace MWWorld class ESMStore; class WorldModel; class Scene; + class AsyncTimer; } namespace MWBase @@ -55,6 +56,7 @@ namespace MWBase Journal* mJournal = nullptr; InputManager* mInputManager = nullptr; StateManager* mStateManager = nullptr; + MWWorld::AsyncTimer* mAsyncTimer = nullptr; LuaManager* mLuaManager = nullptr; Resource::ResourceSystem* mResourceSystem = nullptr; L10n::Manager* mL10nManager = nullptr; @@ -91,6 +93,8 @@ namespace MWBase void setStateManager(StateManager& value) { mStateManager = &value; } + void setAsyncTimer(MWWorld::AsyncTimer& value) { mAsyncTimer = &value; } + void setLuaManager(LuaManager& value) { mLuaManager = &value; } void setResourceSystem(Resource::ResourceSystem& value) { mResourceSystem = &value; } @@ -118,6 +122,8 @@ namespace MWBase Misc::NotNullPtr getStateManager() const { return mStateManager; } + Misc::NotNullPtr getAsyncTimer() const { return mAsyncTimer; } + Misc::NotNullPtr getLuaManager() const { return mLuaManager; } Misc::NotNullPtr getResourceSystem() const { return mResourceSystem; } diff --git a/apps/openmw/mwworld/asynctimer.cpp b/apps/openmw/mwworld/asynctimer.cpp new file mode 100644 index 0000000000..104c459fd6 --- /dev/null +++ b/apps/openmw/mwworld/asynctimer.cpp @@ -0,0 +1,53 @@ +#include +#include +#include +#include +#include +#include + +#include "asynctimer.hpp" + +namespace MWWorld +{ + AsyncTimer::AsyncTimer() = default; + + AsyncTimer::~AsyncTimer() = default; + + void AsyncTimer::updateTimer(){ + auto time_now = std::chrono::steady_clock::now(); + + std::vector expired_callbacks; + + while(!mTasks.empty() && mTasks.begin()->trigger_time <= time_now){ + expired_callbacks.push_back(mTasks.begin()->callback); + mTasks.erase(mTasks.begin()); + } + + for(auto& callback : expired_callbacks){ + callback(); + } + } + + AsyncTimer::TaskId AsyncTimer::setTask(float delay, Callback callback){ + if(delay < 0 || !callback) return -1; + + auto triggerTime = std::chrono::steady_clock::now() + + std::chrono::milliseconds(static_cast(delay * 1000)); + + AsyncTimer::TaskId new_id = mNext_id++; + + mTasks.insert({triggerTime, std::move(callback), new_id}); + return new_id; + } + + void AsyncTimer::cancelTask(AsyncTimer::TaskId id){ + if(id < 0) return; + + for(auto it = mTasks.begin(); it != mTasks.end(); ++it){ + if(it->id == id){ + mTasks.erase(it); + break; + } + } + } +} \ No newline at end of file diff --git a/apps/openmw/mwworld/asynctimer.hpp b/apps/openmw/mwworld/asynctimer.hpp new file mode 100644 index 0000000000..b2f44f957d --- /dev/null +++ b/apps/openmw/mwworld/asynctimer.hpp @@ -0,0 +1,45 @@ +#ifndef GAME_MWWORLD_ASYNCTIMER_H +#define GAME_MWWORLD_ASYNCTIMER_H + +#include +#include +#include + +namespace MWWorld +{ + // @brief Async timer that works when the game is paused + class AsyncTimer + { + public: + AsyncTimer(); + ~AsyncTimer(); + + using TimePoint = std::chrono::steady_clock::time_point; + using Callback = std::function; + using TaskId = uint64_t; + struct TimerTask{ + TimePoint trigger_time; + Callback callback; + TaskId id; + + // Comparator for ordering tasks by execution time (earliest first) + bool operator < (const TimerTask& other) const{ + return trigger_time < other.trigger_time; + } + }; + + // @brief Updates timer state and executes due tasks + void updateTimer(); + + // @brief Schedules a new task for delayed execution + TaskId setTask(float delay, Callback callback); + + // @brief Cancels a task by id + void cancelTask(TaskId id); + + private: + std::multiset mTasks; + TaskId mNext_id = 0; + }; +} +#endif /* GAME_MWWORLD_ASYNCTIMER_H */ \ No newline at end of file