From 33d71be81f8254c24de685eee3c0ba7cf93ca005 Mon Sep 17 00:00:00 2001 From: Petr Mikheev Date: Wed, 6 Oct 2021 20:59:48 +0200 Subject: [PATCH] Add LuaUtil::ScriptsConfiguration --- apps/openmw_test_suite/CMakeLists.txt | 2 +- .../lua/test_configuration.cpp | 58 ++++++ apps/openmw_test_suite/lua/testing_util.hpp | 2 +- components/lua/configuration.cpp | 165 ++++++++++++++++++ components/lua/configuration.hpp | 37 ++++ components/misc/stringops.hpp | 21 ++- 6 files changed, 276 insertions(+), 9 deletions(-) create mode 100644 apps/openmw_test_suite/lua/test_configuration.cpp create mode 100644 components/lua/configuration.cpp create mode 100644 components/lua/configuration.hpp diff --git a/apps/openmw_test_suite/CMakeLists.txt b/apps/openmw_test_suite/CMakeLists.txt index a2a35e3aab..2b96d4c633 100644 --- a/apps/openmw_test_suite/CMakeLists.txt +++ b/apps/openmw_test_suite/CMakeLists.txt @@ -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 diff --git a/apps/openmw_test_suite/lua/test_configuration.cpp b/apps/openmw_test_suite/lua/test_configuration.cpp new file mode 100644 index 0000000000..054ea8cbda --- /dev/null +++ b/apps/openmw_test_suite/lua/test_configuration.cpp @@ -0,0 +1,58 @@ +#include "gmock/gmock.h" +#include + +#include + +#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"); + } + +} diff --git a/apps/openmw_test_suite/lua/testing_util.hpp b/apps/openmw_test_suite/lua/testing_util.hpp index 28c4d59930..2f6810350f 100644 --- a/apps/openmw_test_suite/lua/testing_util.hpp +++ b/apps/openmw_test_suite/lua/testing_util.hpp @@ -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)); } } diff --git a/components/lua/configuration.cpp b/components/lua/configuration.cpp new file mode 100644 index 0000000000..4598ed2508 --- /dev/null +++ b/components/lua/configuration.cpp @@ -0,0 +1,165 @@ +#include "configuration.hpp" + +#include +#include +#include +#include + +#include + +namespace LuaUtil +{ + + namespace + { + const std::map> 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 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 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 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& 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(); + } + +} diff --git a/components/lua/configuration.hpp b/components/lua/configuration.hpp new file mode 100644 index 0000000000..32eddf399c --- /dev/null +++ b/components/lua/configuration.hpp @@ -0,0 +1,37 @@ +#ifndef COMPONENTS_LUA_CONFIGURATION_H +#define COMPONENTS_LUA_CONFIGURATION_H + +#include +#include + +#include + +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 findId(std::string_view path) const; + const std::vector& getListByFlag(ESM::LuaScriptCfg::Flags type) const; + + private: + std::vector mScripts; + std::map> mPathToIndex; + std::map> mScriptsByFlag; + static const std::vector 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 diff --git a/components/misc/stringops.hpp b/components/misc/stringops.hpp index 6c9c1eefa2..dcffa7fdf3 100644 --- a/components/misc/stringops.hpp +++ b/components/misc/stringops.hpp @@ -25,6 +25,7 @@ class StringUtils template static T argument(T value) noexcept { + static_assert(!std::is_same_v, "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); }); + }; }; }