April 25Apr 25 Localization The assets in both Harry Potter and the Deathly Hallows games on PC are stored in .bin.gz files, which are headerless and compressed with ZLIB. After decompressing, I can see the following structure in the first part of each file (this is in ImHex Pattern Language). I can also see DDS files in the archive whose sizes correspond with an entry in the chunk table. I am not sure what the significance of the 44-byte sections are (they are often either padded or slightly larger than that) or where exactly the data starts yet. I'm guessing the first two ints in each chunk entry point to paths/labels in the table at the start, and that the chunks are aligned/padded somehow. struct LabelEntry { u32 id; char path[]; }; struct ChunkEntry { u32 id; s32; u32 size; s32 unknown[7]; }; struct Section1 { u32 int_count; u32 ints[int_count]; u32 chunk_count; ChunkEntry chunk_entries[chunk_count]; }; struct File { u32 next_section_off; u32 entry_count; LabelEntry header_labels[entry_count]; Section1 section_1 @ next_section_off + 4; }; File file_at_0x00 @ 0x00; Here are a few sample files: https://drive.google.com/drive/folders/1OGEMB1gwo8Lz2Sjiqb8kI8j_FCyEe2pp?usp=sharing
April 26Apr 26 Localization OK, took a while, it was quite unusual, but I think I have it (it seems to work for the 4 files you posted)... +--------------------------------------------------------+ | Harry Potter and the Deathly Hallows - Part 1 *.bin.gz | +--------------------------------------------------------+ // The whole archive is compressed using ZLib // ARCHIVE X - Compressed Archive // when uncompressed { // HEADER 4 - Filename Directory Length (including padding) // NAMES DIRECTORY 4 - Number of Names // for each name 4 - Name ID X - Name 1 - null Name Terminator 0-3 - null Padding to a multiple of 4 bytes // PROPERTIES DIRECTORY 4 - Number of Properties // for each property 4 - Property Name ID // DETAILS DIRECTORY 4 - Number of Files // for each file (40 bytes per entry) 4 - Name ID 4 - Reference Name ID (eg TEX files point to a DDS image) (-1 = no reference image) 4 - File Length 4 - Flags (16/128) 4 - File Header Length (can be 0) 4 - External File? (0=file in archive, 1=file is external / not in archive) 4 - Number of Entries in the Unknown Directory for this File 12 - null // UNKNOWN DIRECTORY // for each entry 4 - Unknown // FILE DATA // for each file X - File Header X - File Data X - null Padding to a multiple of 16 bytes } The Names directory contains the names for the files, as well as for something I'm calling Properties, which just seemed to match up based on the text strings The Properties directory just points to the names of the properties, so nothing special there The Details directory has the information to be able to find where the first file starts, and whether it has any padding before it.... The size of the Unknown Directory is the total of the field "Number of Entries in the Unknown Directory for this File" for all the entries, x4 . If you calculate that out, you can find where the file data starts. Some files have a File Header, which looks to be just blank padding. If there is a value in the File Header Length field, you need to skip that number of bytes first, then read the file data. File Lengths are padded to a multiple of 16 bytes. This needs to be calculated from the length of the file, not the offset to the file data. Don't include the File Header length in the padding calculation, it's only padding of the File Data itself. There is also a field that indicates whether the file is in the archive or not (I'm calling it the External File field). If the value = 1, the file isn't in the archive at all, so ignore the length entirely. Some untidy code that might help understanding: // 4 - Filename Directory Length int filenameDirLength = fm.readInt(); // 4 - Number of Names int numNames = fm.readInt(); String[] names = new String[numNames]; for (int i = 0; i < numNames; i++) { // 4 - Name ID fm.skip(4); // X - Name // 1 - null Name Terminator String name = fm.readNullString(); names[i] = name; } fm.relativeSeek(filenameDirLength + 4); // should already be here, but just in case... // 4 - Number of Properties int numProperties = fm.readInt(); // for each property // 4 - Property Name ID fm.skip(numProperties * 4); // 4 - Number Of Files int numFiles = fm.readInt(); // Loop through directory int unknownEntryCount = 0; String[] filenames = new String[numFiles]; int[] lengths = new int[numFiles]; int[] headerLengths = new int[numFiles]; int[] externalFiles = new int[numFiles]; for (int i = 0; i < numFiles; i++) { // 4 - Name ID int nameID = fm.readInt(); String filename = names[nameID]; // 4 - Reference Name ID (eg TEX files point to a DDS image) (-1 = no reference image) int referenceNameID = fm.readInt(); // 4 - File Length int length = fm.readInt(); // 4 - Flags (16/128) int flags = fm.readInt(); // 4 - File Header Length (can be 0) int headerLength = fm.readInt(); // 4 - External File? (0=file in archive, 1=file is external / not in archive) int externalFile = fm.readInt(); // 4 - Number of Entries in the Unknown Directory for this File int unknownEntries = fm.readInt(); unknownEntryCount += unknownEntries; // 12 - null fm.skip(12); filenames[i] = filename; lengths[i] = length; headerLengths[i] = headerLength; externalFiles[i] = externalFile; } // UNKNOWN DIRECTORY // for each entry // 4 - Unknown fm.skip(unknownEntryCount * 4); long offset = fm.getOffset(); for (int i = 0; i < numFiles; i++) { String filename = filenames[i]; if (externalFiles[i] == 1) { // an external file, not in this archive int length = 0; //path,name,offset,length,decompLength,exporter resources[i] = new Resource(path, filename, offset, length); } else { // a file in this archive int length = lengths[i]; int headerLength = headerLengths[i]; offset += headerLength; //path,name,offset,length,decompLength,exporter resources[i] = new Resource(path, filename, offset, length); offset += length; offset += calculatePadding(length, 16); } }
Create an account or sign in to comment