Skip to content

Instantly share code, notes, and snippets.

@DrMcCoy
Last active December 7, 2022 22:53
Show Gist options
  • Save DrMcCoy/39d81320fda6ebffce1830f654bb2375 to your computer and use it in GitHub Desktop.
Save DrMcCoy/39d81320fda6ebffce1830f654bb2375 to your computer and use it in GitHub Desktop.
unobb_kotor2.patch
diff --git a/src/aurora/obbfile.cpp b/src/aurora/obbfile.cpp
index a1fdb06..b2a6b62 100644
--- a/src/aurora/obbfile.cpp
+++ b/src/aurora/obbfile.cpp
@@ -46,11 +46,6 @@ OBBFile::~OBBFile() {
}
void OBBFile::load(Common::SeekableReadStream &obb) {
- /* OBB files have no actual header. But they're made up of zlib compressed chunks,
- * so we just check if we find a zlib header at the start of the file. */
- if (obb.readUint16BE() != 0x789C)
- throw Common::Exception("No zlib header, this doesn't look like an Aspyr OBB virtual filesystem");
-
try {
/* Extract the resource index and read the resource list out of it. */
@@ -106,48 +101,27 @@ void OBBFile::readResList(Common::SeekableReadStream &index) {
Common::SeekableReadStream *OBBFile::getIndex(Common::SeekableReadStream &obb) {
/* Find and decompress the resource index.
*
- * It's the last compressed chunk in the OBB file, so we're searching
- * backwards for 0x78 0x9C (the usual zlib header). That's a bit short,
- * and can lead to false positives.
- *
- * But we also know that each file, after the last chunk, has another
- * 16 bytes with some sort of meta data, of which the last four bytes
- * are always 0x00. Immediately afterwards, the next compressed chunk
- * start. So we can add that to our "marker" to look for.
+ * The index is the last block in the OBB file and it's (apparently) always
+ * zlib-compressed. Each zlib-compressed file contains 16 bytes of metadata
+ * with its offset and uncompressed size.
*
- * That gives us the start of the compressed resource list. To make
- * sure we're not decompression garbage, we're also looking for the
- * end of the list. The resource index is always one chunk, and at
- * the end, there's the usual 16 bytes. For the resource index, the
- * first four of those is the offset of that start of the chunk, and
- * the next four bytes are 0x00. We can use that to figure out end.
+ * So we seek to the end of the OBB file, read the last 16 bytes and use
+ * those to find the start and size of the resource index.
*
- * With that full range, we can decompress the resource index and
- * return the decompressed data.
- *
- * NOTE: Yes, we're taking quite some shortcuts here. The original
- * code probably does it differently and more robust. */
-
- static const byte kZlibHeader[6] = { 0x00, 0x00, 0x00, 0x00, 0x78, 0x9C };
- static const size_t kMaxReadBack = 0xFFFFFF; // Should be enough
+ * NOTE: Should we ever find an OBB file with uncompressed resource index,
+ * this will fail. */
- const size_t lastZlib = Common::searchBackwards(obb, kZlibHeader, sizeof(kZlibHeader), kMaxReadBack);
- if (lastZlib == SIZE_MAX)
- throw Common::Exception("Couldn't find the last zlib header");
+ obb.seek(-16, Common::SeekableReadStream::kOriginEnd);
- byte offsetData[8];
- WRITE_LE_UINT32(offsetData + 0, lastZlib + 4);
- WRITE_LE_UINT32(offsetData + 4, 0);
+ const uint32_t offset = obb.readUint32LE();
+ obb.skip(4); // Always 0. Possibly space for uint64_t?
- Common::SeekableSubReadStream obbZIndexStart(&obb, lastZlib + 4, obb.size());
+ const uint32_t uncompressedSize = obb.readUint32LE();
+ obb.skip(4); // Always 0. Possibly space for uint64_t?
- const size_t indexSize = Common::searchBackwards(obbZIndexStart, offsetData, sizeof(offsetData), 0xFFFFFF);
- if (indexSize == SIZE_MAX)
- throw Common::Exception("Couldn't find the index end marker");
+ Common::SeekableSubReadStream obbZIndex(&obb, offset, obb.size() - 16);
- obbZIndexStart.seek(0);
-
- return Common::decompressDeflateWithoutOutputSize(obbZIndexStart, indexSize, Common::kWindowBitsMax);
+ return Common::decompressDeflate(obbZIndex, obbZIndex.size(), uncompressedSize, Common::kWindowBitsMax);
}
const Archive::ResourceList &OBBFile::getResources() const {
@@ -156,7 +130,7 @@ const Archive::ResourceList &OBBFile::getResources() const {
const OBBFile::IResource &OBBFile::getIResource(uint32_t index) const {
if (index >= _iResources.size())
- throw Common::Exception("Resource index out of range (%u/%u)", index, (uint)_iResources.size());
+ throw Common::Exception("Resource index out of range (%u/%u)", index, static_cast<uint>(_iResources.size()));
return _iResources[index];
}
@@ -165,48 +139,69 @@ uint32_t OBBFile::getResourceSize(uint32_t index) const {
return getIResource(index).uncompressedSize;
}
-Common::SeekableReadStream *OBBFile::getResource(uint32_t index, bool UNUSED(tryNoCopy)) const {
- /* Decompress a single file.
- *
- * Files in OBB virtual filesystems are split up in zlib compressed chunks.
- * Each chunk, after decompression, takes up 4096 bytes (except for the last
- * one, which can be shorter). The compressed size of the chunk is variable.
+Common::SeekableReadStream *OBBFile::getResource(uint32_t index, bool tryNoCopy) const {
+ /* Files in OBB virtual filesystems are either completely uncompressed
+ * (if compressedSize == uncompressedSize) or split up in zlib compressed
+ * blocks.
*
- * Since we know the starting offset of the first chunk of the file, and
- * the uncompressed data size, we simple decompress one chunk after the
- * other, starting with the first of the file. Once we have decompressed
- * as many bytes as the uncompressed size, we know we're done.
+ * Each compressed block, after decompression takes up 4096 bytes, except
+ * for the last one, which can be shorter. The compressed size of a block
+ * is variable. If the file is compressed, there's also an additional 16
+ * bytes of metadata which contains the offset of the first block and the
+ * uncompressed size of the whole file again.
*
- * The OBB virtual filesystem also has a chunk list and extra 16 bytes of
- * meta data at the end of the last compressed chunk, but we don't really
- * care about any of that (also, we don't really know how either of those
- * work).
+ * Uncompressed files we can just read as is. Compressed files we read
+ * block for block, uncompressing them until we have the full uncompressed
+ * file.
*
- * We also can't use the compressed size, because that includes the extra
- * 16 bytes. And for the first file in the OBB, it even includes the
- * compressed size of the chunk list (which is always the second file
- * in the OBB).
- *
- * NOTE: Again, lots of shortcuts. Unlike what the original does. */
+ * There's also a list of all blocks in an OBB file. We ignore that one
+ * for now. */
const IResource &res = getIResource(index);
- _obb->seek(res.offset);
+ const bool isCompressed = res.compressedSize != res.uncompressedSize;
+ if (!isCompressed) {
+ if (tryNoCopy)
+ return new Common::SeekableSubReadStream(_obb.get(), res.offset, res.offset + res.uncompressedSize);
+
+ _obb->seek(res.offset);
+
+ return _obb->readStream(res.uncompressedSize);
+ }
+
+ warning("COMPRESSED");
+ if (res.compressedSize < 16)
+ throw Common::Exception("Invalid OBB resource compressed size (%u)", res.compressedSize);
+
+ _obb->seek(res.offset + res.compressedSize - 16);
+
+ const uint32_t offset = _obb->readUint32LE();
+ _obb->skip(4); // Always 0. Possibly space for uint64_t?
+
+ const uint32_t uncompressedSize = _obb->readUint32LE();
+ _obb->skip(4); // Always 0. Possibly space for uint64_t?
+
+ if ((offset != res.offset) || (uncompressedSize != res.uncompressedSize))
+ throw Common::Exception("Resource metadata mismatch (%u, %u != %u, %u)", offset, uncompressedSize, res.offset, res.uncompressedSize);
std::unique_ptr<byte[]> data = std::make_unique<byte[]>(res.uncompressedSize);
- size_t offset = 0;
- size_t bytesLeft = res.uncompressedSize;
+ size_t readLeft = res.compressedSize - 16;
+ size_t writeLeft = res.uncompressedSize;
+ size_t writeOffset = 0;
- while (bytesLeft > 0) {
- const size_t bytesChunk =
- Common::decompressDeflateChunk(*_obb, Common::kWindowBitsMax,
- data.get() + offset, bytesLeft, 4096);
+ while (readLeft > 0) {
+ const size_t readSize = std::min<size_t>(readLeft, 4096);
+ const size_t bytesChunk = Common::decompressDeflateChunk(*_obb, Common::kWindowBitsMax, data.get() + writeOffset, writeLeft, readSize);
- offset += bytesChunk;
- bytesLeft -= bytesChunk;
+ readLeft -= readSize;
+ writeLeft -= bytesChunk;
+ writeOffset += bytesChunk;
}
+ if (writeLeft > 0)
+ throw Common::Exception("Exhausted input, but didn't get full output");
+
return new Common::MemoryReadStream(data.release(), res.uncompressedSize, true);
}
diff --git a/src/aurora/types.h b/src/aurora/types.h
index baad301..93d1e6b 100644
--- a/src/aurora/types.h
+++ b/src/aurora/types.h
@@ -388,6 +388,12 @@ enum FileType {
kFileTypeXDS = 30000, ///< Texture.
kFileTypeWND = 30001,
+ // Found in the Android version of Knights of the Old Republic 2
+ kFileTypeVERT = 31000, ///< Vertex shader.
+ kFileTypeFRAG = 31001, ///< Fragment shader.
+ kFileTypeGLSL = 31002, ///< OpenGL shader source.
+ kFileTypeTLK_CONTROL = 31003, ///< Talk table for extra control strings, plain text.
+
// Our own types
kFileTypeXEOSITEX = 40000 ///< Intermediate texture.
};
diff --git a/src/aurora/util.cpp b/src/aurora/util.cpp
index 660c8a4..6683af7 100644
--- a/src/aurora/util.cpp
+++ b/src/aurora/util.cpp
@@ -348,6 +348,11 @@ const FileTypeManager::Type FileTypeManager::types[] = {
{kFileTypeXDS, ".xds"},
{kFileTypeWND, ".wnd"},
+ {kFileTypeVERT, ".vert"},
+ {kFileTypeFRAG, ".frag"},
+ {kFileTypeGLSL, ".glsl"},
+ {kFileTypeTLK_CONTROL, ".tlk_control"},
+
{kFileTypeXEOSITEX, ".xoreositex"}
};
diff --git a/src/version/version.cpp b/src/version/version.cpp
index 7835160..68ca5aa 100644
--- a/src/version/version.cpp
+++ b/src/version/version.cpp
@@ -62,6 +62,11 @@
#endif
// Distributions may append an extra version string
+#ifdef XOREOS_DISTRO
+ #undef XOREOS_REV
+#endif
+#define XOREOS_DISTRO "KOTOR2_ANDROID_UNOBB"
+
#ifdef XOREOS_DISTRO
#undef XOREOS_REV
#define XOREOS_REV XOREOS_DISTRO
@@ -81,7 +86,10 @@ static const char *kProjectAuthors =
"Please see the AUTHORS file for details.\n"
"\n"
"This is free software; see the source for copying conditions. There is NO\n"
- "warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.";
+ "warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n"
+ "\n"
+ "NOTE: This is a modified and experimental version of xoreos-tools to read\n"
+ " unobb archives found in the Android port of KotOR2.";
const char *getProjectName() {
return kProjectName;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment