diff --git a/README.md b/README.md
index 9dee67d19..b3c6fcd90 100644
--- a/README.md
+++ b/README.md
@@ -157,11 +157,11 @@ Several modern C++20 libraries for sanely parsing Valve formats, rolled into one
- vpkpp |
+ vpkpp |
007 v1.1, v1.3 (007 - Nightfire) |
✅ |
❌ |
- C C# |
+ C C# |
@@ -182,6 +182,12 @@ Several modern C++20 libraries for sanely parsing Valve formats, rolled into one
| ✅ |
+
+ | HOG (Descent) |
+ ✅ |
+ ❌ |
+
+
| OL (Worldcraft Object Library) |
✅ |
@@ -226,7 +232,7 @@ Several modern C++20 libraries for sanely parsing Valve formats, rolled into one
- | VPP (Red Faction) |
+ VPP v1-2 (Red Faction) |
✅ |
❌ |
@@ -373,6 +379,7 @@ found on PyPI in the [sourcepp](https://pypi.org/project/sourcepp) package.
- `steampp` is based on the [SteamAppPathProvider](https://github.com/Trico-Everfire/SteamAppPathProvider) library by [@Trico Everfire](https://github.com/Trico-Everfire) and [Momentum Mod](https://momentum-mod.org) contributors.
- `vpkpp`'s 007 parser is based on [reverse-engineering work](https://raw.githubusercontent.com/SmileyAG/dumpster/refs/heads/src_jb007nightfirepc_alurazoe/file_format_analysis.txt) by Alhexx.
- `vpkpp`'s GCF parser was contributed by [@bt](https://github.com/caatge) and [@ymgve](https://github.com/ymgve).
+- `vpkpp`'s HOG parser was contributed by [@erysdren](https://github.com/erysdren).
- `vpkpp`'s 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).
- `vpkpp`'s ORE parser is based on [reverse-engineering work](https://github.com/erysdren/narbacular-drop-tools) by [@erysdren](https://github.com/erysdren).
- `vpkpp`'s VPP parser was contributed by [@erysdren](https://github.com/erysdren).
diff --git a/docs/index.md b/docs/index.md
index dc8c90785..0ee0f1685 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -140,11 +140,11 @@ Several modern C++20 libraries for sanely parsing Valve formats, rolled into one
❌ |
- vpkpp |
+ vpkpp |
007 v1.1, v1.3 (007 - Nightfire) |
✅ |
❌ |
- C C# |
+ C C# |
| FPX v10 (Tactical Intervention) |
@@ -161,6 +161,11 @@ Several modern C++20 libraries for sanely parsing Valve formats, rolled into one
✅ |
✅ |
+
+ | HOG (Descent) |
+ ✅ |
+ ❌ |
+
| OL (Worldcraft Object Library) |
✅ |
@@ -199,7 +204,7 @@ Several modern C++20 libraries for sanely parsing Valve formats, rolled into one
✅ |
- | VPP (Red Faction) |
+ VPP v1-2 (Red Faction) |
✅ |
❌ |
@@ -329,6 +334,7 @@ found on PyPI in the [sourcepp](https://pypi.org/project/sourcepp) package.
- `steampp` is based on the [SteamAppPathProvider](https://github.com/Trico-Everfire/SteamAppPathProvider) library by [@Trico Everfire](https://github.com/Trico-Everfire) and [Momentum Mod](https://momentum-mod.org) contributors.
- `vpkpp`'s 007 parser is based on [reverse-engineering work](https://raw.githubusercontent.com/SmileyAG/dumpster/refs/heads/src_jb007nightfirepc_alurazoe/file_format_analysis.txt) by Alhexx.
- `vpkpp`'s GCF parser was contributed by [@bt](https://github.com/caatge) and [@ymgve](https://github.com/ymgve).
+- `vpkpp`'s HOG parser was contributed by [@erysdren](https://github.com/erysdren).
- `vpkpp`'s 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).
- `vpkpp`'s ORE parser is based on [reverse-engineering work](https://github.com/erysdren/narbacular-drop-tools) by [@erysdren](https://github.com/erysdren).
- `vpkpp`'s VPP parser was contributed by [@erysdren](https://github.com/erysdren).
diff --git a/include/vpkpp/format/HOG.h b/include/vpkpp/format/HOG.h
new file mode 100644
index 000000000..e5ff4bfc3
--- /dev/null
+++ b/include/vpkpp/format/HOG.h
@@ -0,0 +1,36 @@
+#pragma once
+
+#include "../PackFile.h"
+
+namespace vpkpp {
+
+constexpr std::string_view HOG_SIGNATURE = "DHF";
+constexpr std::string_view HOG_EXTENSION = ".hog";
+
+class HOG : public PackFileReadOnly {
+public:
+ /// Open a HOG file
+ [[nodiscard]] static std::unique_ptr open(const std::string& path, const EntryCallback& callback = nullptr);
+
+ static constexpr inline std::string_view GUID = "FDE9941424FF4EC1BC4C90A7DA52AF87";
+
+ [[nodiscard]] constexpr std::string_view getGUID() const override {
+ return HOG::GUID;
+ }
+
+ [[nodiscard]] constexpr bool isCaseSensitive() const override {
+ return true;
+ }
+
+ [[nodiscard]] std::optional> readEntry(const std::string& path_) const override;
+
+ [[nodiscard]] Attribute getSupportedEntryAttributes() const override;
+
+protected:
+ using PackFileReadOnly::PackFileReadOnly;
+
+private:
+ VPKPP_REGISTER_PACKFILE_OPEN(HOG_EXTENSION, &HOG::open);
+};
+
+} // namespace vpkpp
diff --git a/include/vpkpp/vpkpp.h b/include/vpkpp/vpkpp.h
index b9effb1db..31620225c 100644
--- a/include/vpkpp/vpkpp.h
+++ b/include/vpkpp/vpkpp.h
@@ -8,6 +8,7 @@
#include "format/FPX.h"
#include "format/GCF.h"
#include "format/GMA.h"
+#include "format/HOG.h"
#include "format/OO7.h"
#include "format/OL.h"
#include "format/ORE.h"
diff --git a/src/vpkpp/_vpkpp.cmake b/src/vpkpp/_vpkpp.cmake
index 23e3f034b..49c3a87fb 100644
--- a/src/vpkpp/_vpkpp.cmake
+++ b/src/vpkpp/_vpkpp.cmake
@@ -5,6 +5,7 @@ add_pretty_parser(vpkpp
"${CMAKE_CURRENT_SOURCE_DIR}/include/vpkpp/format/FPX.h"
"${CMAKE_CURRENT_SOURCE_DIR}/include/vpkpp/format/GCF.h"
"${CMAKE_CURRENT_SOURCE_DIR}/include/vpkpp/format/GMA.h"
+ "${CMAKE_CURRENT_SOURCE_DIR}/include/vpkpp/format/HOG.h"
"${CMAKE_CURRENT_SOURCE_DIR}/include/vpkpp/format/OL.h"
"${CMAKE_CURRENT_SOURCE_DIR}/include/vpkpp/format/OO7.h"
"${CMAKE_CURRENT_SOURCE_DIR}/include/vpkpp/format/ORE.h"
@@ -24,6 +25,7 @@ add_pretty_parser(vpkpp
"${CMAKE_CURRENT_LIST_DIR}/format/FPX.cpp"
"${CMAKE_CURRENT_LIST_DIR}/format/GCF.cpp"
"${CMAKE_CURRENT_LIST_DIR}/format/GMA.cpp"
+ "${CMAKE_CURRENT_LIST_DIR}/format/HOG.cpp"
"${CMAKE_CURRENT_LIST_DIR}/format/OL.cpp"
"${CMAKE_CURRENT_LIST_DIR}/format/OO7.cpp"
"${CMAKE_CURRENT_LIST_DIR}/format/ORE.cpp"
diff --git a/src/vpkpp/format/HOG.cpp b/src/vpkpp/format/HOG.cpp
new file mode 100644
index 000000000..93875eeff
--- /dev/null
+++ b/src/vpkpp/format/HOG.cpp
@@ -0,0 +1,80 @@
+#include
+
+#include
+
+#include
+
+using namespace sourcepp;
+using namespace vpkpp;
+
+std::unique_ptr HOG::open(const std::string& path, const EntryCallback& callback) {
+ if (!std::filesystem::exists(path)) {
+ // File does not exist
+ return nullptr;
+ }
+
+ auto* hog = new HOG{path};
+ auto packFile = std::unique_ptr(hog);
+
+ FileStream reader{hog->fullFilePath};
+ reader.seek_in(0);
+
+ // Verify signature
+ if (const auto signature = reader.read_string(3); signature != HOG_SIGNATURE) {
+ return nullptr;
+ }
+
+ // Read file entries
+ while (true) {
+ // Create new entry
+ Entry entry = createNewEntry();
+
+ // Get file path
+ const auto entryPath = hog->cleanEntryPath(reader.read_string(13));
+
+ // Check if we're at EOF (must perform the check after reading beyond file bounds)
+ if (!reader) {
+ break;
+ }
+
+ // Get file size and offset
+ entry.length = reader.read();
+ entry.offset = reader.tell_in();
+
+ // Seek past file data
+ reader.seek_in_u(entry.offset + entry.length);
+
+ // Put it in
+ hog->entries.emplace(entryPath, entry);
+
+ if (callback) {
+ callback(entryPath, entry);
+ }
+ }
+
+ return packFile;
+}
+
+std::optional> HOG::readEntry(const std::string& path_) const {
+ auto path = this->cleanEntryPath(path_);
+ 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 HOG::getSupportedEntryAttributes() const {
+ using enum Attribute;
+ return LENGTH;
+}
diff --git a/test/vpkpp.cpp b/test/vpkpp.cpp
index 9bfa12a79..93149d247 100644
--- a/test/vpkpp.cpp
+++ b/test/vpkpp.cpp
@@ -5,6 +5,12 @@
using namespace sourcepp;
using namespace vpkpp;
+TEST(vpkpp, hog_read) {
+ const auto hog = PackFile::open(ASSET_ROOT "vpkpp/hog/chaos.hog");
+ ASSERT_TRUE(hog);
+ EXPECT_EQ(hog->getEntryCount(), 5);
+}
+
TEST(vpkpp, vpp_read) {
const auto vpp = PackFile::open(ASSET_ROOT "vpkpp/vpp/v1.vpp");
ASSERT_TRUE(vpp);