mxwcore-legion/dep/CascLib/src/CascRootFile_Diablo3.cpp
2023-11-05 15:26:19 -05:00

1200 lines
43 KiB
C++

/*****************************************************************************/
/* 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;
}