mirror of
https://github.com/OpenMW/openmw.git
synced 2025-03-03 09: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_serialization.cpp
|
||||
lua/test_querypackage.cpp
|
||||
lua/test_omwscriptsparser.cpp
|
||||
lua/test_configuration.cpp
|
||||
|
||||
misc/test_stringops.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"; } \
|
||||
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>
|
||||
static T argument(T value) noexcept
|
||||
{
|
||||
static_assert(!std::is_same_v<T, std::string_view>, "std::string_view is not supported");
|
||||
return value;
|
||||
}
|
||||
|
||||
|
@ -324,14 +325,20 @@ public:
|
|||
}
|
||||
}
|
||||
|
||||
static inline void replaceLast(std::string& str, const std::string& substr, const std::string& with)
|
||||
{
|
||||
size_t pos = str.rfind(substr);
|
||||
if (pos == std::string::npos)
|
||||
return;
|
||||
static inline void replaceLast(std::string& str, const std::string& substr, const std::string& with)
|
||||
{
|
||||
size_t pos = str.rfind(substr);
|
||||
if (pos == std::string::npos)
|
||||
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