summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPetr Mikheev <ptmikheev@gmail.com>2021-12-14 00:39:01 +0100
committerPetr Mikheev <ptmikheev@gmail.com>2022-01-11 11:21:03 +0100
commita182fdeea184a4dd987df57367a426160687f0fd (patch)
tree0e623eddc9b5eaafbca7019353a37476df66dade
parent781b014183c90ed2f92d83de99a2a09363186812 (diff)
Permanent storage for Lua data
-rw-r--r--apps/openmw/engine.cpp2
-rw-r--r--apps/openmw/mwlua/luabindings.cpp39
-rw-r--r--apps/openmw/mwlua/luabindings.hpp6
-rw-r--r--apps/openmw/mwlua/luamanagerimp.cpp29
-rw-r--r--apps/openmw/mwlua/luamanagerimp.hpp11
-rw-r--r--apps/openmw/mwlua/settingsbindings.cpp7
-rw-r--r--apps/openmw_test_suite/CMakeLists.txt1
-rw-r--r--apps/openmw_test_suite/lua/test_storage.cpp103
-rw-r--r--components/CMakeLists.txt4
-rw-r--r--components/lua/storage.cpp198
-rw-r--r--components/lua/storage.hpp81
-rw-r--r--docs/source/reference/lua-scripting/api.rst6
-rw-r--r--docs/source/reference/lua-scripting/openmw_settings.rst5
-rw-r--r--docs/source/reference/lua-scripting/openmw_storage.rst5
-rw-r--r--docs/source/reference/lua-scripting/overview.rst4
-rw-r--r--files/lua_api/openmw/core.lua6
-rw-r--r--files/lua_api/openmw/settings.lua14
-rw-r--r--files/lua_api/openmw/storage.lua96
18 files changed, 583 insertions, 34 deletions
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<ESM::GameSetting>* gmst = &MWBase::Environment::get().getWorld()->getStore().get<ESM::GameSetting>();
+ 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<std::string>(lua->sol(), value.getString());
+ else if (value.getType() == ESM::VT_Int)
+ return sol::make_object<int>(lua->sol(), value.getInteger());
+ else
+ return sol::make_object<float>(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 <components/lua/luastate.hpp>
#include <components/lua/serialization.hpp>
#include <components/lua/scriptscontainer.hpp>
+#include <components/lua/storage.hpp>
#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 <filesystem>
+
#include <components/debug/debuglog.hpp>
#include <components/esm/esmreader.hpp>
@@ -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<LocalScripts>(&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 <map>
#include <set>
-#include <components/lua/luastate.hpp>
#include <components/lua/i18n.hpp>
+#include <components/lua/luastate.hpp>
+#include <components/lua/storage.hpp>
#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<LocalScripts*> mActiveLocalScripts;
@@ -139,6 +145,9 @@ namespace MWLua
std::vector<std::unique_ptr<Action>> mActionQueue;
std::unique_ptr<TeleportAction> mTeleportPlayerAction;
std::vector<std::string> 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 <filesystem>
+#include <gmock/gmock.h>
+#include <gtest/gtest.h>
+
+#include <components/lua/storage.hpp>
+
+namespace
+{
+ using namespace testing;
+
+ template <typename T>
+ T get(sol::state& lua, std::string luaCode)
+ {
+ return lua.safe_script("return " + luaCode).get<T>();
+ }
+
+ 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<int>(mLua, "mutable:get('x')"), 5);
+ EXPECT_EQ(get<int>(mLua, "ro:get('x')"), 5);
+ EXPECT_FALSE(get<bool>(mLua, "mutable:wasChanged()"));
+ EXPECT_TRUE(get<bool>(mLua, "ro:wasChanged()"));
+ EXPECT_FALSE(get<bool>(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<int>(mLua, "t1.x"), 5);
+ EXPECT_EQ(get<int>(mLua, "t2.x"), 5);
+
+ mLua.safe_script("mutable:reset()");
+ EXPECT_TRUE(get<bool>(mLua, "ro:get('x') == nil"));
+
+ mLua.safe_script("mutable:reset({x=4, y=7})");
+ EXPECT_EQ(get<int>(mLua, "ro:get('x')"), 4);
+ EXPECT_EQ(get<int>(mLua, "ro:get('y')"), 7);
+ EXPECT_FALSE(get<bool>(mLua, "mutable:wasChanged()"));
+ EXPECT_TRUE(get<bool>(mLua, "ro:wasChanged()"));
+ EXPECT_FALSE(get<bool>(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<int>(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<int>(mLua, "mutable:get('x').z"), 7);
+ EXPECT_EQ(get<int>(mLua, "ro:get('x').z"), 7);
+ EXPECT_EQ(get<std::string>(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<int>(mLua, "permanent:get('x')"), 1);
+ EXPECT_EQ(get<int>(mLua, "temporary:get('y')"), 2);
+
+ storage.clearTemporary();
+ mLua["permanent"] = storage.getMutableSection("permanent");
+ mLua["temporary"] = storage.getMutableSection("temporary");
+ EXPECT_EQ(get<int>(mLua, "permanent:get('x')"), 1);
+ EXPECT_TRUE(get<bool>(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<int>(mLua, "permanent:get('x')"), 1);
+ EXPECT_TRUE(get<bool>(mLua, "permanent:get('z') == nil"));
+ EXPECT_TRUE(get<bool>(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 <filesystem>
+#include <fstream>
+
+#include <components/debug/debuglog.hpp>
+
+namespace sol
+{
+ template <>
+ struct is_automagical<LuaUtil::LuaStorage::SectionMutableView> : std::false_type {};
+ template <>
+ struct is_automagical<LuaUtil::LuaStorage::SectionReadOnlyView> : 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<SectionReadOnlyView> roView = lua.new_usertype<SectionReadOnlyView>("ReadOnlySection");
+ sol::usertype<SectionMutableView> mutableView = lua.new_usertype<SectionMutableView>("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<sol::table> newValues)
+ {
+ section.mSection->mValues.clear();
+ if (newValues)
+ {
+ for (const auto& [k, v] : *newValues)
+ {
+ try
+ {
+ section.mSection->set(k.as<std::string_view>(), 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<char>(fin)), std::istreambuf_iterator<char>());
+ sol::table data = deserialize(mLua, serializedData);
+ for (const auto& [sectionName, sectionTable] : data)
+ {
+ Section* section = getSection(sectionName.as<std::string_view>());
+ for (const auto& [key, value] : sol::table(sectionTable))
+ section->set(key.as<std::string_view>(), 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<Section>(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<SectionReadOnlyView>(mLua, SectionReadOnlyView{section, section->mChangeCounter});
+ }
+
+ sol::object LuaStorage::getMutableSection(std::string_view sectionName)
+ {
+ Section* section = getSection(sectionName);
+ return sol::make_object<SectionMutableView>(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 <map>
+#include <sol/sol.hpp>
+
+#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(std::string_view, std::string_view, const sol::object&)>;
+ 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<std::string, Value, std::less<>> 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<std::string_view, std::unique_ptr<Section>> mData;
+ std::optional<ListenerFn> 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 <Package openmw.util>` | everywhere | | Defines utility functions and classes like 3D vectors, |
| | | | that don't depend on the game world. |
+---------------------------------------------------------+--------------------+---------------------------------------------------------------+
-|:ref:`openmw.settings <Package openmw.settings>` | everywhere | | Access to GMST records in content files (implemented) and |
-| | | | to mod settings (not implemented). |
+|:ref:`openmw.storage <Package openmw.storage>` | everywhere | | Storage API. In particular can be used to store data |
+| | | | between game sessions. |
+---------------------------------------------------------+--------------------+---------------------------------------------------------------+
|:ref:`openmw.core <Package 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 <Package openmw.util>` | everywhere | | Defines utility functions and classes like 3D vectors, |
| | | | that don't depend on the game world. |
+---------------------------------------------------------+--------------------+---------------------------------------------------------------+
-|:ref:`openmw.settings <Package openmw.settings>` | everywhere | | Access to GMST records in content files (implemented) and |
-| | | | to mod settings (not implemented). |
+|:ref:`openmw.storage <Package openmw.storage>` | everywhere | | Storage API. In particular can be used to store data |
+| | | | between game sessions. |
+---------------------------------------------------------+--------------------+---------------------------------------------------------------+
|:ref:`openmw.core <Package 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
@@ -47,6 +47,12 @@
-- @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.
-- Language files should be stored in VFS as `i18n/<ContextName>/<Lang>.lua`.
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
+