mirror of
https://github.com/TrinityCore/TrinityCore.git
synced 2026-01-19 00:48:56 +01:00
413 lines
14 KiB
C++
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;
|
|
}
|