diff --git a/src/Linking/Linker.cpp b/src/Linking/Linker.cpp index f253b4204..71d0afc0e 100644 --- a/src/Linking/Linker.cpp +++ b/src/Linking/Linker.cpp @@ -8,6 +8,7 @@ #include "SearchPath/SearchPaths.h" #include "Utils/Logging/Log.h" #include "Zone/AssetList/AssetList.h" +#include "Zone/AssetList/AssetListOutputStream.h" #include "Zone/AssetList/AssetListReader.h" #include "Zone/Definition/ZoneDefinitionStream.h" #include "ZoneCreation/ZoneCreationContext.h" @@ -336,6 +337,36 @@ namespace return true; } + static bool WriteAssetList(IOutputPath& outPath, const fs::path& outDir, const Zone& zone) + { + const auto assetListPath = fs::path("assetlist") / std::format("{}.csv", zone.m_name); + const auto stream = outPath.Open(assetListPath.string()); + if (!stream) + { + con::error("Failed to open assetlist for zone: {}", zone.m_name); + return false; + } + + AssetListOutputStream assetListStream(*stream, zone.m_game_id); + for (const auto* asset : zone.m_pools) + { + if (asset->IsReference()) + assetListStream.WriteEntry(AssetListEntry(asset->m_type, asset->ReferencedAssetName(), true)); + else + assetListStream.WriteEntry(AssetListEntry(asset->m_type, asset->m_name, false)); + } + + stream->flush(); + if (!*stream) + { + con::error("Writing assetlist for zone \"{}\" failed.", zone.m_name); + return false; + } + + con::info("Created assetlist \"{}\"", (outDir / assetListPath).string()); + return true; + } + bool BuildFastFile(LinkerPathManager& paths, const std::string& projectName, const std::string& targetName, ZoneDefinition& zoneDefinition) const { const fs::path outDir(paths.m_linker_paths->BuildOutputFolderPath(projectName, zoneDefinition.m_game)); @@ -348,7 +379,11 @@ namespace const auto zone = CreateZoneForDefinition(paths, outDir, cacheDir, targetName, zoneDefinition); auto result = zone != nullptr; if (zone) + { result = WriteZoneToFile(outputPath, *zone); + if (result && m_args.m_generate_asset_lists) + result = WriteAssetList(outputPath, outDir, *zone); + } return result; } diff --git a/src/Linking/LinkerArgs.cpp b/src/Linking/LinkerArgs.cpp index a92229373..fde958a6a 100644 --- a/src/Linking/LinkerArgs.cpp +++ b/src/Linking/LinkerArgs.cpp @@ -57,6 +57,12 @@ const CommandLineOption* const OPTION_OUTPUT_FOLDER = .WithParameter("outputFolderPath") .Build(); +const CommandLineOption* const OPTION_NO_ASSET_LIST = + CommandLineOption::Builder::Create() + .WithLongName("no-assetlist") + .WithDescription("Disables generating an assetlist after successfully linking a zone.") + .Build(); + const CommandLineOption* const OPTION_ADD_ASSET_SEARCH_PATH = CommandLineOption::Builder::Create() .WithLongName("add-asset-search-path") @@ -125,6 +131,7 @@ const CommandLineOption* const COMMAND_LINE_OPTIONS[]{ OPTION_NO_COLOR, OPTION_BASE_FOLDER, OPTION_OUTPUT_FOLDER, + OPTION_NO_ASSET_LIST, OPTION_ADD_ASSET_SEARCH_PATH, OPTION_ASSET_SEARCH_PATH, OPTION_GDT_SEARCH_PATH, @@ -136,7 +143,8 @@ const CommandLineOption* const COMMAND_LINE_OPTIONS[]{ }; LinkerArgs::LinkerArgs() - : m_argument_parser(COMMAND_LINE_OPTIONS, std::extent_v) + : m_generate_asset_lists(true), + m_argument_parser(COMMAND_LINE_OPTIONS, std::extent_v) { } @@ -222,6 +230,9 @@ bool LinkerArgs::ParseArgs(const int argc, const char** argv, bool& shouldContin else m_out_folder = DEFAULT_OUTPUT_FOLDER; + // --no-assetlist + m_generate_asset_lists = !m_argument_parser.IsOptionSpecified(OPTION_NO_ASSET_LIST); + // --asset-search-path if (m_argument_parser.IsOptionSpecified(OPTION_ASSET_SEARCH_PATH)) { diff --git a/src/Linking/LinkerArgs.h b/src/Linking/LinkerArgs.h index e9c93c15c..d5509d7cf 100644 --- a/src/Linking/LinkerArgs.h +++ b/src/Linking/LinkerArgs.h @@ -23,6 +23,7 @@ class LinkerArgs std::string m_bin_folder; std::string m_base_folder; std::string m_out_folder; + bool m_generate_asset_lists; std::set m_asset_search_paths; std::set m_gdt_search_paths; diff --git a/src/ObjCommon/Csv/CsvStream.cpp b/src/ObjCommon/Csv/CsvStream.cpp index 14a55cf15..a88d4425b 100644 --- a/src/ObjCommon/Csv/CsvStream.cpp +++ b/src/ObjCommon/Csv/CsvStream.cpp @@ -217,11 +217,12 @@ namespace } } // namespace -CsvOutputStream::CsvOutputStream(std::ostream& stream) +CsvOutputStream::CsvOutputStream(std::ostream& stream, const bool padColumns) : m_stream(stream), m_column_count(0), m_current_column(0), - m_first_row(true) + m_first_row(true), + m_pad_columns(padColumns) { } @@ -265,7 +266,7 @@ void CsvOutputStream::NextRow() m_first_row = false; m_column_count = m_current_column; } - else + else if (m_pad_columns) { while (m_current_column < m_column_count) { diff --git a/src/ObjCommon/Csv/CsvStream.h b/src/ObjCommon/Csv/CsvStream.h index 5c0f2def0..e1d7b6e66 100644 --- a/src/ObjCommon/Csv/CsvStream.h +++ b/src/ObjCommon/Csv/CsvStream.h @@ -37,7 +37,7 @@ class CsvInputStream class CsvOutputStream { public: - explicit CsvOutputStream(std::ostream& stream); + explicit CsvOutputStream(std::ostream& stream, bool padColumns = true); void WriteColumn(const std::string& value); void NextRow(); @@ -47,4 +47,5 @@ class CsvOutputStream unsigned m_column_count; unsigned m_current_column; bool m_first_row; + bool m_pad_columns; }; diff --git a/src/ZoneCommon/Zone/AssetList/AssetListOutputStream.cpp b/src/ZoneCommon/Zone/AssetList/AssetListOutputStream.cpp index 942b154c9..3b02be739 100644 --- a/src/ZoneCommon/Zone/AssetList/AssetListOutputStream.cpp +++ b/src/ZoneCommon/Zone/AssetList/AssetListOutputStream.cpp @@ -1,7 +1,7 @@ #include "AssetListOutputStream.h" AssetListOutputStream::AssetListOutputStream(std::ostream& stream, const GameId game) - : m_stream(stream), + : m_stream(stream, false), m_game(IGame::GetGameById(game)) { } @@ -9,6 +9,8 @@ AssetListOutputStream::AssetListOutputStream(std::ostream& stream, const GameId void AssetListOutputStream::WriteEntry(const AssetListEntry& entry) { m_stream.WriteColumn(*m_game->GetAssetTypeName(entry.m_type)); + if (entry.m_is_reference) + m_stream.WriteColumn(""); m_stream.WriteColumn(entry.m_name); m_stream.NextRow(); } diff --git a/test/ObjCommonTests/Csv/CsvStreamTests.cpp b/test/ObjCommonTests/Csv/CsvStreamTests.cpp index 136b130ce..575a16cbc 100644 --- a/test/ObjCommonTests/Csv/CsvStreamTests.cpp +++ b/test/ObjCommonTests/Csv/CsvStreamTests.cpp @@ -84,6 +84,22 @@ namespace } } + TEST_CASE("CsvOutputStream can write rows with different column counts", "[csv]") + { + std::ostringstream ss; + CsvOutputStream outputStream(ss, false); + + outputStream.WriteColumn("one"); + outputStream.WriteColumn("two"); + outputStream.WriteColumn("three"); + outputStream.NextRow(); + outputStream.WriteColumn("foo"); + outputStream.WriteColumn("bar"); + outputStream.NextRow(); + + REQUIRE(ss.str() == ("one,two,three" NEW_LINE "foo,bar" NEW_LINE)); + } + TEST_CASE("CsvInputStream", "[csv]") { SECTION("Ensure can write normal single-line csv") diff --git a/test/SystemTests/Game/IW4/AssetList/SimpleZoneIW4.zone b/test/SystemTests/Game/IW4/AssetList/SimpleZoneIW4.zone new file mode 100644 index 000000000..592d1eb1b --- /dev/null +++ b/test/SystemTests/Game/IW4/AssetList/SimpleZoneIW4.zone @@ -0,0 +1,4 @@ +>game,IW4 + +material,,white +rawfile,SimpleZone.txt diff --git a/test/SystemTests/Game/IW4/AssetListIgnore/IgnoreAssetListIW4.zone b/test/SystemTests/Game/IW4/AssetListIgnore/IgnoreAssetListIW4.zone new file mode 100644 index 000000000..8cbe8e3a0 --- /dev/null +++ b/test/SystemTests/Game/IW4/AssetListIgnore/IgnoreAssetListIW4.zone @@ -0,0 +1,7 @@ +>game,IW4 + +ignore,SimpleZoneIW4 + +material,white +rawfile,SimpleZone.txt +rawfile,NotIgnored.txt diff --git a/test/SystemTests/Game/IW4/Simple/NotIgnored.txt b/test/SystemTests/Game/IW4/Simple/NotIgnored.txt new file mode 100644 index 000000000..5c69995aa --- /dev/null +++ b/test/SystemTests/Game/IW4/Simple/NotIgnored.txt @@ -0,0 +1 @@ +This asset is not ignored. diff --git a/test/SystemTests/Game/IW4/SimpleZoneIW4.cpp b/test/SystemTests/Game/IW4/SimpleZoneIW4.cpp index a672e133f..5f076d608 100644 --- a/test/SystemTests/Game/IW4/SimpleZoneIW4.cpp +++ b/test/SystemTests/Game/IW4/SimpleZoneIW4.cpp @@ -7,6 +7,8 @@ #include #include #include +#include +#include #include #include @@ -58,4 +60,148 @@ namespace REQUIRE(zone->m_pools.GetTotalAssetCount() == 1); REQUIRE(zone->m_pools.GetAsset("SimpleZone.txt")); } + + TEST_CASE("Linker generates an assetlist(IW4)", "[iw4][system][assetlist]") + { + const auto assetSearchPath = (oat::paths::GetSystemTestsDirectory() / "Game/IW4/Simple").string(); + const auto sourceSearchPath = (oat::paths::GetSystemTestsDirectory() / "Game/IW4/AssetList").string(); + const auto outputPath = oat::paths::GetTempDirectory("SimpleZoneIW4AssetList").string(); + fs::create_directories(fs::path(outputPath) / ".oat/cache/SimpleZoneIW4"); + + const char* argStrings[]{ + "SystemTests", // bin + "--asset-search-path", + assetSearchPath.c_str(), + "--source-search-path", + sourceSearchPath.c_str(), + "--base-folder", + outputPath.c_str(), + "--output-folder", + outputPath.c_str(), + "SimpleZoneIW4", + }; + + LinkerArgs args; + + bool shouldContinue = true; + const auto couldParseArgs = args.ParseArgs(std::extent_v, argStrings, shouldContinue); + + REQUIRE(couldParseArgs); + REQUIRE(shouldContinue); + + const auto linker = Linker::Create(std::move(args)); + REQUIRE(linker->Start()); + + const auto expectedAssetListPath = fs::path(outputPath) / "assetlist/SimpleZoneIW4.csv"; + std::ifstream assetListStream(expectedAssetListPath, std::ios::binary); + REQUIRE(assetListStream.is_open()); + + const std::string assetListContent(std::istreambuf_iterator(assetListStream), {}); + REQUIRE(assetListContent == "material,,white\nrawfile,SimpleZone.txt\n"); + } + + TEST_CASE("Linker can omit the assetlist(IW4)", "[iw4][system][assetlist]") + { + const auto assetSearchPath = (oat::paths::GetSystemTestsDirectory() / "Game/IW4/Simple").string(); + const auto sourceSearchPath = (oat::paths::GetSystemTestsDirectory() / "Game/IW4/Simple").string(); + const auto outputPath = oat::paths::GetTempDirectory("SimpleZoneIW4NoAssetList").string(); + fs::create_directories(fs::path(outputPath) / ".oat/cache/SimpleZoneIW4"); + + const char* argStrings[]{ + "SystemTests", // bin + "--asset-search-path", + assetSearchPath.c_str(), + "--source-search-path", + sourceSearchPath.c_str(), + "--base-folder", + outputPath.c_str(), + "--output-folder", + outputPath.c_str(), + "--no-assetlist", + "SimpleZoneIW4", + }; + + LinkerArgs args; + + bool shouldContinue = true; + const auto couldParseArgs = args.ParseArgs(std::extent_v, argStrings, shouldContinue); + + REQUIRE(couldParseArgs); + REQUIRE(shouldContinue); + + const auto linker = Linker::Create(std::move(args)); + REQUIRE(linker->Start()); + + const auto expectedAssetListPath = fs::path(outputPath) / "assetlist/SimpleZoneIW4.csv"; + REQUIRE_FALSE(fs::exists(expectedAssetListPath)); + } + + TEST_CASE("Linker can use a generated assetlist as ignore(IW4)", "[iw4][system][assetlist]") + { + const auto assetSearchPath = (oat::paths::GetSystemTestsDirectory() / "Game/IW4/Simple").string(); + const auto assetListSourcePath = (oat::paths::GetSystemTestsDirectory() / "Game/IW4/AssetList").string(); + const auto ignoreSourcePath = (oat::paths::GetSystemTestsDirectory() / "Game/IW4/AssetListIgnore").string(); + const auto outputPath = oat::paths::GetTempDirectory("GeneratedAssetListIgnoreIW4").string(); + fs::create_directories(fs::path(outputPath) / ".oat/cache/SimpleZoneIW4"); + fs::create_directories(fs::path(outputPath) / ".oat/cache/IgnoreAssetListIW4"); + + const char* generateArgStrings[]{ + "SystemTests", // bin + "--asset-search-path", + assetSearchPath.c_str(), + "--source-search-path", + assetListSourcePath.c_str(), + "--base-folder", + outputPath.c_str(), + "--output-folder", + outputPath.c_str(), + "SimpleZoneIW4", + }; + + LinkerArgs generateArgs; + bool shouldContinue = true; + REQUIRE(generateArgs.ParseArgs(std::extent_v, generateArgStrings, shouldContinue)); + REQUIRE(shouldContinue); + REQUIRE(Linker::Create(std::move(generateArgs))->Start()); + + const char* ignoreArgStrings[]{ + "SystemTests", // bin + "--asset-search-path", + assetSearchPath.c_str(), + "--source-search-path", + outputPath.c_str(), + "--add-source-search-path", + ignoreSourcePath.c_str(), + "--base-folder", + outputPath.c_str(), + "--output-folder", + outputPath.c_str(), + "IgnoreAssetListIW4", + }; + + LinkerArgs ignoreArgs; + shouldContinue = true; + REQUIRE(ignoreArgs.ParseArgs(std::extent_v, ignoreArgStrings, shouldContinue)); + REQUIRE(shouldContinue); + REQUIRE(Linker::Create(std::move(ignoreArgs))->Start()); + + const auto expectedZonePath = (fs::path(outputPath) / "IgnoreAssetListIW4.ff").string(); + auto maybeZone = ZoneLoading::LoadZone(expectedZonePath, std::nullopt); + REQUIRE(maybeZone); + + auto zone = std::move(*maybeZone); + REQUIRE(zone->m_pools.GetTotalAssetCount() == 3); + + const auto* material = zone->m_pools.GetAssetOrAssetReference("white"); + REQUIRE(material); + REQUIRE(material->IsReference()); + + const auto* rawFile = zone->m_pools.GetAssetOrAssetReference("SimpleZone.txt"); + REQUIRE(rawFile); + REQUIRE(rawFile->IsReference()); + + const auto* notIgnoredRawFile = zone->m_pools.GetAsset("NotIgnored.txt"); + REQUIRE(notIgnoredRawFile); + REQUIRE_FALSE(notIgnoredRawFile->IsReference()); + } } // namespace