mirror of
https://github.com/OpenMW/openmw.git
synced 2025-03-03 17:09:40 +00:00
Add LuaUtil::ScriptsConfiguration
This commit is contained in:
parent
6aab246879
commit
33d71be81f
6 changed files with 276 additions and 9 deletions
|
@ -20,7 +20,7 @@ if (GTEST_FOUND AND GMOCK_FOUND)
|
||||||
lua/test_utilpackage.cpp
|
lua/test_utilpackage.cpp
|
||||||
lua/test_serialization.cpp
|
lua/test_serialization.cpp
|
||||||
lua/test_querypackage.cpp
|
lua/test_querypackage.cpp
|
||||||
lua/test_omwscriptsparser.cpp
|
lua/test_configuration.cpp
|
||||||
|
|
||||||
misc/test_stringops.cpp
|
misc/test_stringops.cpp
|
||||||
misc/test_endianness.cpp
|
misc/test_endianness.cpp
|
||||||
|
|
58
apps/openmw_test_suite/lua/test_configuration.cpp
Normal file
58
apps/openmw_test_suite/lua/test_configuration.cpp
Normal file
|
@ -0,0 +1,58 @@
|
||||||
|
#include "gmock/gmock.h"
|
||||||
|
#include <gtest/gtest.h>
|
||||||
|
|
||||||
|
#include <components/lua/configuration.hpp>
|
||||||
|
|
||||||
|
#include "testing_util.hpp"
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
|
||||||
|
TEST(LuaConfigurationTest, ValidConfiguration)
|
||||||
|
{
|
||||||
|
ESM::LuaScriptsCfg cfg;
|
||||||
|
LuaUtil::parseOMWScripts(cfg, R"X(
|
||||||
|
# Lines starting with '#' are comments
|
||||||
|
GLOBAL: my_mod/#some_global_script.lua
|
||||||
|
|
||||||
|
# Script that will be automatically attached to the player
|
||||||
|
PLAYER :my_mod/player.lua
|
||||||
|
CUSTOM : my_mod/some_other_script.lua
|
||||||
|
NPC , CREATURE PLAYER : my_mod/some_other_script.lua)X");
|
||||||
|
LuaUtil::parseOMWScripts(cfg, ":my_mod/player.LUA \r\nCONTAINER,CUSTOM: my_mod/container.lua\r\n");
|
||||||
|
|
||||||
|
ASSERT_EQ(cfg.mScripts.size(), 6);
|
||||||
|
EXPECT_EQ(LuaUtil::scriptCfgToString(cfg.mScripts[0]), "GLOBAL : my_mod/#some_global_script.lua");
|
||||||
|
EXPECT_EQ(LuaUtil::scriptCfgToString(cfg.mScripts[1]), "PLAYER : my_mod/player.lua");
|
||||||
|
EXPECT_EQ(LuaUtil::scriptCfgToString(cfg.mScripts[2]), "CUSTOM : my_mod/some_other_script.lua");
|
||||||
|
EXPECT_EQ(LuaUtil::scriptCfgToString(cfg.mScripts[3]), "CREATURE NPC PLAYER : my_mod/some_other_script.lua");
|
||||||
|
EXPECT_EQ(LuaUtil::scriptCfgToString(cfg.mScripts[4]), ": my_mod/player.LUA");
|
||||||
|
EXPECT_EQ(LuaUtil::scriptCfgToString(cfg.mScripts[5]), "CONTAINER CUSTOM : my_mod/container.lua");
|
||||||
|
|
||||||
|
LuaUtil::ScriptsConfiguration conf;
|
||||||
|
conf.init(std::move(cfg));
|
||||||
|
ASSERT_EQ(conf.size(), 3);
|
||||||
|
EXPECT_EQ(LuaUtil::scriptCfgToString(conf[0]), "GLOBAL : my_mod/#some_global_script.lua");
|
||||||
|
// cfg.mScripts[1] is overridden by cfg.mScripts[4]
|
||||||
|
// cfg.mScripts[2] is overridden by cfg.mScripts[3]
|
||||||
|
EXPECT_EQ(LuaUtil::scriptCfgToString(conf[1]), "CREATURE NPC PLAYER : my_mod/some_other_script.lua");
|
||||||
|
// cfg.mScripts[4] is removed because there are no flags
|
||||||
|
EXPECT_EQ(LuaUtil::scriptCfgToString(conf[2]), "CONTAINER CUSTOM : my_mod/container.lua");
|
||||||
|
|
||||||
|
cfg = ESM::LuaScriptsCfg();
|
||||||
|
conf.init(std::move(cfg));
|
||||||
|
ASSERT_EQ(conf.size(), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(LuaConfigurationTest, Errors)
|
||||||
|
{
|
||||||
|
ESM::LuaScriptsCfg cfg;
|
||||||
|
EXPECT_ERROR(LuaUtil::parseOMWScripts(cfg, "GLOBAL: something"),
|
||||||
|
"Lua script should have suffix '.lua', got: GLOBAL: something");
|
||||||
|
EXPECT_ERROR(LuaUtil::parseOMWScripts(cfg, "something.lua"),
|
||||||
|
"No flags found in: something.lua");
|
||||||
|
EXPECT_ERROR(LuaUtil::parseOMWScripts(cfg, "GLOBAL, PLAYER: something.lua"),
|
||||||
|
"Global script can not have local flags");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -52,7 +52,7 @@ namespace
|
||||||
}
|
}
|
||||||
|
|
||||||
#define EXPECT_ERROR(X, ERR_SUBSTR) try { X; FAIL() << "Expected error"; } \
|
#define EXPECT_ERROR(X, ERR_SUBSTR) try { X; FAIL() << "Expected error"; } \
|
||||||
catch (std::exception& e) { EXPECT_THAT(e.what(), HasSubstr(ERR_SUBSTR)); }
|
catch (std::exception& e) { EXPECT_THAT(e.what(), ::testing::HasSubstr(ERR_SUBSTR)); }
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
165
components/lua/configuration.cpp
Normal file
165
components/lua/configuration.cpp
Normal file
|
@ -0,0 +1,165 @@
|
||||||
|
#include "configuration.hpp"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <bitset>
|
||||||
|
#include <cassert>
|
||||||
|
#include <sstream>
|
||||||
|
|
||||||
|
#include <components/misc/stringops.hpp>
|
||||||
|
|
||||||
|
namespace LuaUtil
|
||||||
|
{
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
const std::map<std::string, ESM::LuaScriptCfg::Flags, std::less<>> flagsByName{
|
||||||
|
{"GLOBAL", ESM::LuaScriptCfg::sGlobal},
|
||||||
|
{"CUSTOM", ESM::LuaScriptCfg::sCustom},
|
||||||
|
{"PLAYER", ESM::LuaScriptCfg::sPlayer},
|
||||||
|
{"ACTIVATOR", ESM::LuaScriptCfg::sActivator},
|
||||||
|
{"ARMOR", ESM::LuaScriptCfg::sArmor},
|
||||||
|
{"BOOK", ESM::LuaScriptCfg::sBook},
|
||||||
|
{"CLOTHING", ESM::LuaScriptCfg::sClothing},
|
||||||
|
{"CONTAINER", ESM::LuaScriptCfg::sContainer},
|
||||||
|
{"CREATURE", ESM::LuaScriptCfg::sCreature},
|
||||||
|
{"DOOR", ESM::LuaScriptCfg::sDoor},
|
||||||
|
{"INGREDIENT", ESM::LuaScriptCfg::sIngredient},
|
||||||
|
{"LIGHT", ESM::LuaScriptCfg::sLight},
|
||||||
|
{"MISC_ITEM", ESM::LuaScriptCfg::sMiscItem},
|
||||||
|
{"NPC", ESM::LuaScriptCfg::sNPC},
|
||||||
|
{"POTION", ESM::LuaScriptCfg::sPotion},
|
||||||
|
{"WEAPON", ESM::LuaScriptCfg::sWeapon},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::vector<int> ScriptsConfiguration::sEmpty;
|
||||||
|
|
||||||
|
void ScriptsConfiguration::init(ESM::LuaScriptsCfg cfg)
|
||||||
|
{
|
||||||
|
mScripts.clear();
|
||||||
|
mScriptsByFlag.clear();
|
||||||
|
mPathToIndex.clear();
|
||||||
|
|
||||||
|
// Find duplicates; only the last occurrence will be used.
|
||||||
|
// Search for duplicates is case insensitive.
|
||||||
|
std::vector<bool> skip(cfg.mScripts.size(), false);
|
||||||
|
for (int i = cfg.mScripts.size() - 1; i >= 0; --i)
|
||||||
|
{
|
||||||
|
auto [_, inserted] = mPathToIndex.insert_or_assign(
|
||||||
|
Misc::StringUtils::lowerCase(cfg.mScripts[i].mScriptPath), -1);
|
||||||
|
if (!inserted || cfg.mScripts[i].mFlags == 0)
|
||||||
|
skip[i] = true;
|
||||||
|
}
|
||||||
|
mPathToIndex.clear();
|
||||||
|
int index = 0;
|
||||||
|
for (size_t i = 0; i < cfg.mScripts.size(); ++i)
|
||||||
|
{
|
||||||
|
if (skip[i])
|
||||||
|
continue;
|
||||||
|
ESM::LuaScriptCfg& s = cfg.mScripts[i];
|
||||||
|
mPathToIndex[s.mScriptPath] = index; // Stored paths are case sensitive.
|
||||||
|
ESM::LuaScriptCfg::Flags flags = s.mFlags;
|
||||||
|
ESM::LuaScriptCfg::Flags flag = 1;
|
||||||
|
while (flags != 0)
|
||||||
|
{
|
||||||
|
if (flags & flag)
|
||||||
|
mScriptsByFlag[flag].push_back(index);
|
||||||
|
flags &= ~flag;
|
||||||
|
flag = flag << 1;
|
||||||
|
}
|
||||||
|
mScripts.push_back(std::move(s));
|
||||||
|
index++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::optional<int> ScriptsConfiguration::findId(std::string_view path) const
|
||||||
|
{
|
||||||
|
auto it = mPathToIndex.find(path);
|
||||||
|
if (it != mPathToIndex.end())
|
||||||
|
return it->second;
|
||||||
|
else
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::vector<int>& ScriptsConfiguration::getListByFlag(ESM::LuaScriptCfg::Flags type) const
|
||||||
|
{
|
||||||
|
assert(std::bitset<64>(type).count() <= 1);
|
||||||
|
auto it = mScriptsByFlag.find(type);
|
||||||
|
if (it != mScriptsByFlag.end())
|
||||||
|
return it->second;
|
||||||
|
else
|
||||||
|
return sEmpty;
|
||||||
|
}
|
||||||
|
|
||||||
|
void parseOMWScripts(ESM::LuaScriptsCfg& cfg, std::string_view data)
|
||||||
|
{
|
||||||
|
while (!data.empty())
|
||||||
|
{
|
||||||
|
// Get next line
|
||||||
|
std::string_view line = data.substr(0, data.find('\n'));
|
||||||
|
data = data.substr(std::min(line.size() + 1, data.size()));
|
||||||
|
if (!line.empty() && line.back() == '\r')
|
||||||
|
line = line.substr(0, line.size() - 1);
|
||||||
|
|
||||||
|
while (!line.empty() && std::isspace(line[0]))
|
||||||
|
line = line.substr(1);
|
||||||
|
if (line.empty() || line[0] == '#') // Skip empty lines and comments
|
||||||
|
continue;
|
||||||
|
while (!line.empty() && std::isspace(line.back()))
|
||||||
|
line = line.substr(0, line.size() - 1);
|
||||||
|
|
||||||
|
if (!Misc::StringUtils::ciEndsWith(line, ".lua"))
|
||||||
|
throw std::runtime_error(Misc::StringUtils::format(
|
||||||
|
"Lua script should have suffix '.lua', got: %s", std::string(line.substr(0, 300))));
|
||||||
|
|
||||||
|
// Split flags and script path
|
||||||
|
size_t semicolonPos = line.find(':');
|
||||||
|
if (semicolonPos == std::string::npos)
|
||||||
|
throw std::runtime_error(Misc::StringUtils::format("No flags found in: %s", std::string(line)));
|
||||||
|
std::string_view flagsStr = line.substr(0, semicolonPos);
|
||||||
|
std::string_view scriptPath = line.substr(semicolonPos + 1);
|
||||||
|
while (std::isspace(scriptPath[0]))
|
||||||
|
scriptPath = scriptPath.substr(1);
|
||||||
|
|
||||||
|
// Parse flags
|
||||||
|
ESM::LuaScriptCfg::Flags flags = 0;
|
||||||
|
size_t flagsPos = 0;
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
while (flagsPos < flagsStr.size() && (std::isspace(flagsStr[flagsPos]) || flagsStr[flagsPos] == ','))
|
||||||
|
flagsPos++;
|
||||||
|
size_t startPos = flagsPos;
|
||||||
|
while (flagsPos < flagsStr.size() && !std::isspace(flagsStr[flagsPos]) && flagsStr[flagsPos] != ',')
|
||||||
|
flagsPos++;
|
||||||
|
if (startPos == flagsPos)
|
||||||
|
break;
|
||||||
|
std::string_view flagName = flagsStr.substr(startPos, flagsPos - startPos);
|
||||||
|
auto it = flagsByName.find(flagName);
|
||||||
|
if (it != flagsByName.end())
|
||||||
|
flags |= it->second;
|
||||||
|
else
|
||||||
|
throw std::runtime_error(Misc::StringUtils::format("Unknown flag '%s' in: %s",
|
||||||
|
std::string(flagName), std::string(line)));
|
||||||
|
}
|
||||||
|
if ((flags & ESM::LuaScriptCfg::sGlobal) && flags != ESM::LuaScriptCfg::sGlobal)
|
||||||
|
throw std::runtime_error("Global script can not have local flags");
|
||||||
|
|
||||||
|
cfg.mScripts.push_back(ESM::LuaScriptCfg{std::string(scriptPath), "", flags});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string scriptCfgToString(const ESM::LuaScriptCfg& script)
|
||||||
|
{
|
||||||
|
std::stringstream ss;
|
||||||
|
for (const auto& [flagName, flag] : flagsByName)
|
||||||
|
{
|
||||||
|
if (script.mFlags & flag)
|
||||||
|
ss << flagName << " ";
|
||||||
|
}
|
||||||
|
ss << ": " << script.mScriptPath;
|
||||||
|
if (!script.mInitializationData.empty())
|
||||||
|
ss << " (with data, " << script.mInitializationData.size() << " bytes)";
|
||||||
|
return ss.str();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
37
components/lua/configuration.hpp
Normal file
37
components/lua/configuration.hpp
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
#ifndef COMPONENTS_LUA_CONFIGURATION_H
|
||||||
|
#define COMPONENTS_LUA_CONFIGURATION_H
|
||||||
|
|
||||||
|
#include <map>
|
||||||
|
#include <optional>
|
||||||
|
|
||||||
|
#include <components/esm/luascripts.hpp>
|
||||||
|
|
||||||
|
namespace LuaUtil
|
||||||
|
{
|
||||||
|
|
||||||
|
class ScriptsConfiguration
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
void init(ESM::LuaScriptsCfg);
|
||||||
|
|
||||||
|
size_t size() const { return mScripts.size(); }
|
||||||
|
const ESM::LuaScriptCfg& operator[](int id) const { return mScripts[id]; }
|
||||||
|
|
||||||
|
std::optional<int> findId(std::string_view path) const;
|
||||||
|
const std::vector<int>& getListByFlag(ESM::LuaScriptCfg::Flags type) const;
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::vector<ESM::LuaScriptCfg> mScripts;
|
||||||
|
std::map<std::string, int, std::less<>> mPathToIndex;
|
||||||
|
std::map<ESM::LuaScriptCfg::Flags, std::vector<int>> mScriptsByFlag;
|
||||||
|
static const std::vector<int> sEmpty;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Parse ESM::LuaScriptsCfg from text and add to `cfg`.
|
||||||
|
void parseOMWScripts(ESM::LuaScriptsCfg& cfg, std::string_view data);
|
||||||
|
|
||||||
|
std::string scriptCfgToString(const ESM::LuaScriptCfg& script);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif // COMPONENTS_LUA_CONFIGURATION_H
|
|
@ -25,6 +25,7 @@ class StringUtils
|
||||||
template <typename T>
|
template <typename T>
|
||||||
static T argument(T value) noexcept
|
static T argument(T value) noexcept
|
||||||
{
|
{
|
||||||
|
static_assert(!std::is_same_v<T, std::string_view>, "std::string_view is not supported");
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -324,14 +325,20 @@ public:
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static inline void replaceLast(std::string& str, const std::string& substr, const std::string& with)
|
static inline void replaceLast(std::string& str, const std::string& substr, const std::string& with)
|
||||||
{
|
{
|
||||||
size_t pos = str.rfind(substr);
|
size_t pos = str.rfind(substr);
|
||||||
if (pos == std::string::npos)
|
if (pos == std::string::npos)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
str.replace(pos, substr.size(), with);
|
str.replace(pos, substr.size(), with);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static inline bool ciEndsWith(std::string_view s, std::string_view suffix)
|
||||||
|
{
|
||||||
|
return s.size() >= suffix.size() && std::equal(suffix.rbegin(), suffix.rend(), s.rbegin(),
|
||||||
|
[](char l, char r) { return toLower(l) == toLower(r); });
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue