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
- C
@@ -676,6 +676,11 @@ Supported Formats
✅ |
✅ |
|
+
+ | REZ v1-2 (LithTech Engine) |
+ ✅ |
+ ❌ |
+
| SDAT (PS3, Portal 2 & CS:GO) |
✅ |
@@ -890,6 +895,7 @@ Special Thanks
HOG parser was contributed by @erysdren.
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.
VPP parser was contributed by @erysdren.
WAD3 parser/writer was contributed by @ozxybox.
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);