diff --git a/src/ObjCommon/SearchPath/OutputPathFilesystem.cpp b/src/ObjCommon/SearchPath/OutputPathFilesystem.cpp index c0954f94..39210c76 100644 --- a/src/ObjCommon/SearchPath/OutputPathFilesystem.cpp +++ b/src/ObjCommon/SearchPath/OutputPathFilesystem.cpp @@ -5,13 +5,13 @@ namespace fs = std::filesystem; OutputPathFilesystem::OutputPathFilesystem(const fs::path& path) - : m_path(fs::canonical(path)) + : m_path(fs::weakly_canonical(path)) { } std::unique_ptr OutputPathFilesystem::Open(const std::string& fileName) { - const auto fullNewPath = fs::canonical(m_path / fileName); + const auto fullNewPath = fs::weakly_canonical(m_path / fileName); if (!fullNewPath.string().starts_with(m_path.string())) return nullptr; diff --git a/src/ObjCompiling/Image/IPak/IPakCreator.cpp b/src/ObjCompiling/Image/IPak/IPakCreator.cpp index 9ad4435c..b91abb58 100644 --- a/src/ObjCompiling/Image/IPak/IPakCreator.cpp +++ b/src/ObjCompiling/Image/IPak/IPakCreator.cpp @@ -394,6 +394,11 @@ void IPakToCreate::Build(ISearchPath& searchPath, IOutputPath& outPath) std::cout << std::format("Created ipak {} with {} entries\n", m_name, m_image_names.size()); } +const std::vector& IPakToCreate::GetImageNames() const +{ + return m_image_names; +} + IPakCreator::IPakCreator() : m_kvp_creator(nullptr) { @@ -412,6 +417,7 @@ IPakToCreate* IPakCreator::GetOrAddIPak(const std::string& ipakName) auto newIPak = std::make_unique(ipakName); auto* result = newIPak.get(); + m_ipak_lookup.emplace(ipakName, result); m_ipaks.emplace_back(std::move(newIPak)); assert(m_kvp_creator); diff --git a/src/ObjCompiling/Image/IPak/IPakCreator.h b/src/ObjCompiling/Image/IPak/IPakCreator.h index 1218d63c..31eed0b2 100644 --- a/src/ObjCompiling/Image/IPak/IPakCreator.h +++ b/src/ObjCompiling/Image/IPak/IPakCreator.h @@ -16,6 +16,7 @@ public: void AddImage(std::string imageName); void Build(ISearchPath& searchPath, IOutputPath& outPath); + [[nodiscard]] const std::vector& GetImageNames() const; private: std::string m_name; diff --git a/src/ObjCompiling/Image/ImageIPakPostProcessor.cpp b/src/ObjCompiling/Image/ImageIPakPostProcessor.cpp index 09a66139..24c0ace2 100644 --- a/src/ObjCompiling/Image/ImageIPakPostProcessor.cpp +++ b/src/ObjCompiling/Image/ImageIPakPostProcessor.cpp @@ -3,7 +3,6 @@ #include "IPak/IPakCreator.h" #include -#include AbstractImageIPakPostProcessor::AbstractImageIPakPostProcessor(const ZoneDefinitionContext& zoneDefinition, ISearchPath& searchPath, @@ -57,7 +56,7 @@ void AbstractImageIPakPostProcessor::PostProcessAsset(XAssetInfoGeneric& assetIn while (m_current_ipak && m_zone_definition.m_asset_index_in_definition >= m_current_ipak_end_index) FindNextObjContainer(); - if (m_current_ipak && m_zone_definition.m_asset_index_in_definition <= m_current_ipak_start_index) + if (m_current_ipak && m_zone_definition.m_asset_index_in_definition >= m_current_ipak_start_index) m_current_ipak->AddImage(assetInfo.m_name); } diff --git a/src/ObjCompiling/Iwd/IwdCreator.cpp b/src/ObjCompiling/Iwd/IwdCreator.cpp index 9d4932fe..b25493e3 100644 --- a/src/ObjCompiling/Iwd/IwdCreator.cpp +++ b/src/ObjCompiling/Iwd/IwdCreator.cpp @@ -87,6 +87,7 @@ IwdToCreate* IwdCreator::GetOrAddIwd(const std::string& iwdName) auto newIwd = std::make_unique(iwdName); auto* result = newIwd.get(); + m_iwd_lookup.emplace(iwdName, result); m_iwds.emplace_back(std::move(newIwd)); return result; diff --git a/src/ZoneCommon/Zone/Definition/ZoneDefinition.cpp b/src/ZoneCommon/Zone/Definition/ZoneDefinition.cpp index 58774642..41c401da 100644 --- a/src/ZoneCommon/Zone/Definition/ZoneDefinition.cpp +++ b/src/ZoneCommon/Zone/Definition/ZoneDefinition.cpp @@ -1,10 +1,15 @@ #include "ZoneDefinition.h" ZoneDefinitionObjContainer::ZoneDefinitionObjContainer(std::string name, const ZoneDefinitionObjContainerType type, const unsigned start) + : ZoneDefinitionObjContainer(std::move(name), type, start, 0u) +{ +} + +ZoneDefinitionObjContainer::ZoneDefinitionObjContainer(std::string name, const ZoneDefinitionObjContainerType type, const unsigned start, const unsigned end) : m_name(std::move(name)), m_type(type), m_asset_start(start), - m_asset_end(0u) + m_asset_end(end) { } diff --git a/src/ZoneCommon/Zone/Definition/ZoneDefinition.h b/src/ZoneCommon/Zone/Definition/ZoneDefinition.h index a00a5b57..0d64f161 100644 --- a/src/ZoneCommon/Zone/Definition/ZoneDefinition.h +++ b/src/ZoneCommon/Zone/Definition/ZoneDefinition.h @@ -32,6 +32,7 @@ public: unsigned m_asset_end; ZoneDefinitionObjContainer(std::string name, ZoneDefinitionObjContainerType type, unsigned start); + ZoneDefinitionObjContainer(std::string name, ZoneDefinitionObjContainerType type, unsigned start, unsigned end); }; class ZoneDefinitionAsset diff --git a/test/ObjCommonTestUtils/SearchPath/MockOutputPath.cpp b/test/ObjCommonTestUtils/SearchPath/MockOutputPath.cpp index 6f4c457a..4b4db64c 100644 --- a/test/ObjCommonTestUtils/SearchPath/MockOutputPath.cpp +++ b/test/ObjCommonTestUtils/SearchPath/MockOutputPath.cpp @@ -1,24 +1,98 @@ #include "MockOutputPath.h" -#include +#include "Utils/ObjStream.h" namespace { - class MockFileWrapper final : public std::ostringstream + class MockFileBuffer final : public std::streambuf + { + public: + MockFileBuffer() + : m_pos(0u) + { + } + + std::vector data() + { + return std::move(m_data); + } + + protected: + int_type overflow(const int_type b) override + { + if (!std::char_traits::eq_int_type(b, std::char_traits::eof())) + { + m_data.insert(m_data.begin() + static_cast(m_pos), static_cast(b)); + ++m_pos; + } + + return b; + } + + std::streamsize xsputn(const char* ptr, const std::streamsize count) override + { + const auto curSize = m_data.size(); + const auto overrideCount = m_pos < curSize ? std::min(curSize - m_pos, static_cast(count)) : 0u; + const auto insertCount = count - overrideCount; + + if (overrideCount > 0) + { + memcpy(&m_data[m_pos], ptr, overrideCount); + m_pos += overrideCount; + ptr += overrideCount; + } + + if (insertCount > 0) + { + m_data.insert(m_data.begin() + static_cast(m_pos), ptr, ptr + insertCount); + m_pos += static_cast(insertCount); + } + + return count; + } + + pos_type seekoff(const off_type off, const std::ios_base::seekdir dir, const std::ios_base::openmode mode) override + { + if (dir == std::ios::beg) + return seekpos(off, mode); + if (dir == std::ios::cur) + return seekpos(static_cast(m_pos) + off, mode); + if (off < static_cast(m_data.size())) + return seekpos(static_cast(m_data.size()) - off, mode); + return std::char_traits::eof(); + } + + pos_type seekpos(const pos_type pos, std::ios_base::openmode) override + { + if (pos > m_data.size()) + m_data.resize(static_cast(pos)); + m_pos = static_cast(pos); + + return pos; + } + + private: + std::vector m_data; + std::size_t m_pos; + }; + + class MockFileWrapper final : public std::ostream { public: MockFileWrapper(std::string name, std::vector& files) - : m_name(std::move(name)), + : std::ostream(&m_buf), + m_name(std::move(name)), m_files(files) { } ~MockFileWrapper() override { - m_files.emplace_back(std::move(m_name), str()); + m_files.emplace_back(std::move(m_name), m_buf.data()); } private: + MockFileBuffer m_buf; std::string m_name; std::vector& m_files; }; @@ -26,7 +100,7 @@ namespace MockOutputFile::MockOutputFile() = default; -MockOutputFile::MockOutputFile(std::string name, std::string data) +MockOutputFile::MockOutputFile(std::string name, std::vector data) : m_name(std::move(name)), m_data(std::move(data)) { diff --git a/test/ObjCommonTestUtils/SearchPath/MockOutputPath.h b/test/ObjCommonTestUtils/SearchPath/MockOutputPath.h index 092916b0..e85a4251 100644 --- a/test/ObjCommonTestUtils/SearchPath/MockOutputPath.h +++ b/test/ObjCommonTestUtils/SearchPath/MockOutputPath.h @@ -2,6 +2,7 @@ #include "SearchPath/IOutputPath.h" +#include #include #include #include @@ -10,10 +11,10 @@ class MockOutputFile { public: std::string m_name; - std::string m_data; + std::vector m_data; MockOutputFile(); - MockOutputFile(std::string name, std::string data); + MockOutputFile(std::string name, std::vector data); }; class MockOutputPath final : public IOutputPath diff --git a/test/ObjCompilingTests/Image/ImageIPakPostProcessorTest.cpp b/test/ObjCompilingTests/Image/ImageIPakPostProcessorTest.cpp new file mode 100644 index 00000000..29dd627f --- /dev/null +++ b/test/ObjCompilingTests/Image/ImageIPakPostProcessorTest.cpp @@ -0,0 +1,278 @@ +#include "Image/ImageIPakPostProcessor.h" + +#include "Game/T6/T6.h" +#include "KeyValuePairs/KeyValuePairsCreator.h" +#include "SearchPath/MockOutputPath.h" +#include "SearchPath/MockSearchPath.h" + +#include +#include +#include +#include +#include + +using namespace T6; +using namespace std::string_literals; + +namespace +{ + class TestContext + { + public: + TestContext() + : m_zone("test", 0, IGame::GetGameById(GameId::T6)), + m_zone_definition(), + m_zone_definition_context(m_zone_definition), + m_zone_states(m_zone), + m_creators(m_zone), + m_ignored_assets(), + m_out_dir(), + m_context(m_zone, &m_creators, &m_ignored_assets), + m_ipak_creator(m_zone_states.GetZoneAssetCreationState()) + { + } + + std::unique_ptr CreateSut() + { + return std::make_unique>(m_zone_definition_context, m_search_path, m_zone_states, m_out_dir); + } + + Zone m_zone; + ZoneDefinition m_zone_definition; + ZoneDefinitionContext m_zone_definition_context; + MockSearchPath m_search_path; + ZoneAssetCreationStateContainer m_zone_states; + AssetCreatorCollection m_creators; + IgnoredAssetLookup m_ignored_assets; + MockOutputPath m_out_dir; + AssetCreationContext m_context; + + IPakCreator& m_ipak_creator; + }; +} // namespace + +namespace test::image +{ + TEST_CASE("ImageIPakPostProcessor: Handles asset type of specified asset", "[image]") + { + TestContext testContext; + const auto sut = testContext.CreateSut(); + + REQUIRE(sut->GetHandlingAssetType() == ASSET_TYPE_IMAGE); + } + + TEST_CASE("ImageIPakPostProcessor: Adds images to ipak when obj container is specified", "[image]") + { + TestContext testContext; + testContext.m_zone_definition.m_obj_containers.emplace_back("testIpak", ZoneDefinitionObjContainerType::IPAK, 0, 2); + + const auto sut = testContext.CreateSut(); + + XAssetInfo imageAsset0(ASSET_TYPE_IMAGE, "testImage0", nullptr); + sut->PostProcessAsset(imageAsset0, testContext.m_context); + + ++testContext.m_zone_definition_context.m_asset_index_in_definition; + XAssetInfo imageAsset1(ASSET_TYPE_IMAGE, "testImage1", nullptr); + sut->PostProcessAsset(imageAsset1, testContext.m_context); + + auto result = testContext.m_ipak_creator.GetOrAddIPak("testIpak"); + REQUIRE(result); + + const auto& imageNames = result->GetImageNames(); + REQUIRE(imageNames.size() == 2u); + REQUIRE(imageNames[0] == "testImage0"); + REQUIRE(imageNames[1] == "testImage1"); + } + + TEST_CASE("ImageIPakPostProcessor: Respects lower obj container boundary when adding images to ipak", "[image]") + { + TestContext testContext; + testContext.m_zone_definition.m_obj_containers.emplace_back("testIpak", ZoneDefinitionObjContainerType::IPAK, 1, 3); + + const auto sut = testContext.CreateSut(); + + XAssetInfo imageAsset0(ASSET_TYPE_IMAGE, "testImage0", nullptr); + sut->PostProcessAsset(imageAsset0, testContext.m_context); + + ++testContext.m_zone_definition_context.m_asset_index_in_definition; + XAssetInfo imageAsset1(ASSET_TYPE_IMAGE, "testImage1", nullptr); + sut->PostProcessAsset(imageAsset1, testContext.m_context); + + ++testContext.m_zone_definition_context.m_asset_index_in_definition; + XAssetInfo imageAsset2(ASSET_TYPE_IMAGE, "testImage2", nullptr); + sut->PostProcessAsset(imageAsset2, testContext.m_context); + + auto result = testContext.m_ipak_creator.GetOrAddIPak("testIpak"); + REQUIRE(result); + + const auto& imageNames = result->GetImageNames(); + REQUIRE(imageNames.size() == 2u); + REQUIRE(imageNames[0] == "testImage1"); + REQUIRE(imageNames[1] == "testImage2"); + } + + TEST_CASE("ImageIPakPostProcessor: Respects upper obj container boundary when adding images to ipak", "[image]") + { + TestContext testContext; + testContext.m_zone_definition.m_obj_containers.emplace_back("testIpak", ZoneDefinitionObjContainerType::IPAK, 0, 2); + + const auto sut = testContext.CreateSut(); + + XAssetInfo imageAsset0(ASSET_TYPE_IMAGE, "testImage0", nullptr); + sut->PostProcessAsset(imageAsset0, testContext.m_context); + + ++testContext.m_zone_definition_context.m_asset_index_in_definition; + XAssetInfo imageAsset1(ASSET_TYPE_IMAGE, "testImage1", nullptr); + sut->PostProcessAsset(imageAsset1, testContext.m_context); + + ++testContext.m_zone_definition_context.m_asset_index_in_definition; + XAssetInfo imageAsset2(ASSET_TYPE_IMAGE, "testImage2", nullptr); + sut->PostProcessAsset(imageAsset2, testContext.m_context); + + auto result = testContext.m_ipak_creator.GetOrAddIPak("testIpak"); + REQUIRE(result); + + const auto& imageNames = result->GetImageNames(); + REQUIRE(imageNames.size() == 2u); + REQUIRE(imageNames[0] == "testImage0"); + REQUIRE(imageNames[1] == "testImage1"); + } + + TEST_CASE("ImageIPakPostProcessor: Respects upper and lower obj container boundary when adding images to ipak", "[image]") + { + TestContext testContext; + testContext.m_zone_definition.m_obj_containers.emplace_back("testIpak", ZoneDefinitionObjContainerType::IPAK, 1, 3); + + const auto sut = testContext.CreateSut(); + + XAssetInfo imageAsset0(ASSET_TYPE_IMAGE, "testImage0", nullptr); + sut->PostProcessAsset(imageAsset0, testContext.m_context); + + ++testContext.m_zone_definition_context.m_asset_index_in_definition; + XAssetInfo imageAsset1(ASSET_TYPE_IMAGE, "testImage1", nullptr); + sut->PostProcessAsset(imageAsset1, testContext.m_context); + + ++testContext.m_zone_definition_context.m_asset_index_in_definition; + XAssetInfo imageAsset2(ASSET_TYPE_IMAGE, "testImage2", nullptr); + sut->PostProcessAsset(imageAsset2, testContext.m_context); + + ++testContext.m_zone_definition_context.m_asset_index_in_definition; + XAssetInfo imageAsset3(ASSET_TYPE_IMAGE, "testImage3", nullptr); + sut->PostProcessAsset(imageAsset3, testContext.m_context); + + auto result = testContext.m_ipak_creator.GetOrAddIPak("testIpak"); + REQUIRE(result); + + const auto& imageNames = result->GetImageNames(); + REQUIRE(imageNames.size() == 2u); + REQUIRE(imageNames[0] == "testImage1"); + REQUIRE(imageNames[1] == "testImage2"); + } + + TEST_CASE("ImageIPakPostProcessor: Can add images to multiple ipak", "[image]") + { + TestContext testContext; + testContext.m_zone_definition.m_obj_containers.emplace_back("testIpak0", ZoneDefinitionObjContainerType::IPAK, 0, 2); + testContext.m_zone_definition.m_obj_containers.emplace_back("testIpak1", ZoneDefinitionObjContainerType::IPAK, 2, 4); + + const auto sut = testContext.CreateSut(); + + XAssetInfo imageAsset0(ASSET_TYPE_IMAGE, "testImage0", nullptr); + sut->PostProcessAsset(imageAsset0, testContext.m_context); + + ++testContext.m_zone_definition_context.m_asset_index_in_definition; + XAssetInfo imageAsset1(ASSET_TYPE_IMAGE, "testImage1", nullptr); + sut->PostProcessAsset(imageAsset1, testContext.m_context); + + ++testContext.m_zone_definition_context.m_asset_index_in_definition; + XAssetInfo imageAsset2(ASSET_TYPE_IMAGE, "testImage2", nullptr); + sut->PostProcessAsset(imageAsset2, testContext.m_context); + + ++testContext.m_zone_definition_context.m_asset_index_in_definition; + XAssetInfo imageAsset3(ASSET_TYPE_IMAGE, "testImage3", nullptr); + sut->PostProcessAsset(imageAsset3, testContext.m_context); + + auto result0 = testContext.m_ipak_creator.GetOrAddIPak("testIpak0"); + REQUIRE(result0); + + const auto& imageNames0 = result0->GetImageNames(); + REQUIRE(imageNames0.size() == 2u); + REQUIRE(imageNames0[0] == "testImage0"); + REQUIRE(imageNames0[1] == "testImage1"); + + auto result1 = testContext.m_ipak_creator.GetOrAddIPak("testIpak1"); + REQUIRE(result1); + + const auto& imageNames1 = result1->GetImageNames(); + REQUIRE(imageNames1.size() == 2u); + REQUIRE(imageNames1[0] == "testImage2"); + REQUIRE(imageNames1[1] == "testImage3"); + } + + TEST_CASE("ImageIPakPostProcessor: Can add images to multiple ipak with gap between", "[image]") + { + TestContext testContext; + testContext.m_zone_definition.m_obj_containers.emplace_back("testIpak0", ZoneDefinitionObjContainerType::IPAK, 0, 2); + testContext.m_zone_definition.m_obj_containers.emplace_back("testIpak1", ZoneDefinitionObjContainerType::IPAK, 3, 5); + + const auto sut = testContext.CreateSut(); + + XAssetInfo imageAsset0(ASSET_TYPE_IMAGE, "testImage0", nullptr); + sut->PostProcessAsset(imageAsset0, testContext.m_context); + + ++testContext.m_zone_definition_context.m_asset_index_in_definition; + XAssetInfo imageAsset1(ASSET_TYPE_IMAGE, "testImage1", nullptr); + sut->PostProcessAsset(imageAsset1, testContext.m_context); + + ++testContext.m_zone_definition_context.m_asset_index_in_definition; + XAssetInfo imageAsset2(ASSET_TYPE_IMAGE, "testImage2", nullptr); + sut->PostProcessAsset(imageAsset2, testContext.m_context); + + ++testContext.m_zone_definition_context.m_asset_index_in_definition; + XAssetInfo imageAsset3(ASSET_TYPE_IMAGE, "testImage3", nullptr); + sut->PostProcessAsset(imageAsset3, testContext.m_context); + + ++testContext.m_zone_definition_context.m_asset_index_in_definition; + XAssetInfo imageAsset4(ASSET_TYPE_IMAGE, "testImage4", nullptr); + sut->PostProcessAsset(imageAsset4, testContext.m_context); + + auto result0 = testContext.m_ipak_creator.GetOrAddIPak("testIpak0"); + REQUIRE(result0); + + const auto& imageNames0 = result0->GetImageNames(); + REQUIRE(imageNames0.size() == 2u); + REQUIRE(imageNames0[0] == "testImage0"); + REQUIRE(imageNames0[1] == "testImage1"); + + auto result1 = testContext.m_ipak_creator.GetOrAddIPak("testIpak1"); + REQUIRE(result1); + + const auto& imageNames1 = result1->GetImageNames(); + REQUIRE(imageNames1.size() == 2u); + REQUIRE(imageNames1[0] == "testImage3"); + REQUIRE(imageNames1[1] == "testImage4"); + } + + TEST_CASE("ImageIPakPostProcessor: Writes IPak when finalizing", "[image]") + { + TestContext testContext; + testContext.m_zone_definition.m_obj_containers.emplace_back("testIpak", ZoneDefinitionObjContainerType::IPAK, 0, 2); + + testContext.m_search_path.AddFileData("images/testImage0.iwi", "asdf0"); + testContext.m_search_path.AddFileData("images/testImage1.iwi", "asdf1"); + + const auto sut = testContext.CreateSut(); + + XAssetInfo imageAsset0(ASSET_TYPE_IMAGE, "testImage0", nullptr); + sut->PostProcessAsset(imageAsset0, testContext.m_context); + + ++testContext.m_zone_definition_context.m_asset_index_in_definition; + XAssetInfo imageAsset1(ASSET_TYPE_IMAGE, "testImage1", nullptr); + sut->PostProcessAsset(imageAsset1, testContext.m_context); + + sut->FinalizeZone(testContext.m_context); + + const auto* mockFile = testContext.m_out_dir.GetMockedFile("testIpak.ipak"); + REQUIRE(mockFile); + } +} // namespace test::image