Skip to content

Commit

Permalink
feat(bsa): improve dummy plugin generation Guekka#44
Browse files Browse the repository at this point in the history
  • Loading branch information
mklokocka authored and Guekka committed Jul 20, 2024
1 parent c42605d commit e3c0e59
Show file tree
Hide file tree
Showing 3 changed files with 159 additions and 29 deletions.
51 changes: 32 additions & 19 deletions include/btu/bsa/settings.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,16 @@ static constexpr auto k_ba2_ext = u8".ba2";

static constexpr auto k_archive_extensions = std::to_array<std::u8string_view>({k_bsa_ext, k_ba2_ext});

enum class PluginLoadingMode
{
Limited, // Plugins only load archives "<plugin>[ - Suffix]" and "<plugin> - Textures".
Unlimited // Plugins load archives "<plugin>*".
};

NLOHMANN_JSON_SERIALIZE_ENUM(PluginLoadingMode,
{{PluginLoadingMode::Limited, "limited"},
{PluginLoadingMode::Unlimited, "unlimited"}});

struct AllowedPath
{
static const inline auto k_root = std::u8string(u8"root");
Expand Down Expand Up @@ -116,6 +126,7 @@ struct Settings
std::vector<std::u8string> plugin_extensions;
std::optional<std::vector<std::byte>> dummy_plugin;
std::u8string dummy_extension;
PluginLoadingMode dummy_plugin_loading_mode{};

std::vector<AllowedPath> standard_files;
std::vector<AllowedPath> texture_files;
Expand Down Expand Up @@ -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},
Expand Down Expand Up @@ -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},
Expand Down
47 changes: 39 additions & 8 deletions src/bsa/plugin.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -79,6 +79,28 @@ void remove_suffixes(std::u8string &filename, const Settings &sets)
.to<std::vector>();
}

/// 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<std::vector>();
}

[[nodiscard]] auto plugins_loading_archive(const Path &archive, const Settings &sets) -> std::vector<Path>
{
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<const Path> plugins,
const Settings &sets,
ArchiveType type) -> std::optional<Path>
Expand Down Expand Up @@ -113,6 +135,7 @@ auto find_archive_name_using_plugins(std::span<const Path> 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<Path>
Expand Down Expand Up @@ -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>();
std::vector<Path> 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<std::vector>();

// 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 &)
{
Expand Down
90 changes: 88 additions & 2 deletions tests/bsa/plugin.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -77,19 +77,62 @@ 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(
std::vector{u8"existing.esp"sv, u8"existing - Textures.bsa"sv, u8"unloaded.bsa"sv});

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")
{
Expand All @@ -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());
}
}
}
}

0 comments on commit e3c0e59

Please sign in to comment.