Files
TrinityCore/dep/CascLib/src/CascRootFile_OW.cpp

413 lines
14 KiB
C++

/*****************************************************************************/
/* CascRootFile_Text.cpp Copyright (c) Ladislav Zezula 2017 */
/*---------------------------------------------------------------------------*/
/* Support for loading ROOT files in plain text */
/*---------------------------------------------------------------------------*/
/* Date Ver Who Comment */
/* -------- ---- --- ------- */
/* 28.10.15 1.00 Lad The first version of CascRootFile_Text.cpp */
/*****************************************************************************/
#define __CASCLIB_SELF__
#include "CascLib.h"
#include "CascCommon.h"
// Implemented in "overwatch/apm.cpp"
DWORD LoadApplicationPackageManifestFile(TCascStorage * hs, CASC_FILE_TREE & FileTree, PCASC_CKEY_ENTRY pCKeyEntry, const char * szApmFileName);
// Implemented in "overwatch/cmf.cpp"
DWORD LoadContentManifestFile(TCascStorage * hs, CASC_FILE_TREE & FileTree, PCASC_CKEY_ENTRY pCKeyEntry, const char * szFileName);
//-----------------------------------------------------------------------------
// Structure definitions for APM files
typedef struct _APM_HEADER_V3
{
ULONGLONG BuildNumber; // Build number of the game
ULONGLONG ZeroValue1;
DWORD ZeroValue2;
DWORD PackageCount;
DWORD ZeroValue3;
DWORD EntryCount;
DWORD Checksum;
// Followed by the array of APM_ENTRY (count is in "EntryCount")
// Followed by the array of APM_PACKAGE (count is in "PackageCount")
} APM_HEADER_V3, *PAPM_HEADER_V3;
typedef struct _APM_HEADER_V2
{
ULONGLONG BuildNumber; // Build number of the game
ULONGLONG ZeroValue1;
DWORD PackageCount;
DWORD ZeroValue2;
DWORD EntryCount;
DWORD Checksum;
// Followed by the array of APM_ENTRY (count is in "EntryCount")
// Followed by the array of APM_PACKAGE (count is in "PackageCount")
} APM_HEADER_V2, *PAPM_HEADER_V2;
typedef struct _APM_HEADER_V1
{
ULONGLONG BuildNumber; // Build number of the game
DWORD BuildVersion;
DWORD PackageCount;
DWORD EntryCount;
DWORD Checksum;
// Followed by the array of APM_ENTRY (count is in "EntryCount")
// Followed by the array of APM_PACKAGE (count is in "PackageCount")
} APM_HEADER_V1, *PAPM_HEADER_V1;
// On-disk format, size = 0x0C
typedef struct _APM_ENTRY_V1
{
DWORD Index;
DWORD HashA_Lo; // Must split the hashes in order to make this structure properly aligned
DWORD HashA_Hi;
} APM_ENTRY_V1, *PAPM_ENTRY_V1;
// On-disk format, size = 0x14
typedef struct _APM_ENTRY_V2
{
DWORD Index;
DWORD HashA_Lo; // Must split the hashes in order to make this structure properly aligned
DWORD HashA_Hi;
DWORD HashB_Lo;
DWORD HashB_Hi;
} APM_ENTRY_V2, *PAPM_ENTRY_V2;
// On-disk format
typedef struct _APM_PACKAGE_ENTRY_V1
{
ULONGLONG EntryPointGUID; // virtual most likely
ULONGLONG PrimaryGUID; // real
ULONGLONG SecondaryGUID; // real
ULONGLONG Key; // encryption
ULONGLONG PackageGUID; // 077 file
ULONGLONG Unknown1;
DWORD Unknown2;
} APM_PACKAGE_ENTRY_V1, *PAPM_PACKAGE_ENTRY_V1;
// On-disk format
typedef struct _APM_PACKAGE_ENTRY_V2
{
ULONGLONG PackageGUID; // 077 file
ULONGLONG Unknown1;
DWORD Unknown2;
DWORD Unknown3;
ULONGLONG Unknown4;
} APM_PACKAGE_ENTRY_V2, *PAPM_PACKAGE_ENTRY_V2;
//-----------------------------------------------------------------------------
// Local functions (non-class)
static bool IsManifestFolderName(const char * szFileName, const char * szManifestFolder, size_t nLength)
{
if(!_strnicmp(szFileName, szManifestFolder, nLength))
{
return (szFileName[nLength] == '\\' || szFileName[nLength] == '/');
}
return false;
}
//-----------------------------------------------------------------------------
// Public functions (non-class)
static void BinaryReverse64(LPBYTE GuidReversed, LPBYTE pbGuid)
{
GuidReversed[0] = pbGuid[7];
GuidReversed[1] = pbGuid[6];
GuidReversed[2] = pbGuid[5];
GuidReversed[3] = pbGuid[4];
GuidReversed[4] = pbGuid[3];
GuidReversed[5] = pbGuid[2];
GuidReversed[6] = pbGuid[1];
GuidReversed[7] = pbGuid[0];
}
static const char * ExtractAssetSubString(char * szBuffer, size_t ccBuffer, const char * szPlainName)
{
char * szBufferEnd = szBuffer + ccBuffer - 1;
while(szBuffer < szBufferEnd && szPlainName[0] != 0 && szPlainName[0] != '.' && szPlainName[0] != '_')
*szBuffer++ = *szPlainName++;
if(szBuffer <= szBufferEnd)
szBuffer[0] = 0;
return szPlainName;
}
static const char * AppendAssetSubString(char * szBuffer, size_t ccBuffer, const char * szPlainName)
{
char * szBufferPtr = szBuffer + strlen(szBuffer);
char * szBufferEnd = szBuffer + ccBuffer - 1;
if(szBufferPtr < szBufferEnd)
*szBufferPtr++ = '-';
while(szBufferPtr < szBufferEnd && szPlainName[0] != '_')
*szBufferPtr++ = *szPlainName++;
szBufferPtr[0] = 0;
return szPlainName;
}
size_t BuildAssetFileNameTemplate(
char * szNameTemplate,
size_t ccNameTemplate,
const char * szPrefix,
const char * szAssetName)
{
const char * szFileName = "0000000000000000"; // Base name for 64-bit GUID
const char * szFileExt = NULL;
char * szBufferEnd = szNameTemplate + ccNameTemplate;
char * szBufferPtr = szNameTemplate;
char * szPlainName;
char szPlatform[64] = {0};
char szLocale[64] = {0};
char szAsset[64] = {0};
// Parse the plain name
while(szAssetName[0] != '.')
{
// Watch start of the new field
if(szAssetName[0] == '_')
{
// Extract platform from "_SP"
if(szAssetName[1] == 'S' && szAssetName[2] == 'P' && !_strnicmp(szAssetName, "_SPWin_", 7))
{
CascStrCopy(szPlatform, _countof(szPlatform), "Windows");
szAssetName += 6;
continue;
}
// Extract "RDEV" or "RCN"
if(szAssetName[1] == 'R')
{
szAssetName = AppendAssetSubString(szPlatform, _countof(szPlatform), szAssetName + 1);
continue;
}
// Extract locale
if(szAssetName[1] == 'L')
{
szAssetName = ExtractAssetSubString(szLocale, _countof(szLocale), szAssetName + 2);
continue;
}
// Ignore "_EExt"
if(szAssetName[1] == 'E' && szAssetName[2] == 'E')
{
szAssetName += 5;
continue;
}
// Extract the asset name
szAssetName = ExtractAssetSubString(szAsset, _countof(szAsset), szAssetName + 1);
// Extract a possible extension
//if(!_stricmp(szAsset, "speech"))
// szFileExt = ".wav";
//if(!_stricmp(szAsset, "text"))
// szFileExt = ".text";
continue;
}
szAssetName++;
}
// Combine the path like "%PREFIX%\\%PLATFORM%-%DEV%\\%LOCALE%\\%ASSET%\\%PLAIN_NAME%.%EXTENSSION%"
if(szPrefix && szPrefix[0])
szBufferPtr += CascStrPrintf(szBufferPtr, (szBufferEnd - szBufferPtr), "%s\\", szPrefix);
if(szPlatform[0])
szBufferPtr += CascStrPrintf(szBufferPtr, (szBufferEnd - szBufferPtr), "%s\\", szPlatform);
if(szLocale[0])
szBufferPtr += CascStrPrintf(szBufferPtr, (szBufferEnd - szBufferPtr), "%s\\", szLocale);
if(szAsset[0])
szBufferPtr += CascStrPrintf(szBufferPtr, (szBufferEnd - szBufferPtr), "%s\\", szAsset);
szPlainName = szBufferPtr;
// Append file name and extension
if(szFileName && szFileName[0])
szBufferPtr += CascStrPrintf(szBufferPtr, (szBufferEnd - szBufferPtr), "%s", szFileName);
if(szFileExt && szFileExt[0])
CascStrPrintf(szBufferPtr, (szBufferEnd - szBufferPtr), "%s", szFileExt);
// Return the length of the path
return (szPlainName - szNameTemplate);
}
DWORD InsertAssetFile(
TCascStorage * hs,
CASC_FILE_TREE & FileTree,
char * szFileName,
size_t nPlainName, // Offset of the plain name in the name template
LPBYTE pbCKey,
LPBYTE pbGuid)
{
PCASC_CKEY_ENTRY pCKeyEntry;
DWORD dwErrCode = ERROR_SUCCESS;
BYTE GuidReversed[8];
// Try to find the CKey
if((pCKeyEntry = FindCKeyEntry_CKey(hs, pbCKey)) != NULL)
{
// Save the character at the end of the name (dot or EOS)
char chSaveChar = szFileName[nPlainName + 16];
// Imprint the GUID as binary value
BinaryReverse64(GuidReversed, pbGuid);
StringFromBinary(GuidReversed, sizeof(GuidReversed), szFileName + nPlainName);
szFileName[nPlainName + 16] = chSaveChar;
// Insert the asset to the file tree
if(FileTree.InsertByName(pCKeyEntry, szFileName) == NULL)
dwErrCode = ERROR_NOT_ENOUGH_MEMORY;
}
return dwErrCode;
}
//-----------------------------------------------------------------------------
// Handler definition for OVERWATCH root file
//
// -------------------------------------
// Overwatch ROOT file (build 24919):
// -------------------------------------
// #MD5|CHUNK_ID|FILENAME|INSTALLPATH
// FE3AD8A77EEF77B383DF4929AED816FD|0|RetailClient/GameClientApp.exe|GameClientApp.exe
// 5EDDEFECA544B6472C5CD52BE63BC02F|0|RetailClient/Overwatch Launcher.exe|Overwatch Launcher.exe
// 6DE09F0A67F33F874F2DD8E2AA3B7AAC|0|RetailClient/ca-bundle.crt|ca-bundle.crt
// 99FE9EB6A4BB20209202F8C7884859D9|0|RetailClient/ortp_x64.dll|ortp_x64.dll
//
// -------------------------------------
// Overwatch ROOT file (build 47161):
// -------------------------------------
// #FILEID|MD5|CHUNK_ID|PRIORITY|MPRIORITY|FILENAME|INSTALLPATH
// RetailClient/Overwatch.exe|807F96661280C07E762A8C129FEBDA6F|0|0|255|RetailClient/Overwatch.exe|Overwatch.exe
// RetailClient/Overwatch Launcher.exe|5EDDEFECA544B6472C5CD52BE63BC02F|0|0|255|RetailClient/Overwatch Launcher.exe|Overwatch Launcher.exe
// RetailClient/ortp_x64.dll|7D1B5DEC267480F3E8DAD6B95143A59C|0|0|255|RetailClient/ortp_x64.dll|ortp_x64.dll
//
struct TRootHandler_OW : public TFileTreeRoot
{
TRootHandler_OW() : TFileTreeRoot(0)
{
// We have file names and return CKey as result of search
dwFeatures |= (CASC_FEATURE_FILE_NAMES | CASC_FEATURE_ROOT_CKEY);
}
DWORD Load(TCascStorage * hs, CASC_CSV & Csv, size_t nFileNameIndex, size_t nCKeyIndex)
{
PCASC_CKEY_ENTRY pCKeyEntry;
size_t nFileCount;
DWORD dwErrCode = ERROR_SUCCESS;
BYTE CKey[MD5_HASH_SIZE];
CASCLIB_UNUSED(hs);
// Keep loading every line until there is something
while(Csv.LoadNextLine())
{
const CASC_CSV_COLUMN & FileName = Csv[CSV_ZERO][nFileNameIndex];
const CASC_CSV_COLUMN & CKeyStr = Csv[CSV_ZERO][nCKeyIndex];
// Retrieve the file name and the content key
if(FileName.szValue && CKeyStr.szValue && CKeyStr.nLength == MD5_STRING_SIZE)
{
// Convert the string CKey to binary
if(BinaryFromString(CKeyStr.szValue, MD5_STRING_SIZE, CKey) == ERROR_SUCCESS)
{
// Find the item in the tree
if((pCKeyEntry = FindCKeyEntry_CKey(hs, CKey)) != NULL)
{
// Insert the file name and the CKey into the tree
FileTree.InsertByName(pCKeyEntry, FileName.szValue);
}
}
}
}
// Get the total file count that we loaded so far
nFileCount = FileTree.GetCount();
// Parse Content Manifest Files (.cmf)
for(size_t i = 0; i < nFileCount && dwErrCode == ERROR_SUCCESS; i++)
{
PCASC_FILE_NODE pFileNode;
const char * szExtension;
char szFileName[MAX_PATH];
// Get the n-th file
pFileNode = (PCASC_FILE_NODE)FileTree.PathAt(szFileName, _countof(szFileName), i);
if(pFileNode != NULL)
{
if(IsManifestFolderName(szFileName, "Manifest", 8) || IsManifestFolderName(szFileName, "TactManifest", 12))
{
// Retrieve the file extension
szExtension = GetFileExtension(szFileName);
// Check for content manifest files
if(!_stricmp(szExtension, ".cmf"))
{
dwErrCode = LoadContentManifestFile(hs, FileTree, pFileNode->pCKeyEntry, szFileName);
}
else if(!_stricmp(szExtension, ".apm"))
{
dwErrCode = LoadApplicationPackageManifestFile(hs, FileTree, pFileNode->pCKeyEntry, szFileName);
}
}
}
}
return dwErrCode;
}
};
//-----------------------------------------------------------------------------
// Public functions
// TODO: There is way more files in the Overwatch CASC storage than present in the ROOT file.
DWORD RootHandler_CreateOverwatch(TCascStorage * hs, CASC_BLOB & RootFile)
{
TRootHandler_OW * pRootHandler = NULL;
CASC_CSV Csv(0, true);
size_t Indices[2];
DWORD dwErrCode;
// Load the ROOT file
dwErrCode = Csv.Load(RootFile.pbData, RootFile.cbData);
if(dwErrCode == ERROR_SUCCESS)
{
// Retrieve the indices of the file name and MD5 columns
Indices[0] = Csv.GetColumnIndex("FILENAME");
Indices[1] = Csv.GetColumnIndex("MD5");
// If both indices were found OK, then load the root file
if(Indices[0] != CSV_INVALID_INDEX && Indices[1] != CSV_INVALID_INDEX)
{
pRootHandler = new TRootHandler_OW();
if(pRootHandler != NULL)
{
// Load the root directory. If load failed, we free the object
dwErrCode = pRootHandler->Load(hs, Csv, Indices[0], Indices[1]);
if(dwErrCode != ERROR_SUCCESS)
{
delete pRootHandler;
pRootHandler = NULL;
}
}
}
else
{
dwErrCode = ERROR_BAD_FORMAT;
}
}
// Assign the root directory (or NULL) and return error
hs->pRootHandler = pRootHandler;
return dwErrCode;
}