/*****************************************************************************/ /* CascRootFile_Diablo3.cpp Copyright (c) Ladislav Zezula 2015 */ /*---------------------------------------------------------------------------*/ /* Support for loading Diablo 3 ROOT file */ /* Note: D3 offsets refer to Diablo III.exe 2.2.0.30013 (32-bit) */ /* SHA1: e4f17eca8aad8dde70870bf932ac3f5b85f17a1f */ /*---------------------------------------------------------------------------*/ /* Date Ver Who Comment */ /* -------- ---- --- ------- */ /* 04.03.15 1.00 Lad The first version of CascRootFile_Diablo3.cpp */ /*****************************************************************************/ #define __CASCLIB_SELF__ #include "CascLib.h" #include "CascCommon.h" //----------------------------------------------------------------------------- // Local structures #define DIABLO3_SUBDIR_SIGNATURE 0xEAF1FE87 #define DIABLO3_PACKAGES_SIGNATURE 0xAABB0002 #define DIABLO3_MAX_SUBDIRS 0x20 #define DIABLO3_INVALID_INDEX 0xFFFFFFFF #define DIABLO3_INVALID_FILE 0xFFFFFFFF #define DIABLO3_MAX_ASSETS 70 // Maximum possible number of assets #define DIABLO3_MAX_LEVEL0_LENGTH 0x10 // Maximum length of the level-0 directory name #define INVALID_FILE_INDEX 0xFFFFFFFF #define INVALID_ASSET_INDEX 0xFF #define ENTRY_FLAG_DIRECTORY_ENTRY 0x80 // The file is actually a directory entry #define ENTRY_FLAG_PLAIN_NAME 0x01 // If set, the file entry contains offset of the plain file name #define ENTRY_FLAG_FULL_NAME 0x02 // If set, the file entry contains offset of the full name #define ENTRY_FLAG_FLAGS_MASK 0xF0 // Mask for the entry flags #define ENTRY_FLAG_NAME_MASK 0x0F // Mask for the entry file name type // Values for CASC_FILE_ENTRY::dwFlags #define CASC_ENTRY_SHORT_NAME 0x000000001 // If set, the name is in format XXYYplain-name[\sub-index].ext #define CASC_ENTRY_HAS_SUBINDEX 0x000000002 // If set, the subitem is present in the file name (i.e. XXYYplain-name\sub-index.ext) #define SEARCH_PHASE_NAMES 0 // Searching named entry #define SEARCH_PHASE_FILE_IDS 1 // Searching filed by ID // Macro for constructing 64-bit integer from root-index, file-index and sub-index // The result value is RRAAAAAAAASSSSSS #define MAKE_INDEX64(ri, fi, si) (((ULONGLONG)ri << 0x38) | ((ULONGLONG)fi << 0x18) | ((ULONGLONG)si)) #define INDEX64_ROOT_INDEX(hash) (DWORD)((hash >> 0x38) & 0x000000FF) #define INDEX64_FILE_INDEX(hash) (DWORD)((hash >> 0x18) & 0xFFFFFFFF) #define INDEX64_SUB_INDEX(hash) (DWORD)((hash >> 0x00) & 0x00FFFFFF) // On-disk structure for a file given by file number typedef struct _DIABLO3_FILEID1_ENTRY { ENCODING_KEY EncodingKey; // Encoding key for the file DWORD FileIndex; // File index } DIABLO3_FILEID1_ENTRY, *PDIABLO3_FILEID1_ENTRY; // On-disk structure for a file given by file number and suffix typedef struct _DIABLO3_FILEID2_ENTRY { ENCODING_KEY EncodingKey; // Encoding key for the file DWORD FileIndex; // File index DWORD SubIndex; // File subindex, like "SoundBank\3D Ambience\0000.smp" } DIABLO3_FILEID2_ENTRY, *PDIABLO3_FILEID2_ENTRY; // On-disk structure of the named entry typedef struct _DIABLO3_NAMED_ENTRY { ENCODING_KEY EncodingKey; // Encoding key for the file BYTE szFileName[1]; // ASCIIZ file name (variable length) } DIABLO3_NAMED_ENTRY, *PDIABLO3_NAMED_ENTRY; // On-disk structure of CoreToc.dat header typedef struct _DIABLO3_CORE_TOC_HEADER { DWORD EntryCounts[DIABLO3_MAX_ASSETS]; // Array of number of entries (files) for each asset (level-1 directory) DWORD EntryOffsets[DIABLO3_MAX_ASSETS]; // Array of offsets of each DIABLO3_CORE_TOC_ENTRY, relative to data after header DWORD Unknowns[DIABLO3_MAX_ASSETS]; // Unknown DWORD Alignment; } DIABLO3_CORE_TOC_HEADER, *PDIABLO3_CORE_TOC_HEADER; // On-disk structure of the entry in CoreToc.dat typedef struct _DIABLO3_CORE_TOC_ENTRY { DWORD AssetIndex; // Index of the Diablo3 asset (aka directory) DWORD FileIndex; // File index DWORD NameOffset; // Offset of the plain file name } DIABLO3_CORE_TOC_ENTRY, *PDIABLO3_CORE_TOC_ENTRY; // In-memory structure of parsed directory header typedef struct _DIABLO3_DIR_HEADER { LPBYTE pbEntries1; LPBYTE pbEntries2; LPBYTE pbEntries3; DWORD dwEntries1; DWORD dwEntries2; DWORD dwEntries3; } DIABLO3_DIR_HEADER, *PDIABLO3_DIR_HEADER; // In-memory structure of loaded CoreTOC.dat typedef struct _DIABLO3_CORE_TOC { DIABLO3_CORE_TOC_HEADER Hdr; // Header of CoreTOC.dat LPBYTE pbCoreToc; // Content of the CoreTOC.dat file DIABLO3_CORE_TOC_ENTRY Entries[1]; // Buffer for storing the entries (variable length) } DIABLO3_CORE_TOC, *PDIABLO3_CORE_TOC; // On-disk structure of Packages.dat header typedef struct _DIABLO3_PACKAGES_DAT_HEADER { DWORD Signature; DWORD NumberOfNames; } DIABLO3_PACKAGES_DAT_HEADER, *PDIABLO3_PACKAGES_DAT_HEADER; // Structure for conversion DirectoryID -> Directory name typedef struct _DIABLO3_ASSET_INFO { const char * szDirectoryName; // Directory name const char * szExtension; } DIABLO3_ASSET_INFO; typedef const DIABLO3_ASSET_INFO * PDIABLO3_ASSET_INFO; // In-memory structure of a file entry in the linear file list typedef struct _CASC_FILE_ENTRY { ENCODING_KEY EncodingKey; // Encoding key ULONGLONG FileNameHash; // Hash of the full file name DWORD dwFileName; // Offset of the name (in name's dynamic array) DWORD dwFlags; // Entry flags (see CASC_ENTRY_XXXX) DWORD NameOffset; // Offset of the name (in name's dynamic array) USHORT SubIndex; // File\SubFile index BYTE AssetIndex; // Asset index (aka directory index) BYTE EntryFlags; // Entry flags } CASC_FILE_ENTRY, *PCASC_FILE_ENTRY; //----------------------------------------------------------------------------- // Structure definitions for Diablo3 root file struct TRootHandler_Diablo3 : public TRootHandler { // Linear global list of all files DYNAMIC_ARRAY FileTable; // Linear global list of names DYNAMIC_ARRAY FileNames; // Global map of FileName -> FileEntry PCASC_MAP pRootMap; }; //----------------------------------------------------------------------------- // Local variables static const DIABLO3_ASSET_INFO Assets[] = { // DIR-NAME EXTENSION // ========== ========= {NULL, NULL}, // 0x00 {"Actor", "acr"}, // 0x01 {"Adventure", "adv"}, // 0x02 {NULL, NULL}, // 0x03 {NULL, NULL}, // 0x04 {"AmbientSound", "ams"}, // 0x05 {"Anim", "ani"}, // 0x06 {"Anim2D", "an2"}, // 0x07 {"AnimSet", "ans"}, // 0x08 {"Appearance", "app"}, // 0x09 {NULL, NULL}, // 0x0A {"Cloth", "clt"}, // 0x0B {"Conversation", "cnv"}, // 0x0C {NULL, NULL}, // 0x0D {"EffectGroup", "efg"}, // 0x0E {"Encounter", "enc"}, // 0x0F {NULL, NULL}, // 0x10 {"Explosion", "xpl"}, // 0x11 {NULL, NULL}, // 0x12 {"Font", "fnt"}, // 0x13 {"GameBalance", "gam"}, // 0x14 {"Globals", "glo"}, // 0x15 {"LevelArea", "lvl"}, // 0x16 {"Light", "lit"}, // 0x17 {"MarkerSet", "mrk"}, // 0x18 {"Monster", "mon"}, // 0x19 {"Observer", "obs"}, // 0x1A {"Particle", "prt"}, // 0x1B {"Physics", "phy"}, // 0x1C {"Power", "pow"}, // 0x1D {NULL, NULL}, // 0x1E {"Quest", "qst"}, // 0x1F {"Rope", "rop"}, // 0x20 {"Scene", "scn"}, // 0x21 {"SceneGroup", "scg"}, // 0x22 {NULL, NULL}, // 0x23 {"ShaderMap", "shm"}, // 0x24 {"Shaders", "shd"}, // 0x25 {"Shakes", "shk"}, // 0x26 {"SkillKit", "skl"}, // 0x27 {"Sound", "snd"}, // 0x28 {"SoundBank", "sbk"}, // 0x29 {"StringList", "stl"}, // 0x2A {"Surface", "srf"}, // 0x2B {"Textures", "tex"}, // 0x2C {"Trail", "trl"}, // 0x2D {"UI", "ui"}, // 0x2E {"Weather", "wth"}, // 0x2F {"Worlds", "wrl"}, // 0x30 {"Recipe", "rcp"}, // 0x31 {NULL, NULL}, // 0x32 {"Condition", "cnd"}, // 0x33 {NULL, NULL}, // 0x34 {NULL, NULL}, // 0x35 {NULL, NULL}, // 0x36 {NULL, NULL}, // 0x37 {"Act", "act"}, // 0x38 {"Material", "mat"}, // 0x39 {"QuestRange", "qsr"}, // 0x3A {"Lore", "lor"}, // 0x3B {"Reverb", "rev"}, // 0x3C {"PhysMesh", "phm"}, // 0x3D {"Music", "mus"}, // 0x3E {"Tutorial", "tut"}, // 0x3F {"BossEncounter", "bos"}, // 0x40 {NULL, NULL}, // 0x41 {"Accolade", "aco"}, // 0x42 }; static const DIABLO3_ASSET_INFO UnknownAsset = {"Unknown", "unk"}; #define DIABLO3_ASSET_COUNT (sizeof(Assets) / sizeof(Assets[0])) //----------------------------------------------------------------------------- // Local functions static PDIABLO3_ASSET_INFO GetAssetInfo(DWORD dwAssetIndex) { if(dwAssetIndex < DIABLO3_ASSET_COUNT && Assets[dwAssetIndex].szDirectoryName != NULL) return &Assets[dwAssetIndex]; return &UnknownAsset; } static DWORD VerifyNamedFileEntry(LPBYTE pbNamedEntry, LPBYTE pbFileEnd) { LPBYTE pbFileName = ((PDIABLO3_NAMED_ENTRY)pbNamedEntry)->szFileName; // Find the end of the name while(pbFileName < pbFileEnd && pbFileName[0] != 0) pbFileName++; // Did we get past the end of the root file? if(pbFileName >= pbFileEnd) return 0; pbFileName++; // Return the length of the structure return (DWORD)(pbFileName - pbNamedEntry); } static char * FindPackageName( PCASC_MAP pPackageMap, const char * szAssetName, const char * szPlainName) { char szFileName[MAX_PATH+1]; size_t nLength; // Construct the name without extension and find it in the map nLength = sprintf(szFileName, "%s\\%s", szAssetName, szPlainName); return (char *)Map_FindString(pPackageMap, szFileName, szFileName + nLength); } static size_t CreateShortName( PCASC_MAP pPackageMap, DWORD dwRootIndex, // Level-0-dir: Index of the root subdirectory DWORD dwAssetIndex, // Level-1-dir: Index of the asset name const char * szPlainName, // Plain name of the file, without extension DWORD dwSubIndex, char * szBuffer) { PDIABLO3_ASSET_INFO pAssetInfo = GetAssetInfo(dwAssetIndex); const char * szPackageName = NULL; const char * szFormat; size_t nLength; // Write the level-0 directory index as 2-digit hexa number assert(dwRootIndex < 0x100); *szBuffer++ = IntToHexChar[dwRootIndex >> 0x04]; *szBuffer++ = IntToHexChar[dwRootIndex & 0x0F]; // Write the level-1 directory index as 2-digit hexa number assert(dwAssetIndex < 0x100); *szBuffer++ = IntToHexChar[dwAssetIndex >> 0x04]; *szBuffer++ = IntToHexChar[dwAssetIndex & 0x0F]; // Construct the file name with ending "." for extension szFormat = (dwSubIndex != DIABLO3_INVALID_INDEX) ? "%s\\%04u." : "%s."; nLength = sprintf(szBuffer, szFormat, szPlainName, dwSubIndex); // Try to fixup the file extension from the package name. // File extensions are not predictable because for subitems, // they are not always equal to the main items: // // SoundBank\3D Ambience.sbk // SoundBank\3D Ambience\0000.smp // SoundBank\3D Ambience\0002.smp // ... // SoundBank\Angel.sbk // SoundBank\Angel\0000.fsb // SoundBank\Angel\0002.fsb // // We use the Base\Data_D3\PC\Misc\Packages.dat for real file extensions, where possible // if(pPackageMap != NULL) { // Retrieve the asset name szPackageName = FindPackageName(pPackageMap, pAssetInfo->szDirectoryName, szBuffer); if(szPackageName != NULL) { strcpy(szBuffer, szPackageName + strlen(pAssetInfo->szDirectoryName) + 1); nLength = strlen(szBuffer); } } // If we havent't found the package, we either use the default asset extension or "unk" if(szPackageName == NULL) { if(dwSubIndex == DIABLO3_INVALID_INDEX) { strcpy(szBuffer + nLength, pAssetInfo->szExtension); nLength += strlen(pAssetInfo->szExtension); } else { strcpy(szBuffer + nLength, "unk"); nLength += 3; } } // Return the length of the short file name return nLength + 4; } static size_t CreateFileName( TRootHandler_Diablo3 * pRootHandler, const char * szShortName, // Short file name of the file char * szBuffer) { PCASC_FILE_ENTRY pRootEntry; const char * szNameLevel0; const char * szNameLevel1 = NULL; DWORD dwRootIndex0 = 0; DWORD dwAssetIndex = 0; // Retrieve the level-0 and level-1 directory indexes ConvertStringToInt08(szShortName+0, &dwRootIndex0); ConvertStringToInt08(szShortName+2, &dwAssetIndex); // Retrieve the name of the level-0 directory (aka root subdirectory) pRootEntry = (PCASC_FILE_ENTRY)Array_ItemAt(&pRootHandler->FileTable, dwRootIndex0); szNameLevel0 = (char *)Array_ItemAt(&pRootHandler->FileNames, pRootEntry->dwFileName); // Retrieve the name of the level-1 directory (aka asset name) if(dwAssetIndex < DIABLO3_ASSET_COUNT) szNameLevel1 = Assets[dwAssetIndex].szDirectoryName; if(szNameLevel1 == NULL) szNameLevel1 = UnknownAsset.szDirectoryName; // Copy the rest of the name as-is return sprintf(szBuffer, "%s\\%s\\%s", szNameLevel0, szNameLevel1, szShortName + 4); } // Creates a map of String -> Pointer static PCASC_MAP CreatePackageMap( LPBYTE pbPackagesDat, LPBYTE pbPackagesEnd) { PDIABLO3_PACKAGES_DAT_HEADER pDatHeader = (PDIABLO3_PACKAGES_DAT_HEADER)pbPackagesDat; PCASC_MAP pPackageMap; // Get the header if((pbPackagesDat + sizeof(DIABLO3_PACKAGES_DAT_HEADER)) >= pbPackagesEnd) return NULL; pbPackagesDat += sizeof(DIABLO3_PACKAGES_DAT_HEADER); // Check the signature and name count if(pDatHeader->Signature != DIABLO3_PACKAGES_SIGNATURE) return NULL; // Create the map for fast search of the file name pPackageMap = Map_Create(pDatHeader->NumberOfNames, KEY_LENGTH_STRING, 0); if(pPackageMap != NULL) { char * szFileName = (char *)pbPackagesDat; // Go as long as there is something for(DWORD i = 0; i < pDatHeader->NumberOfNames; i++) { // Get the file extension if((LPBYTE)szFileName >= pbPackagesEnd) break; // Insert the file name to the map. The file extension is not included Map_InsertString(pPackageMap, szFileName, true); szFileName = szFileName + strlen(szFileName) + 1; } } return pPackageMap; } // Insert an entry with file name as-is static int InsertFileEntry( TRootHandler_Diablo3 * pRootHandler, ENCODING_KEY & EncodingKey, const char * szFileName, size_t cchFileName) { PCASC_FILE_ENTRY pFileEntry; // We must not allow the file name array to be reallocated. // Reallocating the array would cause pointers in TRootHandler_Diablo3::pRootMap // become invalid if(pRootHandler->FileTable.ItemCount >= pRootHandler->FileTable.ItemCountMax) { assert(false); return ERROR_NOT_ENOUGH_MEMORY; } // Insert the plain name to the root handler's global name list szFileName = (const char *)Array_Insert(&pRootHandler->FileNames, szFileName, cchFileName); if(szFileName == NULL) return ERROR_NOT_ENOUGH_MEMORY; // Make sure that we don't exceed the file limit at this phase pFileEntry = (PCASC_FILE_ENTRY)Array_Insert(&pRootHandler->FileTable, NULL, 1); assert(pFileEntry != NULL); // Store the info into the file entry pFileEntry->EncodingKey = EncodingKey; pFileEntry->FileNameHash = CalcFileNameHash(szFileName); pFileEntry->dwFileName = (DWORD)Array_IndexOf(&pRootHandler->FileNames, szFileName); pFileEntry->dwFlags = 0; // Verify collisions (debug version only) assert(Map_FindObject(pRootHandler->pRootMap, &pFileEntry->FileNameHash, NULL) == NULL); // Calculate the file name hash Map_InsertObject(pRootHandler->pRootMap, pFileEntry, &pFileEntry->FileNameHash); // Success return ERROR_SUCCESS; } static int ParseDirEntries_FileId1( TRootHandler_Diablo3 * pRootHandler, LPBYTE pbFileEntries, DWORD dwFileEntries, DWORD dwRootDirIndex) { PDIABLO3_FILEID1_ENTRY pEntry = (PDIABLO3_FILEID1_ENTRY)pbFileEntries; PCASC_FILE_ENTRY pFileEntry; // Overflow test if((pRootHandler->FileTable.ItemCount + dwFileEntries) >= pRootHandler->FileTable.ItemCountMax) { assert(false); return ERROR_NOT_ENOUGH_MEMORY; } // Parse the all ID1 entries in the file for(DWORD i = 0; i < dwFileEntries; i++, pEntry++) { // Insert the file entry to the global list pFileEntry = (PCASC_FILE_ENTRY)Array_Insert(&pRootHandler->FileTable, NULL, 1); assert(pFileEntry != NULL); // Fill the index entry pFileEntry->EncodingKey = pEntry->EncodingKey; pFileEntry->FileNameHash = MAKE_INDEX64(dwRootDirIndex, pEntry->FileIndex, 0); pFileEntry->dwFlags = CASC_ENTRY_SHORT_NAME; } return ERROR_SUCCESS; } static int ParseDirEntries_FileId2( TRootHandler_Diablo3 * pRootHandler, LPBYTE pbFileEntries, DWORD dwFileEntries, DWORD dwRootDirIndex) { PDIABLO3_FILEID2_ENTRY pEntry = (PDIABLO3_FILEID2_ENTRY)pbFileEntries; PCASC_FILE_ENTRY pFileEntry; // Overflow test if((pRootHandler->FileTable.ItemCount + dwFileEntries) >= pRootHandler->FileTable.ItemCountMax) { assert(false); return ERROR_NOT_ENOUGH_MEMORY; } // Parse the all ID1 entries in the file for(DWORD i = 0; i < dwFileEntries; i++, pEntry++) { // Insert the file entry to the global list pFileEntry = (PCASC_FILE_ENTRY)Array_Insert(&pRootHandler->FileTable, NULL, 1); assert(pFileEntry != NULL); // Fill the index entry pFileEntry->EncodingKey = pEntry->EncodingKey; pFileEntry->FileNameHash = MAKE_INDEX64(dwRootDirIndex, pEntry->FileIndex, pEntry->SubIndex); pFileEntry->dwFlags = CASC_ENTRY_SHORT_NAME | CASC_ENTRY_HAS_SUBINDEX; } return ERROR_SUCCESS; } static int ParseDirEntries_Named( TRootHandler_Diablo3 * pRootHandler, LPBYTE pbFileEntries, LPBYTE pbFileEnd, DWORD dwFileEntries, DWORD dwRootDirIndex) { char szFileName[MAX_PATH+1]; char * szNamePtr = szFileName; DWORD cbFileEntry; int nError = ERROR_SUCCESS; // Overflow test if((pRootHandler->FileTable.ItemCount + dwFileEntries) >= pRootHandler->FileTable.ItemCountMax) { assert(false); return ERROR_NOT_ENOUGH_MEMORY; } // If we the file is not in the root directory itself, // prepare the prefix for the root directory. if(dwRootDirIndex != DIABLO3_INVALID_INDEX) { PCASC_FILE_ENTRY pRootEntry = (PCASC_FILE_ENTRY)Array_ItemAt(&pRootHandler->FileTable, dwRootDirIndex); const char * szRootName = (const char *)Array_ItemAt(&pRootHandler->FileNames, pRootEntry->dwFileName); // Copy the root directory name while(szRootName[0] != 0) *szNamePtr++ = *szRootName++; // Append the backslash *szNamePtr++ = '\\'; } // Parse the file entry while(pbFileEntries < pbFileEnd) { PDIABLO3_NAMED_ENTRY pNamedEntry = (PDIABLO3_NAMED_ENTRY)pbFileEntries; DWORD cchFileName; // Verify the named entry whether it does not go beyond the EOF cbFileEntry = VerifyNamedFileEntry(pbFileEntries, pbFileEnd); if(cbFileEntry == 0) return ERROR_FILE_CORRUPT; // Append the file name to the prepared file name // This way we obtain the full name and the name lookup // will be fully operational memcpy(szNamePtr, pNamedEntry->szFileName, (cbFileEntry - sizeof(ENCODING_KEY))); cchFileName = (DWORD)((szNamePtr - szFileName) + (cbFileEntry - sizeof(ENCODING_KEY))); // Insert the named entry to the global file table nError = InsertFileEntry(pRootHandler, pNamedEntry->EncodingKey, szFileName, cchFileName); if(nError != ERROR_SUCCESS) return nError; // Move the pointer to the next entry pbFileEntries += cbFileEntry; } return ERROR_SUCCESS; } static void ResolveFullFileNames( TRootHandler_Diablo3 * pRootHandler, PDIABLO3_CORE_TOC_ENTRY pCoreTocEntries, PCASC_MAP pPackageMap, LPBYTE pbCoreTocFile, DWORD dwFileIndexes) { PCASC_FILE_ENTRY pFileEntry; char * szPlainName; char * szNamePtr; size_t nLength; DWORD dwRootIndex; DWORD dwFileIndex; DWORD dwSubIndex; char szShortName[MAX_PATH+1]; char szFullName[MAX_PATH+1]; // Keep compiler happy CASCLIB_UNUSED(dwFileIndexes); // Parse the entire file table for(size_t i = 0; i < pRootHandler->FileTable.ItemCount; i++) { // Retrieve the file entry at n-th position pFileEntry = (PCASC_FILE_ENTRY)Array_ItemAt(&pRootHandler->FileTable, i); // Skip the items that already have full name if(pFileEntry->dwFlags & CASC_ENTRY_SHORT_NAME) { // Retrieve the file index of that file dwRootIndex = INDEX64_ROOT_INDEX(pFileEntry->FileNameHash); dwFileIndex = INDEX64_FILE_INDEX(pFileEntry->FileNameHash); dwSubIndex = (pFileEntry->dwFlags & CASC_ENTRY_HAS_SUBINDEX) ? INDEX64_SUB_INDEX(pFileEntry->FileNameHash) : DIABLO3_INVALID_INDEX; assert(dwFileIndex < dwFileIndexes); // Get the plain name of the file szPlainName = (char *)(pbCoreTocFile + pCoreTocEntries[dwFileIndex].NameOffset); // Create the short file name nLength = CreateShortName(pPackageMap, dwRootIndex, pCoreTocEntries[dwFileIndex].AssetIndex, szPlainName, dwSubIndex, szShortName); // Insert the short name to the list of the names szNamePtr = (char *)Array_Insert(&pRootHandler->FileNames, szShortName, nLength + 1); pFileEntry->dwFileName = (DWORD)Array_IndexOf(&pRootHandler->FileNames, szNamePtr); // Create the full file name nLength = CreateFileName(pRootHandler, szShortName, szFullName); pFileEntry->FileNameHash = CalcFileNameHash(szFullName); // Insert the entry to the name map. Use the mapping of FullName -> FileHash Map_InsertObject(pRootHandler->pRootMap, pFileEntry, &pFileEntry->FileNameHash); } } } static LPBYTE LoadFileToMemory(TCascStorage * hs, LPBYTE pbEncodingKey, DWORD * pcbFileData) { QUERY_KEY EncodingKey; LPBYTE pbFileData = NULL; HANDLE hFile; DWORD cbBytesRead = 0; DWORD cbFileData = 0; // Open the file by encoding key EncodingKey.pbData = pbEncodingKey; EncodingKey.cbData = MD5_HASH_SIZE; if(CascOpenFileByEncodingKey((HANDLE)hs, &EncodingKey, 0, &hFile)) { // Retrieve the file size cbFileData = CascGetFileSize(hFile, NULL); if(cbFileData > 0) { pbFileData = CASC_ALLOC(BYTE, cbFileData); if(pbFileData != NULL) { CascReadFile(hFile, pbFileData, cbFileData, &cbBytesRead); } } // Close the file CascCloseFile(hFile); } // Give the file to the caller if(pcbFileData != NULL) pcbFileData[0] = cbBytesRead; return pbFileData; } static LPBYTE LoadFileToMemory(TCascStorage * hs, const char * szFileName, DWORD * pcbFileData) { LPBYTE pbEncodingKey = NULL; LPBYTE pbFileData = NULL; // Try to find encoding key for the file pbEncodingKey = RootHandler_GetKey(hs->pRootHandler, szFileName); if(pbEncodingKey != NULL) pbFileData = LoadFileToMemory(hs, pbEncodingKey, pcbFileData); return pbFileData; } static int ParseDirectoryHeader( PDIABLO3_DIR_HEADER pDirHeader, LPBYTE pbDirFile, LPBYTE pbFileEnd) { DWORD dwSignature = 0; // // Structure of a Diablo3 directory file // 1) Signature (4 bytes) // 2) Number of DIABLO3_FILEID1_ENTRY entries (4 bytes) // 3) Array of DIABLO3_FILEID1_ENTRY entries // 4) Number of DIABLO3_FILEID2_ENTRY entries (4 bytes) // 5) Array of DIABLO3_FILEID2_ENTRY entries // 6) Number of DIABLO3_NAMED_ENTRY entries (4 bytes) // 7) Array of DIABLO3_NAMED_ENTRY entries // // Prepare the header signature memset(pDirHeader, 0, sizeof(DIABLO3_DIR_HEADER)); // Get the signature if((pbDirFile + sizeof(DWORD)) >= pbFileEnd) return ERROR_BAD_FORMAT; dwSignature = *(PDWORD)pbDirFile; // Check the signature if(dwSignature != CASC_DIABLO3_ROOT_SIGNATURE && dwSignature != DIABLO3_SUBDIR_SIGNATURE) return ERROR_BAD_FORMAT; pbDirFile += sizeof(DWORD); // Subdirectories have extra two arrays if(dwSignature == DIABLO3_SUBDIR_SIGNATURE) { // Get the number of DIABLO3_FILEID1_ENTRY items if((pbDirFile + sizeof(DWORD)) >= pbFileEnd) return ERROR_BAD_FORMAT; pDirHeader->dwEntries1 = *(PDWORD)pbDirFile; // Get the array of DIABLO3_FILEID1_ENTRY pDirHeader->pbEntries1 = (pbDirFile + sizeof(DWORD)); pbDirFile = pbDirFile + sizeof(DWORD) + pDirHeader->dwEntries1 * sizeof(DIABLO3_FILEID1_ENTRY); // Get the number of DIABLO3_FILEID2_ENTRY items if((pbDirFile + sizeof(DWORD)) >= pbFileEnd) return ERROR_BAD_FORMAT; pDirHeader->dwEntries2 = *(PDWORD)pbDirFile; // Get the array of DIABLO3_FILEID2_ENTRY pDirHeader->pbEntries2 = (pbDirFile + sizeof(DWORD)); pbDirFile = pbDirFile + sizeof(DWORD) + pDirHeader->dwEntries2 * sizeof(DIABLO3_FILEID2_ENTRY); } // Get the pointer and length DIABLO3_NAMED_ENTRY array if((pbDirFile + sizeof(DWORD)) >= pbFileEnd) return ERROR_BAD_FORMAT; pDirHeader->dwEntries3 = *(PDWORD)pbDirFile; pDirHeader->pbEntries3 = (pbDirFile + sizeof(DWORD)); return ERROR_SUCCESS; } static DWORD ScanDirectoryFile( TCascStorage * hs, LPBYTE pbRootFile, LPBYTE pbFileEnd) { PDIABLO3_NAMED_ENTRY pNamedEntry; DIABLO3_DIR_HEADER RootHeader; DIABLO3_DIR_HEADER DirHeader; LPBYTE pbSubDir; DWORD dwTotalFileCount; DWORD cbNamedEntry; DWORD cbSubDir; int nError; // Parse the directory header in order to retrieve the items nError = ParseDirectoryHeader(&RootHeader, pbRootFile, pbFileEnd); if(nError != ERROR_SUCCESS) return 0; // Add the root directory's entries dwTotalFileCount = RootHeader.dwEntries1 + RootHeader.dwEntries2 + RootHeader.dwEntries3; // Parse the named entries for(DWORD i = 0; i < RootHeader.dwEntries3; i++) { // Get the this named entry if((cbNamedEntry = VerifyNamedFileEntry(RootHeader.pbEntries3, pbFileEnd)) == 0) return 0; pNamedEntry = (PDIABLO3_NAMED_ENTRY)RootHeader.pbEntries3; RootHeader.pbEntries3 += cbNamedEntry; // Load the subdirectory to memory pbSubDir = LoadFileToMemory(hs, pNamedEntry->EncodingKey.Value, &cbSubDir); if(pbSubDir != NULL) { // Count the files in the subdirectory if(ParseDirectoryHeader(&DirHeader, pbSubDir, pbSubDir + cbSubDir) == ERROR_SUCCESS) { dwTotalFileCount += DirHeader.dwEntries1 + DirHeader.dwEntries2 + DirHeader.dwEntries3; } // Free the subdirectory CASC_FREE(pbSubDir); } } // Return the total number of entries return dwTotalFileCount; } static int ParseDirectoryFile( TRootHandler_Diablo3 * pRootHandler, LPBYTE pbDirFile, LPBYTE pbFileEnd, DWORD dwRootDirIndex) { DIABLO3_DIR_HEADER DirHeader; int nError; // Sanity checks assert(pRootHandler->FileTable.ItemArray != NULL); assert(pRootHandler->FileTable.ItemCount < pRootHandler->FileTable.ItemCountMax); // Parse the directory header in order to retrieve the items nError = ParseDirectoryHeader(&DirHeader, pbDirFile, pbFileEnd); if(nError != ERROR_SUCCESS) return nError; // Process all DIABLO3_FILEID1_ENTRY entries. These are for files // belonging to an asset group, without subitem number. // Example: "SoundBank\SoundFile.smp" // We skip inserting them to the name map, because the names are not known yet if(DirHeader.pbEntries1 && DirHeader.dwEntries1) { assert(dwRootDirIndex != DIABLO3_INVALID_INDEX); nError = ParseDirEntries_FileId1(pRootHandler, DirHeader.pbEntries1, DirHeader.dwEntries1, dwRootDirIndex); if(nError != ERROR_SUCCESS) return nError; } // Parse all DIABLO3_FILEID2_ENTRY entries. These are for files // belonging to an asset group, with a subitem number. // Example: "SoundBank\SoundFile\0001.smp" // We skip inserting them to the name map, because the names are not known yet if(DirHeader.pbEntries2 && DirHeader.dwEntries2) { assert(dwRootDirIndex != DIABLO3_INVALID_INDEX); nError = ParseDirEntries_FileId2(pRootHandler, DirHeader.pbEntries2, DirHeader.dwEntries2, dwRootDirIndex); if(nError != ERROR_SUCCESS) return nError; } // Parse all named entries. These are for files with arbitrary names, // and they do not belong to an asset. if(DirHeader.pbEntries3 && DirHeader.dwEntries3) { nError = ParseDirEntries_Named(pRootHandler, DirHeader.pbEntries3, pbFileEnd, DirHeader.dwEntries3, dwRootDirIndex); if(nError != ERROR_SUCCESS) return nError; } // Give the directory to the caller return nError; } static int ParseCoreTOC( TRootHandler_Diablo3 * pRootHandler, PCASC_MAP pPackageMap, LPBYTE pbCoreTocFile, LPBYTE pbCoreTocEnd) { PDIABLO3_CORE_TOC_HEADER pTocHeader; PDIABLO3_CORE_TOC_ENTRY pSortedEntries; PDIABLO3_CORE_TOC_ENTRY pTocEntry; LPBYTE pbCoreTocNames; DWORD dwFileIndexes = 0; DWORD i; // Check the space for header if((pbCoreTocFile + sizeof(DIABLO3_CORE_TOC_HEADER)) > pbCoreTocEnd) return ERROR_FILE_CORRUPT; pTocHeader = (PDIABLO3_CORE_TOC_HEADER)pbCoreTocFile; pbCoreTocFile += sizeof(DIABLO3_CORE_TOC_HEADER); // Calculate space needed for allocation for(i = 0; i < DIABLO3_MAX_ASSETS; i++) { // Get the first entry pTocEntry = (PDIABLO3_CORE_TOC_ENTRY)(pbCoreTocFile + pTocHeader->EntryOffsets[i]); // Find out the entry with the maximum index for(DWORD n = 0; n < pTocHeader->EntryCounts[i]; n++) { if(pTocEntry->FileIndex >= dwFileIndexes) dwFileIndexes = pTocEntry->FileIndex + 1; pTocEntry++; } } // Allocate and populate the array of DIABLO3_CORE_TOC_ENTRYs pSortedEntries = CASC_ALLOC(DIABLO3_CORE_TOC_ENTRY, dwFileIndexes); if(pSortedEntries != NULL) { // Initialize all entries to invalid memset(pSortedEntries, 0xFF, dwFileIndexes * sizeof(DIABLO3_CORE_TOC_ENTRY)); // Populate the linear array with the entries for(i = 0; i < DIABLO3_MAX_ASSETS; i++) { // Set the pointers pTocEntry = (PDIABLO3_CORE_TOC_ENTRY)(pbCoreTocFile + pTocHeader->EntryOffsets[i]); pbCoreTocNames = (LPBYTE)(pTocEntry + pTocHeader->EntryCounts[i]); // Setup the entries for(DWORD n = 0; n < pTocHeader->EntryCounts[i]; n++) { pSortedEntries[pTocEntry->FileIndex].AssetIndex = pTocEntry->AssetIndex; pSortedEntries[pTocEntry->FileIndex].FileIndex = pTocEntry->FileIndex; pSortedEntries[pTocEntry->FileIndex].NameOffset = (DWORD)(pbCoreTocNames - pbCoreTocFile) + pTocEntry->NameOffset; pTocEntry++; } } // Now use the linear array to resolve the asset indexes and plain names ResolveFullFileNames(pRootHandler, pSortedEntries, pPackageMap, pbCoreTocFile, dwFileIndexes); CASC_FREE(pSortedEntries); } return ERROR_SUCCESS; } //----------------------------------------------------------------------------- // Implementation of Diablo III root file static int D3Handler_Insert(TRootHandler_Diablo3 * pRootHandler, const char * szFileName, LPBYTE pbEncodingKey) { ENCODING_KEY EncodingKey; DWORD dwFileIndex; // Don't let the number of items to overflow if(pRootHandler->FileTable.ItemCount >= pRootHandler->FileTable.ItemCountMax) return ERROR_NOT_ENOUGH_MEMORY; // Insert the item EncodingKey = *(PENCODING_KEY)pbEncodingKey; dwFileIndex = InsertFileEntry(pRootHandler, EncodingKey, szFileName, strlen(szFileName) + 1); return (dwFileIndex != INVALID_FILE_INDEX) ? ERROR_SUCCESS : ERROR_NOT_ENOUGH_MEMORY; } static LPBYTE D3Handler_Search(TRootHandler_Diablo3 * pRootHandler, TCascSearch * pSearch, PDWORD /* PtrFileSize */, PDWORD /* PtrLocaleFlags */, PDWORD /* PtrFileDataId */) { PCASC_FILE_ENTRY pFileEntry; const char * szSrcName = NULL; // Are we still inside the root directory range? while(pSearch->IndexLevel1 < pRootHandler->FileTable.ItemCount) { // Get the n-th directory and the file name pFileEntry = (PCASC_FILE_ENTRY)Array_ItemAt(&pRootHandler->FileTable, pSearch->IndexLevel1); szSrcName = (char *)Array_ItemAt(&pRootHandler->FileNames, pFileEntry->dwFileName); // This is either a full file name or an abbreviated name if(pFileEntry->dwFlags & CASC_ENTRY_SHORT_NAME) { CreateFileName(pRootHandler, szSrcName, pSearch->szFileName); } else { strcpy(pSearch->szFileName, szSrcName); } // Prepare for the next search pSearch->IndexLevel1++; return pFileEntry->EncodingKey.Value; } // No more entries return NULL; } static void D3Handler_EndSearch(TRootHandler_Diablo3 * /* pRootHandler */, TCascSearch * /* pSearch */) { // Do nothing } static LPBYTE D3Handler_GetKey(TRootHandler_Diablo3 * pRootHandler, const char * szFileName) { PCASC_FILE_ENTRY pFileEntry; ULONGLONG FileNameHash = CalcFileNameHash(szFileName); // Find the file in the name table pFileEntry = (PCASC_FILE_ENTRY)Map_FindObject(pRootHandler->pRootMap, &FileNameHash, NULL); return (pFileEntry != NULL) ? pFileEntry->EncodingKey.Value : NULL; } static DWORD D3Handler_GetFileId(TRootHandler_Diablo3 * /* pRootHandler */, const char * /* szFileName */) { // Not implemented for D3 return 0; } static void D3Handler_Close(TRootHandler_Diablo3 * pRootHandler) { if(pRootHandler != NULL) { // Free the file map Map_Free(pRootHandler->pRootMap); // Free the array of the file entries and file names Array_Free(&pRootHandler->FileTable); Array_Free(&pRootHandler->FileNames); // Free the root file itself CASC_FREE(pRootHandler); } } /* static void DumpRootFile(TDumpContext * dc, LPBYTE pbFileData, LPBYTE pbFileDataEnd) { char szMD5Buffer[MD5_STRING_SIZE+1]; DWORD dwSignature; DWORD dwItemCount; DWORD i; dwSignature = *(PDWORD)pbFileData; if(dwSignature != CASC_DIABLO3_SUBDIR_SIGNATURE) return; pbFileData += sizeof(DWORD); // Dump items that contain EncodingKey + AssetId dwItemCount = *(PDWORD)pbFileData; pbFileData += sizeof(DWORD); for(i = 0; i < dwItemCount; i++) { PCASC_DIABLO3_ASSET_ENTRY pEntry = (PCASC_DIABLO3_ASSET_ENTRY)pbFileData; if((pbFileData + sizeof(*pEntry)) > pbFileDataEnd) return; pbFileData += sizeof(*pEntry); dump_print(dc, "%s %08X\n", StringFromMD5(pEntry->EncodingKey, szMD5Buffer), pEntry->AssetId); } // Terminate with two newlines dump_print(dc, "\n"); // Dump items that contain EncodingKey + AssetId + FileNumber dwItemCount = *(PDWORD)pbFileData; pbFileData += sizeof(DWORD); for(i = 0; i < dwItemCount; i++) { PCASC_DIABLO3_ASSET_ENTRY2 pEntry = (PCASC_DIABLO3_ASSET_ENTRY2)pbFileData; if((pbFileData + sizeof(*pEntry)) > pbFileDataEnd) return; pbFileData += sizeof(*pEntry); dump_print(dc, "%s %08X %08X\n", StringFromMD5((LPBYTE)pEntry->EncodingKey, szMD5Buffer), pEntry->AssetId, pEntry->FileNumber); } // Terminate with two newlines dump_print(dc, "\n"); // Dump items that contain EncodingKey + FileName dwItemCount = *(PDWORD)pbFileData; pbFileData += sizeof(DWORD); for(i = 0; i < dwItemCount; i++) { PDIABLO3_NAMED_ENTRY pEntry = (PDIABLO3_NAMED_ENTRY)pbFileData; DWORD dwEntrySize = VerifyNamedFileEntry(pbFileData, pbFileDataEnd); if((pbFileData + dwEntrySize) > pbFileDataEnd) return; pbFileData += dwEntrySize; dump_print(dc, "%s %s\n", StringFromMD5((LPBYTE)pEntry->EncodingKey, szMD5Buffer), pEntry->szFileName); } dump_print(dc, "\n\n"); } */ //----------------------------------------------------------------------------- // Public functions int RootHandler_CreateDiablo3(TCascStorage * hs, LPBYTE pbRootFile, DWORD cbRootFile) { TRootHandler_Diablo3 * pRootHandler; PCASC_MAP pPackageMap = NULL; LPBYTE pbRootFileEnd = pbRootFile + cbRootFile; LPBYTE pbPackagesDat = NULL; DWORD dwTotalFileCount; DWORD cbPackagesDat = 0; int nError; // Allocate the root handler object hs->pRootHandler = pRootHandler = CASC_ALLOC(TRootHandler_Diablo3, 1); if(pRootHandler == NULL) return ERROR_NOT_ENOUGH_MEMORY; // Fill-in the handler functions memset(pRootHandler, 0, sizeof(TRootHandler_Diablo3)); pRootHandler->Insert = (ROOT_INSERT)D3Handler_Insert; pRootHandler->Search = (ROOT_SEARCH)D3Handler_Search; pRootHandler->EndSearch = (ROOT_ENDSEARCH)D3Handler_EndSearch; pRootHandler->GetKey = (ROOT_GETKEY)D3Handler_GetKey; pRootHandler->Close = (ROOT_CLOSE)D3Handler_Close; pRootHandler->GetFileId = (ROOT_GETFILEID)D3Handler_GetFileId; // Fill-in the flags pRootHandler->dwRootFlags |= ROOT_FLAG_HAS_NAMES; // Scan the total number of files in the root directories // Reserve space for extra files dwTotalFileCount = ScanDirectoryFile(hs, pbRootFile, pbRootFileEnd); if(dwTotalFileCount == 0) return ERROR_FILE_CORRUPT; dwTotalFileCount += CASC_EXTRA_FILES; // Allocate the global linear file table // Note: This is about 18 MB of memory for Diablo III PTR build 30013 nError = Array_Create(&pRootHandler->FileTable, CASC_FILE_ENTRY, dwTotalFileCount); if(nError != ERROR_SUCCESS) return nError; // Allocate global buffer for file names. // The size of the buffer was taken from Diablo III build 30013 nError = Array_Create(&pRootHandler->FileNames, char, 0x01000000); if(nError != ERROR_SUCCESS) return nError; // Create map of ROOT_ENTRY -> FileEntry pRootHandler->pRootMap = Map_Create(dwTotalFileCount, sizeof(ULONGLONG), FIELD_OFFSET(CASC_FILE_ENTRY, FileNameHash)); if(pRootHandler->pRootMap == NULL) return ERROR_NOT_ENOUGH_MEMORY; // Parse the ROOT file and insert all entries in the file table nError = ParseDirectoryFile(pRootHandler, pbRootFile, pbRootFileEnd, DIABLO3_INVALID_INDEX); if(nError == ERROR_SUCCESS) { size_t dwRootEntries = pRootHandler->FileTable.ItemCount; // We expect the number of level-0 to be less than maximum assert(dwRootEntries < DIABLO3_MAX_SUBDIRS); // Now parse the all root items and load them for(DWORD i = 0; i < dwRootEntries; i++) { PCASC_FILE_ENTRY pRootEntry = (PCASC_FILE_ENTRY)Array_ItemAt(&pRootHandler->FileTable, i); // Load the entire file to memory pbRootFile = LoadFileToMemory(hs, pRootEntry->EncodingKey.Value, &cbRootFile); if(pbRootFile != NULL) { nError = ParseDirectoryFile(pRootHandler, pbRootFile, pbRootFile + cbRootFile, i); CASC_FREE(pbRootFile); } } } // Note: The file "Base\Data_D3\PC\Misc\Packages.dat" contains the names // of the files (without level-0 and level-1 directory). We can use these // names for supplying the missing extensions if(nError == ERROR_SUCCESS) { // Load the entire file to memory pbPackagesDat = LoadFileToMemory(hs, "Base\\Data_D3\\PC\\Misc\\Packages.dat", &cbPackagesDat); if(pbPackagesDat != NULL) { pPackageMap = CreatePackageMap(pbPackagesDat, pbPackagesDat + cbPackagesDat); } } // Vast majorify of files at this moment don't have names. // We can load the Base\CoreTOC.dat file in order // to get directory asset indexes, file names and extensions if(nError == ERROR_SUCCESS) { LPBYTE pbCoreTOC; DWORD cbCoreTOC = 0; // Load the entire file to memory pbCoreTOC = LoadFileToMemory(hs, "Base\\CoreTOC.dat", &cbCoreTOC); if(pbCoreTOC != NULL) { ParseCoreTOC(pRootHandler, pPackageMap, pbCoreTOC, pbCoreTOC + cbCoreTOC); CASC_FREE(pbCoreTOC); } } // Free the packages map if(pPackageMap != NULL) Map_Free(pPackageMap); if(pbPackagesDat != NULL) CASC_FREE(pbPackagesDat); return nError; }