From e3c0e597932aa80a93074854fc65f15791f35d81 Mon Sep 17 00:00:00 2001 From: Mikulas Klokocka Date: Wed, 17 Jul 2024 04:35:29 +0200 Subject: [PATCH] feat(bsa): improve dummy plugin generation #44 --- include/btu/bsa/settings.hpp | 51 ++++++++++++-------- src/bsa/plugin.cpp | 47 +++++++++++++++---- tests/bsa/plugin.cpp | 90 +++++++++++++++++++++++++++++++++++- 3 files changed, 159 insertions(+), 29 deletions(-) diff --git a/include/btu/bsa/settings.hpp b/include/btu/bsa/settings.hpp index d2cc7f6..70f0ba2 100644 --- a/include/btu/bsa/settings.hpp +++ b/include/btu/bsa/settings.hpp @@ -86,6 +86,16 @@ static constexpr auto k_ba2_ext = u8".ba2"; static constexpr auto k_archive_extensions = std::to_array({k_bsa_ext, k_ba2_ext}); +enum class PluginLoadingMode +{ + Limited, // Plugins only load archives "[ - Suffix]" and " - Textures". + Unlimited // Plugins load archives "*". +}; + +NLOHMANN_JSON_SERIALIZE_ENUM(PluginLoadingMode, + {{PluginLoadingMode::Limited, "limited"}, + {PluginLoadingMode::Unlimited, "unlimited"}}); + struct AllowedPath { static const inline auto k_root = std::u8string(u8"root"); @@ -116,6 +126,7 @@ struct Settings std::vector plugin_extensions; std::optional> dummy_plugin; std::u8string dummy_extension; + PluginLoadingMode dummy_plugin_loading_mode{}; std::vector standard_files; std::vector texture_files; @@ -144,16 +155,17 @@ NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(Settings, static const Settings tes4_default_sets = [=] { Settings sets; - sets.game = Game::TES4; - sets.max_size = 2000ULL * megabyte; - sets.version = ArchiveVersion::tes4; - sets.has_texture_version = false; - sets.texture_suffix = std::nullopt; - sets.extension = u8".bsa"; - sets.plugin_extensions = {u8".esm", u8".esp"}; - sets.dummy_plugin = std::vector(std::begin(dummy::oblivion), std::end(dummy::oblivion)); - sets.dummy_extension = u8".esp"; - sets.standard_files = { + sets.game = Game::TES4; + sets.max_size = 2000ULL * megabyte; + sets.version = ArchiveVersion::tes4; + sets.has_texture_version = false; + sets.texture_suffix = std::nullopt; + sets.extension = u8".bsa"; + sets.plugin_extensions = {u8".esm", u8".esp"}; + sets.dummy_plugin = std::vector(std::begin(dummy::oblivion), std::end(dummy::oblivion)); + sets.dummy_extension = u8".esp"; + sets.dummy_plugin_loading_mode = PluginLoadingMode::Unlimited; + sets.standard_files = { {u8".nif", {u8"meshes"}, TES4ArchiveType::meshes}, {u8".egm", {u8"meshes"}, TES4ArchiveType::meshes}, {u8".egt", {u8"meshes"}, TES4ArchiveType::meshes}, @@ -191,15 +203,16 @@ NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(Settings, }(); static const Settings tes5_default_sets = [=] { - Settings sets = tes4_default_sets; - sets.game = Game::SSE; - sets.version = ArchiveVersion::sse; - sets.has_texture_version = true; - sets.texture_suffix = u8"Textures"; - sets.extension = u8".bsa"; - sets.plugin_extensions = {u8".esl", u8".esm", u8".esp"}; - sets.dummy_plugin = std::vector(std::begin(dummy::sse), std::end(dummy::sse)); - sets.standard_files = { + Settings sets = tes4_default_sets; + sets.game = Game::SSE; + sets.version = ArchiveVersion::sse; + sets.has_texture_version = true; + sets.texture_suffix = u8"Textures"; + sets.extension = u8".bsa"; + sets.plugin_extensions = {u8".esl", u8".esm", u8".esp"}; + sets.dummy_plugin = std::vector(std::begin(dummy::sse), std::end(dummy::sse)); + sets.dummy_plugin_loading_mode = PluginLoadingMode::Limited; + sets.standard_files = { AllowedPath{u8".bgem", {u8"materials"}}, AllowedPath{u8".bgsm", {u8"materials"}}, AllowedPath{u8".bto", {u8"meshes"}, TES4ArchiveType::meshes}, diff --git a/src/bsa/plugin.cpp b/src/bsa/plugin.cpp index a42585e..2738a86 100644 --- a/src/bsa/plugin.cpp +++ b/src/bsa/plugin.cpp @@ -69,7 +69,7 @@ void remove_suffixes(std::u8string &filename, const Settings &sets) filename = str_replace_once(filename, suffix, u8"", common::CaseSensitive::Yes); } -[[nodiscard]] auto plugins_loading_archive(const Path &archive, const Settings &sets) +[[nodiscard]] auto plugins_loading_archive_limited(const Path &archive, const Settings &sets) { auto stem = archive.stem().u8string(); remove_suffixes(stem, sets); @@ -79,6 +79,28 @@ void remove_suffixes(std::u8string &filename, const Settings &sets) .to(); } +/// If plugins can have unlimited archives attached, also look at all the prefixes of stem. +[[nodiscard]] auto plugins_loading_archive_unlimited(const Path &archive, const Settings &sets) +{ + const auto stem = archive.stem().u8string(); + return flux::cartesian_product_map([&stem](const size_t size, + const auto &ext) { return stem.substr(0, size) + ext; }, + flux::iota(size_t{1}, stem.size() + 1).reverse(), + flux::ref(sets.plugin_extensions)) + .map([&archive](const auto &filename) { return archive.parent_path() / filename; }) + .to(); +} + +[[nodiscard]] auto plugins_loading_archive(const Path &archive, const Settings &sets) -> std::vector +{ + switch (sets.dummy_plugin_loading_mode) + { + case PluginLoadingMode::Limited: return plugins_loading_archive_limited(archive, sets); + case PluginLoadingMode::Unlimited: return plugins_loading_archive_unlimited(archive, sets); + } + return {}; +} + auto find_archive_name_using_plugins(std::span plugins, const Settings &sets, ArchiveType type) -> std::optional @@ -113,6 +135,7 @@ auto find_archive_name_using_plugins(std::span plugins, return std::nullopt; } +/// Returns an archive name that is unique, which is guaranteed by trying suitable suffixes auto find_archive_name(const Path &directory, const Settings &sets, ArchiveType type) noexcept -> std::optional @@ -187,13 +210,21 @@ auto list_archive(const Path &dir, const Settings &sets) noexcept -> std::vector { try { - return flux::from_range(fs::directory_iterator(dir)) - .filter([](const auto &f) { return f.is_regular_file(); }) - .filter([&sets](const auto &f) { - return common::to_lower(f.path().extension().u8string()) == sets.extension; - }) - .map([](const auto &f) { return f.path(); }) - .to(); + std::vector archives = flux::from_range(fs::directory_iterator(dir)) + .filter([](const auto &f) { return f.is_regular_file(); }) + .filter([&sets](const auto &f) { + return common::to_lower(f.path().extension().u8string()) + == sets.extension; + }) + .map([](const auto &f) { return f.path(); }) + .to(); + + // When a single plugin can load multiple archives, it loads all archives such that their + // name contains the name of the corresponding plugin as a prefix. Sorting the archive + // names by length should ensure that only the required number of dummy plugins is created. + flux::sort(archives, [](const auto p1, const auto p2) { return p1.stem() <= p2.stem(); }); + + return archives; } catch (const std::exception &) { diff --git a/tests/bsa/plugin.cpp b/tests/bsa/plugin.cpp index 0f91017..818884e 100644 --- a/tests/bsa/plugin.cpp +++ b/tests/bsa/plugin.cpp @@ -77,6 +77,48 @@ TEST_CASE("find_archive_name", "[src]") TEST_CASE("remake dummy plugins") { + GIVEN("a game that can load limited archives per plugin, a directory with no plugins, and two archives") + { + auto dir = prepare_dir(std::vector{u8"Test.bsa"sv, u8"Test1.bsa"sv}); + + WHEN("remake_dummy_plugins is called") + { + auto sets = Settings::get(btu::Game::SSE); + remake_dummy_plugins(dir.path(), sets); + + THEN("two dummy plugins were created") + { + CHECK(list_plugins(dir.path(), sets).size() == 2); + } + AND_THEN("the dummy plugins have the correct content") + { + auto dummy_content = require_expected(btu::common::read_file(dir.path() / u8"Test.esp")); + CHECK(dummy_content == sets.dummy_plugin.value()); + dummy_content = require_expected(btu::common::read_file(dir.path() / u8"Test1.esp")); + CHECK(dummy_content == sets.dummy_plugin.value()); + } + } + } + GIVEN("a game that can load unlimited archives per plugin, a directory with no plugins, and two archives") + { + auto dir = prepare_dir(std::vector{u8"Test.bsa"sv, u8"Test1.bsa"sv}); + + WHEN("remake_dummy_plugins is called") + { + auto sets = Settings::get(btu::Game::FNV); + remake_dummy_plugins(dir.path(), sets); + + THEN("one dummy plugin was created") + { + CHECK(list_plugins(dir.path(), sets).size() == 1); + } + AND_THEN("the dummy plugin has the correct content") + { + auto dummy_content = require_expected(btu::common::read_file(dir.path() / u8"Test.esp")); + CHECK(dummy_content == sets.dummy_plugin.value()); + } + } + } GIVEN("a directory with a plugin, a loaded archive and an unloaded archive") { auto dir = prepare_dir( @@ -84,12 +126,13 @@ TEST_CASE("remake dummy plugins") WHEN("remake_dummy_plugins is called") { - remake_dummy_plugins(dir.path(), Settings::get(btu::Game::SSE)); + auto sets = Settings::get(btu::Game::SSE); + remake_dummy_plugins(dir.path(), sets); THEN("the dummy plugin has the correct content") { auto dummy_content = require_expected(btu::common::read_file(dir.path() / u8"unloaded.esp")); - CHECK(dummy_content == Settings::get(btu::Game::SSE).dummy_plugin.value()); + CHECK(dummy_content == sets.dummy_plugin.value()); } AND_THEN("the existing plugin was not modified") { @@ -99,4 +142,47 @@ TEST_CASE("remake dummy plugins") } } } + GIVEN("a directory with a plugin that can load a limited number of archives, a loaded archive \ + and an unloaded archive with the same prefix") + { + auto dir = prepare_dir(std::vector{u8"plugin.esp"sv, u8"plugin.bsa"sv, u8"plugin1.bsa"sv}); + + WHEN("remake_dummy_plugins is called") + { + auto sets = Settings::get(btu::Game::SSE); + remake_dummy_plugins(dir.path(), sets); + + THEN("the dummy plugin has the correct content") + { + auto dummy_content = require_expected(btu::common::read_file(dir.path() / u8"plugin1.esp")); + CHECK(dummy_content == sets.dummy_plugin.value()); + } + AND_THEN("the existing plugin was not modified") + { + auto existing_content = require_expected(btu::common::read_file(dir.path() / u8"plugin.esp")); + CHECK(existing_content.empty()); + } + } + } + GIVEN("a directory with a plugin that can load an unlimited number of archives and two loaded archives") + { + auto dir = prepare_dir(std::vector{u8"plugin.esp"sv, u8"plugin.bsa"sv, u8"plugin1.bsa"sv}); + + WHEN("remake_dummy_plugins is called") + { + auto sets = Settings::get(btu::Game::FNV); + remake_dummy_plugins(dir.path(), sets); + + THEN("the existing plugin is the single plugin in the folder") + { + const auto plugins = list_plugins(dir.path(), sets); + CHECK(plugins == std::vector{dir.path() / u8"plugin.esp"}); + } + AND_THEN("the existing plugin was not modified") + { + auto existing_content = require_expected(btu::common::read_file(dir.path() / u8"plugin.esp")); + CHECK(existing_content.empty()); + } + } + } }