diff --git a/apps/openmw/mwlua/asyncbindings.cpp b/apps/openmw/mwlua/asyncbindings.cpp index 9bddf75ee4..0ffb2aad8f 100644 --- a/apps/openmw/mwlua/asyncbindings.cpp +++ b/apps/openmw/mwlua/asyncbindings.cpp @@ -19,36 +19,36 @@ namespace MWLua sol::function getAsyncPackageInitializer(const Context& context) { - using TimeUnit = LuaUtil::ScriptsContainer::TimeUnit; + using TimerType = LuaUtil::ScriptsContainer::TimerType; sol::usertype api = context.mLua->sol().new_usertype("AsyncPackage"); api["registerTimerCallback"] = [](const AsyncPackageId& asyncId, std::string_view name, sol::function callback) { asyncId.mContainer->registerTimerCallback(asyncId.mScriptId, name, std::move(callback)); return TimerCallback{asyncId, std::string(name)}; }; - api["newTimerInSeconds"] = [world=context.mWorldView](const AsyncPackageId&, double delay, - const TimerCallback& callback, sol::object callbackArg) + api["newSimulationTimer"] = [world=context.mWorldView](const AsyncPackageId&, double delay, + const TimerCallback& callback, sol::object callbackArg) { callback.mAsyncId.mContainer->setupSerializableTimer( - TimeUnit::SECONDS, world->getGameTimeInSeconds() + delay, + TimerType::SIMULATION_TIME, world->getSimulationTime() + delay, callback.mAsyncId.mScriptId, callback.mName, std::move(callbackArg)); }; - api["newTimerInHours"] = [world=context.mWorldView](const AsyncPackageId&, double delay, - const TimerCallback& callback, sol::object callbackArg) + api["newGameTimer"] = [world=context.mWorldView](const AsyncPackageId&, double delay, + const TimerCallback& callback, sol::object callbackArg) { callback.mAsyncId.mContainer->setupSerializableTimer( - TimeUnit::HOURS, world->getGameTimeInHours() + delay, + TimerType::GAME_TIME, world->getGameTime() + delay, callback.mAsyncId.mScriptId, callback.mName, std::move(callbackArg)); }; - api["newUnsavableTimerInSeconds"] = [world=context.mWorldView](const AsyncPackageId& asyncId, double delay, sol::function callback) + api["newUnsavableSimulationTimer"] = [world=context.mWorldView](const AsyncPackageId& asyncId, double delay, sol::function callback) { asyncId.mContainer->setupUnsavableTimer( - TimeUnit::SECONDS, world->getGameTimeInSeconds() + delay, asyncId.mScriptId, std::move(callback)); + TimerType::SIMULATION_TIME, world->getSimulationTime() + delay, asyncId.mScriptId, std::move(callback)); }; - api["newUnsavableTimerInHours"] = [world=context.mWorldView](const AsyncPackageId& asyncId, double delay, sol::function callback) + api["newUnsavableGameTimer"] = [world=context.mWorldView](const AsyncPackageId& asyncId, double delay, sol::function callback) { asyncId.mContainer->setupUnsavableTimer( - TimeUnit::HOURS, world->getGameTimeInHours() + delay, asyncId.mScriptId, std::move(callback)); + TimerType::GAME_TIME, world->getGameTime() + delay, asyncId.mScriptId, std::move(callback)); }; api["callback"] = [](const AsyncPackageId& asyncId, sol::function fn) { diff --git a/apps/openmw/mwlua/luabindings.cpp b/apps/openmw/mwlua/luabindings.cpp index c525fd8a23..5a9e33faef 100644 --- a/apps/openmw/mwlua/luabindings.cpp +++ b/apps/openmw/mwlua/luabindings.cpp @@ -22,11 +22,32 @@ namespace MWLua return LuaUtil::makeReadOnly(res); } + static void addTimeBindings(sol::table& api, const Context& context, bool global) + { + api["getSimulationTime"] = [world=context.mWorldView]() { return world->getSimulationTime(); }; + api["getSimulationTimeScale"] = [world=context.mWorldView]() { return world->getSimulationTimeScale(); }; + api["getGameTime"] = [world=context.mWorldView]() { return world->getGameTime(); }; + api["getGameTimeScale"] = [world=context.mWorldView]() { return world->getGameTimeScale(); }; + api["isWorldPaused"] = [world=context.mWorldView]() { return world->isPaused(); }; + + if (!global) + return; + + api["setGameTimeScale"] = [world=context.mWorldView](double scale) { world->setGameTimeScale(scale); }; + + // TODO: Ability to make game time slower or faster than real time (needed for example for mechanics like VATS) + // api["setSimulationTimeScale"] = [](double scale) {}; + + // TODO: Ability to pause/resume world from Lua (needed for UI dehardcoding) + // api["pause"] = []() {}; + // api["resume"] = []() {}; + } + sol::table initCorePackage(const Context& context) { auto* lua = context.mLua; sol::table api(lua->sol(), sol::create); - api["API_REVISION"] = 12; + api["API_REVISION"] = 13; api["quit"] = [lua]() { Log(Debug::Warning) << "Quit requested by a Lua script.\n" << lua->debugTraceback(); @@ -36,9 +57,7 @@ namespace MWLua { context.mGlobalEventQueue->push_back({std::move(eventName), LuaUtil::serialize(eventData, context.mSerializer)}); }; - api["getGameTimeInSeconds"] = [world=context.mWorldView]() { return world->getGameTimeInSeconds(); }; - api["getGameTimeInHours"] = [world=context.mWorldView]() { return world->getGameTimeInHours(); }; - api["isWorldPaused"] = [world=context.mWorldView]() { return world->isPaused(); }; + addTimeBindings(api, context, false); api["OBJECT_TYPE"] = definitionList(*lua, { "Activator", "Armor", "Book", "Clothing", "Creature", "Door", "Ingredient", @@ -73,6 +92,7 @@ namespace MWLua { sol::table api(context.mLua->sol(), sol::create); WorldView* worldView = context.mWorldView; + addTimeBindings(api, context, true); api["getCellByName"] = [worldView=context.mWorldView](const std::string& name) -> sol::optional { MWWorld::CellStore* cell = worldView->findNamedCell(name); diff --git a/apps/openmw/mwlua/luamanagerimp.cpp b/apps/openmw/mwlua/luamanagerimp.cpp index be5764c32f..658ee7c809 100644 --- a/apps/openmw/mwlua/luamanagerimp.cpp +++ b/apps/openmw/mwlua/luamanagerimp.cpp @@ -113,13 +113,13 @@ namespace MWLua if (!mWorldView.isPaused()) { // Update time and process timers - double seconds = mWorldView.getGameTimeInSeconds() + frameDuration; - mWorldView.setGameTimeInSeconds(seconds); - double hours = mWorldView.getGameTimeInHours(); + double simulationTime = mWorldView.getSimulationTime() + frameDuration; + mWorldView.setSimulationTime(simulationTime); + double gameTime = mWorldView.getGameTime(); - mGlobalScripts.processTimers(seconds, hours); + mGlobalScripts.processTimers(simulationTime, gameTime); for (LocalScripts* scripts : mActiveLocalScripts) - scripts->processTimers(seconds, hours); + scripts->processTimers(simulationTime, gameTime); } // Receive events diff --git a/apps/openmw/mwlua/worldview.cpp b/apps/openmw/mwlua/worldview.cpp index 219035745a..35b1db7a93 100644 --- a/apps/openmw/mwlua/worldview.cpp +++ b/apps/openmw/mwlua/worldview.cpp @@ -70,16 +70,16 @@ namespace MWLua removeFromGroup(*group, ptr); } - double WorldView::getGameTimeInHours() const + double WorldView::getGameTime() const { MWBase::World* world = MWBase::Environment::get().getWorld(); MWWorld::TimeStamp timeStamp = world->getTimeStamp(); - return static_cast(timeStamp.getDay()) * 24 + timeStamp.getHour(); + return (static_cast(timeStamp.getDay()) * 24 + timeStamp.getHour()) * 3600.0; } void WorldView::load(ESM::ESMReader& esm) { - esm.getHNT(mGameSeconds, "LUAW"); + esm.getHNT(mSimulationTime, "LUAW"); ObjectId lastAssignedId; lastAssignedId.load(esm, true); mObjectRegistry.setLastAssignedId(lastAssignedId); @@ -87,7 +87,7 @@ namespace MWLua void WorldView::save(ESM::ESMWriter& esm) const { - esm.writeHNT("LUAW", mGameSeconds); + esm.writeHNT("LUAW", mSimulationTime); mObjectRegistry.getLastAssignedId().save(esm, true); } diff --git a/apps/openmw/mwlua/worldview.hpp b/apps/openmw/mwlua/worldview.hpp index a8befd4685..e5b5fe4f67 100644 --- a/apps/openmw/mwlua/worldview.hpp +++ b/apps/openmw/mwlua/worldview.hpp @@ -22,13 +22,16 @@ namespace MWLua // Whether the world is paused (i.e. game time is not changing and actors don't move). bool isPaused() const { return mPaused; } - // Returns the number of seconds passed from the beginning of the game. - double getGameTimeInSeconds() const { return mGameSeconds; } - void setGameTimeInSeconds(double t) { mGameSeconds = t; } + // The number of seconds passed from the beginning of the game. + double getSimulationTime() const { return mSimulationTime; } + void setSimulationTime(double t) { mSimulationTime = t; } + double getSimulationTimeScale() const { return 1.0; } - // Returns the number of game hours passed from the beginning of the game. - // Note that the number of seconds in a game hour is not fixed. - double getGameTimeInHours() const; + // The game time (in game seconds) passed from the beginning of the game. + // Note that game time generally goes faster than the simulation time. + double getGameTime() const; + double getGameTimeScale() const { return MWBase::Environment::get().getWorld()->getTimeScaleFactor(); } + void setGameTimeScale(double s) { MWBase::Environment::get().getWorld()->setGlobalFloat("timescale", s); } ObjectIdList getActivatorsInScene() const { return mActivatorsInScene.mList; } ObjectIdList getActorsInScene() const { return mActorsInScene.mList; } @@ -76,7 +79,7 @@ namespace MWLua ObjectGroup mDoorsInScene; ObjectGroup mItemsInScene; - double mGameSeconds = 0; + double mSimulationTime = 0; bool mPaused = false; }; diff --git a/apps/openmw_test_suite/lua/test_scriptscontainer.cpp b/apps/openmw_test_suite/lua/test_scriptscontainer.cpp index 8be02a2f13..779debb4e6 100644 --- a/apps/openmw_test_suite/lua/test_scriptscontainer.cpp +++ b/apps/openmw_test_suite/lua/test_scriptscontainer.cpp @@ -364,7 +364,7 @@ return { TEST_F(LuaScriptsContainerTest, Timers) { - using TimeUnit = LuaUtil::ScriptsContainer::TimeUnit; + using TimerType = LuaUtil::ScriptsContainer::TimerType; LuaUtil::ScriptsContainer scripts(&mLua, "Test"); int test1Id = *mCfg.findId("test1.lua"); int test2Id = *mCfg.findId("test2.lua"); @@ -387,18 +387,18 @@ return { scripts.processTimers(1, 2); - scripts.setupSerializableTimer(TimeUnit::SECONDS, 10, test1Id, "B", sol::make_object(mLua.sol(), 3)); - scripts.setupSerializableTimer(TimeUnit::HOURS, 10, test2Id, "B", sol::make_object(mLua.sol(), 4)); - scripts.setupSerializableTimer(TimeUnit::SECONDS, 5, test1Id, "A", sol::make_object(mLua.sol(), 1)); - scripts.setupSerializableTimer(TimeUnit::HOURS, 5, test2Id, "A", sol::make_object(mLua.sol(), 2)); - scripts.setupSerializableTimer(TimeUnit::SECONDS, 15, test1Id, "A", sol::make_object(mLua.sol(), 10)); - scripts.setupSerializableTimer(TimeUnit::SECONDS, 15, test1Id, "B", sol::make_object(mLua.sol(), 20)); + scripts.setupSerializableTimer(TimerType::SIMULATION_TIME, 10, test1Id, "B", sol::make_object(mLua.sol(), 3)); + scripts.setupSerializableTimer(TimerType::GAME_TIME, 10, test2Id, "B", sol::make_object(mLua.sol(), 4)); + scripts.setupSerializableTimer(TimerType::SIMULATION_TIME, 5, test1Id, "A", sol::make_object(mLua.sol(), 1)); + scripts.setupSerializableTimer(TimerType::GAME_TIME, 5, test2Id, "A", sol::make_object(mLua.sol(), 2)); + scripts.setupSerializableTimer(TimerType::SIMULATION_TIME, 15, test1Id, "A", sol::make_object(mLua.sol(), 10)); + scripts.setupSerializableTimer(TimerType::SIMULATION_TIME, 15, test1Id, "B", sol::make_object(mLua.sol(), 20)); - scripts.setupUnsavableTimer(TimeUnit::SECONDS, 10, test2Id, fn2); - scripts.setupUnsavableTimer(TimeUnit::HOURS, 10, test1Id, fn2); - scripts.setupUnsavableTimer(TimeUnit::SECONDS, 5, test2Id, fn1); - scripts.setupUnsavableTimer(TimeUnit::HOURS, 5, test1Id, fn1); - scripts.setupUnsavableTimer(TimeUnit::SECONDS, 15, test2Id, fn1); + scripts.setupUnsavableTimer(TimerType::SIMULATION_TIME, 10, test2Id, fn2); + scripts.setupUnsavableTimer(TimerType::GAME_TIME, 10, test1Id, fn2); + scripts.setupUnsavableTimer(TimerType::SIMULATION_TIME, 5, test2Id, fn1); + scripts.setupUnsavableTimer(TimerType::GAME_TIME, 5, test1Id, fn1); + scripts.setupUnsavableTimer(TimerType::SIMULATION_TIME, 15, test2Id, fn1); EXPECT_EQ(counter1, 0); EXPECT_EQ(counter3, 0); diff --git a/components/esm/luascripts.cpp b/components/esm/luascripts.cpp index c831cbbbfc..53beb02d82 100644 --- a/components/esm/luascripts.cpp +++ b/components/esm/luascripts.cpp @@ -72,7 +72,7 @@ void ESM::LuaScripts::load(ESMReader& esm) { esm.getSubHeader(); LuaTimer timer; - esm.getT(timer.mUnit); + esm.getT(timer.mType); esm.getT(timer.mTime); timer.mCallbackName = esm.getHNString("LUAC"); timer.mCallbackArgument = loadLuaBinaryData(esm); @@ -91,7 +91,7 @@ void ESM::LuaScripts::save(ESMWriter& esm) const for (const LuaTimer& timer : script.mTimers) { esm.startSubRecord("LUAT"); - esm.writeT(timer.mUnit); + esm.writeT(timer.mType); esm.writeT(timer.mTime); esm.endRecord("LUAT"); esm.writeHNString("LUAC", timer.mCallbackName); diff --git a/components/esm/luascripts.hpp b/components/esm/luascripts.hpp index e6f7113c16..985d756fce 100644 --- a/components/esm/luascripts.hpp +++ b/components/esm/luascripts.hpp @@ -51,13 +51,13 @@ namespace ESM struct LuaTimer { - enum class TimeUnit : bool + enum class Type : bool { - SECONDS = 0, - HOURS = 1, + SIMULATION_TIME = 0, + GAME_TIME = 1, }; - TimeUnit mUnit; + Type mType; double mTime; std::string mCallbackName; std::string mCallbackArgument; // Serialized Lua table. It is a binary data. Can contain '\0'. diff --git a/components/lua/luastate.cpp b/components/lua/luastate.cpp index 6ff698c9c2..bfe9cb513c 100644 --- a/components/lua/luastate.cpp +++ b/components/lua/luastate.cpp @@ -52,7 +52,7 @@ namespace LuaUtil LuaState::LuaState(const VFS::Manager* vfs, const ScriptsConfiguration* conf) : mConf(conf), mVFS(vfs) { mLua.open_libraries(sol::lib::base, sol::lib::coroutine, sol::lib::math, - sol::lib::string, sol::lib::table, sol::lib::debug); + sol::lib::string, sol::lib::table, sol::lib::os, sol::lib::debug); mLua["math"]["randomseed"](static_cast(std::time(nullptr))); mLua["math"]["randomseed"] = []{}; @@ -85,6 +85,11 @@ namespace LuaUtil if (mLua[s] == sol::nil) throw std::logic_error("Lua package not found: " + s); mCommonPackages[s] = mSandboxEnv[s] = makeReadOnly(mLua[s]); } + mCommonPackages["os"] = mSandboxEnv["os"] = makeReadOnly(tableFromPairs({ + {"date", mLua["os"]["date"]}, + {"difftime", mLua["os"]["difftime"]}, + {"time", mLua["os"]["time"]} + })); } LuaState::~LuaState() diff --git a/components/lua/scriptscontainer.cpp b/components/lua/scriptscontainer.cpp index eb9da7a60b..780d10cebd 100644 --- a/components/lua/scriptscontainer.cpp +++ b/components/lua/scriptscontainer.cpp @@ -309,21 +309,21 @@ namespace LuaUtil void ScriptsContainer::save(ESM::LuaScripts& data) { std::map> timers; - auto saveTimerFn = [&](const Timer& timer, TimeUnit timeUnit) + auto saveTimerFn = [&](const Timer& timer, TimerType timerType) { if (!timer.mSerializable) return; ESM::LuaTimer savedTimer; savedTimer.mTime = timer.mTime; - savedTimer.mUnit = timeUnit; + savedTimer.mType = timerType; savedTimer.mCallbackName = std::get(timer.mCallback); savedTimer.mCallbackArgument = timer.mSerializedArg; timers[timer.mScriptId].push_back(std::move(savedTimer)); }; - for (const Timer& timer : mSecondsTimersQueue) - saveTimerFn(timer, TimeUnit::SECONDS); - for (const Timer& timer : mHoursTimersQueue) - saveTimerFn(timer, TimeUnit::HOURS); + for (const Timer& timer : mSimulationTimersQueue) + saveTimerFn(timer, TimerType::SIMULATION_TIME); + for (const Timer& timer : mGameTimersQueue) + saveTimerFn(timer, TimerType::GAME_TIME); data.mScripts.clear(); for (auto& [scriptId, script] : mScripts) { @@ -408,17 +408,17 @@ namespace LuaUtil // updates refnums, so timer.mSerializedArg may be not equal to savedTimer.mCallbackArgument. timer.mSerializedArg = serialize(timer.mArg, mSerializer); - if (savedTimer.mUnit == TimeUnit::HOURS) - mHoursTimersQueue.push_back(std::move(timer)); + if (savedTimer.mType == TimerType::GAME_TIME) + mGameTimersQueue.push_back(std::move(timer)); else - mSecondsTimersQueue.push_back(std::move(timer)); + mSimulationTimersQueue.push_back(std::move(timer)); } catch (std::exception& e) { printError(scriptId, "can not load timer", e); } } } - std::make_heap(mSecondsTimersQueue.begin(), mSecondsTimersQueue.end()); - std::make_heap(mHoursTimersQueue.begin(), mHoursTimersQueue.end()); + std::make_heap(mSimulationTimersQueue.begin(), mSimulationTimersQueue.end()); + std::make_heap(mGameTimersQueue.begin(), mGameTimersQueue.end()); } ScriptsContainer::~ScriptsContainer() @@ -437,8 +437,8 @@ namespace LuaUtil for (auto& [_, handlers] : mEngineHandlers) handlers->mList.clear(); mEventHandlers.clear(); - mSecondsTimersQueue.clear(); - mHoursTimersQueue.clear(); + mSimulationTimersQueue.clear(); + mGameTimersQueue.clear(); mPublicInterfaces.clear(); // Assigned by LuaUtil::makeReadOnly, but `clear` removes it, so we need to assign it again. @@ -464,7 +464,7 @@ namespace LuaUtil std::push_heap(timerQueue.begin(), timerQueue.end()); } - void ScriptsContainer::setupSerializableTimer(TimeUnit timeUnit, double time, int scriptId, + void ScriptsContainer::setupSerializableTimer(TimerType type, double time, int scriptId, std::string_view callbackName, sol::object callbackArg) { Timer t; @@ -474,10 +474,10 @@ namespace LuaUtil t.mTime = time; t.mArg = callbackArg; t.mSerializedArg = serialize(t.mArg, mSerializer); - insertTimer(timeUnit == TimeUnit::HOURS ? mHoursTimersQueue : mSecondsTimersQueue, std::move(t)); + insertTimer(type == TimerType::GAME_TIME ? mGameTimersQueue : mSimulationTimersQueue, std::move(t)); } - void ScriptsContainer::setupUnsavableTimer(TimeUnit timeUnit, double time, int scriptId, sol::function callback) + void ScriptsContainer::setupUnsavableTimer(TimerType type, double time, int scriptId, sol::function callback) { Timer t; t.mScriptId = scriptId; @@ -488,7 +488,7 @@ namespace LuaUtil getScript(t.mScriptId).mTemporaryCallbacks.emplace(mTemporaryCallbackCounter, std::move(callback)); mTemporaryCallbackCounter++; - insertTimer(timeUnit == TimeUnit::HOURS ? mHoursTimersQueue : mSecondsTimersQueue, std::move(t)); + insertTimer(type == TimerType::GAME_TIME ? mGameTimersQueue : mSimulationTimersQueue, std::move(t)); } void ScriptsContainer::callTimer(const Timer& t) @@ -524,10 +524,10 @@ namespace LuaUtil } } - void ScriptsContainer::processTimers(double gameSeconds, double gameHours) + void ScriptsContainer::processTimers(double simulationTime, double gameTime) { - updateTimerQueue(mSecondsTimersQueue, gameSeconds); - updateTimerQueue(mHoursTimersQueue, gameHours); + updateTimerQueue(mSimulationTimersQueue, simulationTime); + updateTimerQueue(mGameTimersQueue, gameTime); } } diff --git a/components/lua/scriptscontainer.hpp b/components/lua/scriptscontainer.hpp index 1863d04669..fcbd2ba0b7 100644 --- a/components/lua/scriptscontainer.hpp +++ b/components/lua/scriptscontainer.hpp @@ -73,7 +73,7 @@ namespace LuaUtil ScriptsContainer* mContainer; int mIndex; // index in LuaUtil::ScriptsConfiguration }; - using TimeUnit = ESM::LuaTimer::TimeUnit; + using TimerType = ESM::LuaTimer::Type; // `namePrefix` is a common prefix for all scripts in the container. Used in logs for error messages and `print` output. // `autoStartMode` specifies the list of scripts that should be autostarted in this container; the list itself is @@ -99,8 +99,7 @@ namespace LuaUtil bool hasScript(int scriptId) const { return mScripts.count(scriptId) != 0; } void removeScript(int scriptId); - // Processes timers. gameSeconds and gameHours are time (in seconds and in game hours) passed from the game start. - void processTimers(double gameSeconds, double gameHours); + void processTimers(double simulationTime, double gameTime); // Calls `onUpdate` (if present) for every script in the container. // Handlers are called in the same order as scripts were added. @@ -134,17 +133,17 @@ namespace LuaUtil void registerTimerCallback(int scriptId, std::string_view callbackName, sol::function callback); // Sets up a timer, that can be automatically saved and loaded. - // timeUnit - game seconds (TimeUnit::Seconds) or game hours (TimeUnit::Hours). + // type - the type of timer, either SIMULATION_TIME or GAME_TIME. // time - the absolute game time (in seconds or in hours) when the timer should be executed. // scriptPath - script path in VFS is used as script id. The script with the given path should already present in the container. // callbackName - callback (should be registered in advance) for this timer. // callbackArg - parameter for the callback (should be serializable). - void setupSerializableTimer(TimeUnit timeUnit, double time, int scriptId, + void setupSerializableTimer(TimerType type, double time, int scriptId, std::string_view callbackName, sol::object callbackArg); - // Creates a timer. `callback` is an arbitrary Lua function. This type of timers is called "unsavable" - // because it can not be stored in saves. I.e. loading a saved game will not fully restore the state. - void setupUnsavableTimer(TimeUnit timeUnit, double time, int scriptId, sol::function callback); + // Creates a timer. `callback` is an arbitrary Lua function. These timers are called "unsavable" + // because they can not be stored in saves. I.e. loading a saved game will not fully restore the state. + void setupUnsavableTimer(TimerType type, double time, int scriptId, sol::function callback); protected: struct Handler @@ -237,8 +236,8 @@ namespace LuaUtil std::map mEngineHandlers; std::map> mEventHandlers; - std::vector mSecondsTimersQueue; - std::vector mHoursTimersQueue; + std::vector mSimulationTimersQueue; + std::vector mGameTimersQueue; int64_t mTemporaryCallbackCounter = 0; }; diff --git a/docs/source/reference/lua-scripting/api.rst b/docs/source/reference/lua-scripting/api.rst index dd9d151482..635fcfb95e 100644 --- a/docs/source/reference/lua-scripting/api.rst +++ b/docs/source/reference/lua-scripting/api.rst @@ -19,6 +19,7 @@ Lua API reference openmw_ui openmw_camera openmw_aux_util + openmw_aux_time interface_camera @@ -75,6 +76,8 @@ Sources can be found in ``resources/vfs/openmw_aux``. In theory mods can overrid +=========================================================+====================+===============================================================+ |:ref:`openmw_aux.util ` | everywhere | | Miscellaneous utils | +---------------------------------------------------------+--------------------+---------------------------------------------------------------+ +|:ref:`openmw_aux.time ` | everywhere | | Timers and game time utils | ++---------------------------------------------------------+--------------------+---------------------------------------------------------------+ **Interfaces of built-in scripts** diff --git a/docs/source/reference/lua-scripting/openmw_aux_time.rst b/docs/source/reference/lua-scripting/openmw_aux_time.rst new file mode 100644 index 0000000000..120d888a01 --- /dev/null +++ b/docs/source/reference/lua-scripting/openmw_aux_time.rst @@ -0,0 +1,5 @@ +Package openmw_aux.time +======================= + +.. raw:: html + :file: generated_html/openmw_aux_time.html diff --git a/docs/source/reference/lua-scripting/overview.rst b/docs/source/reference/lua-scripting/overview.rst index 938611635a..103a93c559 100644 --- a/docs/source/reference/lua-scripting/overview.rst +++ b/docs/source/reference/lua-scripting/overview.rst @@ -15,10 +15,11 @@ Here are starting points for learning Lua: Each script works in a separate sandbox and doesn't have any access to the underlying operating system. Only a limited list of allowed standard libraries can be used: `coroutine `__, -`math `__, +`math `__ (except `math.randomseed` -- it is called by the engine on startup and not available from scripts), `string `__, -`table `__. -These libraries are loaded automatically and are always available (except the function `math.randomseed` -- it is called by the engine on startup and not available from scripts). +`table `__, +`os `__ (only `os.date`, `os.difftime`, `os.time`). +These libraries are loaded automatically and are always available. Allowed `basic functions `__: ``assert``, ``error``, ``ipairs``, ``next``, ``pairs``, ``pcall``, ``print``, ``select``, ``tonumber``, ``tostring``, ``type``, ``unpack``, ``xpcall``, ``rawequal``, ``rawget``, ``rawset``, ``getmetatable``, ``setmetatable``. @@ -366,6 +367,8 @@ Sources can be found in ``resources/vfs/openmw_aux``. In theory mods can overrid +=========================================================+====================+===============================================================+ |:ref:`openmw_aux.util ` | everywhere | | Miscellaneous utils | +---------------------------------------------------------+--------------------+---------------------------------------------------------------+ +|:ref:`openmw_aux.time ` | everywhere | | Timers and game time utils | ++---------------------------------------------------------+--------------------+---------------------------------------------------------------+ They can be loaded with ``require`` the same as API packages. For example: diff --git a/files/builtin_scripts/openmw_aux/time.lua b/files/builtin_scripts/openmw_aux/time.lua new file mode 100644 index 0000000000..a6aac0fa1a --- /dev/null +++ b/files/builtin_scripts/openmw_aux/time.lua @@ -0,0 +1,104 @@ +--- +-- `openmw_aux.time` defines utility functions for timers. +-- Implementation can be found in `resources/vfs/openmw_aux/time.lua`. +-- @module time +-- @usage local time = require('openmw_aux.time') + +local time = { + second = 1, + minute = 60, + hour = 3600, + day = 3600 * 24, + GameTime = 'GameTime', + SimulationTime = 'SimulationTime', +} + +--- +-- Alias of async:registerTimerCallback ; register a function as a timer callback. +-- @function [parent=#time] registerTimerCallback +-- @param #string name +-- @param #function func +-- @return openmw.async#TimerCallback +function time.registerTimerCallback(name, fn) + local async = require('openmw.async') + return async:registerTimerCallback(name, fn) +end + +--- +-- Alias of async:newSimulationTimer ; call callback(arg) in `delay` game seconds. +-- Callback must be registered in advance. +-- @function [parent=#time] newGameTimer +-- @param #number delay +-- @param openmw.async#TimerCallback callback A callback returned by `registerTimerCallback` +-- @param arg An argument for `callback`; can be `nil`. +function time.newGameTimer(delay, callback, callbackArg) + local async = require('openmw.async') + return async:newGameTimer(delay, callback, callbackArg) +end + +--- +-- Alias of async:newSimulationTimer ; call callback(arg) in `delay` simulation seconds. +-- Callback must be registered in advance. +-- @function [parent=#time] newSimulationTimer +-- @param #number delay +-- @param openmw.async#TimerCallback callback A callback returned by `registerTimerCallback` +-- @param arg An argument for `callback`; can be `nil`. +function time.newSimulationTimer(delay, callback, callbackArg) + local async = require('openmw.async') + return async:newSimulationTimer(delay, callback, callbackArg) +end + +--- +-- Run given function repeatedly. +-- Note that loading a save stops the evaluation. If it should work always, call it during initialization of the script (i.e. not in a handler) +-- @function [parent=#time] runRepeatedly +-- @param #function fn the function that should be called +-- @param #number period interval +-- @param #table options additional options `initialDelay` and `type`. +-- `initialDelay` - delay before the first call. If missed then the delay is a random number in range [0, N]. Randomization is used for performance reasons -- to prevent all scripts from doing time consuming operations at the same time. +-- `type` - either `time.SimulationTime` (by default, timer uses simulation time) or `time.GameTime` (timer uses game time). +-- @return #function a function without arguments that can be used to stop the periodical evaluation. +-- @usage +-- local stopFn = time.runRepeatedly(function() print('Test') end, +-- 5 * time.second) -- print 'Test' every 5 seconds +-- stopFn() -- stop printing 'Test' +-- time.runRepeatedly( -- print 'Test' every 5 minutes with initial 30 second delay +-- function() print('Test2') end, 5 * time.minute, +-- { initialDelay = 30 * time.second }) +-- @usage +-- local timeBeforeMidnight = time.day - time.gameTime() % time.day +-- time.runRepeatedly(doSomething, time.day, { +-- initialDelay = timeBeforeMidnight, +-- type = time.GameTime, +-- }) -- call `doSomething` at the end of every game day. +function time.runRepeatedly(fn, period, options) + if period <= 0 then + error('Period must be positive. If you want it to be as small '.. + 'as possible, use the engine handler `onUpdate` instead', 2) + end + local async = require('openmw.async') + local core = require('openmw.core') + local initialDelay = (options and options.initialDelay) or math.random() * period + local getTimeFn, newTimerFn + if (options and options.type) == time.GameTime then + getTimeFn = core.getGameTime + newTimerFn = async.newUnsavableGameTimer + else + getTimeFn = core.getSimulationTime + newTimerFn = async.newUnsavableSimulationTimer + end + local baseTime = getTimeFn() + initialDelay + local breakFlag = false + local wrappedFn + wrappedFn = function() + if breakFlag then return end + fn() + local nextDelay = 1.5 * period - math.fmod(getTimeFn() - baseTime + period / 2, period) + newTimerFn(async, nextDelay, wrappedFn) + end + newTimerFn(async, initialDelay, wrappedFn) + return function() breakFlag = true end +end + +return time + diff --git a/files/builtin_scripts/openmw_aux/util.lua b/files/builtin_scripts/openmw_aux/util.lua index ba52ced2bd..8d4f7c1fd7 100644 --- a/files/builtin_scripts/openmw_aux/util.lua +++ b/files/builtin_scripts/openmw_aux/util.lua @@ -28,72 +28,5 @@ function aux_util.findNearestTo(point, objectList) return res, resDist end -------------------------------------------------------------------------------- --- Runs given function every N game seconds (seconds when the game is not paused). --- Note that loading a save stops the evaluation. If it should work always, call it in 2 places -- --- when a script starts and in the engine handler `onLoad`. --- @function [parent=#util] runEveryNSeconds --- @param #number N interval in seconds --- @param #function fn the function that should be called every N seconds --- @param #number initialDelay optional argument -- delay in seconds before the first call. If missed then the delay is a random number in range [0, N]. Randomization is used for performance reasons -- to prevent all scripts from doing time consuming operations at the same time. --- @return #function a function without arguments that can be used to stop the periodical evaluation. --- @usage --- local stopFn = aux_util.runEveryNSeconds(5, function() print('Test') end) -- print 'Test' every 5 seconds --- stopFn() -- stop printing 'Test' --- aux_util.runEveryNSeconds(5, function() print('Test2') end, 1) -- print 'Test' every 5 seconds starting from the next second -function aux_util.runEveryNSeconds(N, fn, initialDelay) - if N <= 0 then - error('Interval must be positive. If you want it to be as small '.. - 'as possible, use the engine handler `onUpdate` instead', 2) - end - local async = require('openmw.async') - local core = require('openmw.core') - local breakFlag = false - local initialDelay = initialDelay or math.random() * N - local baseTime = core.getGameTimeInSeconds() + initialDelay - local wrappedFn - wrappedFn = function() - if breakFlag then return end - fn() - local nextDelay = 1.5 * N - math.fmod(core.getGameTimeInSeconds() - baseTime + N / 2, N) - async:newUnsavableTimerInSeconds(nextDelay, wrappedFn) - end - async:newUnsavableTimerInSeconds(initialDelay, wrappedFn) - return function() breakFlag = true end -end - -------------------------------------------------------------------------------- --- Runs given function every N game hours. --- Note that loading a save stops the evaluation. If it should work always, call it in 2 places -- --- when a script starts and in the engine handler `onLoad`. --- @function [parent=#util] runEveryNHours --- @param #number N interval in game hours --- @param #function fn the function that should be called every N game hours --- @param #number initialDelay optional argument -- delay in game hours before the first call. If missed then the delay is a random number in range [0, N]. Randomization is used for performance reasons -- to prevent all scripts from doing time consuming operations at the same time. --- @return #function a function without arguments that can be used to stop the periodical evaluation. --- @usage --- local timeBeforeMidnight = 24 - math.fmod(core.getGameTimeInHours(), 24) --- aux_util.runEveryNHours(24, doSomething, timeBeforeMidnight) -- call `doSomething` at the end of every game day. -function aux_util.runEveryNHours(N, fn, initialDelay) - if N <= 0 then - error('Interval must be positive. If you want it to be as small '.. - 'as possible, use the engine handler `onUpdate` instead', 2) - end - local async = require('openmw.async') - local core = require('openmw.core') - local breakFlag = false - local initialDelay = initialDelay or math.random() * N - local baseTime = core.getGameTimeInHours() + initialDelay - local wrappedFn - wrappedFn = function() - if breakFlag then return end - fn() - local nextDelay = 1.5 * N - math.fmod(core.getGameTimeInHours() - baseTime + N / 2, N) - async:newUnsavableTimerInHours(nextDelay, wrappedFn) - end - async:newUnsavableTimerInHours(initialDelay, wrappedFn) - return function() breakFlag = true end -end - return aux_util diff --git a/files/lua_api/openmw/async.lua b/files/lua_api/openmw/async.lua index cc3a233f8a..61d5763a0b 100644 --- a/files/lua_api/openmw/async.lua +++ b/files/lua_api/openmw/async.lua @@ -15,35 +15,35 @@ -- @return #TimerCallback ------------------------------------------------------------------------------- --- Calls callback(arg) in `delay` seconds. +-- Calls callback(arg) in `delay` simulation seconds. -- Callback must be registered in advance. --- @function [parent=#async] newTimerInSeconds +-- @function [parent=#async] newSimulationTimer -- @param self -- @param #number delay -- @param #TimerCallback callback A callback returned by `registerTimerCallback` -- @param arg An argument for `callback`; can be `nil`. ------------------------------------------------------------------------------- --- Calls callback(arg) in `delay` game hours. +-- Calls callback(arg) in `delay` game seconds. -- Callback must be registered in advance. --- @function [parent=#async] newTimerInHours +-- @function [parent=#async] newGameTimer -- @param self -- @param #number delay -- @param #TimerCallback callback A callback returned by `registerTimerCallback` -- @param arg An argument for `callback`; can be `nil`. ------------------------------------------------------------------------------- --- Calls `func()` in `delay` seconds. +-- Calls `func()` in `delay` simulation seconds. -- The timer will be lost if the game is saved and loaded. --- @function [parent=#async] newUnsavableTimerInSeconds +-- @function [parent=#async] newUnsavableSimulationTimer -- @param self -- @param #number delay -- @param #function func ------------------------------------------------------------------------------- --- Calls `func()` in `delay` game hours. +-- Calls `func()` in `delay` game seconds. -- The timer will be lost if the game is saved and loaded. --- @function [parent=#async] newUnsavableTimerInHours +-- @function [parent=#async] newUnsavableGameTimer -- @param self -- @param #number delay -- @param #function func diff --git a/files/lua_api/openmw/core.lua b/files/lua_api/openmw/core.lua index 016267d39d..d18be52406 100644 --- a/files/lua_api/openmw/core.lua +++ b/files/lua_api/openmw/core.lua @@ -21,15 +21,24 @@ -- @param eventData ------------------------------------------------------------------------------- --- Game time in seconds. --- The number of seconds passed in the game world since starting a new game. --- @function [parent=#core] getGameTimeInSeconds +-- Simulation time in seconds. +-- The number of simulation seconds passed in the game world since starting a new game. +-- @function [parent=#core] getSimulationTime -- @return #number ------------------------------------------------------------------------------- --- Current time of the game world in hours. --- Note that the number of game seconds in a game hour is not guaranteed to be fixed. --- @function [parent=#core] getGameTimeInHours +-- The scale of simulation time relative to real time. +-- @function [parent=#core] getSimulationTimeScale +-- @return #number + +------------------------------------------------------------------------------- +-- Game time in seconds. +-- @function [parent=#core] getGameTime +-- @return #number + +------------------------------------------------------------------------------- +-- The scale of game time relative to simulation time. +-- @function [parent=#core] getGameTimeScale -- @return #number ------------------------------------------------------------------------------- diff --git a/files/lua_api/openmw/world.lua b/files/lua_api/openmw/world.lua index 90ea23b4f5..690f0bd566 100644 --- a/files/lua_api/openmw/world.lua +++ b/files/lua_api/openmw/world.lua @@ -29,6 +29,36 @@ -- @param #number gridY -- @return openmw.core#Cell +------------------------------------------------------------------------------- +-- Simulation time in seconds. +-- The number of simulation seconds passed in the game world since starting a new game. +-- @function [parent=#core] getSimulationTime +-- @return #number + +------------------------------------------------------------------------------- +-- The scale of simulation time relative to real time. +-- @function [parent=#core] getSimulationTimeScale +-- @return #number + +------------------------------------------------------------------------------- +-- Game time in seconds. +-- @function [parent=#core] getGameTime +-- @return #number + +------------------------------------------------------------------------------- +-- The scale of game time relative to simulation time. +-- @function [parent=#core] getGameTimeScale +-- @return #number + +------------------------------------------------------------------------------- +-- Set the ratio of game time speed to simulation time speed. +-- @function [parent=#world] setGameTimeScale +-- @param #number ratio + +------------------------------------------------------------------------------- +-- Whether the world is paused (onUpdate doesn't work when the world is paused). +-- @function [parent=#world] isWorldPaused +-- @return #boolean return nil diff --git a/files/lua_api/os.doclua b/files/lua_api/os.doclua new file mode 100644 index 0000000000..127ccd049a --- /dev/null +++ b/files/lua_api/os.doclua @@ -0,0 +1,64 @@ +------------------------------------------------------------------------------- +-- Operating System Facilities. +-- This library is implemented through table os. +-- @module os + + +------------------------------------------------------------------------------- +-- Returns a string or a table containing date and time, formatted according +-- to the given string `format`. +-- +-- If the `time` argument is present, this is the time to be formatted +-- (see the `os.time` function for a description of this value). Otherwise, +-- `date` formats the current time. +-- +-- If `format` starts with '`!`', then the date is formatted in Coordinated +-- Universal Time. After this optional character, if `format` is the string +-- "`*t`", then `date` returns a table with the following fields: +-- +-- * `year` (four digits) +-- * `month` (1--12) +-- * `day` (1--31) +-- * `hour` (0--23) +-- * `min` (0--59) +-- * `sec` (0--61) +-- * `wday` (weekday, Sunday is 1) +-- * `yday` (day of the year) +-- * `isdst` (daylight saving flag, a boolean). +-- +-- If `format` is not "`*t`", then `date` returns the date as a string, +-- formatted according to the same rules as the C function `strftime`. +-- When called without arguments, `date` returns a reasonable date and time +-- representation that depends on the host system and on the current locale +-- (that is, `os.date()` is equivalent to `os.date("%c")`). +-- @function [parent=#os] date +-- @param #string format format of date. (optional) +-- @param #number time time to format. (default value is current time) +-- @return #string a formatted string representation of `time`. + +------------------------------------------------------------------------------- +-- Returns the number of seconds from time `t1` to time `t2`. In POSIX, +-- Windows, and some other systems, this value is exactly `t2`*-*`t1`. +-- @function [parent=#os] difftime +-- @param #number t2 +-- @param #number t1 +-- @return #number the number of seconds from time `t1` to time `t2`. + +------------------------------------------------------------------------------- +-- Returns the current time when called without arguments, or a time +-- representing the date and time specified by the given table. This table +-- must have fields `year`, `month`, and `day`, and may have fields `hour`, +-- `min`, `sec`, and `isdst` (for a description of these fields, see the +-- `os.date` function). +-- +-- The returned value is a number, whose meaning depends on your system. In +-- POSIX, Windows, and some other systems, this number counts the number +-- of seconds since some given start time (the "epoch"). In other systems, +-- the meaning is not specified, and the number returned by `time` can be +-- used only as an argument to `date` and `difftime`. +-- @function [parent=#os] time +-- @param #table table a table which describes a date. +-- @return #number a number meaning a date. + +return nil +