2024-01-25 00:04:39 +01:00
|
|
|
//=============================================================================//
|
|
|
|
//
|
|
|
|
// Purpose: buffered pak encoder
|
|
|
|
//
|
|
|
|
//=============================================================================//
|
2024-01-27 01:38:19 +01:00
|
|
|
#include "tier0/binstream.h"
|
2024-01-25 00:04:39 +01:00
|
|
|
#include "thirdparty/zstd/zstd.h"
|
|
|
|
#include "rtech/ipakfile.h"
|
2024-01-27 01:38:19 +01:00
|
|
|
#include "paktools.h"
|
2024-01-25 00:04:39 +01:00
|
|
|
#include "pakencode.h"
|
|
|
|
|
|
|
|
//-----------------------------------------------------------------------------
|
|
|
|
// encodes the pak file from buffer, we can't do streamed compression as we
|
|
|
|
// need to know the actual decompress size ahead of time, else the runtime will
|
|
|
|
// fail as we wouldn't be able to parse the decompressed size from the frame
|
|
|
|
// header
|
|
|
|
//-----------------------------------------------------------------------------
|
2024-01-27 01:38:19 +01:00
|
|
|
bool Pak_BufferToBufferEncode(const uint8_t* const inBuf, const uint64_t inLen,
|
2024-01-25 00:04:39 +01:00
|
|
|
uint8_t* const outBuf, const uint64_t outLen, const int level)
|
|
|
|
{
|
|
|
|
// offset to the actual pak data, the main file header shouldn't be
|
|
|
|
// compressed
|
|
|
|
const size_t dataOffset = sizeof(PakFileHeader_t);
|
|
|
|
|
|
|
|
const size_t compressSize = ZSTD_compress(
|
|
|
|
outBuf + dataOffset,
|
|
|
|
outLen - dataOffset,
|
|
|
|
inBuf + dataOffset,
|
|
|
|
inLen - dataOffset,
|
|
|
|
level);
|
|
|
|
|
|
|
|
if (ZSTD_isError(compressSize))
|
|
|
|
return false;
|
|
|
|
|
|
|
|
PakFileHeader_t* const outHeader = reinterpret_cast<PakFileHeader_t* const>(outBuf);
|
|
|
|
|
|
|
|
// the compressed size includes the entire buffer, even the data we didn't
|
|
|
|
// compress like the file header
|
|
|
|
outHeader->compressedSize = compressSize + dataOffset;
|
|
|
|
|
|
|
|
// these flags are required for the game's runtime to decide whether or not to
|
|
|
|
// decompress the pak, and how; see Pak_ProcessPakFile() for more details
|
|
|
|
outHeader->flags |= PAK_HEADER_FLAGS_COMPRESSED;
|
2024-01-27 01:45:57 +01:00
|
|
|
outHeader->flags |= PAK_HEADER_FLAGS_ZSTREAM;
|
2024-01-25 00:04:39 +01:00
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2024-01-27 01:38:19 +01:00
|
|
|
//-----------------------------------------------------------------------------
|
|
|
|
// searches for pak patches and updates all referenced files in patch headers
|
|
|
|
//-----------------------------------------------------------------------------
|
|
|
|
bool Pak_UpdatePatchHeaders(uint8_t* const inBuf, const char* const outPakFile)
|
|
|
|
{
|
|
|
|
// file name without extension and patch number ID
|
|
|
|
char baseFilePath[MAX_PATH];
|
|
|
|
snprintf(baseFilePath, sizeof(baseFilePath), "%s", outPakFile);
|
|
|
|
|
|
|
|
const char* const fileNameUnqualified = V_UnqualifiedFileName(baseFilePath);
|
|
|
|
|
|
|
|
V_StripExtension(baseFilePath, baseFilePath, sizeof(baseFilePath));
|
|
|
|
|
|
|
|
// strip the patch id, but make sure we only do this on the file name
|
|
|
|
// and not the path
|
|
|
|
char* const patchIdentifier = strrchr(baseFilePath, '(');
|
|
|
|
|
|
|
|
if (patchIdentifier && patchIdentifier > fileNameUnqualified)
|
|
|
|
*patchIdentifier = '\0';
|
|
|
|
|
|
|
|
// NOTE: we modify the in buffer as the patch headers belong to the
|
|
|
|
// compressed section
|
|
|
|
PakFileHeader_t* const inHeader = reinterpret_cast<PakFileHeader_t* const>(inBuf);
|
|
|
|
|
|
|
|
// update each patch header
|
|
|
|
for (uint16_t i = 0; i < inHeader->patchIndex; i++)
|
|
|
|
{
|
|
|
|
short patchNumber = Pak_GetPatchNumberForIndex(inHeader, i);
|
|
|
|
char patchFile[MAX_PATH];
|
|
|
|
|
|
|
|
// the first patch number does not have an identifier in its name
|
|
|
|
if (patchNumber == 0)
|
|
|
|
snprintf(patchFile, sizeof(patchFile), "%s.rpak", baseFilePath);
|
|
|
|
else
|
|
|
|
snprintf(patchFile, sizeof(patchFile), "%s(%02u).rpak", baseFilePath, patchNumber);
|
|
|
|
|
|
|
|
CIOStream inPatch;
|
|
|
|
|
|
|
|
// unable to open patch while there should be one, we must calculate
|
|
|
|
// new file sizes here, or else the runtime would fail to load them
|
|
|
|
if (!inPatch.Open(patchFile, CIOStream::READ | CIOStream::BINARY))
|
|
|
|
return false;
|
|
|
|
|
|
|
|
const size_t fileSize = inPatch.GetSize();
|
|
|
|
|
|
|
|
// pak appears truncated
|
|
|
|
if (fileSize <= sizeof(PakFileHeader_t))
|
|
|
|
return false;
|
|
|
|
|
|
|
|
DevMsg(eDLL_T::RTECH, "%s: updating patch header for pak '%s', new size = %zu\n",
|
|
|
|
__FUNCTION__, patchFile, fileSize);
|
|
|
|
|
|
|
|
PakPatchFileHeader_t* const patchHeader = Pak_GetPatchFileHeader(inHeader, i);
|
|
|
|
patchHeader->m_sizeDisk = fileSize;
|
|
|
|
}
|
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2024-01-25 00:04:39 +01:00
|
|
|
//-----------------------------------------------------------------------------
|
|
|
|
// encodes the pak file from file name
|
|
|
|
//-----------------------------------------------------------------------------
|
|
|
|
bool Pak_EncodePakFile(const char* const inPakFile, const char* const outPakFile, const int level)
|
|
|
|
{
|
2024-01-27 01:38:19 +01:00
|
|
|
CIOStream inFile;
|
2024-01-25 00:04:39 +01:00
|
|
|
|
2024-01-27 01:38:19 +01:00
|
|
|
// failed to open
|
|
|
|
if (!inFile.Open(inPakFile, CIOStream::READ | CIOStream::BINARY))
|
2024-01-25 00:04:39 +01:00
|
|
|
return false;
|
|
|
|
|
2024-01-27 01:38:19 +01:00
|
|
|
const ssize_t fileSize = inFile.GetSize();
|
2024-01-25 00:04:39 +01:00
|
|
|
|
2024-01-27 01:38:19 +01:00
|
|
|
// file appears truncated
|
2024-01-25 00:04:39 +01:00
|
|
|
if (fileSize <= sizeof(PakFileHeader_t))
|
|
|
|
return false;
|
|
|
|
|
|
|
|
std::unique_ptr<uint8_t[]> pakBuf(new uint8_t[fileSize]);
|
|
|
|
uint8_t* const pPakBuf = pakBuf.get();
|
|
|
|
|
2024-01-27 01:38:19 +01:00
|
|
|
inFile.Read(pPakBuf, fileSize);
|
|
|
|
inFile.Close();
|
2024-01-25 00:04:39 +01:00
|
|
|
|
2024-01-27 01:38:19 +01:00
|
|
|
const PakFileHeader_t* inHeader = reinterpret_cast<PakFileHeader_t* const>(pPakBuf);
|
2024-01-25 00:04:39 +01:00
|
|
|
|
|
|
|
// incompatible pak, or not a pak
|
2024-01-27 01:38:19 +01:00
|
|
|
if (inHeader->magic != PAK_HEADER_MAGIC || inHeader->version != PAK_HEADER_VERSION)
|
2024-01-25 00:04:39 +01:00
|
|
|
return false;
|
|
|
|
|
|
|
|
// already compressed
|
2024-01-27 01:38:19 +01:00
|
|
|
if (inHeader->IsCompressed())
|
2024-01-25 00:04:39 +01:00
|
|
|
return false;
|
|
|
|
|
2024-01-27 01:38:19 +01:00
|
|
|
if (inHeader->patchIndex && !Pak_UpdatePatchHeaders(pPakBuf, outPakFile))
|
|
|
|
{
|
|
|
|
// pak has patches but one or more weren't found; the headers need to
|
|
|
|
// be updated with new compression sizes since the runtime uses them to
|
|
|
|
// check for truncation
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
std::unique_ptr<uint8_t[]> outBuf(new uint8_t[inHeader->decompressedSize]);
|
2024-01-25 00:04:39 +01:00
|
|
|
uint8_t* const pOutBuf = outBuf.get();
|
|
|
|
|
2024-01-27 01:38:19 +01:00
|
|
|
PakFileHeader_t* const outHeader = reinterpret_cast<PakFileHeader_t* const>(pOutBuf);
|
|
|
|
|
|
|
|
// copy the header over
|
|
|
|
*outHeader = *inHeader;
|
|
|
|
|
2024-01-25 00:04:39 +01:00
|
|
|
// encoding failed
|
2024-01-27 01:38:19 +01:00
|
|
|
if (!Pak_BufferToBufferEncode(pPakBuf, fileSize, pOutBuf, inHeader->decompressedSize, level))
|
2024-01-25 00:04:39 +01:00
|
|
|
return false;
|
|
|
|
|
2024-01-27 01:38:19 +01:00
|
|
|
CIOStream outFile;
|
2024-01-25 00:04:39 +01:00
|
|
|
|
|
|
|
// failure to open output file
|
2024-01-27 01:38:19 +01:00
|
|
|
if (!outFile.Open(outPakFile, CIOStream::WRITE | CIOStream::BINARY))
|
2024-01-25 00:04:39 +01:00
|
|
|
return false;
|
|
|
|
|
|
|
|
const PakFileHeader_t* outPakHeader = reinterpret_cast<PakFileHeader_t* const>(pOutBuf);
|
|
|
|
|
|
|
|
// this will be true if the entire buffer has been written
|
2024-01-27 01:38:19 +01:00
|
|
|
outFile.Write(pOutBuf, outPakHeader->compressedSize);
|
|
|
|
return true;
|
2024-01-25 00:04:39 +01:00
|
|
|
}
|