diff --git a/README.md b/README.md index 93b556a5..3bc2c934 100644 --- a/README.md +++ b/README.md @@ -188,11 +188,11 @@ The Python wrappers can be found on PyPI in the [sourcepp](https://pypi.org/proj - vpkpp + vpkpp 007 v1.1, v1.3 (007 - Nightfire) ✅ ❌ - C
C#
Python + C
C#
Python @@ -265,6 +265,12 @@ The Python wrappers can be found on PyPI in the [sourcepp](https://pypi.org/proj ✅ + + REZ v1-2 (LithTech Engine) + ✅ + ❌ + + SDAT (PS3, Portal 2 & CS:GO) ✅ @@ -533,6 +539,7 @@ These are only the tools and games using `sourcepp` that I know of. If you would - HOG parser was contributed by [@erysdren](https://github.com/erysdren). - 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). - 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 0c32e501..1698e90f 100644 --- a/docs/index.html +++ b/docs/index.html @@ -603,7 +603,7 @@

Supported Formats

✅ - + vpkpp diff --git a/include/vpkpp/format/REZ.h b/include/vpkpp/format/REZ.h new file mode 100644 index 00000000..3a7489f9 --- /dev/null +++ b/include/vpkpp/format/REZ.h @@ -0,0 +1,37 @@ +// ReSharper disable CppRedundantQualifier + +#pragma once + +#include "../PackFile.h" + +namespace vpkpp { + +constexpr std::string_view REZ_EXTENSION = ".rez"; + +class REZ : public PackFileReadOnly { +public: + /// Open a REZ file + [[nodiscard]] static std::unique_ptr open(const std::string& path, const EntryCallback& callback = nullptr); + + [[nodiscard]] std::optional> readEntry(const std::string& path_) const override; + + [[nodiscard]] Attribute getSupportedEntryAttributes() const override; + + [[nodiscard]] std::string_view getFileType() const; + + [[nodiscard]] std::string_view getUserTitle() const; + + [[nodiscard]] uint32_t getVersion() const; + +protected: + using PackFileReadOnly::PackFileReadOnly; + + std::string fileType; + std::string userTitle; + uint32_t version; + +private: + VPKPP_REGISTER_PACKFILE_OPEN(REZ_EXTENSION, &REZ::open); +}; + +} // namespace vpkpp diff --git a/include/vpkpp/vpkpp.h b/include/vpkpp/vpkpp.h index e0d9a3d9..f771a9ff 100644 --- a/include/vpkpp/vpkpp.h +++ b/include/vpkpp/vpkpp.h @@ -17,6 +17,7 @@ #include "format/ORE.h" #include "format/PAK.h" #include "format/PCK.h" +#include "format/REZ.h" #include "format/SDAT.h" #include "format/TAB.h" #include "format/VPK.h" diff --git a/lang/c/include/vpkppc/format/REZ.h b/lang/c/include/vpkppc/format/REZ.h new file mode 100644 index 00000000..134ca3f3 --- /dev/null +++ b/lang/c/include/vpkppc/format/REZ.h @@ -0,0 +1,17 @@ +#pragma once + +#include "../PackFile.h" + +VPKPP_EXTERNVAR const char* VPKPP_REZ_EXTENSION; + +VPKPP_API vpkpp_pack_file_handle_t vpkpp_rez_open(const char* path, vpkpp_entry_callback_t callback); // REQUIRES MANUAL FREE: vpkpp_close +VPKPP_API sourcepp_string_t vpkpp_rez_get_file_type(vpkpp_pack_file_handle_t handle); +VPKPP_API sourcepp_string_t vpkpp_rez_get_user_title(vpkpp_pack_file_handle_t handle); +VPKPP_API uint32_t vpkpp_rez_get_version(vpkpp_pack_file_handle_t handle); + +// C++ conversion routines +#ifdef __cplusplus + +#include + +#endif diff --git a/lang/c/include/vpkppc/vpkpp.h b/lang/c/include/vpkppc/vpkpp.h index 05de9666..9643c454 100644 --- a/lang/c/include/vpkppc/vpkpp.h +++ b/lang/c/include/vpkppc/vpkpp.h @@ -17,6 +17,7 @@ #include "format/ORE.h" #include "format/PAK.h" #include "format/PCK.h" +#include "format/REZ.h" #include "format/SDAT.h" #include "format/TAB.h" #include "format/VPK.h" diff --git a/lang/c/src/vpkppc/_vpkppc.cmake b/lang/c/src/vpkppc/_vpkppc.cmake index af01f0b4..131c090c 100644 --- a/lang/c/src/vpkppc/_vpkppc.cmake +++ b/lang/c/src/vpkppc/_vpkppc.cmake @@ -12,6 +12,7 @@ add_pretty_parser(vpkpp C "${CMAKE_CURRENT_SOURCE_DIR}/lang/c/include/vpkppc/format/ORE.h" "${CMAKE_CURRENT_SOURCE_DIR}/lang/c/include/vpkppc/format/PAK.h" "${CMAKE_CURRENT_SOURCE_DIR}/lang/c/include/vpkppc/format/PCK.h" + "${CMAKE_CURRENT_SOURCE_DIR}/lang/c/include/vpkppc/format/REZ.h" "${CMAKE_CURRENT_SOURCE_DIR}/lang/c/include/vpkppc/format/SDAT.h" "${CMAKE_CURRENT_SOURCE_DIR}/lang/c/include/vpkppc/format/TAB.h" "${CMAKE_CURRENT_SOURCE_DIR}/lang/c/include/vpkppc/format/VPK.h" @@ -39,6 +40,7 @@ add_pretty_parser(vpkpp C "${CMAKE_CURRENT_LIST_DIR}/format/ORE.cpp" "${CMAKE_CURRENT_LIST_DIR}/format/PAK.cpp" "${CMAKE_CURRENT_LIST_DIR}/format/PCK.cpp" + "${CMAKE_CURRENT_LIST_DIR}/format/REZ.cpp" "${CMAKE_CURRENT_LIST_DIR}/format/SDAT.cpp" "${CMAKE_CURRENT_LIST_DIR}/format/TAB.cpp" "${CMAKE_CURRENT_LIST_DIR}/format/VPK.cpp" diff --git a/lang/c/src/vpkppc/format/REZ.cpp b/lang/c/src/vpkppc/format/REZ.cpp new file mode 100644 index 00000000..3bb11f8f --- /dev/null +++ b/lang/c/src/vpkppc/format/REZ.cpp @@ -0,0 +1,47 @@ +#include + +#include + +using namespace sourceppc; +using namespace vpkpp; + +const char* VPKPP_REZ_EXTENSION = REZ_EXTENSION.data(); + +VPKPP_API vpkpp_pack_file_handle_t vpkpp_rez_open(const char* path, vpkpp_entry_callback_t callback) { + SOURCEPP_EARLY_RETURN_VAL(path, nullptr); + + auto packFile = REZ::open(path, callback ? [callback](const std::string& entryPath, const Entry& entry) { + callback(entryPath.c_str(), const_cast(&entry)); + } : static_cast(nullptr)); + if (!packFile) { + return nullptr; + } + return packFile.release(); +} + +VPKPP_API sourcepp_string_t vpkpp_rez_get_file_type(vpkpp_pack_file_handle_t handle) { + SOURCEPP_EARLY_RETURN_VAL(handle, SOURCEPP_STRING_INVALID); + + const auto* rez = dynamic_cast(convert::handle(handle)); + SOURCEPP_EARLY_RETURN_VAL(rez, SOURCEPP_STRING_INVALID); + + return convert::toString(rez->getFileType()); +} + +VPKPP_API sourcepp_string_t vpkpp_rez_get_user_title(vpkpp_pack_file_handle_t handle) { + SOURCEPP_EARLY_RETURN_VAL(handle, SOURCEPP_STRING_INVALID); + + const auto* rez = dynamic_cast(convert::handle(handle)); + SOURCEPP_EARLY_RETURN_VAL(rez, SOURCEPP_STRING_INVALID); + + return convert::toString(rez->getUserTitle()); +} + +VPKPP_API uint32_t vpkpp_rez_get_version(vpkpp_pack_file_handle_t handle) { + SOURCEPP_EARLY_RETURN_VAL(handle, 0); + + const auto* rez = dynamic_cast(convert::handle(handle)); + SOURCEPP_EARLY_RETURN_VAL(rez, 0); + + return rez->getVersion(); +} diff --git a/lang/csharp/src/vpkpp/DLL.cs b/lang/csharp/src/vpkpp/DLL.cs index 51d31a6c..1e91b60c 100644 --- a/lang/csharp/src/vpkpp/DLL.cs +++ b/lang/csharp/src/vpkpp/DLL.cs @@ -101,6 +101,18 @@ internal static partial class DLL [LibraryImport(Name, StringMarshalling = StringMarshalling.Utf8)] public static partial nint vpkpp_pck_open(string path, EntryCallbackNative? callback); + [LibraryImport(Name, StringMarshalling = StringMarshalling.Utf8)] + public static partial nint vpkpp_rez_open(string path, EntryCallbackNative? callback); + + [LibraryImport(Name)] + public static partial sourcepp.DLL.String vpkpp_rez_get_file_type(nint handle); + + [LibraryImport(Name)] + public static partial sourcepp.DLL.String vpkpp_rez_get_user_title(nint handle); + + [LibraryImport(Name)] + public static partial uint vpkpp_rez_get_version(nint handle); + [LibraryImport(Name, StringMarshalling = StringMarshalling.Utf8)] public static partial nint vpkpp_sdat_create(string path); diff --git a/lang/csharp/src/vpkpp/Format/REZ.cs b/lang/csharp/src/vpkpp/Format/REZ.cs new file mode 100644 index 00000000..67ef2d6f --- /dev/null +++ b/lang/csharp/src/vpkpp/Format/REZ.cs @@ -0,0 +1,49 @@ +using System; + +namespace sourcepp.vpkpp.Format; + +using EntryCallback = Action; +using OpenPropertyRequest = Func; + +public class REZ : PackFile +{ + protected REZ(nint handle, bool managed = true) : base(handle, managed) + { + } + + public new static REZ? Open(string path, EntryCallback? callback = null, OpenPropertyRequest? requestProperty = null) + { + var handle = DLL.vpkpp_rez_open(path, callback is not null ? (entryPath, entry) => + { + callback(entryPath, new Entry(entry, false)); + } : null); + return handle == nint.Zero ? null : new REZ(handle); + } + + public string FileType + { + get + { + ThrowIfDisposed(); + return new sourcepp.String(DLL.vpkpp_rez_get_file_type(Handle)).Read(); + } + } + + public string UserTitle + { + get + { + ThrowIfDisposed(); + return new sourcepp.String(DLL.vpkpp_rez_get_user_title(Handle)).Read(); + } + } + + public uint Version + { + get + { + ThrowIfDisposed(); + return DLL.vpkpp_rez_get_version(Handle); + } + } +} diff --git a/lang/python/src/vpkpp.h b/lang/python/src/vpkpp.h index f6a66353..d44658fc 100644 --- a/lang/python/src/vpkpp.h +++ b/lang/python/src/vpkpp.h @@ -263,11 +263,13 @@ inline void register_python(py::module_& m) { .def("get_godot_version", &PCK::getGodotVersion) .def("set_godot_version", &PCK::setGodotVersion, "major"_a = 0, "minor"_a = 0, "patch"_a = 0); - vpkpp.attr("TAB_EXTENSION") = TAB_EXTENSION; - vpkpp.attr("TAB_FILENAME_MAX_SIZE") = TAB_FILENAME_MAX_SIZE; - vpkpp.attr("TAB_HASHED_FILEPATH_PREFIX") = TAB_HASHED_FILEPATH_PREFIX; - vpkpp.attr("ARC_EXTENSION") = ARC_EXTENSION; - vpkpp.attr("ARC_CHUNK_SIZE") = ARC_CHUNK_SIZE; + vpkpp.attr("REZ_EXTENSION") = REZ_EXTENSION; + + py::class_(vpkpp, "REZ") + .def_static("open", &REZ::open, "path"_a, "callback"_a = nullptr) + .def_prop_ro("file_type", &REZ::getFileType) + .def_prop_ro("user_title", &REZ::getUserTitle) + .def_prop_ro("version", &REZ::getVersion); vpkpp.attr("SDAT_SIGNATURE") = SDAT_SIGNATURE; vpkpp.attr("SDAT_EXTENSION") = SDAT_EXTENSION; @@ -276,6 +278,12 @@ inline void register_python(py::module_& m) { .def_static("create", &SDAT::create, "path"_a) .def_static("open", &SDAT::open, "path"_a, "callback"_a = nullptr); + vpkpp.attr("TAB_EXTENSION") = TAB_EXTENSION; + vpkpp.attr("TAB_FILENAME_MAX_SIZE") = TAB_FILENAME_MAX_SIZE; + vpkpp.attr("TAB_HASHED_FILEPATH_PREFIX") = TAB_HASHED_FILEPATH_PREFIX; + vpkpp.attr("ARC_EXTENSION") = ARC_EXTENSION; + vpkpp.attr("ARC_CHUNK_SIZE") = ARC_CHUNK_SIZE; + auto cTAB = py::class_(vpkpp, "TAB"); py::enum_(cTAB, "Version") diff --git a/src/vpkpp/_vpkpp.cmake b/src/vpkpp/_vpkpp.cmake index 2f88844f..df3412ae 100644 --- a/src/vpkpp/_vpkpp.cmake +++ b/src/vpkpp/_vpkpp.cmake @@ -14,6 +14,7 @@ add_pretty_parser(vpkpp "${CMAKE_CURRENT_SOURCE_DIR}/include/vpkpp/format/ORE.h" "${CMAKE_CURRENT_SOURCE_DIR}/include/vpkpp/format/PAK.h" "${CMAKE_CURRENT_SOURCE_DIR}/include/vpkpp/format/PCK.h" + "${CMAKE_CURRENT_SOURCE_DIR}/include/vpkpp/format/REZ.h" "${CMAKE_CURRENT_SOURCE_DIR}/include/vpkpp/format/SDAT.h" "${CMAKE_CURRENT_SOURCE_DIR}/include/vpkpp/format/TAB.h" "${CMAKE_CURRENT_SOURCE_DIR}/include/vpkpp/format/VPK.h" @@ -40,6 +41,7 @@ add_pretty_parser(vpkpp "${CMAKE_CURRENT_LIST_DIR}/format/ORE.cpp" "${CMAKE_CURRENT_LIST_DIR}/format/PAK.cpp" "${CMAKE_CURRENT_LIST_DIR}/format/PCK.cpp" + "${CMAKE_CURRENT_LIST_DIR}/format/REZ.cpp" "${CMAKE_CURRENT_LIST_DIR}/format/SDAT.cpp" "${CMAKE_CURRENT_LIST_DIR}/format/TAB.cpp" "${CMAKE_CURRENT_LIST_DIR}/format/VPK.cpp" diff --git a/src/vpkpp/format/REZ.cpp b/src/vpkpp/format/REZ.cpp new file mode 100644 index 00000000..9aced6da --- /dev/null +++ b/src/vpkpp/format/REZ.cpp @@ -0,0 +1,179 @@ +#include + +#include + +#include + +using namespace sourcepp; +using namespace vpkpp; + +std::unique_ptr REZ::open(const std::string& path, const EntryCallback& callback) { + if (!std::filesystem::exists(path)) { + // File does not exist + return nullptr; + } + + auto* rez = new REZ{path}; + auto packFile = std::unique_ptr(rez); + + FileStream reader{rez->fullFilePath}; + reader.seek_in(0); + + if (const auto c = reader.read(); c != '\r' && c != '&') { + return nullptr; + } + if (const auto c = reader.read(); c != '\n' && c != '#') { + return nullptr; + } + reader.read(rez->fileType, 60, false); + string::rtrim(rez->fileType); + + if (const auto c = reader.read(); c != '\r' && c != '!') { + return nullptr; + } + if (const auto c = reader.read(); c != '\n' && c != '\"') { + return nullptr; + } + reader.read(rez->userTitle, 60, false); + string::rtrim(rez->userTitle); + + if (const auto c = reader.read(); c != '\r' && c != '%') { + return nullptr; + } + if (const auto c = reader.read(); c != '\n' && c != '\'') { + return nullptr; + } + rez->version = reader.read(); + + switch (rez->version) { + case 0x1A: // EOF (1) + reader.read(rez->version); + if (rez->version != 1) { + reader.skip_in(7).read(rez->version); + if (rez->version != 2) { + return nullptr; + } + } + break; + case 0x2A: { // EOF (2) + reader.skip_in(68).read(rez->version); + if (rez->version != 1) { + return nullptr; + } + } + default: + return nullptr; + } + + const auto rootDirOffset = reader.read(); + const auto rootDirSize = reader.read(); + reader.skip_in(); // rootDirTime + reader.skip_in(); // nextWriteOffset + reader.skip_in(); // time + reader.skip_in(); // largestKeyArray + reader.skip_in(); // largestDirNameSize + reader.skip_in(); // largestFileNameSize + reader.skip_in(); // largestCommentSize + reader.skip_in(); // isSorted + + // Read the file tree recursively + std::function)> readBlock; + readBlock = [&callback, &rez, &reader, &readBlock](const std::string& parentPath, std::span blockData) { + BufferStreamReadOnly blockStream{blockData.data(), blockData.size()}; + + while (blockStream.tell() != blockStream.size()) { + const auto blockType = blockStream.read(); + const auto blockOffset = blockStream.read(); + const auto blockLength = blockStream.read(); + blockStream.skip(); // blockTime + + if (blockType == 0) { + // File + Entry entry = createNewEntry(); + + //const auto resourceID = blockStream.read(); + blockStream.skip(); + auto resourceExtension = blockStream.read_string(4); + const auto resourceKeyCount = blockStream.read(); + const auto resourceName = blockStream.read_string(); + const auto resourceDesc = blockStream.read_string(); + blockStream.skip(resourceKeyCount); + + auto entryPath = parentPath; + entryPath += '/' + resourceName; + if (!resourceExtension.empty()) { + std::ranges::reverse(resourceExtension); + entryPath += '.' + resourceExtension; + } + entryPath = rez->cleanEntryPath(entryPath); + + entry.length = blockLength; + entry.offset = blockOffset; + + rez->entries.emplace(entryPath, entry); + + if (callback) { + callback(entryPath, entry); + } + } else if (blockType == 1) { + // Directory + const auto dirName = blockStream.read_string(); + if (blockLength) { + std::string newParentPath; + if (!parentPath.empty()) { + newParentPath = parentPath; + newParentPath += '/' + dirName; + } else { + newParentPath = dirName; + } + readBlock(newParentPath, reader.seek_in(blockOffset).read_bytes(blockLength)); + } + } else { + // Invalid + return false; + } + } + return true; + }; + if (!readBlock("", reader.seek_in(rootDirOffset).read_bytes(rootDirSize))) { + return nullptr; + } + + return packFile; +} + +std::optional> REZ::readEntry(const std::string& path_) const { + const auto path = this->cleanEntryPath(path_); + const auto entry = this->findEntry(path); + if (!entry) { + return std::nullopt; + } + if (entry->unbaked) { + return readUnbakedEntry(*entry); + } + + // It's baked into the file on disk + FileStream stream{this->fullFilePath}; + if (!stream) { + return std::nullopt; + } + stream.seek_in_u(entry->offset); + return stream.read_bytes(entry->length); +} + +Attribute REZ::getSupportedEntryAttributes() const { + using enum Attribute; + return LENGTH; +} + +std::string_view REZ::getFileType() const { + return this->fileType; +} + +std::string_view REZ::getUserTitle() const { + return this->userTitle; +} + +uint32_t REZ::getVersion() const { + return this->version; +} diff --git a/test/vpkpp.cpp b/test/vpkpp.cpp index 4547dd71..247009bd 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, rez_v1_read) { + const auto rez = PackFile::open(ASSET_ROOT "vpkpp/rez/v1.rez"); + ASSERT_TRUE(rez); + VPKPP_PRINT_ALL_PATHS(rez); + EXPECT_EQ(rez->getEntryCount(), 1); + EXPECT_TRUE(rez->hasEntry("patch.txt")); +} + TEST(vpkpp, sdat) { const auto sdat = PackFile::open(ASSET_ROOT "vpkpp/sdat/steam_resources.sdat"); ASSERT_TRUE(sdat);