From 864b006eb4719cfff9f0af7eab07a8af216aab39 Mon Sep 17 00:00:00 2001 From: erysdren Date: Fri, 9 Oct 2026 15:08:12 -0500 Subject: [PATCH 1/2] vpkpp: add support for SiN Reloaded PAKs --- include/vpkpp/format/PAK.h | 3 + lang/c/include/vpkppc/format/PAK.h | 17 ++-- lang/c/src/vpkppc/format/PAK.cpp | 2 + lang/csharp/src/vpkpp/Format/PAK.cs | 1 + lang/python/src/vpkpp.h | 2 + src/vpkpp/format/PAK.cpp | 153 ++++++++++++++++++++++------ test/vpkpp.cpp | 8 ++ 7 files changed, 150 insertions(+), 36 deletions(-) diff --git a/include/vpkpp/format/PAK.h b/include/vpkpp/format/PAK.h index 057b208c9..cae1e41cd 100644 --- a/include/vpkpp/format/PAK.h +++ b/include/vpkpp/format/PAK.h @@ -14,6 +14,8 @@ constexpr auto PAK_SIGNATURE = sourcepp::parser::binary::makeFourCC("PACK"); constexpr uint8_t PAK_SIN_FILENAME_MAX_SIZE = 120; constexpr auto PAK_SIN_SIGNATURE = sourcepp::parser::binary::makeFourCC("SPAK"); +constexpr auto PAK_SIN_RELOADED_SIGNATURE = sourcepp::parser::binary::makeFourCC("SRPK"); + constexpr uint8_t PAK_HROT_FILENAME_MAX_SIZE = 120; constexpr auto PAK_HROT_SIGNATURE = sourcepp::parser::binary::makeFourCC("HROT"); @@ -25,6 +27,7 @@ class PAK : public PackFile { enum class Type { PAK, SIN, + SIN_RELOADED, HROT, }; diff --git a/lang/c/include/vpkppc/format/PAK.h b/lang/c/include/vpkppc/format/PAK.h index c4db3da4d..02e52e370 100644 --- a/lang/c/include/vpkppc/format/PAK.h +++ b/lang/c/include/vpkppc/format/PAK.h @@ -8,6 +8,8 @@ VPKPP_EXTERNVAR const uint32_t VPKPP_PAK_SIGNATURE; VPKPP_EXTERNVAR const uint8_t VPKPP_PAK_SIN_FILENAME_MAX_SIZE; VPKPP_EXTERNVAR const uint32_t VPKPP_PAK_SIN_SIGNATURE; +VPKPP_EXTERNVAR const uint32_t VPKPP_PAK_SIN_RELOADED_SIGNATURE; + VPKPP_EXTERNVAR const uint8_t VPKPP_PAK_HROT_FILENAME_MAX_SIZE; VPKPP_EXTERNVAR const uint32_t VPKPP_PAK_HROT_SIGNATURE; @@ -17,6 +19,7 @@ VPKPP_EXTERNVAR const char* VPKPP_SIN_EXTENSION; VPKPP_EXTERN typedef enum { VPKPP_PAK_TYPE_PAK, VPKPP_PAK_TYPE_SIN, + VPKPP_PAK_TYPE_SIN_RELOADED, VPKPP_PAK_TYPE_HROT, } vpkpp_pak_type_e; @@ -35,18 +38,20 @@ namespace sourceppc::convert { inline vpkpp::PAK::Type cast(vpkpp_pak_type_e value) { switch (value) { - case VPKPP_PAK_TYPE_PAK: return vpkpp::PAK::Type::PAK; - case VPKPP_PAK_TYPE_SIN: return vpkpp::PAK::Type::SIN; - case VPKPP_PAK_TYPE_HROT: return vpkpp::PAK::Type::HROT; + case VPKPP_PAK_TYPE_PAK: return vpkpp::PAK::Type::PAK; + case VPKPP_PAK_TYPE_SIN: return vpkpp::PAK::Type::SIN; + case VPKPP_PAK_TYPE_SIN_RELOADED: return vpkpp::PAK::Type::SIN_RELOADED; + case VPKPP_PAK_TYPE_HROT: return vpkpp::PAK::Type::HROT; } return vpkpp::PAK::Type::PAK; } inline vpkpp_pak_type_e cast(vpkpp::PAK::Type value) { switch (value) { - case vpkpp::PAK::Type::PAK: return VPKPP_PAK_TYPE_PAK; - case vpkpp::PAK::Type::SIN: return VPKPP_PAK_TYPE_SIN; - case vpkpp::PAK::Type::HROT: return VPKPP_PAK_TYPE_HROT; + case vpkpp::PAK::Type::PAK: return VPKPP_PAK_TYPE_PAK; + case vpkpp::PAK::Type::SIN: return VPKPP_PAK_TYPE_SIN; + case vpkpp::PAK::Type::SIN_RELOADED: return VPKPP_PAK_TYPE_SIN_RELOADED; + case vpkpp::PAK::Type::HROT: return VPKPP_PAK_TYPE_HROT; } return VPKPP_PAK_TYPE_PAK; } diff --git a/lang/c/src/vpkppc/format/PAK.cpp b/lang/c/src/vpkppc/format/PAK.cpp index d37553658..5067c8571 100644 --- a/lang/c/src/vpkppc/format/PAK.cpp +++ b/lang/c/src/vpkppc/format/PAK.cpp @@ -11,6 +11,8 @@ const uint32_t VPKPP_PAK_SIGNATURE = PAK_SIGNATURE; const uint8_t VPKPP_PAK_SIN_FILENAME_MAX_SIZE = PAK_SIN_FILENAME_MAX_SIZE; const uint32_t VPKPP_PAK_SIN_SIGNATURE = PAK_SIN_SIGNATURE; +const uint32_t VPKPP_PAK_SIN_RELOADED_SIGNATURE = PAK_SIN_RELOADED_SIGNATURE; + const uint8_t VPKPP_PAK_HROT_FILENAME_MAX_SIZE = PAK_HROT_FILENAME_MAX_SIZE; const uint32_t VPKPP_PAK_HROT_SIGNATURE = PAK_HROT_SIGNATURE; diff --git a/lang/csharp/src/vpkpp/Format/PAK.cs b/lang/csharp/src/vpkpp/Format/PAK.cs index 8c8ae56b9..d7e5a279f 100644 --- a/lang/csharp/src/vpkpp/Format/PAK.cs +++ b/lang/csharp/src/vpkpp/Format/PAK.cs @@ -11,6 +11,7 @@ public enum PAKType { PAK, SIN, + SIN_RELOADED, HROT, } diff --git a/lang/python/src/vpkpp.h b/lang/python/src/vpkpp.h index 8fd86ae95..814c9aa04 100644 --- a/lang/python/src/vpkpp.h +++ b/lang/python/src/vpkpp.h @@ -233,6 +233,7 @@ inline void register_python(py::module_& m) { vpkpp.attr("PAK_SIGNATURE") = PAK_SIGNATURE; vpkpp.attr("PAK_SIN_FILENAME_MAX_SIZE") = PAK_SIN_FILENAME_MAX_SIZE; vpkpp.attr("PAK_SIN_SIGNATURE") = PAK_SIN_SIGNATURE; + vpkpp.attr("PAK_SIN_RELOADED_SIGNATURE") = PAK_SIN_RELOADED_SIGNATURE; vpkpp.attr("PAK_HROT_FILENAME_MAX_SIZE") = PAK_HROT_FILENAME_MAX_SIZE; vpkpp.attr("PAK_HROT_SIGNATURE") = PAK_HROT_SIGNATURE; vpkpp.attr("PAK_EXTENSION") = PAK_EXTENSION; @@ -243,6 +244,7 @@ inline void register_python(py::module_& m) { py::enum_(cPAK, "Type") .value("PAK", PAK::Type::PAK) .value("SIN", PAK::Type::SIN) + .value("SIN_RELOADED", PAK::Type::SIN_RELOADED) .value("HROT", PAK::Type::HROT); cPAK diff --git a/src/vpkpp/format/PAK.cpp b/src/vpkpp/format/PAK.cpp index 955bccd10..f709f78fa 100644 --- a/src/vpkpp/format/PAK.cpp +++ b/src/vpkpp/format/PAK.cpp @@ -19,6 +19,9 @@ std::unique_ptr PAK::create(const std::string& path, Type type) { case Type::SIN: stream << PAK_SIN_SIGNATURE; break; + case Type::SIN_RELOADED: + stream << PAK_SIN_RELOADED_SIGNATURE; + break; case Type::HROT: stream << PAK_HROT_SIGNATURE; break; @@ -46,6 +49,8 @@ std::unique_ptr PAK::open(const std::string& path, const EntryCallback pak->type = Type::PAK; } else if (signature == PAK_SIN_SIGNATURE) { pak->type = Type::SIN; + } else if (signature == PAK_SIN_RELOADED_SIGNATURE) { + pak->type = Type::SIN_RELOADED; } else if (signature == PAK_HROT_SIGNATURE) { pak->type = Type::HROT; } else { @@ -53,23 +58,57 @@ std::unique_ptr PAK::open(const std::string& path, const EntryCallback return nullptr; } - const auto directoryOffset = reader.read(); - // Directory size / file entry size - const auto fileCount = reader.read() / (sizeof(uint32_t) * 2 + pak->getFilenameLength()); + if (pak->type == Type::SIN_RELOADED) { + reader.skip_in(); // reserved + const auto directoryOffset = reader.read(); + const auto namesOffset = reader.read(); + const auto fileCount = reader.read(); + const auto namesLength = reader.read(); + + // read names buffer + std::string namesBuffer; + reader.seek_in_u(namesOffset); + reader.read(namesBuffer, namesLength, false); + + // read directory + reader.seek_in_u(directoryOffset); + for (uint32_t i = 0; i < fileCount; i++) { + Entry entry = createNewEntry(); + + const auto filePos = reader.read(); + const auto fileLen = reader.read(); + const auto nameOfs = reader.read(); + + auto entryPath = pak->cleanEntryPath(namesBuffer.c_str() + nameOfs); + + entry.offset = filePos; + entry.length = fileLen; + + pak->entries.emplace(entryPath, entry); + + if (callback) { + callback(entryPath, entry); + } + } + } else { + const auto directoryOffset = reader.read(); + // Directory size / file entry size + const auto fileCount = reader.read() / (sizeof(uint32_t) * 2 + pak->getFilenameLength()); - reader.seek_in(directoryOffset); - for (uint32_t i = 0; i < fileCount; i++) { - Entry entry = createNewEntry(); + reader.seek_in(directoryOffset); + for (uint32_t i = 0; i < fileCount; i++) { + Entry entry = createNewEntry(); - auto entryPath = pak->cleanEntryPath(reader.read_string(pak->getFilenameLength())); + auto entryPath = pak->cleanEntryPath(reader.read_string(pak->getFilenameLength())); - entry.offset = reader.read(); - entry.length = reader.read(); + entry.offset = reader.read(); + entry.length = reader.read(); - pak->entries.emplace(entryPath, entry); + pak->entries.emplace(entryPath, entry); - if (callback) { - callback(entryPath, entry); + if (callback) { + callback(entryPath, entry); + } } } @@ -95,14 +134,14 @@ std::optional> PAK::readEntry(const std::string& path_) c return stream.read_bytes(entry->length); } -void PAK::addEntryInternal(Entry& entry, const std::string& path, std::vector& buffer, EntryOptions options) { +void PAK::addEntryInternal(Entry& entry, const std::string&, std::vector& buffer, EntryOptions) { entry.length = buffer.size(); // Offset will be reset when it's baked entry.offset = 0; } -bool PAK::bake(const std::string& outputDir_, BakeOptions options, const EntryCallback& callback) { +bool PAK::bake(const std::string& outputDir_, BakeOptions, const EntryCallback& callback) { // Get the proper file output folder const std::string outputDir = this->getBakeOutputDir(outputDir_); const std::string outputPath = outputDir + '/' + this->getFilename(); @@ -115,11 +154,20 @@ bool PAK::bake(const std::string& outputDir_, BakeOptions options, const EntryCa // Read data before overwriting, we don't know if we're writing to ourself std::vector fileData; + std::vector nameData; + std::vector nameOffsets; for (auto& [path, entry] : entriesToBake) { if (auto binData = this->readEntry(path)) { entry->offset = fileData.size(); fileData.insert(fileData.end(), binData->begin(), binData->end()); + + if (this->type == Type::SIN_RELOADED) { + const auto oldSize = nameData.size(); + nameData.resize(oldSize + path.size() + 1); + memcpy(&nameData[oldSize], path.data(), path.size()); + nameOffsets.push_back(oldSize); + } } else { entry->offset = 0; entry->length = 0; @@ -133,24 +181,65 @@ bool PAK::bake(const std::string& outputDir_, BakeOptions options, const EntryCa // Signature stream.write(this->getSignature()); - // Offset and size of directory - static constexpr auto HEADER_OFFSET = sizeof(uint32_t) * 3; - stream - .write(HEADER_OFFSET + fileData.size()) - .write(entriesToBake.size() * (sizeof(uint32_t) * 2 + this->getFilenameLength())); - - // File data - stream.write(fileData); + // Header + if (this->type == Type::SIN_RELOADED) { + // Reserved value + // Basically a version identifier, but not used...? + stream.write(0); + + // Offset of directory + static constexpr auto HEADER_OFFSET = sizeof(uint32_t) * 3 + sizeof(uint64_t) * 2; + stream.write(HEADER_OFFSET + fileData.size()); + + // Offset of names + const auto numFiles = entriesToBake.size(); + const auto directorySize = (sizeof(uint64_t) + sizeof(uint32_t) * 2) * numFiles; + stream.write(HEADER_OFFSET + fileData.size() + directorySize); + + // Number of files + stream.write(numFiles); + + // Size of names + stream.write(nameData.size()); + + // File data + stream.write(fileData); + + // Directory + size_t i = 0; + for (const auto& [path, entry] : entriesToBake) { + stream + .write(entry->offset + HEADER_OFFSET) + .write(entry->length) + .write(nameOffsets[i++]); + + if (callback) { + callback(path, *entry); + } + } - // Directory - for (const auto& [path, entry] : entriesToBake) { + // Names + stream.write(nameData); + } else { + // Offset and size of directory + static constexpr auto HEADER_OFFSET = sizeof(uint32_t) * 3; stream - .write(path, true, this->getFilenameLength()) - .write(entry->offset + HEADER_OFFSET) - .write(entry->length); - - if (callback) { - callback(path, *entry); + .write(HEADER_OFFSET + fileData.size()) + .write(entriesToBake.size() * (sizeof(uint32_t) * 2 + this->getFilenameLength())); + + // File data + stream.write(fileData); + + // Directory + for (const auto& [path, entry] : entriesToBake) { + stream + .write(path, true, this->getFilenameLength()) + .write(entry->offset + HEADER_OFFSET) + .write(entry->length); + + if (callback) { + callback(path, *entry); + } } } } @@ -180,6 +269,8 @@ uint32_t PAK::getSignature() const { return PAK_SIGNATURE; case Type::SIN: return PAK_SIN_SIGNATURE; + case Type::SIN_RELOADED: + return PAK_SIN_RELOADED_SIGNATURE; case Type::HROT: return PAK_HROT_SIGNATURE; } @@ -192,6 +283,8 @@ uint8_t PAK::getFilenameLength() const { return PAK_FILENAME_MAX_SIZE; case Type::SIN: return PAK_SIN_FILENAME_MAX_SIZE; + case Type::SIN_RELOADED: + return 0; // variable case Type::HROT: return PAK_HROT_FILENAME_MAX_SIZE; } diff --git a/test/vpkpp.cpp b/test/vpkpp.cpp index 247009bd7..f7a2e0935 100644 --- a/test/vpkpp.cpp +++ b/test/vpkpp.cpp @@ -27,6 +27,14 @@ TEST(vpkpp, ore_read) { EXPECT_TRUE(ore->hasEntry("startup.cfg")); } +TEST(vpkpp, pak_sin_read) { + const auto sin = PackFile::open(ASSET_ROOT "vpkpp/sin/test.sin"); + ASSERT_TRUE(sin); + VPKPP_PRINT_ALL_PATHS(sin); + EXPECT_EQ(sin->getEntryCount(), 3); + EXPECT_TRUE(sin->hasEntry("vault/vault.json")); +} + TEST(vpkpp, rez_v1_read) { const auto rez = PackFile::open(ASSET_ROOT "vpkpp/rez/v1.rez"); ASSERT_TRUE(rez); From 8d06c46d1cae3e5b02379a653f8c57f7c6edc15b Mon Sep 17 00:00:00 2001 From: craftablescience Date: Fri, 23 Oct 2026 08:26:32 -0700 Subject: [PATCH 2/2] docs: document support for SiN Reloaded PAKs --- README.md | 2 ++ docs/index.html | 2 ++ 2 files changed, 4 insertions(+) diff --git a/README.md b/README.md index 77512ca03..fc46a8b64 100644 --- a/README.md +++ b/README.md @@ -253,6 +253,7 @@ The Python wrappers can be found on PyPI in the [sourcepp](https://pypi.org/proj PAK (Quake, WON Half-Life)
• SiN modifications +
• SiN Reloaded modifications
• HROT modifications ✅ @@ -546,6 +547,7 @@ These are only the tools and games using `sourcepp` that I know of. If you would - OL parser is based on [reverse-engineering work](https://github.com/erysdren/scratch/blob/main/kaitai/worldcraft_ol.ksy) by [@erysdren](https://github.com/erysdren). - ORE parser is based on [reverse-engineering work](https://github.com/erysdren/narbacular-drop-tools) by [@erysdren](https://github.com/erysdren). - REZ parser is based on [RezExtract](https://github.com/no-lith/RezExtract) by [@no-lith](https://github.com/no-lith). + - SiN Reloaded parser was contributed by [@erysdren](https://github.com/erysdren). - VPP parser was contributed by [@erysdren](https://github.com/erysdren). - WAD3 parser/writer was contributed by [@ozxybox](https://github.com/ozxybox). - `vtfpp` diff --git a/docs/index.html b/docs/index.html index e8727a27c..e770f182a 100644 --- a/docs/index.html +++ b/docs/index.html @@ -665,6 +665,7 @@

Supported Formats

PAK (Quake, WON Half-Life)
  • SiN modifications
  • +
  • SiN Reloaded modifications
  • HROT modifications
@@ -901,6 +902,7 @@

Special Thanks

  • OL parser is based on reverse-engineering work by @erysdren.
  • ORE parser is based on reverse-engineering work by @erysdren.
  • REZ parser is based on RezExtract by @no-lith.
  • +
  • SiN Reloaded parser was contributed by @erysdren.
  • VPP parser was contributed by @erysdren.
  • WAD3 parser/writer was contributed by @ozxybox.