From fb1e1ad3609f7d44fb847b5d0ba1e8f0cad3991c Mon Sep 17 00:00:00 2001 From: Oliver Hamlet Date: Sun, 20 Apr 2025 12:59:19 +0100 Subject: [PATCH] Sync with libloot v0.26.0 --- Cargo.lock | 8 +- Cargo.toml | 2 +- README.md | 2 +- cxx/CMakeLists.txt | 3 +- cxx/Cargo.toml | 2 +- cxx/README.md | 4 +- cxx/build.rs | 2 + cxx/cmake/tests.cmake | 5 +- cxx/include/loot/api.h | 5 +- cxx/include/loot/database_interface.h | 52 +++-- .../loot/exception/undefined_group_error.h | 3 +- cxx/include/loot/game_interface.h | 7 +- cxx/include/loot/loot_version.h | 4 +- .../loot/metadata/conditional_metadata.h | 3 +- cxx/include/loot/metadata/file.h | 16 +- cxx/include/loot/metadata/filename.h | 3 +- cxx/include/loot/metadata/group.h | 7 +- cxx/include/loot/metadata/location.h | 5 +- cxx/include/loot/metadata/message.h | 7 +- cxx/include/loot/metadata/message_content.h | 9 +- .../loot/metadata/plugin_cleaning_data.h | 5 +- cxx/include/loot/metadata/plugin_metadata.h | 7 +- cxx/include/loot/metadata/tag.h | 5 +- cxx/include/loot/plugin_interface.h | 3 +- cxx/include/loot/vertex.h | 5 +- cxx/src/api/api.cpp | 22 +- cxx/src/api/convert.cpp | 47 +++-- cxx/src/api/convert.h | 4 + cxx/src/api/database.cpp | 61 +++--- cxx/src/api/database.h | 21 +- cxx/src/api/exception.cpp | 32 +-- cxx/src/api/game.cpp | 43 +--- cxx/src/api/game.h | 8 +- cxx/src/api/metadata/conditional_metadata.cpp | 2 +- cxx/src/api/metadata/file.cpp | 23 ++- cxx/src/api/metadata/filename.cpp | 3 +- cxx/src/api/metadata/group.cpp | 4 +- cxx/src/api/metadata/location.cpp | 2 +- cxx/src/api/metadata/message.cpp | 6 +- cxx/src/api/metadata/message_content.cpp | 6 +- cxx/src/api/metadata/plugin_cleaning_data.cpp | 5 +- cxx/src/api/metadata/plugin_metadata.cpp | 21 +- cxx/src/api/metadata/tag.cpp | 4 +- cxx/src/api/plugin.cpp | 13 +- cxx/src/api/plugin.h | 2 +- cxx/src/api/resource.rc | 4 +- cxx/src/api/sorting/undefined_group_error.cpp | 5 +- cxx/src/api/vertex.cpp | 4 +- cxx/src/database.rs | 8 + cxx/src/lib.rs | 6 + cxx/src/metadata.rs | 8 + .../api/interface/database_interface_test.h | 193 ++++++++++-------- .../tests/api/interface/game_interface_test.h | 2 +- cxx/src/tests/api/interface/main.cpp | 48 ++++- cxx/src/tests/api/internals/main.cpp | 2 +- cxx/src/tests/common_game_test_fixture.h | 17 +- ffi-errors/Cargo.toml | 2 +- pyo3/Cargo.toml | 2 +- pyo3/src/database.rs | 8 + pyo3/src/metadata.rs | 22 +- src/database/conditions.rs | 13 +- src/database/mod.rs | 27 ++- src/error.rs | 8 + src/game.rs | 96 +++++++-- src/metadata/file.rs | 65 +++++- src/metadata/message.rs | 2 +- src/metadata/plugin_metadata.rs | 28 ++- src/metadata/tag.rs | 2 +- src/metadata/yaml/parse.rs | 3 +- src/sorting/plugins.rs | 8 +- 70 files changed, 737 insertions(+), 349 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 8b6fe925..dca8714f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -565,7 +565,7 @@ dependencies = [ [[package]] name = "libloot" -version = "0.25.5" +version = "0.26.0" dependencies = [ "crc32fast", "esplugin", @@ -587,7 +587,7 @@ dependencies = [ [[package]] name = "libloot-cxx" -version = "0.25.5" +version = "0.26.0" dependencies = [ "cxx", "cxx-build", @@ -599,7 +599,7 @@ dependencies = [ [[package]] name = "libloot-ffi-errors" -version = "0.25.5" +version = "0.26.0" dependencies = [ "esplugin", "libloadorder", @@ -609,7 +609,7 @@ dependencies = [ [[package]] name = "libloot_pyo3" -version = "0.25.5" +version = "0.26.0" dependencies = [ "libloot", "libloot-ffi-errors", diff --git a/Cargo.toml b/Cargo.toml index a35ade9b..9f0142dd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "libloot" -version = "0.25.5" +version = "0.26.0" edition = "2024" license = "GPL-3.0" diff --git a/README.md b/README.md index 4a572279..0e94e94d 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # libloot-rs -This is an **experimental** reimplementation of [libloot](https://github.com/loot/libloot) using Rust instead of C++, that should match libloot v0.25.5. +This is an **experimental** reimplementation of [libloot](https://github.com/loot/libloot) using Rust instead of C++, that should match libloot v0.26.0. See the `cxx` and `pyo3` subdirectories for C++ and Python wrappers respectively. diff --git a/cxx/CMakeLists.txt b/cxx/CMakeLists.txt index 11cfda75..2a55eb68 100644 --- a/cxx/CMakeLists.txt +++ b/cxx/CMakeLists.txt @@ -154,6 +154,7 @@ target_include_directories(loot SYSTEM PRIVATE if(CMAKE_SYSTEM_NAME STREQUAL "Windows") target_compile_definitions(loot PRIVATE UNICODE _UNICODE LOOT_EXPORT) + target_compile_options(loot PRIVATE "/Zc:__cplusplus") set(LOOT_LIBS ntdll ws2_32 bcrypt) @@ -224,7 +225,7 @@ endif() # Install ######################################## -set(LIBLOOT_VERSION "0.25.4") +set(LIBLOOT_VERSION "0.26.0") set_property(TARGET loot PROPERTY VERSION ${LIBLOOT_VERSION}) set_property(TARGET loot PROPERTY SOVERSION 0) diff --git a/cxx/Cargo.toml b/cxx/Cargo.toml index 04d3b425..ae45aad3 100644 --- a/cxx/Cargo.toml +++ b/cxx/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "libloot-cxx" -version = "0.25.5" +version = "0.26.0" edition = "2024" license = "GPL-3.0" diff --git a/cxx/README.md b/cxx/README.md index 1c929778..a02d9f1d 100644 --- a/cxx/README.md +++ b/cxx/README.md @@ -1,6 +1,6 @@ # libloot-rs C++ wrapper -This is an **experimental** wrapper around the Rust reimplementation of libloot that provides a C++ interface that's ABI-compatible with libloot v0.25.5. +This is an **experimental** wrapper around the Rust reimplementation of libloot that provides a C++ interface that's ABI-compatible with libloot v0.26.0. The wrapper has two layers: @@ -47,7 +47,7 @@ cmake --build build --parallel ### Tests & Packaging -The build process also builds a copy of the public API tests from C++ libloot v0.25.5 by default. To skip building the tests, pass `-DLIBLOOT_BUILD_TESTS=OFF` when first running CMake. +The build process also builds a copy of the public API tests from C++ libloot v0.26.0 by default. To skip building the tests, pass `-DLIBLOOT_BUILD_TESTS=OFF` when first running CMake. If built, the tests can be run using: diff --git a/cxx/build.rs b/cxx/build.rs index 833a7145..d805cf29 100644 --- a/cxx/build.rs +++ b/cxx/build.rs @@ -1,6 +1,8 @@ fn main() { cxx_build::bridge("src/lib.rs") .std("c++17") + .flag_if_supported("/Zc:__cplusplus") + .flag_if_supported("/permissive-") .compile("libloot-cxx"); // From diff --git a/cxx/cmake/tests.cmake b/cxx/cmake/tests.cmake index f415887e..4ff94610 100644 --- a/cxx/cmake/tests.cmake +++ b/cxx/cmake/tests.cmake @@ -133,13 +133,14 @@ endif() if(MSVC) # Turn off permissive mode to be more standards-compliant and avoid compiler errors. - target_compile_options(libloot_tests PRIVATE "/permissive-" "/W4") + target_compile_options(libloot_tests PRIVATE "/permissive-" "/W4" "/Zc:__cplusplus") # Set /bigobj to allow building Debug and RelWithDebInfo tests target_compile_options(libloot_internals_tests PRIVATE "/permissive-" "/W4" - "$<$,$>:/bigobj>") + "$<$,$>:/bigobj>" + "/Zc:__cplusplus") endif() diff --git a/cxx/include/loot/api.h b/cxx/include/loot/api.h index 80c7095b..b8d551e1 100644 --- a/cxx/include/loot/api.h +++ b/cxx/include/loot/api.h @@ -29,6 +29,7 @@ #include #include #include +#include #include "loot/api_decorator.h" #include "loot/enum/game_type.h" @@ -50,14 +51,12 @@ namespace loot { /** * @brief Set the callback function that is called when logging. - * @details If this function is not called, the default behaviour is to - * print messages to the console. * @param callback * The function called when logging. The first parameter is the * level of the message being logged, and the second is the message. */ LOOT_API void SetLoggingCallback( - std::function callback); + std::function callback); /** * @brief Set the log severity level. diff --git a/cxx/include/loot/database_interface.h b/cxx/include/loot/database_interface.h index 3ce1263e..136b8627 100644 --- a/cxx/include/loot/database_interface.h +++ b/cxx/include/loot/database_interface.h @@ -27,6 +27,7 @@ #include #include #include +#include #include #include "loot/exception/cyclic_interaction_error.h" @@ -46,26 +47,41 @@ public: */ /** - * @brief Loads the masterlist, userlist and masterlist prelude from the - * paths specified. + * @brief Loads the masterlist from the path specified. * @details Can be called multiple times, each time replacing the * previously-loaded data. * @param masterlist_path * The relative or absolute path to the masterlist file that should be * loaded. - * @param userlist_path - * The relative or absolute path to the userlist file that should be - * loaded, or an empty path. If an empty path, no userlist will be + */ + virtual void LoadMasterlist( + const std::filesystem::path& masterlistPath) = 0; + + /** + * @brief Loads the masterlist and masterlist prelude from the paths + specified. + * @details Can be called multiple times, each time replacing the + * previously-loaded data. + * @param masterlist_path + * The relative or absolute path to the masterlist file that should be * loaded. * @param masterlist_prelude_path * The relative or absolute path to the masterlist prelude file that - * should be loaded. If an empty path, no masterlist prelude will be + * should be loaded. + */ + virtual void LoadMasterlistWithPrelude( + const std::filesystem::path& masterlistPath, + const std::filesystem::path& masterlistPreludePath) = 0; + + /** + * @brief Loads the userlist from the path specified. + * @details Can be called multiple times, each time replacing the + * previously-loaded data. + * @param userlist_path + * The relative or absolute path to the userlist file that should be * loaded. */ - virtual void LoadLists( - const std::filesystem::path& masterlist_path, - const std::filesystem::path& userlist_path = "", - const std::filesystem::path& masterlist_prelude_path = "") = 0; + virtual void LoadUserlist(const std::filesystem::path& userlistPath) = 0; /** * Writes a metadata file containing all loaded user-added metadata. @@ -91,6 +107,12 @@ public: virtual void WriteMinimalList(const std::filesystem::path& outputFile, const bool overwrite) const = 0; + /** + * @brief Evaluate the given condition string. + * @param condition A condition string. + */ + virtual bool Evaluate(const std::string& condition) const = 0; + /** * @} * @name Non-plugin Data Access @@ -160,8 +182,8 @@ public: * exists. */ virtual std::vector GetGroupsPath( - const std::string& fromGroupName, - const std::string& toGroupName) const = 0; + std::string_view fromGroupName, + std::string_view toGroupName) const = 0; /** * @} @@ -185,7 +207,7 @@ public: * otherwise an optional containing no value. */ virtual std::optional GetPluginMetadata( - const std::string& plugin, + std::string_view plugin, bool includeUserMetadata = true, bool evaluateConditions = false) const = 0; @@ -201,7 +223,7 @@ public: * that metadata, otherwise an optional containing no value. */ virtual std::optional GetPluginUserMetadata( - const std::string& plugin, + std::string_view plugin, bool evaluateConditions = false) const = 0; /** @@ -220,7 +242,7 @@ public: * The filename of the plugin for which all user-added metadata * should be deleted. */ - virtual void DiscardPluginUserMetadata(const std::string& plugin) = 0; + virtual void DiscardPluginUserMetadata(std::string_view plugin) = 0; /** * @brief Discards all loaded user metadata for all plugins, and any diff --git a/cxx/include/loot/exception/undefined_group_error.h b/cxx/include/loot/exception/undefined_group_error.h index 008b3ae0..27dfea1e 100644 --- a/cxx/include/loot/exception/undefined_group_error.h +++ b/cxx/include/loot/exception/undefined_group_error.h @@ -26,6 +26,7 @@ #define LOOT_EXCEPTION_UNDEFINED_GROUP_ERROR #include +#include #include "loot/api_decorator.h" @@ -39,7 +40,7 @@ public: * @brief Construct an exception for an undefined group. * @param groupName The name of the group that is undefined. */ - LOOT_API UndefinedGroupError(const std::string& groupName); + LOOT_API UndefinedGroupError(std::string_view groupName); /** * Get the name of the undefined group. diff --git a/cxx/include/loot/game_interface.h b/cxx/include/loot/game_interface.h index 9e41c83a..3ebcf1f2 100644 --- a/cxx/include/loot/game_interface.h +++ b/cxx/include/loot/game_interface.h @@ -141,8 +141,8 @@ public: * is called, this GameInterface is destroyed, or until a plugin with * a case-insensitively equal filename is loaded. */ - virtual const PluginInterface* GetPlugin( - const std::string& pluginName) const = 0; + virtual std::shared_ptr GetPlugin( + std::string_view pluginName) const = 0; /** * @brief Get a set of const references to all loaded plugins' PluginInterface @@ -152,7 +152,8 @@ public: * this GameInterface is destroyed, or until a plugin with a * case-insensitively equal filename is loaded. */ - virtual std::vector GetLoadedPlugins() const = 0; + virtual std::vector> GetLoadedPlugins() + const = 0; /** * @} diff --git a/cxx/include/loot/loot_version.h b/cxx/include/loot/loot_version.h index f39f504b..961407b0 100644 --- a/cxx/include/loot/loot_version.h +++ b/cxx/include/loot/loot_version.h @@ -34,10 +34,10 @@ namespace loot { inline constexpr unsigned int LIBLOOT_VERSION_MAJOR = 0; /** @brief libloot's minor version number. */ -inline constexpr unsigned int LIBLOOT_VERSION_MINOR = 25; +inline constexpr unsigned int LIBLOOT_VERSION_MINOR = 26; /** @brief libloot's patch version number. */ -inline constexpr unsigned int LIBLOOT_VERSION_PATCH = 5; +inline constexpr unsigned int LIBLOOT_VERSION_PATCH = 0; /** * @brief Get the library version. diff --git a/cxx/include/loot/metadata/conditional_metadata.h b/cxx/include/loot/metadata/conditional_metadata.h index cd47658b..e175670c 100644 --- a/cxx/include/loot/metadata/conditional_metadata.h +++ b/cxx/include/loot/metadata/conditional_metadata.h @@ -25,6 +25,7 @@ #define LOOT_METADATA_CONDITIONAL_METADATA #include +#include #include "loot/api_decorator.h" @@ -48,7 +49,7 @@ public: * documentation. * @return A ConditionalMetadata object. */ - LOOT_API explicit ConditionalMetadata(const std::string& condition); + LOOT_API explicit ConditionalMetadata(std::string_view condition); /** * Check if the condition string is non-empty. diff --git a/cxx/include/loot/metadata/file.h b/cxx/include/loot/metadata/file.h index e7d1934d..c9ea1335 100644 --- a/cxx/include/loot/metadata/file.h +++ b/cxx/include/loot/metadata/file.h @@ -25,6 +25,7 @@ #define LOOT_METADATA_FILE #include +#include #include "loot/api_decorator.h" #include "loot/metadata/conditional_metadata.h" @@ -56,12 +57,16 @@ public: * The detail message content, which may be appended to any messages * generated for this file. If multilingual, one language must be * English. + * @param constraint + * A condition string that must evaluate to true for the file's existence + * to be recognised. * @return A File object. */ - LOOT_API explicit File(const std::string& name, - const std::string& display = "", - const std::string& condition = "", - const std::vector& detail = {}); + LOOT_API explicit File(std::string_view name, + std::string_view display = "", + std::string_view condition = "", + const std::vector& detail = {}, + std::string_view constraint = ""); /** * Get the filename of the file. @@ -84,10 +89,13 @@ public: */ LOOT_API std::vector GetDetail() const; + LOOT_API std::string GetConstraint() const; + private: Filename name_; std::string display_; std::vector detail_; + std::string constraint_; }; /** diff --git a/cxx/include/loot/metadata/filename.h b/cxx/include/loot/metadata/filename.h index b09fa2be..3c7fc9c9 100644 --- a/cxx/include/loot/metadata/filename.h +++ b/cxx/include/loot/metadata/filename.h @@ -25,6 +25,7 @@ #define LOOT_METADATA_FILENAME #include +#include #include "loot/api_decorator.h" @@ -44,7 +45,7 @@ public: * Construct a Filename using the given string. * @return A Filename object. */ - LOOT_API explicit Filename(const std::string& filename); + LOOT_API explicit Filename(std::string_view filename); /** * Get this Filename as a string. diff --git a/cxx/include/loot/metadata/group.h b/cxx/include/loot/metadata/group.h index adf78ce3..ba5a0071 100644 --- a/cxx/include/loot/metadata/group.h +++ b/cxx/include/loot/metadata/group.h @@ -25,6 +25,7 @@ #define LOOT_METADATA_GROUP #include +#include #include #include "loot/api_decorator.h" @@ -38,7 +39,7 @@ public: /** * The name of the group to which all plugins belong by default. */ - static constexpr const char* DEFAULT_NAME = "default"; + static constexpr std::string_view DEFAULT_NAME = "default"; /** * Construct a Group with the name "default" and an empty set of groups to @@ -58,9 +59,9 @@ public: * A description of the group. * @return A Group object. */ - LOOT_API explicit Group(const std::string& name, + LOOT_API explicit Group(std::string_view name, const std::vector& afterGroups = {}, - const std::string& description = ""); + std::string_view description = ""); /** * Get the name of the group. diff --git a/cxx/include/loot/metadata/location.h b/cxx/include/loot/metadata/location.h index 66b319f7..7f32eca4 100644 --- a/cxx/include/loot/metadata/location.h +++ b/cxx/include/loot/metadata/location.h @@ -25,6 +25,7 @@ #define LOOT_METADATA_LOCATION #include +#include #include #include "loot/api_decorator.h" @@ -49,8 +50,8 @@ public: * A name for the URL, eg. the page or site name. * @return A Location object. */ - LOOT_API explicit Location(const std::string& url, - const std::string& name = ""); + LOOT_API explicit Location(std::string_view url, + std::string_view name = ""); /** * Get the object's URL. diff --git a/cxx/include/loot/metadata/message.h b/cxx/include/loot/metadata/message.h index 735d59b1..1509a640 100644 --- a/cxx/include/loot/metadata/message.h +++ b/cxx/include/loot/metadata/message.h @@ -25,6 +25,7 @@ #define LOOT_METADATA_MESSAGE #include +#include #include #include "loot/api_decorator.h" @@ -57,8 +58,8 @@ public: * @return A Message object. */ LOOT_API explicit Message(const MessageType type, - const std::string& content, - const std::string& condition = ""); + std::string_view content, + std::string_view condition = ""); /** * Construct a Message object with the given type, content and condition @@ -73,7 +74,7 @@ public: */ LOOT_API explicit Message(const MessageType type, const std::vector& content, - const std::string& condition = ""); + std::string_view condition = ""); /** * Get the message type. diff --git a/cxx/include/loot/metadata/message_content.h b/cxx/include/loot/metadata/message_content.h index 9a3b4c07..9f23335f 100644 --- a/cxx/include/loot/metadata/message_content.h +++ b/cxx/include/loot/metadata/message_content.h @@ -26,6 +26,7 @@ #include #include +#include #include #include "loot/api_decorator.h" @@ -40,7 +41,7 @@ public: * The code for the default language assumed for message content, which is * "en" (English). */ - static constexpr const char* DEFAULT_LANGUAGE = "en"; + static constexpr std::string_view DEFAULT_LANGUAGE = "en"; /** * Construct a MessageContent object with an empty English message string. @@ -57,8 +58,8 @@ public: * @return A MessageContent object. */ LOOT_API explicit MessageContent( - const std::string& text, - const std::string& language = DEFAULT_LANGUAGE); + std::string_view text, + std::string_view language = DEFAULT_LANGUAGE); /** * Get the message text. @@ -144,7 +145,7 @@ LOOT_API bool operator>=(const MessageContent& lhs, const MessageContent& rhs); */ LOOT_API std::optional SelectMessageContent( const std::vector content, - const std::string& language); + std::string_view language); } #endif diff --git a/cxx/include/loot/metadata/plugin_cleaning_data.h b/cxx/include/loot/metadata/plugin_cleaning_data.h index 8ed59662..bb429927 100644 --- a/cxx/include/loot/metadata/plugin_cleaning_data.h +++ b/cxx/include/loot/metadata/plugin_cleaning_data.h @@ -27,6 +27,7 @@ #include #include +#include #include "loot/api_decorator.h" #include "loot/metadata/message.h" @@ -57,7 +58,7 @@ public: * @return A PluginCleaningData object. */ LOOT_API explicit PluginCleaningData(uint32_t crc, - const std::string& utility); + std::string_view utility); /** * Construct a PluginCleaningData object with the given values. @@ -78,7 +79,7 @@ public: */ LOOT_API explicit PluginCleaningData( uint32_t crc, - const std::string& utility, + std::string_view utility, const std::vector& detail, unsigned int itm, unsigned int ref, diff --git a/cxx/include/loot/metadata/plugin_metadata.h b/cxx/include/loot/metadata/plugin_metadata.h index da821a90..59619544 100644 --- a/cxx/include/loot/metadata/plugin_metadata.h +++ b/cxx/include/loot/metadata/plugin_metadata.h @@ -30,6 +30,7 @@ #include #include #include +#include #include #include "loot/api_decorator.h" @@ -58,7 +59,7 @@ public: * The filename of the plugin that the object is constructed for. * @return A PluginMetadata object. */ - LOOT_API explicit PluginMetadata(const std::string& name); + LOOT_API explicit PluginMetadata(std::string_view name); /** * Merge metadata from the given PluginMetadata object into this object. @@ -138,7 +139,7 @@ public: * @param group * The name of the group this plugin belongs to. */ - LOOT_API void SetGroup(const std::string& group); + LOOT_API void SetGroup(std::string_view group); /** * Unsets the plugin's group. @@ -227,7 +228,7 @@ public: * @returns True if the given plugin name matches this metadata's plugin * name, false otherwise. */ - LOOT_API bool NameMatches(const std::string& pluginName) const; + LOOT_API bool NameMatches(std::string_view pluginName) const; /** * @brief Serialises the plugin metadata as YAML. diff --git a/cxx/include/loot/metadata/tag.h b/cxx/include/loot/metadata/tag.h index 1138b3cc..09f9249e 100644 --- a/cxx/include/loot/metadata/tag.h +++ b/cxx/include/loot/metadata/tag.h @@ -25,6 +25,7 @@ #define LOOT_METADATA_TAG #include +#include #include "loot/api_decorator.h" #include "loot/metadata/conditional_metadata.h" @@ -53,9 +54,9 @@ public: * A condition string. * @return A Tag object. */ - LOOT_API explicit Tag(const std::string& tag, + LOOT_API explicit Tag(std::string_view tag, const bool isAddition = true, - const std::string& condition = ""); + std::string_view condition = ""); /** * Check if the tag should be added. diff --git a/cxx/include/loot/plugin_interface.h b/cxx/include/loot/plugin_interface.h index 4bec165d..dc152a01 100644 --- a/cxx/include/loot/plugin_interface.h +++ b/cxx/include/loot/plugin_interface.h @@ -28,6 +28,7 @@ #include #include #include +#include #include #include "loot/metadata/message.h" @@ -79,7 +80,7 @@ public: * @return A set of Bash Tags. The order of elements in the set holds no * semantics. */ - virtual std::vector GetBashTags() const = 0; + virtual std::vector GetBashTags() const = 0; /** * Get the plugin's CRC-32 checksum. diff --git a/cxx/include/loot/vertex.h b/cxx/include/loot/vertex.h index 861d652c..f18de6b3 100644 --- a/cxx/include/loot/vertex.h +++ b/cxx/include/loot/vertex.h @@ -27,6 +27,7 @@ along with LOOT. If not, see #include #include +#include #include "loot/api_decorator.h" #include "loot/enum/edge_type.h" @@ -42,14 +43,14 @@ public: * @brief Construct a Vertex with the given name and no out edge. * @param name The name of the plugin or group that this vertex represents. */ - LOOT_API explicit Vertex(std::string name); + LOOT_API explicit Vertex(std::string_view name); /** * @brief Construct a Vertex with the given name and out edge type. * @param name The name of the plugin or group that this vertex represents. * @param outEdgeType The type of the edge going out from this vertex. */ - LOOT_API explicit Vertex(std::string name, EdgeType outEdgeType); + LOOT_API explicit Vertex(std::string_view name, EdgeType outEdgeType); /** * @brief Get the name of the plugin or group. diff --git a/cxx/src/api/api.cpp b/cxx/src/api/api.cpp index cc7e32bd..3878917c 100644 --- a/cxx/src/api/api.cpp +++ b/cxx/src/api/api.cpp @@ -32,7 +32,7 @@ void libloot_set_logging_callback(void (*callback)(uint8_t, const char*, void*), namespace { using loot::LogLevel; -static std::function STORED_CALLBACK; +static std::function STORED_CALLBACK; LogLevel convert(uint8_t level) { if (level == LIBLOOT_LOG_LEVEL_TRACE) { @@ -53,25 +53,25 @@ LogLevel convert(uint8_t level) { loot::rust::LogLevel convert(LogLevel level) { switch (level) { case LogLevel::trace: - return loot::rust::LogLevel::Trace; + return loot::rust::LogLevel::Trace; case LogLevel::debug: - return loot::rust::LogLevel::Debug; + return loot::rust::LogLevel::Debug; case LogLevel::info: - return loot::rust::LogLevel::Info; + return loot::rust::LogLevel::Info; case LogLevel::warning: - return loot::rust::LogLevel::Warning; + return loot::rust::LogLevel::Warning; case LogLevel::error: - return loot::rust::LogLevel::Error; + return loot::rust::LogLevel::Error; case LogLevel::fatal: - return loot::rust::LogLevel::Fatal; - default: - return loot::rust::LogLevel::Trace; + return loot::rust::LogLevel::Fatal; + default: + return loot::rust::LogLevel::Trace; } } void logging_callback(uint8_t level, const char* message, void* context) { auto callbackPtr = - static_cast*>(context); + static_cast*>(context); (*callbackPtr)(convert(level), message); } @@ -79,7 +79,7 @@ void logging_callback(uint8_t level, const char* message, void* context) { namespace loot { LOOT_API void SetLoggingCallback( - std::function callback) { + std::function callback) { STORED_CALLBACK = callback; libloot_set_logging_callback(logging_callback, &STORED_CALLBACK); } diff --git a/cxx/src/api/convert.cpp b/cxx/src/api/convert.cpp index 08405f27..b9546d35 100644 --- a/cxx/src/api/convert.cpp +++ b/cxx/src/api/convert.cpp @@ -10,17 +10,25 @@ std::string convert(const ::rust::String& string) { return std::string(string); } +// Although there's an explicit conversion operator declared, it seems that +// building the CXX wrapper with MSVC doesn't set __cplusplus correctly as using +// the operator causes a linker error, so this just reimpls it as a function. +std::string_view convert(::rust::Str str) { + return std::string_view(str.data(), str.length()); +} + loot::Group convert(const loot::rust::Group& group) { - return loot::Group(std::string(group.name()), + return loot::Group(convert(group.name()), convert(group.after_groups()), - std::string(group.description())); + convert(group.description())); } loot::File convert(const loot::rust::File& file) { - return loot::File(std::string(file.filename().as_str()), - std::string(file.display_name()), - std::string(file.condition()), - convert(file.detail())); + return loot::File(convert(file.filename().as_str()), + convert(file.display_name()), + convert(file.condition()), + convert(file.detail()), + convert(file.constraint())); } loot::MessageType convert(loot::rust::MessageType messageType) { @@ -37,24 +45,24 @@ loot::MessageType convert(loot::rust::MessageType messageType) { } loot::MessageContent convert(const loot::rust::MessageContent& content) { - return loot::MessageContent(std::string(content.text()), - std::string(content.language())); + return loot::MessageContent(convert(content.text()), + convert(content.language())); } loot::Message convert(const loot::rust::Message& message) { return loot::Message(convert(message.message_type()), convert(message.content()), - std::string(message.condition())); + convert(message.condition())); } loot::Tag convert(const loot::rust::Tag& tag) { return loot::Tag( - std::string(tag.name()), tag.is_addition(), std::string(tag.condition())); + convert(tag.name()), tag.is_addition(), convert(tag.condition())); } loot::PluginCleaningData convert(const loot::rust::PluginCleaningData& data) { return loot::PluginCleaningData(data.crc(), - std::string(data.cleaning_utility()), + convert(data.cleaning_utility()), convert(data.detail()), data.itm_count(), data.deleted_reference_count(), @@ -62,15 +70,14 @@ loot::PluginCleaningData convert(const loot::rust::PluginCleaningData& data) { } loot::Location convert(const loot::rust::Location& location) { - return loot::Location(std::string(location.url()), - std::string(location.name())); + return loot::Location(convert(location.url()), convert(location.name())); } loot::PluginMetadata convert(const loot::rust::PluginMetadata& metadata) { - auto output = loot::PluginMetadata(std::string(metadata.name())); + auto output = loot::PluginMetadata(convert(metadata.name())); if (!metadata.group().empty()) { - output.SetGroup(std::string(metadata.group())); + output.SetGroup(convert(metadata.group())); } output.SetLoadAfterFiles(convert(metadata.load_after_files())); @@ -123,9 +130,9 @@ loot::Vertex convert(const loot::rust::Vertex& vertex) { try { const auto outEdgeType = convert(vertex.out_edge_type()); if (outEdgeType.has_value()) { - return loot::Vertex(std::string(vertex.name()), outEdgeType.value()); + return loot::Vertex(convert(vertex.name()), outEdgeType.value()); } else { - return loot::Vertex(std::string(vertex.name())); + return loot::Vertex(convert(vertex.name())); } } catch (const ::rust::Error& e) { std::rethrow_exception(mapError(e)); @@ -135,6 +142,10 @@ loot::Vertex convert(const loot::rust::Vertex& vertex) { // From public types /////////////////////// +::rust::Str convert(std::string_view view) { + return ::rust::Str(view.data(), view.length()); +} + ::rust::Box convert(const loot::Group& group) { auto output = loot::rust::new_group(group.GetName()); output->set_after_groups(convert(group.GetAfterGroups())); @@ -156,6 +167,8 @@ loot::Vertex convert(const loot::rust::Vertex& vertex) { output->set_condition(file.GetCondition()); + output->set_constraint(file.GetConstraint()); + return output; } diff --git a/cxx/src/api/convert.h b/cxx/src/api/convert.h index 7b2c9034..998c3d72 100644 --- a/cxx/src/api/convert.h +++ b/cxx/src/api/convert.h @@ -12,6 +12,8 @@ namespace loot { std::string convert(const ::rust::String& string); +std::string_view convert(::rust::Str string); + loot::Group convert(const loot::rust::Group& group); loot::File convert(const loot::rust::File& file); @@ -37,6 +39,8 @@ loot::Vertex convert(const loot::rust::Vertex& vertex); // From public types /////////////////////// +::rust::Str convert(std::string_view view); + ::rust::Box convert(const loot::Group& group); ::rust::Box convert(const loot::File& file); diff --git a/cxx/src/api/database.cpp b/cxx/src/api/database.cpp index 0bdb68e0..d5620ae9 100644 --- a/cxx/src/api/database.cpp +++ b/cxx/src/api/database.cpp @@ -8,22 +8,28 @@ namespace loot { Database::Database(::rust::Box&& database) : database_(std::move(database)) {} -void Database::LoadLists(const std::filesystem::path& masterlistPath, - const std::filesystem::path& userlistPath, - const std::filesystem::path& masterlistPreludePath) { +void Database::LoadMasterlist(const std::filesystem::path& masterlistPath) { try { - if (!masterlistPath.empty()) { - if (!masterlistPreludePath.empty()) { - database_->load_masterlist_with_prelude( - masterlistPath.u8string(), masterlistPreludePath.u8string()); - } else { - database_->load_masterlist(masterlistPath.u8string()); - } - } + database_->load_masterlist(masterlistPath.u8string()); + } catch (const ::rust::Error& e) { + std::rethrow_exception(mapError(e)); + } +} - if (!userlistPath.empty()) { - database_->load_userlist(userlistPath.u8string()); - } +void Database::LoadMasterlistWithPrelude( + const std::filesystem::path& masterlistPath, + const std::filesystem::path& masterlistPreludePath) { + try { + database_->load_masterlist_with_prelude(masterlistPath.u8string(), + masterlistPreludePath.u8string()); + } catch (const ::rust::Error& e) { + std::rethrow_exception(mapError(e)); + } +} + +void Database::LoadUserlist(const std::filesystem::path& userlistPath) { + try { + database_->load_userlist(userlistPath.u8string()); } catch (const ::rust::Error& e) { std::rethrow_exception(mapError(e)); } @@ -38,6 +44,14 @@ void Database::WriteUserMetadata(const std::filesystem::path& outputFile, } } +bool Database::Evaluate(const std::string& condition) const { + try { + return database_->evaluate(condition); + } catch (const ::rust::Error& e) { + std::rethrow_exception(mapError(e)); + } +} + std::vector Database::GetKnownBashTags() const { try { return convert(database_->known_bash_tags()); @@ -81,22 +95,23 @@ void Database::SetUserGroups(const std::vector& groups) { } std::vector Database::GetGroupsPath( - const std::string& fromGroupName, - const std::string& toGroupName) const { + std::string_view fromGroupName, + std::string_view toGroupName) const { try { - return convert(database_->groups_path(fromGroupName, toGroupName)); + return convert( + database_->groups_path(convert(fromGroupName), convert(toGroupName))); } catch (const ::rust::Error& e) { std::rethrow_exception(mapError(e)); } } std::optional Database::GetPluginMetadata( - const std::string& plugin, + std::string_view plugin, bool includeUserMetadata, bool evaluateConditions) const { try { const auto metadata = database_->plugin_metadata( - plugin, includeUserMetadata, evaluateConditions); + convert(plugin), includeUserMetadata, evaluateConditions); if (metadata->is_some()) { return convert(metadata->as_ref()); } else { @@ -108,11 +123,11 @@ std::optional Database::GetPluginMetadata( } std::optional Database::GetPluginUserMetadata( - const std::string& plugin, + std::string_view plugin, bool evaluateConditions) const { try { const auto metadata = - database_->plugin_user_metadata(plugin, evaluateConditions); + database_->plugin_user_metadata(convert(plugin), evaluateConditions); if (metadata->is_some()) { return convert(metadata->as_ref()); } else { @@ -131,9 +146,9 @@ void Database::SetPluginUserMetadata(const PluginMetadata& pluginMetadata) { } } -void Database::DiscardPluginUserMetadata(const std::string& plugin) { +void Database::DiscardPluginUserMetadata(std::string_view plugin) { try { - database_->discard_plugin_user_metadata(plugin); + database_->discard_plugin_user_metadata(convert(plugin)); } catch (const ::rust::Error& e) { std::rethrow_exception(mapError(e)); } diff --git a/cxx/src/api/database.h b/cxx/src/api/database.h index 84c0f2d4..393625e9 100644 --- a/cxx/src/api/database.h +++ b/cxx/src/api/database.h @@ -10,10 +10,13 @@ class Database final : public DatabaseInterface { public: explicit Database(::rust::Box&& database); - void LoadLists( + void LoadMasterlist(const std::filesystem::path& masterlist_path) override; + + void LoadMasterlistWithPrelude( const std::filesystem::path& masterlist_path, - const std::filesystem::path& userlist_path = "", - const std::filesystem::path& masterlist_prelude_path = "") override; + const std::filesystem::path& masterlist_prelude_path) override; + + void LoadUserlist(const std::filesystem::path& userlist_path) override; void WriteUserMetadata(const std::filesystem::path& outputFile, const bool overwrite) const override; @@ -21,6 +24,8 @@ public: void WriteMinimalList(const std::filesystem::path& outputFile, const bool overwrite) const override; + bool Evaluate(const std::string& condition) const override; + std::vector GetKnownBashTags() const override; std::vector GetGeneralMessages( @@ -30,21 +35,21 @@ public: std::vector GetUserGroups() const override; void SetUserGroups(const std::vector& groups) override; std::vector GetGroupsPath( - const std::string& fromGroupName, - const std::string& toGroupName) const override; + std::string_view fromGroupName, + std::string_view toGroupName) const override; std::optional GetPluginMetadata( - const std::string& plugin, + std::string_view plugin, bool includeUserMetadata = true, bool evaluateConditions = false) const override; std::optional GetPluginUserMetadata( - const std::string& plugin, + std::string_view plugin, bool evaluateConditions = false) const override; void SetPluginUserMetadata(const PluginMetadata& pluginMetadata) override; - void DiscardPluginUserMetadata(const std::string& plugin) override; + void DiscardPluginUserMetadata(std::string_view plugin) override; void DiscardAllUserMetadata() override; diff --git a/cxx/src/api/exception.cpp b/cxx/src/api/exception.cpp index acec793c..775ea61b 100644 --- a/cxx/src/api/exception.cpp +++ b/cxx/src/api/exception.cpp @@ -10,19 +10,20 @@ #include "loot/vertex.h" namespace { +using std::string_view_literals::operator""sv; using loot::EdgeType; using loot::Vertex; -constexpr std::string_view CYCLIC_ERROR_PREFIX = "CyclicInteractionError: "; +constexpr std::string_view CYCLIC_ERROR_PREFIX = "CyclicInteractionError: "sv; constexpr std::string_view UNDEFINED_GROUP_ERROR_PREFIX = - "UndefinedGroupError: "; -constexpr std::string_view ESPLUGIN_ERROR_PREFIX = "EspluginError: "; -constexpr std::string_view LIBLOADORDER_ERROR_PREFIX = "LibloadorderError: "; -constexpr std::string_view LCI_ERROR_PREFIX = "LciError: "; -constexpr std::string_view FILE_ACCESS_ERROR_PREFIX = "FileAccessError: "; -constexpr std::string_view INVALID_ARGUMENT_PREFIX = "InvalidArgument: "; + "UndefinedGroupError: "sv; +constexpr std::string_view ESPLUGIN_ERROR_PREFIX = "EspluginError: "sv; +constexpr std::string_view LIBLOADORDER_ERROR_PREFIX = "LibloadorderError: "sv; +constexpr std::string_view LCI_ERROR_PREFIX = "LciError: "sv; +constexpr std::string_view FILE_ACCESS_ERROR_PREFIX = "FileAccessError: "sv; +constexpr std::string_view INVALID_ARGUMENT_PREFIX = "InvalidArgument: "sv; -bool startsWith(const std::string_view& str, const std::string_view& prefix) { +bool startsWith(std::string_view str, std::string_view prefix) { if (str.size() < prefix.size()) { return false; } @@ -30,9 +31,9 @@ bool startsWith(const std::string_view& str, const std::string_view& prefix) { return str.substr(0, prefix.size()) == prefix; } -std::string replace(const std::string_view& str, - const std::string_view& from, - const std::string_view& to) { +std::string replace(std::string_view str, + std::string_view from, + std::string_view to) { std::string out; out.reserve(str.size()); @@ -50,7 +51,7 @@ std::string replace(const std::string_view& str, return out; } -EdgeType toEdgeType(const std::string_view& edgeTypeDisplay) { +EdgeType toEdgeType(std::string_view edgeTypeDisplay) { if (edgeTypeDisplay == "Hardcoded") { return EdgeType::hardcoded; } else if (edgeTypeDisplay == "Master Flag") { @@ -84,7 +85,7 @@ EdgeType toEdgeType(const std::string_view& edgeTypeDisplay) { } } -std::vector parseCyclicError(const std::string_view& what) { +std::vector parseCyclicError(std::string_view what) { const auto suffix = what.substr(0, CYCLIC_ERROR_PREFIX.size()); std::vector vertices; @@ -111,14 +112,13 @@ std::vector parseCyclicError(const std::string_view& what) { return vertices; } -std::string getErrorSuffix(const std::string_view& what) { +std::string getErrorSuffix(std::string_view what) { const auto sepPos = what.find(": "); return std::string(what.substr(sepPos + 2)); } -std::pair parseSystemError( - const std::string_view& whatSuffix) { +std::pair parseSystemError(std::string_view whatSuffix) { const auto sepPos = whatSuffix.find(": "); int code; const auto result = diff --git a/cxx/src/api/game.cpp b/cxx/src/api/game.cpp index 324149eb..e8d17c4e 100644 --- a/cxx/src/api/game.cpp +++ b/cxx/src/api/game.cpp @@ -160,52 +160,31 @@ void Game::LoadPlugins(const std::vector& pluginPaths, } catch (const ::rust::Error& e) { std::rethrow_exception(mapError(e)); } - - for (const auto& path : pluginPaths) { - auto key = Filename(path.filename().u8string()); - auto it = plugins_.find(key); - if (it != plugins_.end()) { - plugins_.erase(it); - } - } } -void Game::ClearLoadedPlugins() { - game_->clear_loaded_plugins(); - plugins_.clear(); -} +void Game::ClearLoadedPlugins() { game_->clear_loaded_plugins(); } -const PluginInterface* Game::GetPlugin(const std::string& pluginName) const { - const auto pluginOpt = game_->plugin(pluginName); +std::shared_ptr Game::GetPlugin( + std::string_view pluginName) const { + const auto pluginOpt = game_->plugin(convert(pluginName)); if (!pluginOpt->is_some()) { return nullptr; } - auto key = Filename(pluginName); - const auto it = plugins_.find(key); - if (it != plugins_.end()) { - return it->second.get(); - } - try { - auto plugin = std::make_shared(std::move(pluginOpt->as_ref().boxed_clone())); - const auto result = plugins_.emplace(key, plugin); - - return result.first->second.get(); + return std::make_shared( + std::move(pluginOpt->as_ref().boxed_clone())); } catch (const ::rust::Error& e) { std::rethrow_exception(mapError(e)); } } -std::vector Game::GetLoadedPlugins() const { - std::vector plugins; +std::vector> Game::GetLoadedPlugins() + const { + std::vector> plugins; for (const auto& pluginRef : game_->loaded_plugins()) { - const auto plugin = - std::make_shared(std::move(pluginRef.boxed_clone())); - - const auto result = - plugins_.insert_or_assign(Filename(plugin->GetName()), plugin); - plugins.push_back(result.first->second.get()); + plugins.push_back( + std::make_shared(std::move(pluginRef.boxed_clone()))); } return plugins; diff --git a/cxx/src/api/game.h b/cxx/src/api/game.h index 819712ba..85fb4886 100644 --- a/cxx/src/api/game.h +++ b/cxx/src/api/game.h @@ -37,10 +37,11 @@ public: void ClearLoadedPlugins() override; - const PluginInterface* GetPlugin( - const std::string& pluginName) const override; + std::shared_ptr GetPlugin( + std::string_view pluginName) const override; - std::vector GetLoadedPlugins() const override; + std::vector> GetLoadedPlugins() + const override; std::vector SortPlugins( const std::vector& pluginFilenames) override; @@ -60,7 +61,6 @@ public: private: ::rust::Box game_; Database database_; - mutable std::map> plugins_; }; } diff --git a/cxx/src/api/metadata/conditional_metadata.cpp b/cxx/src/api/metadata/conditional_metadata.cpp index f778f686..c660ed80 100644 --- a/cxx/src/api/metadata/conditional_metadata.cpp +++ b/cxx/src/api/metadata/conditional_metadata.cpp @@ -25,7 +25,7 @@ #include "loot/metadata/conditional_metadata.h" namespace loot { -ConditionalMetadata::ConditionalMetadata(const std::string& condition) : +ConditionalMetadata::ConditionalMetadata(std::string_view condition) : condition_(condition) {} bool ConditionalMetadata::IsConditional() const { return !condition_.empty(); } diff --git a/cxx/src/api/metadata/file.cpp b/cxx/src/api/metadata/file.cpp index 132ec0fa..c199ac3c 100644 --- a/cxx/src/api/metadata/file.cpp +++ b/cxx/src/api/metadata/file.cpp @@ -25,14 +25,16 @@ #include "loot/metadata/file.h" namespace loot { -File::File(const std::string& name, - const std::string& display, - const std::string& condition, - const std::vector& detail) : +File::File(std::string_view name, + std::string_view display, + std::string_view condition, + const std::vector& detail, + std::string_view constraint) : ConditionalMetadata(condition), name_(Filename(name)), display_(display), - detail_(detail) {} + detail_(detail), + constraint_(constraint) {} Filename File::GetName() const { return name_; } @@ -40,9 +42,12 @@ std::string File::GetDisplayName() const { return display_; } std::vector File::GetDetail() const { return detail_; } +std::string File::GetConstraint() const { return constraint_; } + bool operator==(const File& lhs, const File& rhs) { return lhs.GetDisplayName() == rhs.GetDisplayName() && lhs.GetCondition() == rhs.GetCondition() && + lhs.GetConstraint() == rhs.GetConstraint() && lhs.GetName() == rhs.GetName() && lhs.GetDetail() == rhs.GetDetail(); } @@ -65,6 +70,14 @@ bool operator<(const File& lhs, const File& rhs) { return false; } + if (lhs.GetConstraint() < rhs.GetConstraint()) { + return true; + } + + if (rhs.GetConstraint() < lhs.GetConstraint()) { + return false; + } + if (lhs.GetName() < rhs.GetName()) { return true; } diff --git a/cxx/src/api/metadata/filename.cpp b/cxx/src/api/metadata/filename.cpp index 34e9178e..54a34964 100644 --- a/cxx/src/api/metadata/filename.cpp +++ b/cxx/src/api/metadata/filename.cpp @@ -30,8 +30,7 @@ #include "libloot-cxx/src/lib.rs.h" namespace loot { -Filename::Filename(const std::string& filename) : - filename_(filename) {} +Filename::Filename(std::string_view filename) : filename_(filename) {} Filename::operator std::string() const { return filename_; } diff --git a/cxx/src/api/metadata/group.cpp b/cxx/src/api/metadata/group.cpp index a3117f16..4205d17f 100644 --- a/cxx/src/api/metadata/group.cpp +++ b/cxx/src/api/metadata/group.cpp @@ -25,9 +25,9 @@ #include "loot/metadata/group.h" namespace loot { -Group::Group(const std::string& name, +Group::Group(std::string_view name, const std::vector& afterGroups, - const std::string& description) : + std::string_view description) : name_(name), description_(description), afterGroups_(afterGroups) {} std::string Group::GetName() const { return name_; } diff --git a/cxx/src/api/metadata/location.cpp b/cxx/src/api/metadata/location.cpp index ac669ee6..7b540212 100644 --- a/cxx/src/api/metadata/location.cpp +++ b/cxx/src/api/metadata/location.cpp @@ -25,7 +25,7 @@ #include "loot/metadata/location.h" namespace loot { -Location::Location(const std::string& url, const std::string& name) : +Location::Location(std::string_view url, std::string_view name) : url_(url), name_(name) {} std::string Location::GetURL() const { return url_; } diff --git a/cxx/src/api/metadata/message.cpp b/cxx/src/api/metadata/message.cpp index ca152ab2..c3dec404 100644 --- a/cxx/src/api/metadata/message.cpp +++ b/cxx/src/api/metadata/message.cpp @@ -28,15 +28,15 @@ namespace loot { Message::Message(const MessageType type, - const std::string& content, - const std::string& condition) : + std::string_view content, + std::string_view condition) : ConditionalMetadata(condition), type_(type), content_({MessageContent(content)}) {} Message::Message(const MessageType type, const std::vector& content, - const std::string& condition) : + std::string_view condition) : ConditionalMetadata(condition), type_(type), content_(content) { if (content.size() > 1) { bool englishStringExists = false; diff --git a/cxx/src/api/metadata/message_content.cpp b/cxx/src/api/metadata/message_content.cpp index 4458551a..c184af48 100644 --- a/cxx/src/api/metadata/message_content.cpp +++ b/cxx/src/api/metadata/message_content.cpp @@ -25,8 +25,8 @@ #include "loot/metadata/message_content.h" namespace loot { -MessageContent::MessageContent(const std::string& text, - const std::string& language) : +MessageContent::MessageContent(std::string_view text, + std::string_view language) : text_(text), language_(language) {} std::string MessageContent::GetText() const { return text_; } @@ -68,7 +68,7 @@ bool operator>=(const MessageContent& lhs, const MessageContent& rhs) { std::optional SelectMessageContent( const std::vector content, - const std::string& language) { + std::string_view language) { if (content.empty()) return std::nullopt; else if (content.size() == 1) diff --git a/cxx/src/api/metadata/plugin_cleaning_data.cpp b/cxx/src/api/metadata/plugin_cleaning_data.cpp index 8dc5d9fc..3664d327 100644 --- a/cxx/src/api/metadata/plugin_cleaning_data.cpp +++ b/cxx/src/api/metadata/plugin_cleaning_data.cpp @@ -25,13 +25,12 @@ #include "loot/metadata/plugin_cleaning_data.h" namespace loot { -PluginCleaningData::PluginCleaningData(uint32_t crc, - const std::string& utility) : +PluginCleaningData::PluginCleaningData(uint32_t crc, std::string_view utility) : crc_(crc), utility_(utility) {} PluginCleaningData::PluginCleaningData( uint32_t crc, - const std::string& utility, + std::string_view utility, const std::vector& detail, unsigned int itm, unsigned int ref, diff --git a/cxx/src/api/metadata/plugin_metadata.cpp b/cxx/src/api/metadata/plugin_metadata.cpp index 2297c2a2..7430b95b 100644 --- a/cxx/src/api/metadata/plugin_metadata.cpp +++ b/cxx/src/api/metadata/plugin_metadata.cpp @@ -50,9 +50,10 @@ std::vector mergeVectors(std::vector first, return first; } -std::string TrimDotGhostExtension(const std::string& filename) { +std::string TrimDotGhostExtension(std::string&& filename) { + using std::string_view_literals::operator""sv; // If the name passed ends in '.ghost', that should be trimmed. - constexpr std::string_view GHOST_FILE_EXTENSION = ".ghost"; + constexpr std::string_view GHOST_FILE_EXTENSION = ".ghost"sv; if (filename.length() < GHOST_FILE_EXTENSION.length()) { return filename; @@ -77,10 +78,9 @@ std::string TrimDotGhostExtension(const std::string& filename) { } namespace loot { -PluginMetadata::PluginMetadata(const std::string& n) : name_(n) { - // If the name passed ends in '.ghost', that should be trimmed. - name_ = TrimDotGhostExtension(n); - +// If the name passed ends in '.ghost', that should be trimmed. +PluginMetadata::PluginMetadata(std::string_view n) : + name_(TrimDotGhostExtension(std::string(n))) { if (IsRegexPlugin()) { nameRegex_ = std::regex(name_, std::regex::ECMAScript | std::regex::icase); } @@ -144,7 +144,7 @@ std::vector PluginMetadata::GetLocations() const { return locations_; } -void PluginMetadata::SetGroup(const std::string& group) { group_ = group; } +void PluginMetadata::SetGroup(std::string_view group) { group_ = group; } void PluginMetadata::UnsetGroup() { group_ = std::nullopt; } @@ -192,16 +192,17 @@ bool PluginMetadata::IsRegexPlugin() const { return strpbrk(name_.c_str(), ":\\*?|") != nullptr; } -bool PluginMetadata::NameMatches(const std::string& pluginName) const { +bool PluginMetadata::NameMatches(std::string_view pluginName) const { if (IsRegexPlugin()) { if (!nameRegex_.has_value()) { throw std::runtime_error("Regex plugin does not have regex object"); } - return std::regex_match(pluginName, nameRegex_.value()); + return std::regex_match( + pluginName.begin(), pluginName.end(), nameRegex_.value()); } - return loot::rust::compare_filenames(name_, pluginName) == 0; + return loot::rust::compare_filenames(name_, convert(pluginName)) == 0; } std::string PluginMetadata::AsYaml() const { diff --git a/cxx/src/api/metadata/tag.cpp b/cxx/src/api/metadata/tag.cpp index 1a551ad5..80640d0b 100644 --- a/cxx/src/api/metadata/tag.cpp +++ b/cxx/src/api/metadata/tag.cpp @@ -25,9 +25,9 @@ #include "loot/metadata/tag.h" namespace loot { -Tag::Tag(const std::string& tag, +Tag::Tag(std::string_view tag, const bool isAddition, - const std::string& condition) : + std::string_view condition) : ConditionalMetadata(condition), name_(tag), addTag_(isAddition) {} bool Tag::IsAddition() const { return addTag_; } diff --git a/cxx/src/api/plugin.cpp b/cxx/src/api/plugin.cpp index 592a1a53..3446e8d1 100644 --- a/cxx/src/api/plugin.cpp +++ b/cxx/src/api/plugin.cpp @@ -38,13 +38,8 @@ std::vector Plugin::GetMasters() const { } } -std::vector Plugin::GetBashTags() const { - std::vector tags; - for (const auto& tag : plugin_->bash_tags()) { - tags.push_back(Tag(std::string(tag))); - } - - return tags; +std::vector Plugin::GetBashTags() const { + return convert(plugin_->bash_tags()); } std::optional Plugin::GetCRC() const { @@ -102,7 +97,9 @@ bool Plugin::DoRecordsOverlap(const PluginInterface& plugin) const { return plugin_->do_records_overlap(*otherPlugin.plugin_); } catch (std::bad_cast&) { - throw std::invalid_argument("Tried to check if records overlapped with a different concrete type implementing PluginInterface"); + throw std::invalid_argument( + "Tried to check if records overlapped with a different concrete type " + "implementing PluginInterface"); } catch (const ::rust::Error& e) { std::rethrow_exception(mapError(e)); } diff --git a/cxx/src/api/plugin.h b/cxx/src/api/plugin.h index 79eb6d40..32107e02 100644 --- a/cxx/src/api/plugin.h +++ b/cxx/src/api/plugin.h @@ -22,7 +22,7 @@ public: std::optional GetHeaderVersion() const override; std::optional GetVersion() const override; std::vector GetMasters() const override; - std::vector GetBashTags() const override; + std::vector GetBashTags() const override; std::optional GetCRC() const override; bool IsMaster() const override; diff --git a/cxx/src/api/resource.rc b/cxx/src/api/resource.rc index 80d6b3bd..a119a2db 100644 --- a/cxx/src/api/resource.rc +++ b/cxx/src/api/resource.rc @@ -13,12 +13,12 @@ BLOCK "040904b0" BEGIN VALUE "CompanyName", "LOOT" VALUE "FileDescription", "Library providing LOOT's core functionality" -VALUE "FileVersion", "0.25.5" +VALUE "FileVersion", "0.26.0" VALUE "InternalName", "loot" VALUE "LegalCopyright", "Copyright (C) 2013-2022 Oliver Hamlet" VALUE "OriginalFilename", "loot.dll" VALUE "ProductName", "LOOT" -VALUE "ProductVersion", "0.25.5" +VALUE "ProductVersion", "0.26.0" END END BLOCK "VarFileInfo" diff --git a/cxx/src/api/sorting/undefined_group_error.cpp b/cxx/src/api/sorting/undefined_group_error.cpp index 50fa72da..c62aadac 100644 --- a/cxx/src/api/sorting/undefined_group_error.cpp +++ b/cxx/src/api/sorting/undefined_group_error.cpp @@ -24,8 +24,9 @@ #include "loot/exception/undefined_group_error.h" namespace loot { -UndefinedGroupError::UndefinedGroupError(const std::string& groupName) : - std::runtime_error("The group \"" + groupName + "\" does not exist"), +UndefinedGroupError::UndefinedGroupError(std::string_view groupName) : + std::runtime_error("The group \"" + std::string(groupName) + + "\" does not exist"), groupName_(groupName) {} std::string UndefinedGroupError::GetGroupName() const { return groupName_; } diff --git a/cxx/src/api/vertex.cpp b/cxx/src/api/vertex.cpp index fb6a345b..ee1bf9af 100644 --- a/cxx/src/api/vertex.cpp +++ b/cxx/src/api/vertex.cpp @@ -24,9 +24,9 @@ #include "loot/vertex.h" namespace loot { -Vertex::Vertex(std::string name) : name_(name) {} +Vertex::Vertex(std::string_view name) : name_(name) {} -Vertex::Vertex(std::string name, EdgeType outEdgeType) : +Vertex::Vertex(std::string_view name, EdgeType outEdgeType) : name_(name), outEdgeType_(outEdgeType) {} std::string Vertex::GetName() const { return name_; } diff --git a/cxx/src/database.rs b/cxx/src/database.rs index 1b6fea2a..660c75ec 100644 --- a/cxx/src/database.rs +++ b/cxx/src/database.rs @@ -85,6 +85,14 @@ impl Database { .map_err(Into::into) } + pub fn evaluate(&self, condition: &str) -> Result { + self.0 + .read() + .map_err(|_| DatabaseLockPoisonError)? + .evaluate(condition) + .map_err(Into::into) + } + pub fn known_bash_tags(&self) -> Result, VerboseError> { Ok(self .0 diff --git a/cxx/src/lib.rs b/cxx/src/lib.rs index 4e7e80b3..66c965e5 100644 --- a/cxx/src/lib.rs +++ b/cxx/src/lib.rs @@ -232,6 +232,8 @@ mod ffi { pub fn write_minimal_list(&self, output_path: &str, overwrite: bool) -> Result<()>; + pub fn evaluate(&self, condition: &str) -> Result; + pub fn known_bash_tags(&self) -> Result>; pub fn general_messages(&self, evaluate_conditions: bool) -> Result>; @@ -489,6 +491,10 @@ mod ffi { pub fn set_condition(&mut self, condition: String); + pub fn constraint(&self) -> &str; + + pub fn set_constraint(&mut self, constraint: String); + pub fn boxed_clone(&self) -> Box; } diff --git a/cxx/src/metadata.rs b/cxx/src/metadata.rs index 13e22e50..7eff0ebb 100644 --- a/cxx/src/metadata.rs +++ b/cxx/src/metadata.rs @@ -408,6 +408,14 @@ impl File { self.0.set_condition(condition); } + pub fn constraint(&self) -> &str { + self.0.constraint().unwrap_or("") + } + + pub fn set_constraint(&mut self, constraint: String) { + self.0.set_constraint(constraint); + } + pub fn boxed_clone(&self) -> Box { Box::new(Self(self.0.clone())) } diff --git a/cxx/src/tests/api/interface/database_interface_test.h b/cxx/src/tests/api/interface/database_interface_test.h index 2a4511b2..2ee00b95 100644 --- a/cxx/src/tests/api/interface/database_interface_test.h +++ b/cxx/src/tests/api/interface/database_interface_test.h @@ -43,6 +43,25 @@ protected: ASSERT_FALSE(std::filesystem::exists(minimalOutputPath_)); } + std::string GetExpectedMinimalContent() const { + using std::endl; + + std::stringstream expectedContent; + expectedContent << "plugins:" << endl + << " - name: '" << blankDifferentEsm << "'" << endl + << " dirty:" << endl + << " - crc: 0x7D22F9DF" << endl + << " util: 'TES4Edit'" << endl + << " udr: 4" << endl + << " - name: '" << blankEsm << "'" << endl + << " tag:" << endl + << " - Actors.ACBS" << endl + << " - Actors.AIData" << endl + << " - -C.Water"; + + return expectedContent.str(); + } + std::string GetFileContent(const std::filesystem::path& file) { std::ifstream stream(file); std::stringstream content; @@ -102,58 +121,42 @@ INSTANTIATE_TEST_SUITE_P(, GameType::tes5se)); TEST_P(DatabaseInterfaceTest, - loadListsShouldSucceedEvenIfGameHandleIsDiscarded) { + loadMasterlistShouldSucceedEvenIfGameHandleIsDiscarded) { handle_ = CreateGameHandle(GetParam(), gamePath, localPath); ASSERT_NO_THROW(GenerateMasterlist()); - EXPECT_NO_THROW(handle_->GetDatabase().LoadLists(masterlistPath, "")); -} - -TEST_P(DatabaseInterfaceTest, loadListsShouldThrowIfNoMasterlistIsPresent) { - EXPECT_THROW(handle_->GetDatabase().LoadLists(masterlistPath, ""), - FileAccessError); -} - -TEST_P( - DatabaseInterfaceTest, - loadListsShouldThrowIfAMasterlistIsPresentButAUserlistDoesNotExistAtTheGivenPath) { - ASSERT_NO_THROW(GenerateMasterlist()); - EXPECT_THROW(handle_->GetDatabase().LoadLists(masterlistPath, userlistPath_), - FileAccessError); -} - -TEST_P( - DatabaseInterfaceTest, - loadListsShouldSucceedIfTheMasterlistIsPresentAndTheUserlistPathIsAnEmptyString) { - ASSERT_NO_THROW(GenerateMasterlist()); - - EXPECT_NO_THROW(handle_->GetDatabase().LoadLists(masterlistPath, "")); + EXPECT_NO_THROW(handle_->GetDatabase().LoadMasterlist(masterlistPath)); } TEST_P(DatabaseInterfaceTest, - loadListsShouldSucceedIfTheMasterlistAndUserlistAreBothPresent) { - ASSERT_NO_THROW(GenerateMasterlist()); - ASSERT_NO_THROW(std::filesystem::copy(masterlistPath, userlistPath_)); + loadMasterlistShouldThrowIfNoMasterlistIsPresent) { + EXPECT_THROW(handle_->GetDatabase().LoadMasterlist(masterlistPath), + FileAccessError); +} - EXPECT_NO_THROW( - handle_->GetDatabase().LoadLists(masterlistPath, userlistPath_)); +TEST_P(DatabaseInterfaceTest, + loadMasterlistShouldSucceedIfTheMasterlistIsPresent) { + ASSERT_NO_THROW(GenerateMasterlist()); + + EXPECT_NO_THROW(handle_->GetDatabase().LoadMasterlist(masterlistPath)); } TEST_P( DatabaseInterfaceTest, - loadListsShouldThrowIfAMasterlistIsPresentButAPreludeDoesNotExistAtTheGivenPath) { + loadMasterlistWithPreludeShouldThrowIfAMasterlistIsPresentButAPreludeDoesNotExistAtTheGivenPath) { ASSERT_NO_THROW(GenerateMasterlist()); auto preludePath = localPath / "prelude.yaml"; - EXPECT_THROW( - handle_->GetDatabase().LoadLists(masterlistPath, "", preludePath), - FileAccessError); + EXPECT_THROW(handle_->GetDatabase().LoadMasterlistWithPrelude(masterlistPath, + preludePath), + FileAccessError); } -TEST_P(DatabaseInterfaceTest, - loadListsShouldSucceedIfTheMasterlistAndPreludeAreBothPresent) { +TEST_P( + DatabaseInterfaceTest, + loadMasterlistWithPreludeShouldSucceedIfTheMasterlistAndPreludeAreBothPresent) { using std::endl; std::ofstream out(masterlistPath); @@ -173,8 +176,8 @@ TEST_P(DatabaseInterfaceTest, << " type: say" << endl << " content: Loaded from prelude" << endl; - EXPECT_NO_THROW( - handle_->GetDatabase().LoadLists(masterlistPath, "", preludePath)); + EXPECT_NO_THROW(handle_->GetDatabase().LoadMasterlistWithPrelude( + masterlistPath, preludePath)); auto messages = handle_->GetDatabase().GetGeneralMessages(); ASSERT_EQ(1, messages.size()); @@ -183,6 +186,20 @@ TEST_P(DatabaseInterfaceTest, EXPECT_EQ("Loaded from prelude", messages[0].GetContent()[0].GetText()); } +TEST_P(DatabaseInterfaceTest, + loadUserlistShouldThrowIfAUserlistDoesNotExistAtTheGivenPath) { + ASSERT_NO_THROW(GenerateMasterlist()); + EXPECT_THROW(handle_->GetDatabase().LoadUserlist(userlistPath_), + FileAccessError); +} + +TEST_P(DatabaseInterfaceTest, loadUserlistShouldSucceedIfTheUserlistIsPresent) { + ASSERT_NO_THROW(GenerateMasterlist()); + ASSERT_NO_THROW(std::filesystem::copy(masterlistPath, userlistPath_)); + + EXPECT_NO_THROW(handle_->GetDatabase().LoadUserlist(userlistPath_)); +} + TEST_P( DatabaseInterfaceTest, writeUserMetadataShouldThrowIfTheFileAlreadyExistsAndTheOverwriteArgumentIsFalse) { @@ -232,7 +249,7 @@ TEST_P(DatabaseInterfaceTest, TEST_P(DatabaseInterfaceTest, writeUserMetadataShouldShouldNotWriteMasterlistMetadata) { ASSERT_NO_THROW(GenerateMasterlist()); - ASSERT_NO_THROW(handle_->GetDatabase().LoadLists(masterlistPath, "")); + ASSERT_NO_THROW(handle_->GetDatabase().LoadMasterlist(masterlistPath)); EXPECT_NO_THROW( handle_->GetDatabase().WriteUserMetadata(minimalOutputPath_, true)); @@ -248,8 +265,8 @@ TEST_P(DatabaseInterfaceTest, writeUserMetadataShouldShouldWriteUserMetadata) { masterlist << "bash_tags:\n []\nglobals:\n []\nplugins:\n []"; masterlist.close(); - ASSERT_NO_THROW( - handle_->GetDatabase().LoadLists(masterlistPath, userlistPath_)); + ASSERT_NO_THROW(handle_->GetDatabase().LoadMasterlist(masterlistPath)); + ASSERT_NO_THROW(handle_->GetDatabase().LoadUserlist(userlistPath_)); EXPECT_NO_THROW( handle_->GetDatabase().WriteUserMetadata(minimalOutputPath_, true)); @@ -257,13 +274,21 @@ TEST_P(DatabaseInterfaceTest, writeUserMetadataShouldShouldWriteUserMetadata) { EXPECT_FALSE(GetFileContent(minimalOutputPath_).empty()); } +TEST_P(DatabaseInterfaceTest, evaluateShouldReturnTrueIfTheConditionIsTrue) { + EXPECT_TRUE(handle_->GetDatabase().Evaluate("file(\"Blank.esp\")")); +} + +TEST_P(DatabaseInterfaceTest, evaluateShouldReturnFalseIfTheConditionIsFalse) { + EXPECT_FALSE(handle_->GetDatabase().Evaluate("file(\"missing.esp\")")); +} + TEST_P(DatabaseInterfaceTest, getGroupsShouldReturnAllGroupsListedInTheLoadedMetadata) { ASSERT_NO_THROW(GenerateMasterlist()); ASSERT_NO_THROW(GenerateUserlist()); - ASSERT_NO_THROW( - handle_->GetDatabase().LoadLists(masterlistPath, userlistPath_)); + ASSERT_NO_THROW(handle_->GetDatabase().LoadMasterlist(masterlistPath)); + ASSERT_NO_THROW(handle_->GetDatabase().LoadUserlist(userlistPath_)); auto groups = handle_->GetDatabase().GetGroups(); @@ -298,8 +323,8 @@ TEST_P(DatabaseInterfaceTest, ASSERT_NO_THROW(GenerateMasterlist()); ASSERT_NO_THROW(GenerateUserlist()); - ASSERT_NO_THROW( - handle_->GetDatabase().LoadLists(masterlistPath, userlistPath_)); + ASSERT_NO_THROW(handle_->GetDatabase().LoadMasterlist(masterlistPath)); + ASSERT_NO_THROW(handle_->GetDatabase().LoadUserlist(userlistPath_)); auto groups = handle_->GetDatabase().GetGroups(false); @@ -331,8 +356,8 @@ TEST_P(DatabaseInterfaceTest, ASSERT_NO_THROW(GenerateMasterlist()); ASSERT_NO_THROW(GenerateUserlist()); - ASSERT_NO_THROW( - handle_->GetDatabase().LoadLists(masterlistPath, userlistPath_)); + ASSERT_NO_THROW(handle_->GetDatabase().LoadMasterlist(masterlistPath)); + ASSERT_NO_THROW(handle_->GetDatabase().LoadUserlist(userlistPath_)); auto groups = handle_->GetDatabase().GetUserGroups(); @@ -354,8 +379,8 @@ TEST_P( ASSERT_NO_THROW(GenerateMasterlist()); ASSERT_NO_THROW(GenerateUserlist()); - ASSERT_NO_THROW( - handle_->GetDatabase().LoadLists(masterlistPath, userlistPath_)); + ASSERT_NO_THROW(handle_->GetDatabase().LoadMasterlist(masterlistPath)); + ASSERT_NO_THROW(handle_->GetDatabase().LoadUserlist(userlistPath_)); handle_->GetDatabase().SetUserGroups(std::vector({ Group("group4"), @@ -377,8 +402,8 @@ TEST_P(DatabaseInterfaceTest, ASSERT_NO_THROW(GenerateMasterlist()); ASSERT_NO_THROW(GenerateUserlist()); - ASSERT_NO_THROW( - handle_->GetDatabase().LoadLists(masterlistPath, userlistPath_)); + ASSERT_NO_THROW(handle_->GetDatabase().LoadMasterlist(masterlistPath)); + ASSERT_NO_THROW(handle_->GetDatabase().LoadUserlist(userlistPath_)); auto path = handle_->GetDatabase().GetGroupsPath("group1", "group3"); @@ -394,8 +419,8 @@ TEST_P(DatabaseInterfaceTest, ASSERT_NO_THROW(GenerateMasterlist()); ASSERT_NO_THROW(GenerateUserlist()); - ASSERT_NO_THROW( - handle_->GetDatabase().LoadLists(masterlistPath, userlistPath_)); + ASSERT_NO_THROW(handle_->GetDatabase().LoadMasterlist(masterlistPath)); + ASSERT_NO_THROW(handle_->GetDatabase().LoadUserlist(userlistPath_)); auto tags = handle_->GetDatabase().GetKnownBashTags(); @@ -412,8 +437,8 @@ TEST_P(DatabaseInterfaceTest, getGeneralMessagesShouldGetGeneralMessagesFromTheMasterlistAndUserlist) { ASSERT_NO_THROW(GenerateMasterlist()); ASSERT_NO_THROW(GenerateUserlist()); - ASSERT_NO_THROW( - handle_->GetDatabase().LoadLists(masterlistPath, userlistPath_)); + ASSERT_NO_THROW(handle_->GetDatabase().LoadMasterlist(masterlistPath)); + ASSERT_NO_THROW(handle_->GetDatabase().LoadUserlist(userlistPath_)); auto messages = handle_->GetDatabase().GetGeneralMessages(); @@ -430,7 +455,7 @@ TEST_P( DatabaseInterfaceTest, getGeneralMessagesShouldReturnOnlyValidMessagesIfConditionsAreEvaluated) { ASSERT_NO_THROW(GenerateMasterlist()); - ASSERT_NO_THROW(handle_->GetDatabase().LoadLists(masterlistPath, "")); + ASSERT_NO_THROW(handle_->GetDatabase().LoadMasterlist(masterlistPath)); auto messages = handle_->GetDatabase().GetGeneralMessages(true); @@ -447,8 +472,8 @@ TEST_P( getPluginMetadataShouldReturnMergedMasterAndUserMetadataForTheGivenPluginIfIncludeUserMetadataIsTrue) { ASSERT_NO_THROW(GenerateMasterlist()); ASSERT_NO_THROW(GenerateUserlist()); - ASSERT_NO_THROW( - handle_->GetDatabase().LoadLists(masterlistPath, userlistPath_)); + ASSERT_NO_THROW(handle_->GetDatabase().LoadMasterlist(masterlistPath)); + ASSERT_NO_THROW(handle_->GetDatabase().LoadUserlist(userlistPath_)); auto metadata = handle_->GetDatabase().GetPluginMetadata(blankEsm, true).value(); @@ -464,8 +489,8 @@ TEST_P(DatabaseInterfaceTest, getPluginMetadataShouldPreferUserMetadataWhenMergingMetadata) { ASSERT_NO_THROW(GenerateMasterlist()); ASSERT_NO_THROW(GenerateUserlist()); - ASSERT_NO_THROW( - handle_->GetDatabase().LoadLists(masterlistPath, userlistPath_)); + ASSERT_NO_THROW(handle_->GetDatabase().LoadMasterlist(masterlistPath)); + ASSERT_NO_THROW(handle_->GetDatabase().LoadUserlist(userlistPath_)); auto metadata = handle_->GetDatabase().GetPluginMetadata(blankEsm, true).value(); @@ -486,8 +511,8 @@ TEST_P( getPluginMetadataShouldReturnOnlyMasterlistMetadataForTheGivenPluginIfIncludeUserMetadataIsFalse) { ASSERT_NO_THROW(GenerateMasterlist()); ASSERT_NO_THROW(GenerateUserlist()); - ASSERT_NO_THROW( - handle_->GetDatabase().LoadLists(masterlistPath, userlistPath_)); + ASSERT_NO_THROW(handle_->GetDatabase().LoadMasterlist(masterlistPath)); + ASSERT_NO_THROW(handle_->GetDatabase().LoadUserlist(userlistPath_)); auto metadata = handle_->GetDatabase().GetPluginMetadata(blankEsm, false).value(); @@ -502,7 +527,7 @@ TEST_P( DatabaseInterfaceTest, getPluginMetadataShouldReturnOnlyValidMetadataForTheGivenPluginIfConditionsAreEvaluated) { ASSERT_NO_THROW(GenerateMasterlist()); - ASSERT_NO_THROW(handle_->GetDatabase().LoadLists(masterlistPath, "")); + ASSERT_NO_THROW(handle_->GetDatabase().LoadMasterlist(masterlistPath)); auto metadata = handle_->GetDatabase().GetPluginMetadata(blankEsm, false, true).value(); @@ -515,8 +540,8 @@ TEST_P( getPluginUserMetadataShouldReturnAnEmptyPluginMetadataObjectIfThePluginHasNoUserMetadata) { ASSERT_NO_THROW(GenerateMasterlist()); ASSERT_NO_THROW(GenerateUserlist()); - ASSERT_NO_THROW( - handle_->GetDatabase().LoadLists(masterlistPath, userlistPath_)); + ASSERT_NO_THROW(handle_->GetDatabase().LoadMasterlist(masterlistPath)); + ASSERT_NO_THROW(handle_->GetDatabase().LoadUserlist(userlistPath_)); EXPECT_FALSE(handle_->GetDatabase().GetPluginUserMetadata(blankDifferentEsm)); } @@ -525,8 +550,8 @@ TEST_P(DatabaseInterfaceTest, getPluginUserMetadataShouldReturnOnlyUserMetadataForTheGivenPlugin) { ASSERT_NO_THROW(GenerateMasterlist()); ASSERT_NO_THROW(GenerateUserlist()); - ASSERT_NO_THROW( - handle_->GetDatabase().LoadLists(masterlistPath, userlistPath_)); + ASSERT_NO_THROW(handle_->GetDatabase().LoadMasterlist(masterlistPath)); + ASSERT_NO_THROW(handle_->GetDatabase().LoadUserlist(userlistPath_)); auto metadata = handle_->GetDatabase().GetPluginUserMetadata(blankEsm).value(); @@ -542,8 +567,8 @@ TEST_P( getPluginUserMetadataShouldReturnOnlyValidMetadataForTheGivenPluginIfConditionsAreEvaluated) { ASSERT_NO_THROW(GenerateMasterlist()); ASSERT_NO_THROW(GenerateUserlist()); - ASSERT_NO_THROW( - handle_->GetDatabase().LoadLists(masterlistPath, userlistPath_)); + ASSERT_NO_THROW(handle_->GetDatabase().LoadMasterlist(masterlistPath)); + ASSERT_NO_THROW(handle_->GetDatabase().LoadUserlist(userlistPath_)); auto metadata = handle_->GetDatabase().GetPluginMetadata(blankEsm, false, true).value(); @@ -556,8 +581,8 @@ TEST_P( setPluginUserMetadataShouldReplaceExistingUserMetadataWithTheGivenMetadata) { ASSERT_NO_THROW(GenerateMasterlist()); ASSERT_NO_THROW(GenerateUserlist()); - ASSERT_NO_THROW( - handle_->GetDatabase().LoadLists(masterlistPath, userlistPath_)); + ASSERT_NO_THROW(handle_->GetDatabase().LoadMasterlist(masterlistPath)); + ASSERT_NO_THROW(handle_->GetDatabase().LoadUserlist(userlistPath_)); PluginMetadata newMetadata(blankDifferentEsp); newMetadata.SetRequirements(std::vector({File(masterFile)})); @@ -578,8 +603,8 @@ TEST_P(DatabaseInterfaceTest, setPluginUserMetadataShouldNotAffectExistingMasterlistMetadata) { ASSERT_NO_THROW(GenerateMasterlist()); ASSERT_NO_THROW(GenerateUserlist()); - ASSERT_NO_THROW( - handle_->GetDatabase().LoadLists(masterlistPath, userlistPath_)); + ASSERT_NO_THROW(handle_->GetDatabase().LoadMasterlist(masterlistPath)); + ASSERT_NO_THROW(handle_->GetDatabase().LoadUserlist(userlistPath_)); PluginMetadata newMetadata(blankEsm); newMetadata.SetRequirements(std::vector({File(masterFile)})); @@ -598,8 +623,8 @@ TEST_P(DatabaseInterfaceTest, discardPluginUserMetadataShouldDiscardAllUserMetadataForTheGivenPlugin) { ASSERT_NO_THROW(GenerateMasterlist()); ASSERT_NO_THROW(GenerateUserlist()); - ASSERT_NO_THROW( - handle_->GetDatabase().LoadLists(masterlistPath, userlistPath_)); + ASSERT_NO_THROW(handle_->GetDatabase().LoadMasterlist(masterlistPath)); + ASSERT_NO_THROW(handle_->GetDatabase().LoadUserlist(userlistPath_)); handle_->GetDatabase().DiscardPluginUserMetadata(blankEsm); @@ -611,8 +636,8 @@ TEST_P( discardPluginUserMetadataShouldNotDiscardMasterlistMetadataForTheGivenPlugin) { ASSERT_NO_THROW(GenerateMasterlist()); ASSERT_NO_THROW(GenerateUserlist()); - ASSERT_NO_THROW( - handle_->GetDatabase().LoadLists(masterlistPath, userlistPath_)); + ASSERT_NO_THROW(handle_->GetDatabase().LoadMasterlist(masterlistPath)); + ASSERT_NO_THROW(handle_->GetDatabase().LoadUserlist(userlistPath_)); handle_->GetDatabase().DiscardPluginUserMetadata(blankEsm); @@ -628,8 +653,8 @@ TEST_P(DatabaseInterfaceTest, discardPluginUserMetadataShouldNotDiscardUserMetadataForOtherPlugins) { ASSERT_NO_THROW(GenerateMasterlist()); ASSERT_NO_THROW(GenerateUserlist()); - ASSERT_NO_THROW( - handle_->GetDatabase().LoadLists(masterlistPath, userlistPath_)); + ASSERT_NO_THROW(handle_->GetDatabase().LoadMasterlist(masterlistPath)); + ASSERT_NO_THROW(handle_->GetDatabase().LoadUserlist(userlistPath_)); handle_->GetDatabase().DiscardPluginUserMetadata(blankEsm); @@ -643,8 +668,8 @@ TEST_P(DatabaseInterfaceTest, discardPluginUserMetadataShouldNotDiscardGeneralMessages) { ASSERT_NO_THROW(GenerateMasterlist()); ASSERT_NO_THROW(GenerateUserlist()); - ASSERT_NO_THROW( - handle_->GetDatabase().LoadLists(masterlistPath, userlistPath_)); + ASSERT_NO_THROW(handle_->GetDatabase().LoadMasterlist(masterlistPath)); + ASSERT_NO_THROW(handle_->GetDatabase().LoadUserlist(userlistPath_)); handle_->GetDatabase().DiscardPluginUserMetadata(blankEsm); @@ -663,8 +688,8 @@ TEST_P(DatabaseInterfaceTest, discardPluginUserMetadataShouldNotDiscardKnownBashTags) { ASSERT_NO_THROW(GenerateMasterlist()); ASSERT_NO_THROW(GenerateUserlist()); - ASSERT_NO_THROW( - handle_->GetDatabase().LoadLists(masterlistPath, userlistPath_)); + ASSERT_NO_THROW(handle_->GetDatabase().LoadMasterlist(masterlistPath)); + ASSERT_NO_THROW(handle_->GetDatabase().LoadUserlist(userlistPath_)); handle_->GetDatabase().DiscardPluginUserMetadata(blankEsm); @@ -684,8 +709,8 @@ TEST_P( discardAllUserMetadataShouldDiscardAllUserMetadataAndNoMasterlistMetadata) { ASSERT_NO_THROW(GenerateMasterlist()); ASSERT_NO_THROW(GenerateUserlist()); - ASSERT_NO_THROW( - handle_->GetDatabase().LoadLists(masterlistPath, userlistPath_)); + ASSERT_NO_THROW(handle_->GetDatabase().LoadMasterlist(masterlistPath)); + ASSERT_NO_THROW(handle_->GetDatabase().LoadUserlist(userlistPath_)); handle_->GetDatabase().DiscardAllUserMetadata(); @@ -775,7 +800,7 @@ TEST_P(DatabaseInterfaceTest, using std::endl; ASSERT_NO_THROW(GenerateMasterlist()); - ASSERT_NO_THROW(handle_->GetDatabase().LoadLists(masterlistPath, "")); + ASSERT_NO_THROW(handle_->GetDatabase().LoadMasterlist(masterlistPath)); EXPECT_NO_THROW( handle_->GetDatabase().WriteMinimalList(minimalOutputPath_, true)); diff --git a/cxx/src/tests/api/interface/game_interface_test.h b/cxx/src/tests/api/interface/game_interface_test.h index 4545d3dd..f5c699bf 100644 --- a/cxx/src/tests/api/interface/game_interface_test.h +++ b/cxx/src/tests/api/interface/game_interface_test.h @@ -235,7 +235,7 @@ TEST_P(GameInterfaceTest, sortPluginsShouldSucceedIfPassedValidArguments) { } ASSERT_NO_THROW(GenerateMasterlist()); - ASSERT_NO_THROW(handle_->GetDatabase().LoadLists(masterlistPath, "")); + ASSERT_NO_THROW(handle_->GetDatabase().LoadMasterlist(masterlistPath)); if (GetParam() == GameType::fo4 || GetParam() == GameType::tes5se) { pluginsToLoad.push_back(blankEsl); diff --git a/cxx/src/tests/api/interface/main.cpp b/cxx/src/tests/api/interface/main.cpp index a501bd5e..29027fef 100644 --- a/cxx/src/tests/api/interface/main.cpp +++ b/cxx/src/tests/api/interface/main.cpp @@ -37,12 +37,12 @@ int main(int argc, char **argv) { namespace loot { namespace test { -void testLoggingCallback(LogLevel, const char *) { +void testLoggingCallback(LogLevel, std::string_view) { // Do nothing. } struct TestLogger { - void callback(LogLevel, const char *message) { + void callback(LogLevel, std::string_view message) { loggedMessages += std::string(message); } @@ -56,7 +56,7 @@ TEST(SetLoggingCallback, shouldAcceptAFreeFunction) { CreateGameHandle(GameType::tes4, "dummy"); FAIL(); } catch (...) { - SetLoggingCallback([](LogLevel, const char *) {}); + SetLoggingCallback([](LogLevel, std::string_view) {}); } } @@ -77,13 +77,13 @@ TEST(SetLoggingCallback, shouldAcceptAMemberFunction) { "IV: Oblivion\" with game path \"dummy\"", testLogger.loggedMessages); - SetLoggingCallback([](LogLevel, const char *) {}); + SetLoggingCallback([](LogLevel, std::string_view) {}); } } TEST(SetLoggingCallback, shouldAcceptALambdaFunction) { std::string loggedMessages; - auto callback = [&](LogLevel, const char *string) { + auto callback = [&](LogLevel, std::string_view string) { loggedMessages += std::string(string); }; SetLoggingCallback(callback); @@ -97,7 +97,7 @@ TEST(SetLoggingCallback, shouldAcceptALambdaFunction) { "IV: Oblivion\" with game path \"dummy\"", loggedMessages); - SetLoggingCallback([](LogLevel, const char *) {}); + SetLoggingCallback([](LogLevel, std::string_view) {}); } } @@ -105,7 +105,7 @@ TEST(SetLoggingCallback, shouldNotBreakLoggingIfPassedLambdaFunctionGoesOutOfScope) { std::string loggedMessages; { - SetLoggingCallback([&](LogLevel, const char *string) { + SetLoggingCallback([&](LogLevel, std::string_view string) { loggedMessages += std::string(string); }); } @@ -119,7 +119,39 @@ TEST(SetLoggingCallback, "IV: Oblivion\" with game path \"dummy\"", loggedMessages); - SetLoggingCallback([](LogLevel, const char *) {}); + SetLoggingCallback([](LogLevel, std::string_view) {}); + } +} + +TEST(SetLogLevel, shouldOnlyRunTheCallbackForMessagesAtOrAboveTheGivenLevel) { + std::vector> loggedMessages; + auto callback = [&](LogLevel level, std::string_view string) { + loggedMessages.push_back(std::make_pair(level, std::string(string))); + }; + SetLoggingCallback(callback); + SetLogLevel(LogLevel::fatal); + + try { + CreateGameHandle(GameType::tes4, "dummy"); + FAIL(); + } catch (...) { + EXPECT_TRUE(loggedMessages.empty()); + } + + SetLogLevel(LogLevel::info); + + try { + CreateGameHandle(GameType::tes4, "dummy"); + FAIL(); + } catch (...) { + ASSERT_EQ(1, loggedMessages.size()); + EXPECT_EQ(LogLevel::info, loggedMessages[0].first); + EXPECT_EQ( + "Attempting to create a game handle for game type \"The Elder Scrolls " + "IV: Oblivion\" with game path \"dummy\"", + loggedMessages[0].second); + + SetLoggingCallback([](LogLevel, std::string_view) {}); } } } diff --git a/cxx/src/tests/api/internals/main.cpp b/cxx/src/tests/api/internals/main.cpp index e05ed9be..a2ed24fa 100644 --- a/cxx/src/tests/api/internals/main.cpp +++ b/cxx/src/tests/api/internals/main.cpp @@ -19,7 +19,7 @@ namespace loot::rust { TEST(libloot_version, shouldReturnExpectedValue) { auto version = libloot_version(); - EXPECT_EQ(version, "0.25.5"); + EXPECT_EQ(version, "0.26.0"); } TEST(libloot_revision, shouldReturnExpectedValue) { diff --git a/cxx/src/tests/common_game_test_fixture.h b/cxx/src/tests/common_game_test_fixture.h index 5288e62a..316a5068 100644 --- a/cxx/src/tests/common_game_test_fixture.h +++ b/cxx/src/tests/common_game_test_fixture.h @@ -185,7 +185,7 @@ protected: } void copyPlugin(const std::filesystem::path& sourceParentPath, - const std::string& filename) { + std::string_view filename) { std::filesystem::copy_file(sourceParentPath / filename, dataPath / filename); ASSERT_TRUE(std::filesystem::exists(dataPath / filename)); @@ -342,6 +342,21 @@ protected: blankDifferentEsp, blankMasterDependentEsp, }; + } else if (supportsLightPlugins(gameType_)) { + return { + masterFile, + blankEsm, + blankDifferentEsm, + blankMasterDependentEsm, + blankDifferentMasterDependentEsm, + blankEsl, + blankEsp, + blankDifferentEsp, + blankMasterDependentEsp, + blankDifferentMasterDependentEsp, + blankPluginDependentEsp, + blankDifferentPluginDependentEsp, + }; } else { return { masterFile, diff --git a/ffi-errors/Cargo.toml b/ffi-errors/Cargo.toml index 1cc1b72f..600bcc53 100644 --- a/ffi-errors/Cargo.toml +++ b/ffi-errors/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "libloot-ffi-errors" -version = "0.25.5" +version = "0.26.0" edition = "2024" [dependencies] diff --git a/pyo3/Cargo.toml b/pyo3/Cargo.toml index fde8fe24..6579b46c 100644 --- a/pyo3/Cargo.toml +++ b/pyo3/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "libloot_pyo3" -version = "0.25.5" +version = "0.26.0" edition = "2024" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html diff --git a/pyo3/src/database.rs b/pyo3/src/database.rs index c1b30d16..e94bc6b9 100644 --- a/pyo3/src/database.rs +++ b/pyo3/src/database.rs @@ -85,6 +85,14 @@ impl Database { .map_err(Into::into) } + pub fn evaluate(&self, condition: &str) -> Result { + self.0 + .read() + .map_err(|_| DatabaseLockPoisonError)? + .evaluate(condition) + .map_err(Into::into) + } + pub fn known_bash_tags(&self) -> Result, VerboseError> { Ok(self .0 diff --git a/pyo3/src/metadata.rs b/pyo3/src/metadata.rs index 9c82e88b..826d0227 100644 --- a/pyo3/src/metadata.rs +++ b/pyo3/src/metadata.rs @@ -305,12 +305,13 @@ pub struct File(libloot::metadata::File); #[pymethods] impl File { #[new] - #[pyo3(signature = (name, display_name = None, detail = None, condition = None))] + #[pyo3(signature = (name, display_name = None, detail = None, condition = None, constraint = None))] fn new( name: String, display_name: Option, detail: Option>, condition: Option, + constraint: Option, ) -> Result { let mut file = libloot::metadata::File::new(name); @@ -327,6 +328,10 @@ impl File { file.set_condition(condition); } + if let Some(constraint) = constraint { + file.set_constraint(constraint); + } + Ok(Self(file)) } @@ -367,16 +372,27 @@ impl File { self.0.set_condition(condition); } + #[getter] + fn constraint(&self) -> Option<&str> { + self.0.constraint() + } + + #[setter] + fn set_constraint(&mut self, constraint: String) { + self.0.set_constraint(constraint); + } + fn __repr__(slf: &Bound<'_, Self>) -> PyResult { let class_name = slf.get_type().qualname()?; let inner = &slf.borrow().0; Ok(format!( - "{}({}, {}, {}, {})", + "{}({}, {}, {}, {}, {})", class_name, inner.name(), inner.display_name().unwrap_or(NONE_REPR), repr_message_contents(inner.detail()), - inner.condition().unwrap_or(NONE_REPR) + inner.condition().unwrap_or(NONE_REPR), + inner.constraint().unwrap_or(NONE_REPR) )) } diff --git a/src/database/conditions.rs b/src/database/conditions.rs index 9ce0a71a..d30a3e5a 100644 --- a/src/database/conditions.rs +++ b/src/database/conditions.rs @@ -57,12 +57,19 @@ pub fn evaluate_all_conditions( } } -fn evaluate_condition( +pub fn evaluate_condition( + condition: &str, + state: &loot_condition_interpreter::State, +) -> Result { + Expression::from_str(condition).and_then(|e| e.eval(state)) +} + +fn evaluate_condition_option( condition: Option<&str>, state: &loot_condition_interpreter::State, ) -> Result { if let Some(condition) = condition { - Expression::from_str(condition).and_then(|e| e.eval(state)) + evaluate_condition(condition, state) } else { Ok(true) } @@ -73,7 +80,7 @@ pub fn filter_map_on_condition( condition: Option<&str>, state: &loot_condition_interpreter::State, ) -> Option> { - evaluate_condition(condition, state) + evaluate_condition_option(condition, state) .map(|r| r.then(|| item.clone())) .transpose() } diff --git a/src/database/mod.rs b/src/database/mod.rs index c57d546a..f08f78f2 100644 --- a/src/database/mod.rs +++ b/src/database/mod.rs @@ -3,7 +3,7 @@ mod error; use std::{collections::HashMap, path::Path}; -use conditions::{evaluate_all_conditions, filter_map_on_condition}; +use conditions::{evaluate_all_conditions, evaluate_condition, filter_map_on_condition}; use crate::{ logging, @@ -127,6 +127,11 @@ impl Database { doc.save(output_path) } + /// Evaluate the given condition string. + pub fn evaluate(&self, condition: &str) -> Result { + evaluate_condition(condition, &self.condition_evaluator_state).map_err(Into::into) + } + /// Gets the Bash Tags that are listed in the loaded metadata lists. /// /// Bash Tag suggestions can include Bash Tags not in this list. @@ -803,6 +808,26 @@ plugins: } } + mod evaluate { + use super::*; + + #[test] + fn should_return_true_if_the_condition_is_true() { + let fixture = Fixture::new(GameType::TES4); + let database = fixture.database(); + + assert!(database.evaluate("file(\"Blank.esp\")").unwrap()); + } + + #[test] + fn should_return_false_if_the_condition_is_false() { + let fixture = Fixture::new(GameType::TES4); + let database = fixture.database(); + + assert!(!database.evaluate("file(\"missing.esp\")").unwrap()); + } + } + mod groups { use super::*; diff --git a/src/error.rs b/src/error.rs index 00d998dd..fc9a4a71 100644 --- a/src/error.rs +++ b/src/error.rs @@ -273,3 +273,11 @@ impl From for SortPluginsError { SortPluginsError::MetadataRetrievalError(value) } } + +impl From for SortPluginsError { + fn from(value: ConditionEvaluationError) -> Self { + SortPluginsError::MetadataRetrievalError(MetadataRetrievalError::ConditionEvaluationError( + value, + )) + } +} diff --git a/src/game.rs b/src/game.rs index dff8dd91..25c2333e 100644 --- a/src/game.rs +++ b/src/game.rs @@ -455,17 +455,7 @@ impl Game { let plugins_sorting_data = plugins .into_iter() .enumerate() - .map(|(i, p)| { - let masterlist_metadata = database.plugin_metadata(p.name(), false, true)?; - let user_metadata = database.plugin_user_metadata(p.name(), true)?; - let plugin = PluginSortingData::new( - p.as_ref(), - masterlist_metadata.as_ref(), - user_metadata.as_ref(), - i, - )?; - Ok::<_, SortPluginsError>(plugin) - }) + .map(|(i, p)| to_plugin_sorting_data(&database, p, i)) .collect::, _>>()?; if is_log_enabled(LogLevel::Debug) { @@ -747,6 +737,30 @@ fn update_loaded_plugin_state<'a>( } } +fn to_plugin_sorting_data<'a>( + database: &Database, + plugin: &'a Arc, + load_order_index: usize, +) -> Result, SortPluginsError> { + let masterlist_metadata = database + .plugin_metadata(plugin.name(), false, true)? + .map(|m| m.filter_by_constraints(database)) + .transpose()?; + + let user_metadata = database + .plugin_user_metadata(plugin.name(), true)? + .map(|m| m.filter_by_constraints(database)) + .transpose()?; + + PluginSortingData::new( + plugin.as_ref(), + masterlist_metadata.as_ref(), + user_metadata.as_ref(), + load_order_index, + ) + .map_err(Into::into) +} + #[derive(Clone, Debug, Default, Eq, PartialEq)] pub(crate) struct GameCache { plugins: HashMap>, @@ -793,7 +807,13 @@ mod tests { use rstest_reuse::apply; - use crate::tests::{Fixture, all_game_types}; + use crate::{ + metadata::{File, PluginMetadata}, + tests::{ + BLANK_DIFFERENT_ESM, BLANK_DIFFERENT_ESP, BLANK_ESM, BLANK_ESP, + BLANK_MASTER_DEPENDENT_ESM, Fixture, all_game_types, + }, + }; mod game { use std::path::Component; @@ -2027,6 +2047,58 @@ mod tests { } } + #[test] + fn to_plugin_sorting_data_should_filter_out_files_with_false_constraints() { + let game_type = GameType::TES4; + let true_constraint = "file(\"Blank.esm\")"; + let false_constraint = "file(\"missing.esm\")"; + + let fixture = Fixture::new(game_type); + + let plugin = Arc::new( + Plugin::new( + game_type, + &GameCache::default(), + &fixture.data_path().join(BLANK_ESP), + LoadScope::HeaderOnly, + ) + .unwrap(), + ); + + let mut database = Database::new(loot_condition_interpreter::State::new( + game_type.into(), + fixture.data_path(), + )); + + let masterlist_path = fixture.local_path.join("masterlist.yaml"); + let masterlist = format!( + "{{plugins: [{{name: Blank.esp, after: [{{name: A.esp, constraint: '{true_constraint}'}}, {{name: B.esp, constraint: '{false_constraint}'}}], req: [{{name: C.esp, constraint: '{true_constraint}'}}, {{name: D.esp, constraint: '{false_constraint}'}}]}}]}}" + ); + std::fs::write(&masterlist_path, masterlist).unwrap(); + + database.load_masterlist(&masterlist_path).unwrap(); + + let mut user_metadata = PluginMetadata::new(BLANK_ESP).unwrap(); + user_metadata.set_load_after_files(vec![ + File::new(BLANK_ESM.to_owned()).with_constraint(true_constraint.to_owned()), + File::new(BLANK_DIFFERENT_ESM.to_owned()).with_constraint(false_constraint.to_owned()), + ]); + user_metadata.set_requirements(vec![ + File::new(BLANK_DIFFERENT_ESP.to_owned()).with_constraint(true_constraint.to_owned()), + File::new(BLANK_MASTER_DEPENDENT_ESM.to_owned()) + .with_constraint(false_constraint.to_owned()), + ]); + + database.set_plugin_user_metadata(user_metadata); + + let data = to_plugin_sorting_data(&database, &plugin, 0).unwrap(); + + assert_eq!(["A.esp".to_owned()], *data.masterlist_load_after); + assert_eq!(["C.esp".to_owned()], *data.masterlist_req); + assert_eq!([BLANK_ESM.to_owned()], *data.user_load_after); + assert_eq!([BLANK_DIFFERENT_ESP.to_owned()], *data.user_req); + } + mod game_cache { use super::*; diff --git a/src/metadata/file.rs b/src/metadata/file.rs index 06aa7137..0a9d1f75 100644 --- a/src/metadata/file.rs +++ b/src/metadata/file.rs @@ -21,6 +21,7 @@ pub struct File { display_name: Option>, detail: Box<[MessageContent]>, condition: Option>, + constraint: Option>, } impl File { @@ -59,6 +60,13 @@ impl File { Ok(self) } + /// Set the constraint string. + #[must_use] + pub fn with_constraint(mut self, constraint: String) -> Self { + self.set_constraint(constraint); + self + } + /// Gets the name of the file (which may actually be a path). pub fn name(&self) -> &Filename { &self.name @@ -107,6 +115,17 @@ impl File { self.condition = Some(condition.into_boxed_str()); self } + + /// Get the constraint string. + pub fn constraint(&self) -> Option<&str> { + self.constraint.as_deref() + } + + /// Set the constraint string. + pub fn set_constraint(&mut self, constraint: String) -> &mut Self { + self.constraint = Some(constraint.into_boxed_str()); + self + } } /// Represents a case-insensitive filename. @@ -172,6 +191,7 @@ impl TryFromYaml for File { display_name: None, detail: Box::default(), condition: None, + constraint: None, }), YamlData::Mapping(h) => { let name = @@ -188,13 +208,16 @@ impl TryFromYaml for File { None => Box::default(), }; - let condition = parse_condition(h, YamlObjectType::File)?; + let condition = parse_condition(h, "condition", YamlObjectType::File)?; + + let constraint = parse_condition(h, "constraint", YamlObjectType::File)?; Ok(File { name: Filename::new(name.to_string()), display_name: display_name.map(|(_, s)| s.into()), detail, condition, + constraint, }) } _ => Err(ParseMetadataError::unexpected_type( @@ -208,7 +231,10 @@ impl TryFromYaml for File { impl EmitYaml for File { fn is_scalar(&self) -> bool { - self.condition.is_none() && self.detail.is_empty() && self.display_name.is_none() + self.condition.is_none() + && self.constraint.is_none() + && self.detail.is_empty() + && self.display_name.is_none() } fn emit_yaml(&self, emitter: &mut YamlEmitter) { @@ -230,6 +256,11 @@ impl EmitYaml for File { emitter.single_quoted_str(condition); } + if let Some(constraint) = &self.constraint { + emitter.map_key("constraint"); + emitter.single_quoted_str(constraint); + } + emit_message_contents(&self.detail, emitter, "detail"); emitter.end_map(); @@ -277,6 +308,7 @@ mod tests { assert_eq!("name1", file.name().as_str()); assert!(file.display_name().is_none()); assert!(file.condition().is_none()); + assert!(file.constraint().is_none()); assert!(file.detail().is_empty()); } @@ -301,10 +333,17 @@ mod tests { assert!(File::try_from_yaml(&yaml).is_err()); } + #[test] + fn should_error_if_given_an_invalid_constraint() { + let yaml = parse("{name: name1, constraint: invalid}"); + + assert!(File::try_from_yaml(&yaml).is_err()); + } + #[test] fn should_set_all_given_fields() { let yaml = parse( - "{name: name1, display: display1, condition: 'file(\"Foo.esp\")', detail: 'details'}", + "{name: name1, display: display1, condition: 'file(\"Foo.esp\")', constraint: 'file(\"Bar.esp\")', detail: 'details'}", ); let file = File::try_from_yaml(&yaml).unwrap(); @@ -312,6 +351,7 @@ mod tests { assert_eq!("name1", file.name().as_str()); assert_eq!("display1", file.display_name().unwrap()); assert_eq!("file(\"Foo.esp\")", file.condition().unwrap()); + assert_eq!("file(\"Bar.esp\")", file.constraint().unwrap()); assert_eq!(&[MessageContent::new("details".into())], file.detail()); } @@ -324,6 +364,7 @@ mod tests { assert_eq!("name1", file.name().as_str()); assert!(file.display_name().is_none()); assert!(file.condition().is_none()); + assert!(file.constraint().is_none()); assert!(file.detail().is_empty()); } @@ -409,6 +450,21 @@ mod tests { ); } + #[test] + fn should_emit_map_with_constraint_if_it_is_not_empty() { + let file = File::new("filename".into()).with_constraint("constraint1".into()); + let yaml = emit(&file); + + assert_eq!( + format!( + "name: '{}'\nconstraint: '{}'", + file.name.as_str(), + file.constraint.unwrap() + ), + yaml + ); + } + #[test] fn should_emit_map_with_a_detail_string_if_detail_is_monolingual() { let file = File::new("filename".into()) @@ -459,6 +515,7 @@ detail: let file = File::new("filename".into()) .with_display_name("display1".into()) .with_condition("condition1".into()) + .with_constraint("constraint1".into()) .with_detail(vec![ MessageContent::new("english".into()).with_language("en".into()), MessageContent::new("french".into()).with_language("fr".into()), @@ -471,6 +528,7 @@ detail: "name: '{}' display: '{}' condition: '{}' +constraint: '{}' detail: - lang: {} text: '{}' @@ -479,6 +537,7 @@ detail: file.name.as_str(), file.display_name.unwrap(), file.condition.unwrap(), + file.constraint.unwrap(), file.detail[0].language(), file.detail[0].text(), file.detail[1].language(), diff --git a/src/metadata/message.rs b/src/metadata/message.rs index 8ed1d615..cf5070e7 100644 --- a/src/metadata/message.rs +++ b/src/metadata/message.rs @@ -364,7 +364,7 @@ impl TryFromYaml for Message { } } - let condition = parse_condition(mapping, YamlObjectType::Message)?; + let condition = parse_condition(mapping, "condition", YamlObjectType::Message)?; Ok(Message { message_type, diff --git a/src/metadata/plugin_metadata.rs b/src/metadata/plugin_metadata.rs index 7a991eb3..1f2bfd9d 100644 --- a/src/metadata/plugin_metadata.rs +++ b/src/metadata/plugin_metadata.rs @@ -3,7 +3,7 @@ use std::borrow::Cow; use fancy_regex::{Error as RegexImplError, Regex}; use saphyr::MarkedYaml; -use crate::{case_insensitive_regex, logging}; +use crate::{Database, case_insensitive_regex, error::ConditionEvaluationError, logging}; use super::{ error::{MetadataParsingErrorReason, ParseMetadataError, RegexError}, @@ -217,6 +217,32 @@ impl PluginMetadata { self.emit_yaml(&mut emitter); emitter.into_string() } + + pub(crate) fn filter_by_constraints( + mut self, + database: &Database, + ) -> Result { + self.load_after = filter_files_by_constraint(self.load_after, database)?; + self.requirements = filter_files_by_constraint(self.requirements, database)?; + + Ok(self) + } +} + +fn filter_files_by_constraint( + files: Box<[File]>, + database: &Database, +) -> Result, ConditionEvaluationError> { + files + .into_iter() + .filter_map(|f| { + if let Some(c) = f.constraint() { + database.evaluate(c).map(|r| r.then_some(f)).transpose() + } else { + Some(Ok(f)) + } + }) + .collect() } #[derive(Clone, Debug, Default)] diff --git a/src/metadata/tag.rs b/src/metadata/tag.rs index 92c8965b..be2b7e72 100644 --- a/src/metadata/tag.rs +++ b/src/metadata/tag.rs @@ -79,7 +79,7 @@ impl TryFromYaml for Tag { let name = get_required_string_value(value.span.start, h, "name", YamlObjectType::Tag)?; - let condition = parse_condition(h, YamlObjectType::Tag)?; + let condition = parse_condition(h, "condition", YamlObjectType::Tag)?; let (name, suggestion) = name_and_suggestion(name); Ok(Tag { diff --git a/src/metadata/yaml/parse.rs b/src/metadata/yaml/parse.rs index e8e5b564..1605549c 100644 --- a/src/metadata/yaml/parse.rs +++ b/src/metadata/yaml/parse.rs @@ -180,9 +180,10 @@ pub fn get_as_slice<'a>( pub fn parse_condition( mapping: &saphyr::AnnotatedMapping, + key: &'static str, yaml_type: YamlObjectType, ) -> Result>, ParseMetadataError> { - match get_string_value(mapping, "condition", yaml_type)? { + match get_string_value(mapping, key, yaml_type)? { Some((marker, s)) => { let s = s.to_string(); if let Err(e) = Expression::from_str(&s) { diff --git a/src/sorting/plugins.rs b/src/sorting/plugins.rs index 57c393d7..2798d776 100644 --- a/src/sorting/plugins.rs +++ b/src/sorting/plugins.rs @@ -34,10 +34,10 @@ pub struct PluginSortingData<'a, T: SortingPlugin> { pub(super) group: Box, group_is_user_metadata: bool, - pub(super) masterlist_load_after: Box<[String]>, - pub(super) user_load_after: Box<[String]>, - pub(super) masterlist_req: Box<[String]>, - pub(super) user_req: Box<[String]>, + pub(crate) masterlist_load_after: Box<[String]>, + pub(crate) user_load_after: Box<[String]>, + pub(crate) masterlist_req: Box<[String]>, + pub(crate) user_req: Box<[String]>, } impl<'a, T: SortingPlugin> PluginSortingData<'a, T> {