mirror of https://github.com/OpenMW/openmw.git
Permanent storage for Lua data
parent
781b014183
commit
a182fdeea1
@ -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"));
|
||||
}
|
||||
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
@ -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
|
@ -1,5 +0,0 @@
|
||||
Package openmw.settings
|
||||
=======================
|
||||
|
||||
.. raw:: html
|
||||
:file: generated_html/openmw_settings.html
|
@ -0,0 +1,5 @@
|
||||
Package openmw.storage
|
||||
======================
|
||||
|
||||
.. raw:: html
|
||||
:file: generated_html/openmw_storage.html
|
@ -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
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue