/*****************************************************************************/ /* 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_MAX_ASSETS 70 // Maximum possible number of assets #define DIABLO3_MAX_ROOT_FOLDERS 0x20 // Maximum count of root directory named entries // On-disk structure for a file given by file number typedef struct _DIABLO3_ASSET_ENTRY { CONTENT_KEY CKey; // Content key for the file DWORD FileIndex; // File index } DIABLO3_ASSET_ENTRY, *PDIABLO3_ASSET_ENTRY; // On-disk structure for a file given by file number and suffix typedef struct _DIABLO3_ASSETIDX_ENTRY { CONTENT_KEY CKey; // Content key for the file DWORD FileIndex; // File index DWORD SubIndex; // File subindex, like "SoundBank\3D Ambience\0000.smp" } DIABLO3_ASSETIDX_ENTRY, *PDIABLO3_ASSETIDX_ENTRY; // In-memory structure of the named entry typedef struct _DIABLO3_NAMED_ENTRY { PCONTENT_KEY pCKey; // Pointer to the content key const char * szFileName; // Pointer to the zero-terminated file name const char * szFileEnd; // Position of the zero terminator (aka end of the file name) } 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 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; // 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 parsed directory data struct DIABLO3_DIRECTORY { DIABLO3_DIRECTORY() { pbAssetEntries = pbAssetIdxEntries = pbNamedEntries = NULL; dwAssetEntries = dwAssetIdxEntries = dwNamedEntries = 0; dwNodeIndex = 0; } CASC_BLOB Data; // The complete copy of the directory data LPBYTE pbAssetEntries; // Pointer to asset entries without subitem number. Example: "SoundBank\SoundFile.smp" LPBYTE pbAssetIdxEntries; // Pointer to asset entries with subitem number LPBYTE pbNamedEntries; // Pointer to named entries. These are for files with arbitrary names, and they do not belong to an asset DWORD dwAssetEntries; // Number of asset entries without subitem number DWORD dwAssetIdxEntries; DWORD dwNamedEntries; DWORD dwNodeIndex; // Index of file node for this folder }; //----------------------------------------------------------------------------- // 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 }; #define DIABLO3_ASSET_COUNT (sizeof(Assets) / sizeof(Assets[0])) //----------------------------------------------------------------------------- // Handler definitions for Diablo3 root file struct TDiabloRoot : public TFileTreeRoot { public: TDiabloRoot() : TFileTreeRoot(0) { memset(RootFolders, 0, sizeof(RootFolders)); pbCoreTocData = NULL; pFileIndices = NULL; nFileIndices = 0; // Map for searching a real file extension memset(&PackagesMap, 0, sizeof(CASC_MAP)); // We have file names and return CKey as result of search dwFeatures |= (CASC_FEATURE_FILE_NAMES | CASC_FEATURE_ROOT_CKEY); } ~TDiabloRoot() { FreeLoadingStuff(); } PDIABLO3_ASSET_INFO GetAssetInfo(DWORD dwAssetIndex) { if(dwAssetIndex < DIABLO3_ASSET_COUNT && Assets[dwAssetIndex].szDirectoryName != NULL) return &Assets[dwAssetIndex]; return NULL; } char * FindPackageName(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 = CascStrPrintf(szFileName, _countof(szFileName), "%s\\%s", szAssetName, szPlainName); return (char *)PackagesMap.FindString(szFileName, szFileName + nLength); } DWORD LoadFileToMemory(TCascStorage * hs, const char * szFileName, CASC_BLOB & FileData) { PCASC_CKEY_ENTRY pCKeyEntry; DWORD dwErrCode = ERROR_FILE_NOT_FOUND; // Try to find CKey for the file pCKeyEntry = GetFile(hs, szFileName); if(pCKeyEntry != NULL) dwErrCode = LoadInternalFileToMemory(hs, pCKeyEntry, FileData); return dwErrCode; } static DWORD CaptureDirectoryData( DIABLO3_DIRECTORY & DirHeader, CASC_BLOB & Directory) { LPBYTE pbDirectory; LPBYTE pbDataEnd; DWORD Signature = 0; // // Structure of a Diablo3 directory header // 1) Signature (4 bytes) // 2) Number of DIABLO3_ASSET_ENTRY entries (4 bytes) // 3) Array of DIABLO3_ASSET_ENTRY entries // 4) Number of DIABLO3_ASSETIDX_ENTRY entries (4 bytes) // 5) Array of DIABLO3_ASSETIDX_ENTRY entries // 6) Number of DIABLO3_NAMED_ENTRY entries (4 bytes) // 7) Array of DIABLO3_NAMED_ENTRY entries // // Clone the input data DirHeader.Data.MoveFrom(Directory); pbDirectory = DirHeader.Data.pbData; pbDataEnd = DirHeader.Data.End(); // Get the header signature pbDirectory = CaptureInteger32(pbDirectory, pbDataEnd, &Signature); if((pbDirectory == NULL) || (Signature != CASC_DIABLO3_ROOT_SIGNATURE && Signature != DIABLO3_SUBDIR_SIGNATURE)) return ERROR_BAD_FORMAT; // Subdirectories have extra two arrays if(Signature == DIABLO3_SUBDIR_SIGNATURE) { // Capture the number of DIABLO3_ASSET_ENTRY items pbDirectory = CaptureInteger32(pbDirectory, pbDataEnd, &DirHeader.dwAssetEntries); if(pbDirectory == NULL) return ERROR_BAD_FORMAT; // Capture the array of DIABLO3_ASSET_ENTRY pbDirectory = CaptureArrayAsByte(pbDirectory, pbDataEnd, &DirHeader.pbAssetEntries, DirHeader.dwAssetEntries); if(pbDirectory == NULL) return ERROR_BAD_FORMAT; // Capture the number of DIABLO3_ASSETIDX_ENTRY items pbDirectory = CaptureInteger32(pbDirectory, pbDataEnd, &DirHeader.dwAssetIdxEntries); if(pbDirectory == NULL) return ERROR_BAD_FORMAT; // Capture the array of DIABLO3_ASSETIDX_ENTRY pbDirectory = CaptureArrayAsByte(pbDirectory, pbDataEnd, &DirHeader.pbAssetIdxEntries, DirHeader.dwAssetIdxEntries); if(pbDirectory == NULL) return ERROR_BAD_FORMAT; } // Capture the number of DIABLO3_NAMED_ENTRY array pbDirectory = CaptureInteger32(pbDirectory, pbDataEnd, &DirHeader.dwNamedEntries); if(pbDirectory == NULL) return ERROR_BAD_FORMAT; // Note: Do not capture the array here. We will do that later, // when we will be parsing the directory DirHeader.pbNamedEntries = pbDirectory; return ERROR_SUCCESS; } LPBYTE CaptureCoreTocHeader( PDIABLO3_CORE_TOC_HEADER * PtrHeader, PDWORD PtrMaxIndex, LPBYTE pbDataPtr, LPBYTE pbDataEnd) { PDIABLO3_CORE_TOC_HEADER pTocHeader = (PDIABLO3_CORE_TOC_HEADER)pbDataPtr; DWORD dwMaxFileIndex = 0; // Check the space for header if((pbDataPtr + sizeof(DIABLO3_CORE_TOC_HEADER)) > pbDataEnd) return NULL; pbDataPtr += sizeof(DIABLO3_CORE_TOC_HEADER); // Verify all asset arrays for(size_t i = 0; i < DIABLO3_MAX_ASSETS; i++) { PDIABLO3_CORE_TOC_ENTRY pTocEntry = (PDIABLO3_CORE_TOC_ENTRY)(pbDataPtr + pTocHeader->EntryOffsets[i]); DWORD EntryOffset = pTocHeader->EntryOffsets[i]; DWORD EntryCount = pTocHeader->EntryCounts[i]; // Verify file range if((pbDataPtr + EntryOffset + EntryCount * sizeof(DIABLO3_CORE_TOC_ENTRY)) > pbDataEnd) return NULL; // Find out the entry with the maximum index for(DWORD n = 0; n < EntryCount; n++) { if(pTocEntry->FileIndex >= dwMaxFileIndex) dwMaxFileIndex = pTocEntry->FileIndex; pTocEntry++; } } // Give data and return PtrMaxIndex[0] = dwMaxFileIndex; PtrHeader[0] = pTocHeader; return pbDataPtr; } LPBYTE CaptureNamedEntry( LPBYTE pbDataPtr, LPBYTE pbDataEnd, PDIABLO3_NAMED_ENTRY pEntry) { // Capture the content key pbDataPtr = CaptureContentKey(pbDataPtr, pbDataEnd, &pEntry->pCKey); if(pbDataPtr == NULL) return NULL; // Capture file name. Must be ASCIIZ file name pEntry->szFileName = (const char *)pbDataPtr; while(pbDataPtr < pbDataEnd && pbDataPtr[0] != 0) pbDataPtr++; // Did we find a zero char? if(pbDataPtr < pbDataEnd && pbDataPtr[0] == 0) { pEntry->szFileEnd = (const char *)pbDataPtr; return pbDataPtr + 1; } return NULL; } DWORD LoadDirectoryFile(TCascStorage * hs, DIABLO3_DIRECTORY & DirHeader, PCASC_CKEY_ENTRY pCKeyEntry) { CASC_BLOB Data; DWORD dwErrCode; // Load the n-th folder, if exists dwErrCode = LoadInternalFileToMemory(hs, pCKeyEntry, Data); if(dwErrCode == ERROR_SUCCESS && Data.cbData) return CaptureDirectoryData(DirHeader, Data); // If the folder is not there, ignore the error return ERROR_SUCCESS; } bool CreateAssetFileName( CASC_PATH & PathBuffer, DWORD FileIndex, DWORD SubIndex) { PDIABLO3_CORE_TOC_ENTRY pTocEntry; PDIABLO3_ASSET_INFO pAssetInfo; LPCSTR szPackageName = NULL; LPCSTR szPlainName; LPCSTR szFormat; char szBuffer[MAX_PATH]; // Find and check the entry pTocEntry = pFileIndices + FileIndex; if(pTocEntry->FileIndex == FileIndex) { // Retrieve the asset information szPlainName = (LPCSTR)(pbCoreTocData + pTocEntry->NameOffset); pAssetInfo = GetAssetInfo(pTocEntry->AssetIndex); // Construct the file name, up to the extension. Don't include the '.' szFormat = (SubIndex != CASC_INVALID_INDEX) ? "%s\\%04u" : "%s"; CascStrPrintf(szBuffer, _countof(szBuffer), szFormat, szPlainName, SubIndex); // 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(pAssetInfo != NULL) { // Retrieve the asset name szPackageName = FindPackageName(pAssetInfo->szDirectoryName, szBuffer); if(szPackageName != NULL) { PathBuffer.AppendString(szPackageName, false); return true; } // Append the directory name PathBuffer.AppendString(pAssetInfo->szDirectoryName, false); } else { // Append generic name "Asset##" and continue PathBuffer.AppendString("Asset", false); PathBuffer.AppendChar((char)('0' + (pTocEntry->AssetIndex / 10))); PathBuffer.AppendChar((char)('0' + (pTocEntry->AssetIndex % 10))); } // Append the content of the buffer PathBuffer.AppendString(szBuffer, true); // If we have an extension, use it. Otherwise, supply "a##" if(pAssetInfo != NULL && pAssetInfo->szExtension != NULL) { PathBuffer.AppendChar('.'); PathBuffer.AppendString(pAssetInfo->szExtension, false); } else { CascStrPrintf(szBuffer, _countof(szBuffer), ".a%02u", pTocEntry->AssetIndex); PathBuffer.AppendString(szBuffer, false); } return true; } return false; } // Parse the asset entries DWORD ParseAssetEntries( TCascStorage * hs, DIABLO3_DIRECTORY & Directory, CASC_PATH & PathBuffer) { PDIABLO3_ASSET_ENTRY pEntry = (PDIABLO3_ASSET_ENTRY)Directory.pbAssetEntries; PCASC_CKEY_ENTRY pCKeyEntry; size_t nSavePos = PathBuffer.Save(); DWORD dwEntries = Directory.dwAssetEntries; // Do nothing if there is no entries if(pEntry != NULL && dwEntries != 0) { // Insert all asset entries to the file tree for(DWORD i = 0; i < dwEntries; i++, pEntry++) { pCKeyEntry = FindCKeyEntry_CKey(hs, pEntry->CKey.Value); if(pCKeyEntry != NULL) { // Construct the full path name of the entry if(CreateAssetFileName(PathBuffer, pEntry->FileIndex, CASC_INVALID_INDEX)) { // Insert the entry to the file tree FileTree.InsertByName(pCKeyEntry, PathBuffer); } // Restore the path buffer position PathBuffer.Restore(nSavePos); } } } return ERROR_SUCCESS; } DWORD ParseAssetAndIdxEntries( TCascStorage * hs, DIABLO3_DIRECTORY & Directory, CASC_PATH & PathBuffer) { PDIABLO3_ASSETIDX_ENTRY pEntry = (PDIABLO3_ASSETIDX_ENTRY)Directory.pbAssetIdxEntries; PCASC_CKEY_ENTRY pCKeyEntry; size_t nSavePos = PathBuffer.Save(); DWORD dwEntries = Directory.dwAssetIdxEntries; // Do nothing if there is no entries if(pEntry != NULL && dwEntries != 0) { // Insert all asset entries to the file tree for(DWORD i = 0; i < dwEntries; i++, pEntry++) { pCKeyEntry = FindCKeyEntry_CKey(hs, pEntry->CKey.Value); if(pCKeyEntry != NULL) { // Construct the full path name of the entry if(CreateAssetFileName(PathBuffer, pEntry->FileIndex, pEntry->SubIndex)) { // Insert the entry to the file tree // fprintf(fp, "%08u %04u %s\n", pEntry->FileIndex, pEntry->SubIndex, PathBuffer.szBegin); FileTree.InsertByName(pCKeyEntry, PathBuffer); } // Restore the path buffer position PathBuffer.Restore(nSavePos); } } } return ERROR_SUCCESS; } // Parse the named entries of all folders DWORD ParseDirectory_Phase1( TCascStorage * hs, DIABLO3_DIRECTORY & Directory, CASC_PATH & PathBuffer, bool bIsRootDirectory) { DIABLO3_NAMED_ENTRY NamedEntry; size_t nFolderIndex = 0; size_t nSavePos = PathBuffer.Save(); DWORD dwErrCode = ERROR_SUCCESS; // Do nothing if there is no named headers if(Directory.pbNamedEntries && Directory.dwNamedEntries) { PCASC_CKEY_ENTRY pCKeyEntry; PCASC_FILE_NODE pFileNode; LPBYTE pbDataPtr = Directory.pbNamedEntries; LPBYTE pbDataEnd = Directory.Data.End(); DWORD dwNodeIndex; // Parse all entries while(pbDataPtr < pbDataEnd) { // Capture the named entry pbDataPtr = CaptureNamedEntry(pbDataPtr, pbDataEnd, &NamedEntry); if(pbDataPtr == NULL) return ERROR_BAD_FORMAT; // Append the path fragment to the total path PathBuffer.AppendStringN(NamedEntry.szFileName, (NamedEntry.szFileEnd - NamedEntry.szFileName), true); // Check whether the file exists in the storage pCKeyEntry = FindCKeyEntry_CKey(hs, NamedEntry.pCKey->Value); if(pCKeyEntry != NULL) { // Create file node belonging to this folder pFileNode = FileTree.InsertByName(pCKeyEntry, PathBuffer); dwNodeIndex = (DWORD)FileTree.IndexOf(pFileNode); // If we are parsing root folder, we also need to load the data of the sub-folder file if(bIsRootDirectory) { // Mark the node as directory pCKeyEntry->Flags |= CASC_CE_FOLDER_ENTRY; pFileNode->Flags |= CFN_FLAG_FOLDER; // Load the sub-directory file dwErrCode = LoadDirectoryFile(hs, RootFolders[nFolderIndex], pCKeyEntry); if(dwErrCode != ERROR_SUCCESS) return dwErrCode; // Parse the sub-directory file dwErrCode = ParseDirectory_Phase1(hs, RootFolders[nFolderIndex], PathBuffer, false); if(dwErrCode != ERROR_SUCCESS) return dwErrCode; // Also save the item pointer and increment the folder index RootFolders[nFolderIndex].dwNodeIndex = dwNodeIndex; nFolderIndex++; } // Restore the path pointer PathBuffer.Restore(nSavePos); } } } return dwErrCode; } // Parse the nameless entries of all folders int ParseDirectory_Phase2(TCascStorage * hs) { CASC_PATH PathBuffer; char szBuffer[MAX_PATH]; // Parse each root subdirectory for(size_t i = 0; i < DIABLO3_MAX_ROOT_FOLDERS; i++) { // Is this root folder loaded? if(RootFolders[i].Data.pbData != NULL) { // Retrieve the parent name if(RootFolders[i].dwNodeIndex != 0) { FileTree.PathAt(szBuffer, _countof(szBuffer), RootFolders[i].dwNodeIndex); PathBuffer.SetPathRoot(szBuffer); } // Array of DIABLO3_ASSET_ENTRY entries. // These are for files belonging to an asset, without subitem number. // Example: "SoundBank\SoundFile.smp" ParseAssetEntries(hs, RootFolders[i], PathBuffer); // Array of DIABLO3_ASSETIDX_ENTRY entries. // These are for files belonging to an asset, with a subitem number. // Example: "SoundBank\SoundFile\0001.smp" ParseAssetAndIdxEntries(hs, RootFolders[i], PathBuffer); } } return ERROR_SUCCESS; } // Creates an array of DIABLO3_CORE_TOC_ENTRY entries indexed by FileIndex // Used as lookup table when we have FileIndex and need Asset+PlainName DWORD CreateMapOfFileIndices(TCascStorage * hs, const char * szFileName) { PDIABLO3_CORE_TOC_HEADER pTocHeader = NULL; DWORD dwMaxFileIndex = 0; DWORD dwErrCode; // Load the entire file to memory dwErrCode = LoadFileToMemory(hs, szFileName, CoreTocFile); if(dwErrCode == ERROR_SUCCESS && CoreTocFile.cbData) { LPBYTE pbCoreTocPtr = CoreTocFile.pbData; LPBYTE pbCoreTocEnd = CoreTocFile.End(); // Capture the header if((pbCoreTocPtr = CaptureCoreTocHeader(&pTocHeader, &dwMaxFileIndex, pbCoreTocPtr, pbCoreTocEnd)) == NULL) return ERROR_BAD_FORMAT; // If there are no indices, return NULL if(dwMaxFileIndex == 0) return ERROR_SUCCESS; // Allocate and populate the array of DIABLO3_CORE_TOC_ENTRYs pFileIndices = CASC_ALLOC(dwMaxFileIndex + 1); if(pFileIndices != NULL) { // Initialize all entries to invalid memset(pFileIndices, 0xFF, (dwMaxFileIndex + 1) * sizeof(DIABLO3_CORE_TOC_ENTRY)); // Populate the linear array with the file indices for(size_t i = 0; i < DIABLO3_MAX_ASSETS; i++) { PDIABLO3_CORE_TOC_ENTRY pTocEntry = (PDIABLO3_CORE_TOC_ENTRY)(pbCoreTocPtr + pTocHeader->EntryOffsets[i]); LPBYTE pbCoreTocNames = (LPBYTE)(pTocEntry + pTocHeader->EntryCounts[i]); // Setup the entries for(DWORD n = 0; n < pTocHeader->EntryCounts[i]; n++) { DWORD dwFileIndex = pTocEntry->FileIndex; pFileIndices[dwFileIndex].AssetIndex = pTocEntry->AssetIndex; pFileIndices[dwFileIndex].FileIndex = pTocEntry->FileIndex; pFileIndices[dwFileIndex].NameOffset = (DWORD)(pbCoreTocNames - pbCoreTocPtr) + pTocEntry->NameOffset; pTocEntry++; } } // Save the file to the root handler pbCoreTocData = pbCoreTocPtr; nFileIndices = dwMaxFileIndex; dwErrCode = ERROR_SUCCESS; } } return dwErrCode; } // Packages.dat contains a list of full file names (without locale prefix). // They are not sorted, nor they correspond to file IDs. // Does the sort order mean something? Perhaps we could use them as listfile? DWORD CreateMapOfRealNames(TCascStorage * hs, const char * szFileName) { DWORD Signature = 0; DWORD NumberOfNames = 0; DWORD dwErrCode; // Load the entire file to memory dwErrCode = LoadFileToMemory(hs, szFileName, PackagesDat); if(dwErrCode == ERROR_SUCCESS && PackagesDat.cbData) { LPBYTE pbPackagesPtr = PackagesDat.pbData; LPBYTE pbPackagesEnd = PackagesDat.End(); // Get the header. There is just Signature + NumberOfNames if((pbPackagesPtr = CaptureInteger32(pbPackagesPtr, pbPackagesEnd, &Signature)) == NULL) return ERROR_BAD_FORMAT; if((pbPackagesPtr = CaptureInteger32(pbPackagesPtr, pbPackagesEnd, &NumberOfNames)) == NULL) return ERROR_BAD_FORMAT; if(Signature != DIABLO3_PACKAGES_SIGNATURE || NumberOfNames == 0) return ERROR_BAD_FORMAT; // Create the map for fast search of the file name if(PackagesMap.Create(NumberOfNames, 0, 0, KeyIsString) == ERROR_SUCCESS) { const char * szPackageName = (const char *)pbPackagesPtr; // Go as long as there is something for(DWORD i = 0; i < NumberOfNames; i++) { // Get the file extension if((LPBYTE)szPackageName >= pbPackagesEnd) break; // Insert the file name to the map. The file extension is not included PackagesMap.InsertString(szPackageName, true); szPackageName = szPackageName + strlen(szPackageName) + 1; } } } return ERROR_SUCCESS; } DWORD Load(TCascStorage * hs, DIABLO3_DIRECTORY & RootDirectory) { CASC_PATH PathBuffer; DWORD dwErrCode; // Always parse the named entries first. They always point to a file. // These are entries with arbitrary names, and they do not belong to an asset dwErrCode = ParseDirectory_Phase1(hs, RootDirectory, PathBuffer, true); if(dwErrCode == ERROR_SUCCESS) { // The asset entries in the ROOT file don't contain file names, but indices. // To convert a file index to a file name, we need to load and parse the "Base\\CoreTOC.dat" file. dwErrCode = CreateMapOfFileIndices(hs, "Base\\CoreTOC.dat"); if(dwErrCode == ERROR_SUCCESS) { // The file "Base\Data_D3\PC\Misc\Packages.dat" contains the file names // (without level-0 and level-1 directory). // We can use these names for supplying the missing extensions CreateMapOfRealNames(hs, "Base\\Data_D3\\PC\\Misc\\Packages.dat"); // Now parse all folders and resolve the full names ParseDirectory_Phase2(hs); } // Free all stuff that was used during loading of the ROOT file FreeLoadingStuff(); } return dwErrCode; } void FreeLoadingStuff() { // Free the package map PackagesMap.Free(); // Free the array of file indices CASC_FREE(pFileIndices); } // Array of root directory subdirectories DIABLO3_DIRECTORY RootFolders[DIABLO3_MAX_ROOT_FOLDERS]; // Array of DIABLO3_TOC_ENTRY structures, sorted by the file index // Used for converting FileIndex -> Asset+PlainName during loading PDIABLO3_CORE_TOC_ENTRY pFileIndices; CASC_BLOB CoreTocFile; LPBYTE pbCoreTocData; size_t nFileIndices; // Map for searching a real file extension CASC_BLOB PackagesDat; CASC_MAP PackagesMap; }; //----------------------------------------------------------------------------- // Public functions DWORD RootHandler_CreateDiablo3(TCascStorage * hs, CASC_BLOB & RootFile) { TDiabloRoot * pRootHandler = NULL; DIABLO3_DIRECTORY RootDirectory; DWORD dwErrCode = ERROR_BAD_FORMAT; // Verify the header of the ROOT file if((dwErrCode = TDiabloRoot::CaptureDirectoryData(RootDirectory, RootFile)) == ERROR_SUCCESS) { // Allocate the root handler object if((pRootHandler = new TDiabloRoot()) != NULL) { // Load the root directory. If load failed, we free the object dwErrCode = pRootHandler->Load(hs, RootDirectory); if(dwErrCode != ERROR_SUCCESS) { delete pRootHandler; pRootHandler = NULL; } } } // Assign the root directory (or NULL) and return error hs->pRootHandler = pRootHandler; return dwErrCode; }