diff --git a/apps/openmw/mwlua/luamanagerimp.cpp b/apps/openmw/mwlua/luamanagerimp.cpp index 80dfef16cd..602f32710f 100644 --- a/apps/openmw/mwlua/luamanagerimp.cpp +++ b/apps/openmw/mwlua/luamanagerimp.cpp @@ -277,8 +277,8 @@ namespace MWLua mPlayer.getRefData().setLuaScripts(nullptr); mPlayer = MWWorld::Ptr(); } - mGlobalStorage.clearTemporary(); - mPlayerStorage.clearTemporary(); + mGlobalStorage.clearTemporaryAndRemoveCallbacks(); + mPlayerStorage.clearTemporaryAndRemoveCallbacks(); } void LuaManager::setupPlayer(const MWWorld::Ptr& ptr) diff --git a/apps/openmw_test_suite/lua/test_storage.cpp b/apps/openmw_test_suite/lua/test_storage.cpp index a1c6af6e0a..c1d8cc4c4f 100644 --- a/apps/openmw_test_suite/lua/test_storage.cpp +++ b/apps/openmw_test_suite/lua/test_storage.cpp @@ -2,6 +2,7 @@ #include #include +#include #include namespace @@ -19,15 +20,27 @@ namespace sol::state mLua; LuaUtil::LuaStorage::initLuaBindings(mLua); LuaUtil::LuaStorage storage(mLua); + + std::vector callbackCalls; + LuaUtil::Callback callback{ + sol::make_object(mLua, [&](const std::string& section, const sol::optional& key) + { + if (key) + callbackCalls.push_back(section + "_" + *key); + else + callbackCalls.push_back(section + "_*"); + }), + sol::table(mLua, sol::create) + }; + callback.mHiddenData[LuaUtil::ScriptsContainer::sScriptIdKey] = "fakeId"; + mLua["mutable"] = storage.getMutableSection("test"); mLua["ro"] = storage.getReadOnlySection("test"); + mLua["ro"]["subscribe"](mLua["ro"], callback); mLua.safe_script("mutable:set('x', 5)"); EXPECT_EQ(get(mLua, "mutable:get('x')"), 5); EXPECT_EQ(get(mLua, "ro:get('x')"), 5); - EXPECT_FALSE(get(mLua, "mutable:wasChanged()")); - EXPECT_TRUE(get(mLua, "ro:wasChanged()")); - EXPECT_FALSE(get(mLua, "ro:wasChanged()")); EXPECT_THROW(mLua.safe_script("ro:set('y', 3)"), std::exception); @@ -42,9 +55,8 @@ namespace mLua.safe_script("mutable:reset({x=4, y=7})"); EXPECT_EQ(get(mLua, "ro:get('x')"), 4); EXPECT_EQ(get(mLua, "ro:get('y')"), 7); - EXPECT_FALSE(get(mLua, "mutable:wasChanged()")); - EXPECT_TRUE(get(mLua, "ro:wasChanged()")); - EXPECT_FALSE(get(mLua, "ro:wasChanged()")); + + EXPECT_THAT(callbackCalls, ::testing::ElementsAre("test_x", "test_*", "test_*")); } TEST(LuaUtilStorageTest, Table) @@ -81,7 +93,7 @@ namespace EXPECT_EQ(get(mLua, "permanent:get('x')"), 1); EXPECT_EQ(get(mLua, "temporary:get('y')"), 2); - storage.clearTemporary(); + storage.clearTemporaryAndRemoveCallbacks(); mLua["permanent"] = storage.getMutableSection("permanent"); mLua["temporary"] = storage.getMutableSection("temporary"); EXPECT_EQ(get(mLua, "permanent:get('x')"), 1); diff --git a/components/lua/scriptscontainer.hpp b/components/lua/scriptscontainer.hpp index d968c13a45..e065c487c4 100644 --- a/components/lua/scriptscontainer.hpp +++ b/components/lua/scriptscontainer.hpp @@ -249,16 +249,28 @@ namespace LuaUtil sol::function mFunc; sol::table mHiddenData; // same object as Script::mHiddenData in ScriptsContainer + bool isValid() const { return mHiddenData[ScriptsContainer::sScriptIdKey] != sol::nil; } + template sol::object operator()(Args&&... args) const { - if (mHiddenData[ScriptsContainer::sScriptIdKey] != sol::nil) + if (isValid()) return LuaUtil::call(mFunc, std::forward(args)...); else Log(Debug::Debug) << "Ignored callback to the removed script " << mHiddenData.get(ScriptsContainer::sScriptDebugNameKey); return sol::nil; } + + template + void tryCall(Args&&... args) const + { + try { (*this)(std::forward(args)...); } + catch (std::exception& e) + { + Log(Debug::Error) << "Error in callback: " << e.what(); + } + } }; } diff --git a/components/lua/storage.cpp b/components/lua/storage.cpp index 91a339cb03..eff4578d00 100644 --- a/components/lua/storage.cpp +++ b/components/lua/storage.cpp @@ -8,9 +8,7 @@ namespace sol { template <> - struct is_automagical : std::false_type {}; - template <> - struct is_automagical : std::false_type {}; + struct is_automagical : std::false_type {}; } namespace LuaUtil @@ -38,19 +36,49 @@ namespace LuaUtil return sEmpty; } - void LuaStorage::Section::set(std::string_view key, const sol::object& value) + void LuaStorage::Section::runCallbacks(sol::optional changedKey) { - mValues[std::string(key)] = Value(value); - mChangeCounter++; - if (mStorage->mListener) - (*mStorage->mListener)(mSectionName, key, value); + mStorage->mRunningCallbacks = true; + mCallbacks.erase(std::remove_if(mCallbacks.begin(), mCallbacks.end(), [&](const Callback& callback) + { + bool valid = callback.isValid(); + if (valid) + callback.tryCall(mSectionName, changedKey); + return !valid; + }), mCallbacks.end()); + mStorage->mRunningCallbacks = false; } - bool LuaStorage::Section::wasChanged(int64_t& lastCheck) + void LuaStorage::Section::set(std::string_view key, const sol::object& value) { - bool res = lastCheck < mChangeCounter; - lastCheck = mChangeCounter; - return res; + if (mStorage->mRunningCallbacks) + throw std::runtime_error("Not allowed to change storage in storage handlers because it can lead to an infinite recursion"); + if (value != sol::nil) + mValues[std::string(key)] = Value(value); + else + { + auto it = mValues.find(key); + if (it != mValues.end()) + mValues.erase(it); + } + if (mStorage->mListener) + mStorage->mListener->valueChanged(mSectionName, key, value); + runCallbacks(key); + } + + void LuaStorage::Section::setAll(const sol::optional& values) + { + if (mStorage->mRunningCallbacks) + throw std::runtime_error("Not allowed to change storage in storage handlers because it can lead to an infinite recursion"); + mValues.clear(); + if (values) + { + for (const auto& [k, v] : *values) + mValues[k.as()] = Value(v); + } + if (mStorage->mListener) + mStorage->mListener->sectionReplaced(mSectionName, values); + runCallbacks(sol::nullopt); } sol::table LuaStorage::Section::asTable() @@ -64,62 +92,53 @@ namespace LuaUtil void LuaStorage::initLuaBindings(lua_State* L) { sol::state_view lua(L); - sol::usertype roView = lua.new_usertype("ReadOnlySection"); - sol::usertype mutableView = lua.new_usertype("MutableSection"); - roView["get"] = [](sol::this_state s, SectionReadOnlyView& section, std::string_view key) + sol::usertype sview = lua.new_usertype("Section"); + sview["get"] = [](sol::this_state s, const SectionView& section, std::string_view key) { return section.mSection->get(key).getReadOnly(s); }; - roView["getCopy"] = [](sol::this_state s, SectionReadOnlyView& section, std::string_view key) + sview["getCopy"] = [](sol::this_state s, const SectionView& section, std::string_view key) { return section.mSection->get(key).getCopy(s); }; - roView["wasChanged"] = [](SectionReadOnlyView& section) { return section.mSection->wasChanged(section.mLastCheck); }; - roView["asTable"] = [](SectionReadOnlyView& section) { return section.mSection->asTable(); }; - mutableView["get"] = [](sol::this_state s, SectionMutableView& section, std::string_view key) + sview["asTable"] = [](const SectionView& section) { return section.mSection->asTable(); }; + sview["subscribe"] = [](const SectionView& section, const Callback& callback) { - return section.mSection->get(key).getReadOnly(s); - }; - mutableView["getCopy"] = [](sol::this_state s, SectionMutableView& section, std::string_view key) - { - return section.mSection->get(key).getCopy(s); - }; - mutableView["wasChanged"] = [](SectionMutableView& section) { return section.mSection->wasChanged(section.mLastCheck); }; - mutableView["asTable"] = [](SectionMutableView& section) { return section.mSection->asTable(); }; - mutableView["reset"] = [](SectionMutableView& section, sol::optional newValues) - { - section.mSection->mValues.clear(); - if (newValues) + std::vector& callbacks = section.mSection->mCallbacks; + if (!callbacks.empty() && callbacks.size() == callbacks.capacity()) { - for (const auto& [k, v] : *newValues) - { - try - { - section.mSection->set(k.as(), v); - } - catch (std::exception& e) - { - Log(Debug::Error) << "LuaUtil::LuaStorage::Section::reset(table): " << e.what(); - } - } + callbacks.erase(std::remove_if(callbacks.begin(), callbacks.end(), + [&](const Callback& c) { return !c.isValid(); }), + callbacks.end()); } - section.mSection->mChangeCounter++; - section.mLastCheck = section.mSection->mChangeCounter; + callbacks.push_back(callback); }; - mutableView["removeOnExit"] = [](SectionMutableView& section) { section.mSection->mPermanent = false; }; - mutableView["set"] = [](SectionMutableView& section, std::string_view key, const sol::object& value) + sview["reset"] = [](const SectionView& section, const sol::optional& newValues) { - if (section.mLastCheck == section.mSection->mChangeCounter) - section.mLastCheck++; + if (section.mReadOnly) + throw std::runtime_error("Access to storage is read only"); + section.mSection->setAll(newValues); + }; + sview["removeOnExit"] = [](const SectionView& section) + { + if (section.mReadOnly) + throw std::runtime_error("Access to storage is read only"); + section.mSection->mPermanent = false; + }; + sview["set"] = [](const SectionView& section, std::string_view key, const sol::object& value) + { + if (section.mReadOnly) + throw std::runtime_error("Access to storage is read only"); section.mSection->set(key, value); }; } - void LuaStorage::clearTemporary() + void LuaStorage::clearTemporaryAndRemoveCallbacks() { auto it = mData.begin(); while (it != mData.end()) { + it->second->mCallbacks.clear(); if (!it->second->mPermanent) { it->second->mValues.clear(); @@ -157,7 +176,7 @@ namespace LuaUtil sol::table data(mLua, sol::create); for (const auto& [sectionName, section] : mData) { - if (section->mPermanent) + if (section->mPermanent && !section->mValues.empty()) data[sectionName] = section->asTable(); } std::string serializedData = serialize(data); @@ -178,23 +197,17 @@ namespace LuaUtil return newIt->second; } - sol::object LuaStorage::getReadOnlySection(std::string_view sectionName) + sol::object LuaStorage::getSection(std::string_view sectionName, bool readOnly) { const std::shared_ptr
& section = getSection(sectionName); - return sol::make_object(mLua, SectionReadOnlyView{section, section->mChangeCounter}); + return sol::make_object(mLua, SectionView{section, readOnly}); } - sol::object LuaStorage::getMutableSection(std::string_view sectionName) - { - const std::shared_ptr
& section = getSection(sectionName); - return sol::make_object(mLua, SectionMutableView{section, section->mChangeCounter}); - } - - sol::table LuaStorage::getAllSections() + sol::table LuaStorage::getAllSections(bool readOnly) { sol::table res(mLua, sol::create); for (const auto& [sectionName, _] : mData) - res[sectionName] = getMutableSection(sectionName); + res[sectionName] = getSection(sectionName, readOnly); return res; } diff --git a/components/lua/storage.hpp b/components/lua/storage.hpp index c23e417f0f..11ea91f039 100644 --- a/components/lua/storage.hpp +++ b/components/lua/storage.hpp @@ -4,6 +4,7 @@ #include #include +#include "scriptscontainer.hpp" #include "serialization.hpp" namespace LuaUtil @@ -16,18 +17,28 @@ namespace LuaUtil explicit LuaStorage(lua_State* lua) : mLua(lua) {} - void clearTemporary(); + void clearTemporaryAndRemoveCallbacks(); void load(const std::string& path); void save(const std::string& path) const; - sol::object getReadOnlySection(std::string_view sectionName); - sol::object getMutableSection(std::string_view sectionName); - sol::table getAllSections(); + sol::object getSection(std::string_view sectionName, bool readOnly); + sol::object getMutableSection(std::string_view sectionName) { return getSection(sectionName, false); } + sol::object getReadOnlySection(std::string_view sectionName) { return getSection(sectionName, true); } + sol::table getAllSections(bool readOnly = false); - void set(std::string_view section, std::string_view key, const sol::object& value) { getSection(section)->set(key, value); } + void setSingleValue(std::string_view section, std::string_view key, const sol::object& value) + { getSection(section)->set(key, value); } - using ListenerFn = std::function; - void setListener(ListenerFn fn) { mListener = std::move(fn); } + void setSectionValues(std::string_view section, const sol::optional& values) + { getSection(section)->setAll(values); } + + class Listener + { + public: + virtual void valueChanged(std::string_view section, std::string_view key, const sol::object& value) const = 0; + virtual void sectionReplaced(std::string_view section, const sol::optional& values) const = 0; + }; + void setListener(const Listener* listener) { mListener = listener; } private: class Value @@ -48,32 +59,29 @@ namespace LuaUtil explicit Section(LuaStorage* storage, std::string name) : mStorage(storage), mSectionName(std::move(name)) {} const Value& get(std::string_view key) const; void set(std::string_view key, const sol::object& value); - bool wasChanged(int64_t& lastCheck); + void setAll(const sol::optional& values); sol::table asTable(); + void runCallbacks(sol::optional changedKey); LuaStorage* mStorage; std::string mSectionName; std::map> mValues; + std::vector mCallbacks; bool mPermanent = true; - int64_t mChangeCounter = 0; static Value sEmpty; }; - struct SectionMutableView + struct SectionView { - std::shared_ptr
mSection = nullptr; - int64_t mLastCheck = 0; - }; - struct SectionReadOnlyView - { - std::shared_ptr
mSection = nullptr; - int64_t mLastCheck = 0; + std::shared_ptr
mSection; + bool mReadOnly; }; const std::shared_ptr
& getSection(std::string_view sectionName); lua_State* mLua; std::map> mData; - std::optional mListener; + const Listener* mListener = nullptr; + bool mRunningCallbacks = false; }; } diff --git a/files/lua_api/openmw/storage.lua b/files/lua_api/openmw/storage.lua index 5499eefb9c..353c1ca49c 100644 --- a/files/lua_api/openmw/storage.lua +++ b/files/lua_api/openmw/storage.lua @@ -6,14 +6,14 @@ -- local myModData = storage.globalSection('MyModExample') -- myModData:set("someVariable", 1.0) -- myModData:set("anotherVariable", { exampleStr='abc', exampleBool=true }) --- local function update() --- if myModCfg:checkChanged() then --- print('Data was changes by another script:') --- print('MyModExample.someVariable =', myModData:get('someVariable')) --- print('MyModExample.anotherVariable.exampleStr =', --- myModData:get('anotherVariable').exampleStr) +-- local async = require('openmw.async') +-- myModData:subscribe(async:callback(function(section, key) +-- if key then +-- print('Value is changed:', key, '=', myModData:get(key)) +-- else +-- print('All values are changed') -- end --- end +-- end)) --- -- Get a section of the global storage; can be used by any script, but only global scripts can change values. @@ -58,10 +58,12 @@ -- @param #string key --- --- Return `True` if any value in this section was changed by another script since the last `wasChanged`. --- @function [parent=#StorageSection] wasChanged +-- Subscribe to changes in this section. +-- First argument of the callback is the name of the section (so one callback can be used for different sections). +-- The second argument is the changed key (or `nil` if `reset` was used and all values were changed at the same time) +-- @function [parent=#StorageSection] subscribe -- @param self --- @return #boolean +-- @param openmw.async#Callback callback --- -- Copy all values and return them as a table. @@ -71,14 +73,14 @@ --- -- Remove all existing values and assign values from given (the arg is optional) table. --- Note: `section:reset()` removes all values, but not the section itself. Use `section:removeOnExit()` to remove the section completely. +-- This function can not be used for a global storage section from a local script. +-- Note: `section:reset()` removes the section. -- @function [parent=#StorageSection] reset -- @param self -- @param #table values (optional) New values --- -- Make the whole section temporary: will be removed on exit or when load a save. --- No section can be removed immediately because other scripts may use it at the moment. -- Temporary sections have the same interface to get/set values, the only difference is they will not -- be saved to the permanent storage on exit. -- This function can not be used for a global storage section from a local script.