1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
|
/*****************************************************************************/
/* 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;
}
|