From a182fdeea184a4dd987df57367a426160687f0fd Mon Sep 17 00:00:00 2001 From: Petr Mikheev Date: Tue, 14 Dec 2021 00:39:01 +0100 Subject: [PATCH] Permanent storage for Lua data --- apps/openmw/engine.cpp | 2 + apps/openmw/mwlua/luabindings.cpp | 39 +++- apps/openmw/mwlua/luabindings.hpp | 6 +- apps/openmw/mwlua/luamanagerimp.cpp | 29 ++- apps/openmw/mwlua/luamanagerimp.hpp | 11 +- apps/openmw/mwlua/settingsbindings.cpp | 7 +- apps/openmw_test_suite/CMakeLists.txt | 1 + apps/openmw_test_suite/lua/test_storage.cpp | 103 +++++++++ components/CMakeLists.txt | 4 +- components/lua/storage.cpp | 198 ++++++++++++++++++ components/lua/storage.hpp | 81 +++++++ docs/source/reference/lua-scripting/api.rst | 6 +- .../lua-scripting/openmw_settings.rst | 5 - .../lua-scripting/openmw_storage.rst | 5 + .../reference/lua-scripting/overview.rst | 4 +- files/lua_api/openmw/core.lua | 6 + files/lua_api/openmw/settings.lua | 14 -- files/lua_api/openmw/storage.lua | 96 +++++++++ 18 files changed, 583 insertions(+), 34 deletions(-) create mode 100644 apps/openmw_test_suite/lua/test_storage.cpp create mode 100644 components/lua/storage.cpp create mode 100644 components/lua/storage.hpp delete mode 100644 docs/source/reference/lua-scripting/openmw_settings.rst create mode 100644 docs/source/reference/lua-scripting/openmw_storage.rst delete mode 100644 files/lua_api/openmw/settings.lua create mode 100644 files/lua_api/openmw/storage.lua diff --git a/apps/openmw/engine.cpp b/apps/openmw/engine.cpp index afba76edce..4ad140465b 100644 --- a/apps/openmw/engine.cpp +++ b/apps/openmw/engine.cpp @@ -881,6 +881,7 @@ void OMW::Engine::prepareEngine (Settings::Manager & settings) } mLuaManager->init(); + mLuaManager->loadPermanentStorage(mCfgMgr.getUserConfigPath().string()); } class OMW::Engine::LuaWorker @@ -1103,6 +1104,7 @@ void OMW::Engine::go() // Save user settings settings.saveUser(settingspath); + mLuaManager->savePermanentStorage(mCfgMgr.getUserConfigPath().string()); Log(Debug::Info) << "Quitting peacefully."; } diff --git a/apps/openmw/mwlua/luabindings.cpp b/apps/openmw/mwlua/luabindings.cpp index 5a9e33faef..cf60e00728 100644 --- a/apps/openmw/mwlua/luabindings.cpp +++ b/apps/openmw/mwlua/luabindings.cpp @@ -6,7 +6,9 @@ #include "../mwbase/environment.hpp" #include "../mwbase/statemanager.hpp" +#include "../mwworld/esmstore.hpp" #include "../mwworld/inventorystore.hpp" +#include "../mwworld/store.hpp" #include "eventqueue.hpp" #include "worldview.hpp" @@ -47,7 +49,7 @@ namespace MWLua { auto* lua = context.mLua; sol::table api(lua->sol(), sol::create); - api["API_REVISION"] = 13; + api["API_REVISION"] = 14; api["quit"] = [lua]() { Log(Debug::Warning) << "Quit requested by a Lua script.\n" << lua->debugTraceback(); @@ -85,6 +87,17 @@ namespace MWLua {"Ammunition", MWWorld::InventoryStore::Slot_Ammunition} })); api["i18n"] = [i18n=context.mI18n](const std::string& context) { return i18n->getContext(context); }; + const MWWorld::Store* gmst = &MWBase::Environment::get().getWorld()->getStore().get(); + api["getGMST"] = [lua=context.mLua, gmst](const std::string& setting) -> sol::object + { + const ESM::Variant& value = gmst->find(setting)->mValue; + if (value.getType() == ESM::VT_String) + return sol::make_object(lua->sol(), value.getString()); + else if (value.getType() == ESM::VT_Int) + return sol::make_object(lua->sol(), value.getInteger()); + else + return sol::make_object(lua->sol(), value.getFloat()); + }; return LuaUtil::makeReadOnly(api); } @@ -163,5 +176,29 @@ namespace MWLua return LuaUtil::makeReadOnly(res); } + sol::table initGlobalStoragePackage(const Context& context, LuaUtil::LuaStorage* globalStorage) + { + sol::table res(context.mLua->sol(), sol::create); + res["globalSection"] = [globalStorage](std::string_view section) { return globalStorage->getMutableSection(section); }; + res["allGlobalSections"] = [globalStorage]() { return globalStorage->getAllSections(); }; + return LuaUtil::makeReadOnly(res); + } + + sol::table initLocalStoragePackage(const Context& context, LuaUtil::LuaStorage* globalStorage) + { + sol::table res(context.mLua->sol(), sol::create); + res["globalSection"] = [globalStorage](std::string_view section) { return globalStorage->getReadOnlySection(section); }; + return LuaUtil::makeReadOnly(res); + } + + sol::table initPlayerStoragePackage(const Context& context, LuaUtil::LuaStorage* globalStorage, LuaUtil::LuaStorage* playerStorage) + { + sol::table res(context.mLua->sol(), sol::create); + res["globalSection"] = [globalStorage](std::string_view section) { return globalStorage->getReadOnlySection(section); }; + res["playerSection"] = [playerStorage](std::string_view section) { return playerStorage->getMutableSection(section); }; + res["allPlayerSections"] = [playerStorage]() { return playerStorage->getAllSections(); }; + return LuaUtil::makeReadOnly(res); + } + } diff --git a/apps/openmw/mwlua/luabindings.hpp b/apps/openmw/mwlua/luabindings.hpp index b021354a75..cfaf50c4f6 100644 --- a/apps/openmw/mwlua/luabindings.hpp +++ b/apps/openmw/mwlua/luabindings.hpp @@ -4,6 +4,7 @@ #include #include #include +#include #include "context.hpp" #include "eventqueue.hpp" @@ -25,6 +26,10 @@ namespace MWLua sol::table initFieldGroup(const Context&, const QueryFieldGroup&); + sol::table initGlobalStoragePackage(const Context&, LuaUtil::LuaStorage* globalStorage); + sol::table initLocalStoragePackage(const Context&, LuaUtil::LuaStorage* globalStorage); + sol::table initPlayerStoragePackage(const Context&, LuaUtil::LuaStorage* globalStorage, LuaUtil::LuaStorage* playerStorage); + // Implemented in nearbybindings.cpp sol::table initNearbyPackage(const Context&); @@ -65,7 +70,6 @@ namespace MWLua // Implemented in settingsbindings.cpp sol::table initGlobalSettingsPackage(const Context&); - sol::table initLocalSettingsPackage(const Context&); sol::table initPlayerSettingsPackage(const Context&); // openmw.self package is implemented in localscripts.cpp diff --git a/apps/openmw/mwlua/luamanagerimp.cpp b/apps/openmw/mwlua/luamanagerimp.cpp index 658ee7c809..39b944807a 100644 --- a/apps/openmw/mwlua/luamanagerimp.cpp +++ b/apps/openmw/mwlua/luamanagerimp.cpp @@ -1,5 +1,7 @@ #include "luamanagerimp.hpp" +#include + #include #include @@ -69,6 +71,7 @@ namespace MWLua initObjectBindingsForLocalScripts(localContext); initCellBindingsForLocalScripts(localContext); LocalScripts::initializeSelfPackage(localContext); + LuaUtil::LuaStorage::initLuaBindings(mLua.sol()); mLua.addCommonPackage("openmw.async", getAsyncPackageInitializer(context)); mLua.addCommonPackage("openmw.util", LuaUtil::initUtilPackage(mLua.sol())); @@ -76,17 +79,37 @@ namespace MWLua mLua.addCommonPackage("openmw.query", initQueryPackage(context)); mGlobalScripts.addPackage("openmw.world", initWorldPackage(context)); mGlobalScripts.addPackage("openmw.settings", initGlobalSettingsPackage(context)); + mGlobalScripts.addPackage("openmw.storage", initGlobalStoragePackage(context, &mGlobalStorage)); mCameraPackage = initCameraPackage(localContext); mUserInterfacePackage = initUserInterfacePackage(localContext); mInputPackage = initInputPackage(localContext); mNearbyPackage = initNearbyPackage(localContext); - mLocalSettingsPackage = initLocalSettingsPackage(localContext); + mLocalSettingsPackage = initGlobalSettingsPackage(localContext); mPlayerSettingsPackage = initPlayerSettingsPackage(localContext); + mLocalStoragePackage = initLocalStoragePackage(localContext, &mGlobalStorage); + mPlayerStoragePackage = initPlayerStoragePackage(localContext, &mGlobalStorage, &mPlayerStorage); initConfiguration(); mInitialized = true; } + void LuaManager::loadPermanentStorage(const std::string& userConfigPath) + { + auto globalPath = std::filesystem::path(userConfigPath) / "global_storage.bin"; + auto playerPath = std::filesystem::path(userConfigPath) / "player_storage.bin"; + if (std::filesystem::exists(globalPath)) + mGlobalStorage.load(globalPath.string()); + if (std::filesystem::exists(playerPath)) + mPlayerStorage.load(playerPath.string()); + } + + void LuaManager::savePermanentStorage(const std::string& userConfigPath) + { + std::filesystem::path confDir(userConfigPath); + mGlobalStorage.save((confDir / "global_storage.bin").string()); + mPlayerStorage.save((confDir / "player_storage.bin").string()); + } + void LuaManager::update() { if (mPlayer.isEmpty()) @@ -232,6 +255,8 @@ namespace MWLua mPlayer = MWWorld::Ptr(); } clearUserInterface(); + mGlobalStorage.clearTemporary(); + mPlayerStorage.clearTemporary(); } void LuaManager::setupPlayer(const MWWorld::Ptr& ptr) @@ -346,11 +371,13 @@ namespace MWLua scripts->addPackage("openmw.camera", mCameraPackage); scripts->addPackage("openmw.input", mInputPackage); scripts->addPackage("openmw.settings", mPlayerSettingsPackage); + scripts->addPackage("openmw.storage", mPlayerStoragePackage); } else { scripts = std::make_shared(&mLua, LObject(getId(ptr), mWorldView.getObjectRegistry()), flag); scripts->addPackage("openmw.settings", mLocalSettingsPackage); + scripts->addPackage("openmw.storage", mLocalStoragePackage); } scripts->addPackage("openmw.nearby", mNearbyPackage); scripts->setSerializer(mLocalSerializer.get()); diff --git a/apps/openmw/mwlua/luamanagerimp.hpp b/apps/openmw/mwlua/luamanagerimp.hpp index d050cb9413..753cc6ca49 100644 --- a/apps/openmw/mwlua/luamanagerimp.hpp +++ b/apps/openmw/mwlua/luamanagerimp.hpp @@ -4,8 +4,9 @@ #include #include -#include #include +#include +#include #include "../mwbase/luamanager.hpp" @@ -28,6 +29,9 @@ namespace MWLua // Called by engine.cpp when the environment is fully initialized. void init(); + void loadPermanentStorage(const std::string& userConfigPath); + void savePermanentStorage(const std::string& userConfigPath); + // Called by engine.cpp every frame. For performance reasons it works in a separate // thread (in parallel with osg Cull). Can not use scene graph. void update(); @@ -99,6 +103,8 @@ namespace MWLua sol::table mInputPackage; sol::table mLocalSettingsPackage; sol::table mPlayerSettingsPackage; + sol::table mLocalStoragePackage; + sol::table mPlayerStoragePackage; GlobalScripts mGlobalScripts{&mLua}; std::set mActiveLocalScripts; @@ -139,6 +145,9 @@ namespace MWLua std::vector> mActionQueue; std::unique_ptr mTeleportPlayerAction; std::vector mUIMessages; + + LuaUtil::LuaStorage mGlobalStorage{mLua.sol()}; + LuaUtil::LuaStorage mPlayerStorage{mLua.sol()}; }; } diff --git a/apps/openmw/mwlua/settingsbindings.cpp b/apps/openmw/mwlua/settingsbindings.cpp index 12dd69f73a..afd852b1a0 100644 --- a/apps/openmw/mwlua/settingsbindings.cpp +++ b/apps/openmw/mwlua/settingsbindings.cpp @@ -8,7 +8,7 @@ namespace MWLua { - static sol::table initSettingsPackage(const Context& context, bool /*global*/, bool player) + static sol::table initSettingsPackage(const Context& context, bool player) { LuaUtil::LuaState* lua = context.mLua; sol::table config(lua->sol(), sol::create); @@ -65,8 +65,7 @@ namespace MWLua return LuaUtil::makeReadOnly(config); } - sol::table initGlobalSettingsPackage(const Context& context) { return initSettingsPackage(context, true, false); } - sol::table initLocalSettingsPackage(const Context& context) { return initSettingsPackage(context, false, false); } - sol::table initPlayerSettingsPackage(const Context& context) { return initSettingsPackage(context, false, true); } + sol::table initGlobalSettingsPackage(const Context& context) { return initSettingsPackage(context, false); } + sol::table initPlayerSettingsPackage(const Context& context) { return initSettingsPackage(context, true); } } diff --git a/apps/openmw_test_suite/CMakeLists.txt b/apps/openmw_test_suite/CMakeLists.txt index 9465d59b47..da68a21998 100644 --- a/apps/openmw_test_suite/CMakeLists.txt +++ b/apps/openmw_test_suite/CMakeLists.txt @@ -24,6 +24,7 @@ if (GTEST_FOUND AND GMOCK_FOUND) lua/test_querypackage.cpp lua/test_configuration.cpp lua/test_i18n.cpp + lua/test_storage.cpp lua/test_ui_content.cpp diff --git a/apps/openmw_test_suite/lua/test_storage.cpp b/apps/openmw_test_suite/lua/test_storage.cpp new file mode 100644 index 0000000000..fafba008a1 --- /dev/null +++ b/apps/openmw_test_suite/lua/test_storage.cpp @@ -0,0 +1,103 @@ +#include +#include +#include + +#include + +namespace +{ + using namespace testing; + + template + T get(sol::state& lua, std::string luaCode) + { + return lua.safe_script("return " + luaCode).get(); + } + + TEST(LuaUtilStorageTest, Basic) + { + sol::state mLua; + LuaUtil::LuaStorage::initLuaBindings(mLua); + LuaUtil::LuaStorage storage(mLua); + mLua["mutable"] = storage.getMutableSection("test"); + mLua["ro"] = storage.getReadOnlySection("test"); + + 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); + + mLua.safe_script("t1 = mutable:asTable()"); + mLua.safe_script("t2 = ro:asTable()"); + EXPECT_EQ(get(mLua, "t1.x"), 5); + EXPECT_EQ(get(mLua, "t2.x"), 5); + + mLua.safe_script("mutable:reset()"); + EXPECT_TRUE(get(mLua, "ro:get('x') == nil")); + + 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()")); + } + + TEST(LuaUtilStorageTest, Table) + { + sol::state mLua; + LuaUtil::LuaStorage::initLuaBindings(mLua); + LuaUtil::LuaStorage storage(mLua); + mLua["mutable"] = storage.getMutableSection("test"); + mLua["ro"] = storage.getReadOnlySection("test"); + + mLua.safe_script("mutable:set('x', { y = 'abc', z = 7 })"); + EXPECT_EQ(get(mLua, "mutable:get('x').z"), 7); + EXPECT_THROW(mLua.safe_script("mutable:get('x').z = 3"), std::exception); + EXPECT_NO_THROW(mLua.safe_script("mutable:getCopy('x').z = 3")); + EXPECT_EQ(get(mLua, "mutable:get('x').z"), 7); + EXPECT_EQ(get(mLua, "ro:get('x').z"), 7); + EXPECT_EQ(get(mLua, "ro:get('x').y"), "abc"); + } + + TEST(LuaUtilStorageTest, Saving) + { + sol::state mLua; + LuaUtil::LuaStorage::initLuaBindings(mLua); + LuaUtil::LuaStorage storage(mLua); + + mLua["permanent"] = storage.getMutableSection("permanent"); + mLua["temporary"] = storage.getMutableSection("temporary"); + mLua.safe_script("temporary:removeOnExit()"); + mLua.safe_script("permanent:set('x', 1)"); + mLua.safe_script("temporary:set('y', 2)"); + + std::string tmpFile = (std::filesystem::temp_directory_path() / "test_storage.bin").string(); + storage.save(tmpFile); + EXPECT_EQ(get(mLua, "permanent:get('x')"), 1); + EXPECT_EQ(get(mLua, "temporary:get('y')"), 2); + + storage.clearTemporary(); + mLua["permanent"] = storage.getMutableSection("permanent"); + mLua["temporary"] = storage.getMutableSection("temporary"); + EXPECT_EQ(get(mLua, "permanent:get('x')"), 1); + EXPECT_TRUE(get(mLua, "temporary:get('y') == nil")); + + mLua.safe_script("permanent:set('x', 3)"); + mLua.safe_script("permanent:set('z', 4)"); + + LuaUtil::LuaStorage storage2(mLua); + mLua["permanent"] = storage2.getMutableSection("permanent"); + mLua["temporary"] = storage2.getMutableSection("temporary"); + + storage2.load(tmpFile); + EXPECT_EQ(get(mLua, "permanent:get('x')"), 1); + EXPECT_TRUE(get(mLua, "permanent:get('z') == nil")); + EXPECT_TRUE(get(mLua, "temporary:get('y') == nil")); + } + +} diff --git a/components/CMakeLists.txt b/components/CMakeLists.txt index 6fedf25b4c..f7382db1c7 100644 --- a/components/CMakeLists.txt +++ b/components/CMakeLists.txt @@ -29,7 +29,7 @@ endif (GIT_CHECKOUT) # source files add_component_dir (lua - luastate scriptscontainer utilpackage serialization configuration i18n + luastate scriptscontainer utilpackage serialization configuration i18n storage ) add_component_dir (settings @@ -160,7 +160,7 @@ add_component_dir (fallback add_component_dir (queries query luabindings ) - + add_component_dir (lua_ui widget widgetlist element layers content text textedit window diff --git a/components/lua/storage.cpp b/components/lua/storage.cpp new file mode 100644 index 0000000000..def38fdd67 --- /dev/null +++ b/components/lua/storage.cpp @@ -0,0 +1,198 @@ +#include "storage.hpp" + +#include +#include + +#include + +namespace sol +{ + template <> + struct is_automagical : std::false_type {}; + template <> + struct is_automagical : std::false_type {}; +} + +namespace LuaUtil +{ + LuaStorage::Value LuaStorage::Section::sEmpty; + + sol::object LuaStorage::Value::getCopy(lua_State* L) const + { + return deserialize(L, mSerializedValue); + } + + sol::object LuaStorage::Value::getReadOnly(lua_State* L) const + { + if (mReadOnlyValue == sol::nil && !mSerializedValue.empty()) + mReadOnlyValue = deserialize(L, mSerializedValue, nullptr, true); + return mReadOnlyValue; + } + + const LuaStorage::Value& LuaStorage::Section::get(std::string_view key) const + { + auto it = mValues.find(key); + if (it != mValues.end()) + return it->second; + else + return sEmpty; + } + + void LuaStorage::Section::set(std::string_view key, const sol::object& value) + { + mValues[std::string(key)] = Value(value); + mChangeCounter++; + if (mStorage->mListener) + (*mStorage->mListener)(mSectionName, key, value); + } + + bool LuaStorage::Section::wasChanged(int64_t& lastCheck) + { + bool res = lastCheck < mChangeCounter; + lastCheck = mChangeCounter; + return res; + } + + sol::table LuaStorage::Section::asTable() + { + sol::table res(mStorage->mLua, sol::create); + for (const auto& [k, v] : mValues) + res[k] = v.getCopy(mStorage->mLua); + return res; + } + + 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) + { + return section.mSection->get(key).getReadOnly(s); + }; + roView["getCopy"] = [](sol::this_state s, SectionReadOnlyView& 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) + { + 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) + { + 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(); + } + } + } + section.mSection->mChangeCounter++; + section.mLastCheck = section.mSection->mChangeCounter; + }; + mutableView["removeOnExit"] = [](SectionMutableView& section) { section.mSection->mPermanent = false; }; + mutableView["set"] = [](SectionMutableView& section, std::string_view key, const sol::object& value) + { + if (section.mLastCheck == section.mSection->mChangeCounter) + section.mLastCheck++; + section.mSection->set(key, value); + }; + } + + void LuaStorage::clearTemporary() + { + auto it = mData.begin(); + while (it != mData.end()) + { + if (!it->second->mPermanent) + it = mData.erase(it); + else + ++it; + } + } + + void LuaStorage::load(const std::string& path) + { + mData.clear(); + try + { + Log(Debug::Info) << "Loading Lua storage \"" << path << "\" (" << std::filesystem::file_size(path) << " bytes)"; + std::ifstream fin(path, std::fstream::binary); + std::string serializedData((std::istreambuf_iterator(fin)), std::istreambuf_iterator()); + sol::table data = deserialize(mLua, serializedData); + for (const auto& [sectionName, sectionTable] : data) + { + Section* section = getSection(sectionName.as()); + for (const auto& [key, value] : sol::table(sectionTable)) + section->set(key.as(), value); + } + } + catch (std::exception& e) + { + Log(Debug::Error) << "Can not read \"" << path << "\": " << e.what(); + } + } + + void LuaStorage::save(const std::string& path) const + { + sol::table data(mLua, sol::create); + for (const auto& [sectionName, section] : mData) + { + if (section->mPermanent) + data[sectionName] = section->asTable(); + } + std::string serializedData = serialize(data); + Log(Debug::Info) << "Saving Lua storage \"" << path << "\" (" << serializedData.size() << " bytes)"; + std::ofstream fout(path, std::fstream::binary); + fout.write(serializedData.data(), serializedData.size()); + fout.close(); + } + + LuaStorage::Section* LuaStorage::getSection(std::string_view sectionName) + { + auto it = mData.find(sectionName); + if (it != mData.end()) + return it->second.get(); + auto section = std::make_unique
(this, std::string(sectionName)); + sectionName = section->mSectionName; + auto [newIt, _] = mData.emplace(sectionName, std::move(section)); + return newIt->second.get(); + } + + sol::object LuaStorage::getReadOnlySection(std::string_view sectionName) + { + Section* section = getSection(sectionName); + return sol::make_object(mLua, SectionReadOnlyView{section, section->mChangeCounter}); + } + + sol::object LuaStorage::getMutableSection(std::string_view sectionName) + { + Section* section = getSection(sectionName); + return sol::make_object(mLua, SectionMutableView{section, section->mChangeCounter}); + } + + sol::table LuaStorage::getAllSections() + { + sol::table res(mLua, sol::create); + for (const auto& [sectionName, _] : mData) + res[sectionName] = getMutableSection(sectionName); + return res; + } + +} diff --git a/components/lua/storage.hpp b/components/lua/storage.hpp new file mode 100644 index 0000000000..e847604aeb --- /dev/null +++ b/components/lua/storage.hpp @@ -0,0 +1,81 @@ +#ifndef COMPONENTS_LUA_STORAGE_H +#define COMPONENTS_LUA_STORAGE_H + +#include +#include + +#include "serialization.hpp" + +namespace LuaUtil +{ + + class LuaStorage + { + public: + static void initLuaBindings(lua_State*); + + explicit LuaStorage(lua_State* lua) : mLua(lua) {} + + void clearTemporary(); + 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(); + + void set(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); } + + private: + class Value + { + public: + Value() {} + Value(const sol::object& value) : mSerializedValue(serialize(value)) {} + sol::object getCopy(lua_State* L) const; + sol::object getReadOnly(lua_State* L) const; + + private: + std::string mSerializedValue; + mutable sol::object mReadOnlyValue = sol::nil; + }; + + struct Section + { + 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); + sol::table asTable(); + + LuaStorage* mStorage; + std::string mSectionName; + std::map> mValues; + bool mPermanent = true; + int64_t mChangeCounter = 0; + static Value sEmpty; + }; + struct SectionMutableView + { + Section* mSection = nullptr; + int64_t mLastCheck = 0; + }; + struct SectionReadOnlyView + { + Section* mSection = nullptr; + int64_t mLastCheck = 0; + }; + + Section* getSection(std::string_view sectionName); + + lua_State* mLua; + std::map> mData; + std::optional mListener; + }; + +} + +#endif // COMPONENTS_LUA_STORAGE_H diff --git a/docs/source/reference/lua-scripting/api.rst b/docs/source/reference/lua-scripting/api.rst index d6e85389b8..ef831e734a 100644 --- a/docs/source/reference/lua-scripting/api.rst +++ b/docs/source/reference/lua-scripting/api.rst @@ -8,7 +8,7 @@ Lua API reference engine_handlers user_interface openmw_util - openmw_settings + openmw_storage openmw_core openmw_async openmw_query @@ -45,8 +45,8 @@ Player scripts are local scripts that are attached to a player. |:ref:`openmw.util ` | everywhere | | Defines utility functions and classes like 3D vectors, | | | | | that don't depend on the game world. | +---------------------------------------------------------+--------------------+---------------------------------------------------------------+ -|:ref:`openmw.settings ` | everywhere | | Access to GMST records in content files (implemented) and | -| | | | to mod settings (not implemented). | +|:ref:`openmw.storage ` | everywhere | | Storage API. In particular can be used to store data | +| | | | between game sessions. | +---------------------------------------------------------+--------------------+---------------------------------------------------------------+ |:ref:`openmw.core ` | everywhere | | Functions that are common for both global and local scripts | +---------------------------------------------------------+--------------------+---------------------------------------------------------------+ diff --git a/docs/source/reference/lua-scripting/openmw_settings.rst b/docs/source/reference/lua-scripting/openmw_settings.rst deleted file mode 100644 index f3d26882bb..0000000000 --- a/docs/source/reference/lua-scripting/openmw_settings.rst +++ /dev/null @@ -1,5 +0,0 @@ -Package openmw.settings -======================= - -.. raw:: html - :file: generated_html/openmw_settings.html diff --git a/docs/source/reference/lua-scripting/openmw_storage.rst b/docs/source/reference/lua-scripting/openmw_storage.rst new file mode 100644 index 0000000000..5abf664e1a --- /dev/null +++ b/docs/source/reference/lua-scripting/openmw_storage.rst @@ -0,0 +1,5 @@ +Package openmw.storage +====================== + +.. raw:: html + :file: generated_html/openmw_storage.html diff --git a/docs/source/reference/lua-scripting/overview.rst b/docs/source/reference/lua-scripting/overview.rst index eab3fc962a..9de283b029 100644 --- a/docs/source/reference/lua-scripting/overview.rst +++ b/docs/source/reference/lua-scripting/overview.rst @@ -334,8 +334,8 @@ Player scripts are local scripts that are attached to a player. |:ref:`openmw.util ` | everywhere | | Defines utility functions and classes like 3D vectors, | | | | | that don't depend on the game world. | +---------------------------------------------------------+--------------------+---------------------------------------------------------------+ -|:ref:`openmw.settings ` | everywhere | | Access to GMST records in content files (implemented) and | -| | | | to mod settings (not implemented). | +|:ref:`openmw.storage ` | everywhere | | Storage API. In particular can be used to store data | +| | | | between game sessions. | +---------------------------------------------------------+--------------------+---------------------------------------------------------------+ |:ref:`openmw.core ` | everywhere | | Functions that are common for both global and local scripts | +---------------------------------------------------------+--------------------+---------------------------------------------------------------+ diff --git a/files/lua_api/openmw/core.lua b/files/lua_api/openmw/core.lua index d18be52406..8415690067 100644 --- a/files/lua_api/openmw/core.lua +++ b/files/lua_api/openmw/core.lua @@ -46,6 +46,12 @@ -- @function [parent=#core] isWorldPaused -- @return #boolean +------------------------------------------------------------------------------- +-- Get a GMST setting from content files. +-- @function [parent=#core] getGMST +-- @param #string setting Setting name +-- @return #any + ------------------------------------------------------------------------------- -- Return i18n formatting function for the given context. -- It is based on `i18n.lua` library. diff --git a/files/lua_api/openmw/settings.lua b/files/lua_api/openmw/settings.lua deleted file mode 100644 index 6c35caa8af..0000000000 --- a/files/lua_api/openmw/settings.lua +++ /dev/null @@ -1,14 +0,0 @@ -------------------------------------------------------------------------------- --- `openmw.settings` provides read-only access to GMST records in content files. --- @module settings --- @usage --- local settings = require('openmw.settings') - -------------------------------------------------------------------------------- --- Get a GMST setting from content files. --- @function [parent=#settings] getGMST --- @param #string setting - - -return nil - diff --git a/files/lua_api/openmw/storage.lua b/files/lua_api/openmw/storage.lua new file mode 100644 index 0000000000..5499eefb9c --- /dev/null +++ b/files/lua_api/openmw/storage.lua @@ -0,0 +1,96 @@ +--- +-- `openmw.storage` contains functions to work with permanent Lua storage. +-- @module storage +-- @usage +-- local storage = require('openmw.storage') +-- 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) +-- end +-- end + +--- +-- Get a section of the global storage; can be used by any script, but only global scripts can change values. +-- Creates the section if it doesn't exist. +-- @function [parent=#storage] globalSection +-- @param #string sectionName +-- @return #StorageSection + +--- +-- Get a section of the player storage; can be used by player scripts only. +-- Creates the section if it doesn't exist. +-- @function [parent=#storage] playerSection +-- @param #string sectionName +-- @return #StorageSection + +--- +-- Get all global sections as a table; can be used by global scripts only. +-- Note that adding/removing items to the returned table doesn't create or remove sections. +-- @function [parent=#storage] allGlobalSections +-- @return #table + +--- +-- Get all global sections as a table; can be used by player scripts only. +-- Note that adding/removing items to the returned table doesn't create or remove sections. +-- @function [parent=#storage] allPlayerSections +-- @return #table + +--- +-- A map `key -> value` that represents a storage section. +-- @type StorageSection + +--- +-- Get value by a string key; if value is a table makes it readonly. +-- @function [parent=#StorageSection] get +-- @param self +-- @param #string key + +--- +-- Get value by a string key; if value is a table returns a copy. +-- @function [parent=#StorageSection] getCopy +-- @param self +-- @param #string key + +--- +-- Return `True` if any value in this section was changed by another script since the last `wasChanged`. +-- @function [parent=#StorageSection] wasChanged +-- @param self +-- @return #boolean + +--- +-- Copy all values and return them as a table. +-- @function [parent=#StorageSection] asTable +-- @param self +-- @return #table + +--- +-- 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. +-- @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. +-- @function [parent=#StorageSection] removeOnExit +-- @param self + +--- +-- Set value by a string key; can not be used for global storage from a local script. +-- @function [parent=#StorageSection] set +-- @param self +-- @param #string key +-- @param #any value + +return nil +