diff --git a/src/ObjLoading/Game/T6/ObjLoaderT6.cpp b/src/ObjLoading/Game/T6/ObjLoaderT6.cpp index cbce8c48..68230945 100644 --- a/src/ObjLoading/Game/T6/ObjLoaderT6.cpp +++ b/src/ObjLoading/Game/T6/ObjLoaderT6.cpp @@ -165,13 +165,13 @@ namespace T6 if (ObjLoading::Configuration.Verbose) std::cout << std::format("Trying to load ipak '{}' for zone '{}'\n", ipakName, zone.m_name); - auto* existingIPak = IPak::Repository.GetContainerByName(ipakName); + auto* existingIPak = IIPak::Repository.GetContainerByName(ipakName); if (existingIPak != nullptr) { if (ObjLoading::Configuration.Verbose) std::cout << std::format("Referencing loaded ipak '{}'.\n", ipakName); - IPak::Repository.AddContainerReference(existingIPak, &zone); + IIPak::Repository.AddContainerReference(existingIPak, &zone); return; } @@ -180,11 +180,11 @@ namespace T6 auto file = searchPath.Open(ipakFilename); if (file.IsOpen()) { - auto ipak = std::make_unique(ipakFilename, std::move(file.m_stream)); + auto ipak = IIPak::Create(ipakFilename, std::move(file.m_stream)); if (ipak->Initialize()) { - IPak::Repository.AddContainer(std::move(ipak), &zone); + IIPak::Repository.AddContainer(std::move(ipak), &zone); if (ObjLoading::Configuration.Verbose) std::cout << std::format("Found and loaded ipak '{}'.\n", ipakFilename); @@ -277,7 +277,7 @@ namespace T6 void ObjLoader::UnloadContainersOfZone(Zone& zone) const { - IPak::Repository.RemoveContainerReferences(&zone); + IIPak::Repository.RemoveContainerReferences(&zone); } namespace diff --git a/src/ObjLoading/ObjContainer/IPak/IPak.cpp b/src/ObjLoading/ObjContainer/IPak/IPak.cpp index 5f88c264..a963f28d 100644 --- a/src/ObjLoading/ObjContainer/IPak/IPak.cpp +++ b/src/ObjLoading/ObjContainer/IPak/IPak.cpp @@ -1,35 +1,22 @@ #include "IPak.h" -#include "Exception/IPakLoadException.h" #include "IPakStreamManager.h" #include "ObjContainer/IPak/IPakTypes.h" -#include "Utils/FileUtils.h" #include "zlib.h" #include +#include +#include #include -#include #include namespace fs = std::filesystem; -ObjContainerRepository IPak::Repository; +ObjContainerRepository IIPak::Repository; -class IPak::Impl : public ObjContainerReferenceable +namespace { - std::string m_path; - std::unique_ptr m_stream; - - bool m_initialized; - - std::unique_ptr m_index_section; - std::unique_ptr m_data_section; - - std::vector m_index_entries; - - IPakStreamManager m_stream_manager; - - static uint32_t R_HashString(const char* str, uint32_t hash) + std::uint32_t R_HashString(const char* str, std::uint32_t hash) { for (const auto* pos = str; *pos; pos++) { @@ -38,203 +25,189 @@ class IPak::Impl : public ObjContainerReferenceable return hash; } +} // namespace - bool ReadIndexSection() +namespace +{ + class IPak final : public IIPak { - m_stream->seekg(m_index_section->offset); - IPakIndexEntry indexEntry{}; - - for (unsigned itemIndex = 0; itemIndex < m_index_section->itemCount; itemIndex++) + public: + IPak(std::string path, std::unique_ptr stream) + : m_path(std::move(path)), + m_stream(std::move(stream)), + m_initialized(false), + m_index_section(nullptr), + m_data_section(nullptr), + m_stream_manager(*m_stream) { - m_stream->read(reinterpret_cast(&indexEntry), sizeof(indexEntry)); - if (m_stream->gcount() != sizeof(indexEntry)) - { - printf("Unexpected eof when trying to load index entry %u.\n", itemIndex); + } + + bool Initialize() override + { + if (m_initialized) + return true; + + if (!ReadHeader()) return false; - } - m_index_entries.push_back(indexEntry); - } - - std::ranges::sort(m_index_entries, - [](const IPakIndexEntry& entry1, const IPakIndexEntry& entry2) - { - return entry1.key.combinedKey < entry2.key.combinedKey; - }); - - return true; - } - - bool ReadSection() - { - IPakSection section{}; - - m_stream->read(reinterpret_cast(§ion), sizeof(section)); - if (m_stream->gcount() != sizeof(section)) - { - printf("Unexpected eof when trying to load section.\n"); - return false; - } - - switch (section.type) - { - case ipak_consts::IPAK_INDEX_SECTION: - m_index_section = std::make_unique(section); - break; - - case ipak_consts::IPAK_DATA_SECTION: - m_data_section = std::make_unique(section); - break; - - default: - break; - } - - return true; - } - - bool ReadHeader() - { - IPakHeader header{}; - - m_stream->read(reinterpret_cast(&header), sizeof(header)); - if (m_stream->gcount() != sizeof(header)) - { - printf("Unexpected eof when trying to load header.\n"); - return false; - } - - if (header.magic != ipak_consts::IPAK_MAGIC) - { - printf("Invalid ipak magic '0x%x'.\n", header.magic); - return false; - } - - if (header.version != ipak_consts::IPAK_VERSION) - { - printf("Unsupported ipak version '%u'.\n", header.version); - return false; - } - - for (unsigned section = 0; section < header.sectionCount; section++) - { - if (!ReadSection()) - return false; - } - - if (m_index_section == nullptr) - { - printf("IPak does not contain an index section.\n"); - return false; - } - - if (m_data_section == nullptr) - { - printf("IPak does not contain a data section.\n"); - return false; - } - - if (!ReadIndexSection()) - return false; - - return true; - } - -public: - Impl(std::string path, std::unique_ptr stream) - : m_path(std::move(path)), - m_stream(std::move(stream)), - m_initialized(false), - m_index_section(nullptr), - m_data_section(nullptr), - m_stream_manager(*m_stream) - { - } - - ~Impl() override = default; - - std::string GetName() override - { - return fs::path(m_path).filename().replace_extension("").string(); - } - - bool Initialize() - { - if (m_initialized) + m_initialized = true; return true; - - if (!ReadHeader()) - return false; - - m_initialized = true; - return true; - } - - std::unique_ptr GetEntryData(const Hash nameHash, const Hash dataHash) - { - IPakIndexEntryKey wantedKey{}; - wantedKey.nameHash = nameHash; - wantedKey.dataHash = dataHash; - - for (auto& entry : m_index_entries) - { - if (entry.key.combinedKey == wantedKey.combinedKey) - { - return m_stream_manager.OpenStream(static_cast(m_data_section->offset) + entry.offset, entry.size); - } - else if (entry.key.combinedKey > wantedKey.combinedKey) - { - // The index entries are sorted so if the current entry is higher than the wanted entry we can cancel here - return nullptr; - } } - return nullptr; - } + [[nodiscard]] std::unique_ptr GetEntryStream(const Hash nameHash, const Hash dataHash) const override + { + IPakIndexEntryKey wantedKey{}; + wantedKey.nameHash = nameHash; + wantedKey.dataHash = dataHash; - static Hash HashString(const std::string& str) - { - return R_HashString(str.c_str(), 0); - } + for (auto& entry : m_index_entries) + { + if (entry.key.combinedKey == wantedKey.combinedKey) + { + return m_stream_manager.OpenStream(static_cast(m_data_section->offset) + entry.offset, entry.size); + } + else if (entry.key.combinedKey > wantedKey.combinedKey) + { + // The index entries are sorted so if the current entry is higher than the wanted entry we can cancel here + return nullptr; + } + } - static Hash HashData(const void* data, const size_t dataSize) - { - return crc32(0, static_cast(data), dataSize); - } -}; + return nullptr; + } -IPak::IPak(std::string path, std::unique_ptr stream) + std::string GetName() override + { + return fs::path(m_path).filename().replace_extension("").string(); + } + + private: + bool ReadIndexSection() + { + m_stream->seekg(m_index_section->offset); + IPakIndexEntry indexEntry{}; + + for (unsigned itemIndex = 0; itemIndex < m_index_section->itemCount; itemIndex++) + { + m_stream->read(reinterpret_cast(&indexEntry), sizeof(indexEntry)); + if (m_stream->gcount() != sizeof(indexEntry)) + { + std::cerr << std::format("Unexpected eof when trying to load index entry {}.\n", itemIndex); + return false; + } + + m_index_entries.push_back(indexEntry); + } + + std::ranges::sort(m_index_entries, + [](const IPakIndexEntry& entry1, const IPakIndexEntry& entry2) + { + return entry1.key.combinedKey < entry2.key.combinedKey; + }); + + return true; + } + + bool ReadSection() + { + IPakSection section{}; + + m_stream->read(reinterpret_cast(§ion), sizeof(section)); + if (m_stream->gcount() != sizeof(section)) + { + std::cerr << "Unexpected eof when trying to load section.\n"; + return false; + } + + switch (section.type) + { + case ipak_consts::IPAK_INDEX_SECTION: + m_index_section = std::make_unique(section); + break; + + case ipak_consts::IPAK_DATA_SECTION: + m_data_section = std::make_unique(section); + break; + + default: + break; + } + + return true; + } + + bool ReadHeader() + { + IPakHeader header{}; + + m_stream->read(reinterpret_cast(&header), sizeof(header)); + if (m_stream->gcount() != sizeof(header)) + { + std::cerr << "Unexpected eof when trying to load header.\n"; + return false; + } + + if (header.magic != ipak_consts::IPAK_MAGIC) + { + std::cerr << std::format("Invalid ipak magic '{:#x}'.\n", header.magic); + return false; + } + + if (header.version != ipak_consts::IPAK_VERSION) + { + std::cerr << std::format("Unsupported ipak version '{}'.\n", header.version); + return false; + } + + for (unsigned section = 0; section < header.sectionCount; section++) + { + if (!ReadSection()) + return false; + } + + if (m_index_section == nullptr) + { + std::cerr << "IPak does not contain an index section.\n"; + return false; + } + + if (m_data_section == nullptr) + { + std::cerr << "IPak does not contain a data section.\n"; + return false; + } + + if (!ReadIndexSection()) + return false; + + return true; + } + + std::string m_path; + std::unique_ptr m_stream; + + bool m_initialized; + + std::unique_ptr m_index_section; + std::unique_ptr m_data_section; + + std::vector m_index_entries; + + IPakStreamManager m_stream_manager; + }; +} // namespace + +std::unique_ptr IIPak::Create(std::string path, std::unique_ptr stream) { - m_impl = new Impl(std::move(path), std::move(stream)); + return std::make_unique(std::move(path), std::move(stream)); } -IPak::~IPak() +IIPak::Hash IIPak::HashString(const std::string& str) { - delete m_impl; - m_impl = nullptr; + return R_HashString(str.c_str(), 0); } -std::string IPak::GetName() +IIPak::Hash IIPak::HashData(const void* data, const size_t dataSize) { - return m_impl->GetName(); -} - -bool IPak::Initialize() -{ - return m_impl->Initialize(); -} - -std::unique_ptr IPak::GetEntryStream(const Hash nameHash, const Hash dataHash) const -{ - return m_impl->GetEntryData(nameHash, dataHash); -} - -IPak::Hash IPak::HashString(const std::string& str) -{ - return Impl::HashString(str); -} - -IPak::Hash IPak::HashData(const void* data, const size_t dataSize) -{ - return Impl::HashData(data, dataSize); + return crc32(0, static_cast(data), dataSize); } diff --git a/src/ObjLoading/ObjContainer/IPak/IPak.h b/src/ObjLoading/ObjContainer/IPak/IPak.h index f60c1ba5..308513a5 100644 --- a/src/ObjLoading/ObjContainer/IPak/IPak.h +++ b/src/ObjLoading/ObjContainer/IPak/IPak.h @@ -2,30 +2,31 @@ #include "ObjContainer/ObjContainerReferenceable.h" #include "ObjContainer/ObjContainerRepository.h" -#include "Utils/ClassUtils.h" #include "Utils/ObjStream.h" #include "Zone/Zone.h" +#include #include +#include +#include -class IPak final : public ObjContainerReferenceable +class IIPak : public ObjContainerReferenceable { - class Impl; - Impl* m_impl; - public: - typedef uint32_t Hash; + static ObjContainerRepository Repository; + typedef std::uint32_t Hash; - static ObjContainerRepository Repository; + IIPak() = default; + virtual ~IIPak() = default; + IIPak(const IIPak& other) = default; + IIPak(IIPak&& other) noexcept = default; + IIPak& operator=(const IIPak& other) = default; + IIPak& operator=(IIPak&& other) noexcept = default; - IPak(std::string path, std::unique_ptr stream); - ~IPak() override; - - std::string GetName() override; - - bool Initialize(); - _NODISCARD std::unique_ptr GetEntryStream(Hash nameHash, Hash dataHash) const; + virtual bool Initialize() = 0; + [[nodiscard]] virtual std::unique_ptr GetEntryStream(Hash nameHash, Hash dataHash) const = 0; + static std::unique_ptr Create(std::string path, std::unique_ptr stream); static Hash HashString(const std::string& str); static Hash HashData(const void* data, size_t dataSize); }; diff --git a/src/ObjWriting/Game/T6/AssetDumpers/AssetDumperGfxImage.cpp b/src/ObjWriting/Game/T6/AssetDumpers/AssetDumperGfxImage.cpp index bb89ddd3..60eb52c2 100644 --- a/src/ObjWriting/Game/T6/AssetDumpers/AssetDumperGfxImage.cpp +++ b/src/ObjWriting/Game/T6/AssetDumpers/AssetDumperGfxImage.cpp @@ -38,7 +38,7 @@ namespace { if (image->streamedPartCount > 0) { - for (auto* ipak : IPak::Repository) + for (auto* ipak : IIPak::Repository) { auto ipakStream = ipak->GetEntryStream(image->hash, image->streamedParts[0].hash); diff --git a/test/ObjCommonTestUtils/SearchPath/MockOutputPath.cpp b/test/ObjCommonTestUtils/SearchPath/MockOutputPath.cpp index 5b0d761a..25993a48 100644 --- a/test/ObjCommonTestUtils/SearchPath/MockOutputPath.cpp +++ b/test/ObjCommonTestUtils/SearchPath/MockOutputPath.cpp @@ -108,6 +108,11 @@ MockOutputFile::MockOutputFile(std::string name, std::vector data) { } +std::string MockOutputFile::AsString() const +{ + return std::string(reinterpret_cast(m_data.data()), m_data.size()); +} + std::unique_ptr MockOutputPath::Open(const std::string& fileName) { return std::make_unique(fileName, m_files); @@ -123,3 +128,8 @@ const MockOutputFile* MockOutputPath::GetMockedFile(const std::string& name) con return nullptr; } + +const std::vector& MockOutputPath::GetMockedFileList() const +{ + return m_files; +} diff --git a/test/ObjCommonTestUtils/SearchPath/MockOutputPath.h b/test/ObjCommonTestUtils/SearchPath/MockOutputPath.h index e85a4251..42abdcce 100644 --- a/test/ObjCommonTestUtils/SearchPath/MockOutputPath.h +++ b/test/ObjCommonTestUtils/SearchPath/MockOutputPath.h @@ -15,6 +15,8 @@ public: MockOutputFile(); MockOutputFile(std::string name, std::vector data); + + [[nodiscard]] std::string AsString() const; }; class MockOutputPath final : public IOutputPath @@ -23,6 +25,7 @@ public: std::unique_ptr Open(const std::string& fileName) override; [[nodiscard]] const MockOutputFile* GetMockedFile(const std::string& name) const; + [[nodiscard]] const std::vector& GetMockedFileList() const; private: std::vector m_files; diff --git a/test/ObjCompilingTests/Image/IPak/IPakCreatorTest.cpp b/test/ObjCompilingTests/Image/IPak/IPakCreatorTest.cpp new file mode 100644 index 00000000..bd49745f --- /dev/null +++ b/test/ObjCompilingTests/Image/IPak/IPakCreatorTest.cpp @@ -0,0 +1,86 @@ +#include "Image/IPak/IPakCreator.h" + +#include "Asset/AssetCreatorCollection.h" +#include "ObjContainer/IPak/IPak.h" +#include "SearchPath/MockOutputPath.h" +#include "SearchPath/MockSearchPath.h" + +#include +#include +#include +#include +#include +#include + +using namespace std::string_literals; + +namespace +{ + class TestContext + { + public: + TestContext() + : m_zone("test", 0, IGame::GetGameById(GameId::T6)), + m_zone_states(m_zone), + m_out_dir() + { + } + + IPakCreator& CreateSut() + { + return m_zone_states.GetZoneAssetCreationState(); + } + + Zone m_zone; + MockSearchPath m_search_path; + ZoneAssetCreationStateContainer m_zone_states; + MockOutputPath m_out_dir; + }; +} // namespace + +namespace test::image::ipak +{ + TEST_CASE("IPakCreator: Does nothing if no ipak was defined", "[image]") + { + TestContext testContext; + auto& sut = testContext.CreateSut(); + + sut.Finalize(testContext.m_search_path, testContext.m_out_dir); + REQUIRE(testContext.m_out_dir.GetMockedFileList().empty()); + } + + TEST_CASE("IPakCreator: Writes IPak file", "[image]") + { + TestContext testContext; + auto& sut = testContext.CreateSut(); + + auto* ipak = sut.GetOrAddIPak("amazing"); + ipak->AddImage("random"); + + constexpr auto iwiData = "hello world"; + testContext.m_search_path.AddFileData("images/random.iwi", iwiData); + + sut.Finalize(testContext.m_search_path, testContext.m_out_dir); + + const auto* file = testContext.m_out_dir.GetMockedFile("amazing.ipak"); + REQUIRE(file); + + const auto* data = file->m_data.data(); + REQUIRE(data[0] == 'K'); + REQUIRE(data[1] == 'A'); + REQUIRE(data[2] == 'P'); + REQUIRE(data[3] == 'I'); + + auto readIpak = IIPak::Create("amazing.ipak", std::make_unique(file->AsString())); + REQUIRE(readIpak->Initialize()); + + auto entry = readIpak->GetEntryStream(IIPak::HashString("random"), IIPak::HashData(iwiData, std::char_traits::length(iwiData))); + REQUIRE(entry); + + char readBuffer[std::char_traits::length(iwiData) + 10]; + entry->read(readBuffer, sizeof(readBuffer)); + + REQUIRE(entry->gcount() == std::char_traits::length(iwiData)); + REQUIRE(std::strncmp(iwiData, readBuffer, std::char_traits::length(iwiData)) == 0); + } +} // namespace test::image::ipak