// ExpandX.cpp : Defines the entry point for the console application. // #include "stdafx.h" #include #include // Chunk-specific variables int iol, xpos, zpos, xstart, zstart, n; int i, blockstart, datastart, blocklightstart, skylightstart, biomestart; int y_plane, h; int local_biome_list[256]; // Variables for file I/O FILE *input_file_f, *output_file_f, *log_file_f; char *afile_path, *aoutput_path, *status; unsigned long file_size, bytes_read, template_length, section_template_length; // All our memory handling unsigned char *nbuffer, *xbuffer, *rbuffer, *ebuffer, *sbuffer, s_num; unsigned char *biome_list; int broken = 0, missing = -295; // Contained in the Biomes.cpp file void populate_biomes(unsigned char *biome_list); // Takes an input int and flips it from Big to Little Endian or vice versa. int EndianFlip(int _input_int) { int retval; unsigned char *_input_ptr = (unsigned char *)&_input_int; unsigned char *_p_retval = (unsigned char *)&retval; for (int _i = 0; _i < 4; _i ++) { _p_retval[3 - _i] = _input_ptr[_i]; } return retval; } // Find the offset of the Blocks section of the XBox file, in bytes int BlockStart(unsigned char *_in_buffer, int _size) { int _i = 0; // Loop through looking for the Blocks section do { if (_in_buffer[_i] == 'B') if (_in_buffer[_i + 1] == 'l') if (_in_buffer[_i + 5] == 's') return _i + 10; _i ++; } while (_i < _size); return -1; } // Find the offset of the Biomes section of the XBox file, in bytes int BiomeStart(unsigned char *_in_buffer, int _size) { int _i = 0; // Loop through looking for the Blocks section do { if (_in_buffer[_i] == 'B') if (_in_buffer[_i + 1] == 'i') if (_in_buffer[_i + 5] == 's') return _i + 10; _i ++; } while (_i < _size); return -1; } // Gets the higher order nybble of a byte unsigned char HiNybble(unsigned char _in_byte) { return _in_byte >> 4; } // Gets the lower order nybble of a byte unsigned char LoNybble(unsigned char _in_byte) { return _in_byte & 0xF; } // Gets the offset of the BlockLight section in the XBox file, in bytes int BlockLight(unsigned char *_in_buffer, int _size) { int _i = 0; // Loop through looking for the BlockLight section do { if (_in_buffer[_i] == 'B') if (_in_buffer[_i + 1] == 'l') if (_in_buffer[_i + 7] == 'g') if (_in_buffer[_i + 8] == 'h') return _i + 14; _i ++; } while (_i < _size); return -1; } // Gets the offset of the SkyLight section in the XBox file, in bytes int SkyLight(unsigned char *_in_buffer, int _size) { int _i = 0; // Loop through looking for the SkyLight section do { if (_in_buffer[_i] == 'S') if (_in_buffer[_i + 1] == 'k') if (_in_buffer[_i + 5] == 'g') if (_in_buffer[_i + 6] == 'h') return _i + 12; _i ++; } while (_i < _size); return -1; } // Takes a short and flips it from Big Endian to Little Endian or vice versa. short EndianFlip(short _input_short) { short retval; unsigned char *_input_ptr = (unsigned char *)&_input_short; unsigned char *_p_retval = (unsigned char *)&retval; for (int _i = 0; _i < 2; _i ++) { _p_retval[1 - _i] = _input_ptr[_i]; } return retval; } // Gets the offset of the data section of the XBox file, in bytes int DataStart(unsigned char *_in_buffer, int _size) { int _i = 0; // Loop through looking for the Blocks section do { if (_in_buffer[_i] == 'D') if (_in_buffer[_i + 1] == 'a') if (_in_buffer[_i + 3] == 'a') return _i + 8; _i ++; } while (_i < _size); return -1; } // This analyzes an NBT tag to make sure it is not broken. // _out_buffer: pointer to the start of the tag in memory. // _size: length of the buffer // _tag_type: the tag type as defined by Minecraft // returns: the total length of the tag, in bytes, or -1 if the tag is invalid. int ProcessTag(unsigned char *_out_buffer, int _size, unsigned char _tag_type) { int _n = 0, _i; int _tag_num; short _name_length; int _result; // Tag-specific processing now occurs switch (_tag_type) { case 1: // Bytes should be OK. Skip over this tag. return _n + 1; case 2: // Shorts can be encoded with the strange negative encoding // We check for this by seeing if there is an alphanumeric character for the next tag earlier than expected return _n + 2; case 3: // Integers can be encoded with the strange negative encoding // We check for this by seeing if there is an alphanumeric character for the next tag earlier than expected return _n + 4; case 4: // Longs can be encoded with the strange negative encoding // We check for this by seeing if there is an alphanumeric character for the next tag earlier than expected return _n + 8; case 5: // Floats should be OK. Skip this tag. return _n + 4; case 6: // Doubles should be OK; skip this tag. return _n + 8; case 7: // Bytes arrays should be OK. First, get the size, then skip over this tag. return _n + 4 + EndianFlip(*((int *)(_out_buffer + _n))); case 8: // Strings should be OK; first get the length, then skip this tag return _n + 2 + EndianFlip(*((short *)(_out_buffer + _n))); case 9: // Here are the tricky ones. This list type will require sub-processing. // First, get the length, then loop through all the entries _tag_type = _out_buffer[_n]; _n += 5; _tag_num = EndianFlip(*((int *)(_out_buffer + _n - 4))); for (_i = 0; _i < _tag_num; _i ++) { _result = ProcessTag(_out_buffer + _n, _size, _tag_type); if (_result == -1) return -1; _n += _result; } return _n; case 10: // This is a compound tag. All subtags will be processed in order. do { // Grab the tag type; break if this is the terminating tag (a null byte) _tag_type = _out_buffer[_n]; if (_tag_type == 0) return _n + 1; // Grab the name length, then skip over the name _name_length = EndianFlip(*((short *)(_out_buffer + _n + 1))); _n += _name_length + 3; // Process the sub tag. _result = ProcessTag(_out_buffer + _n, _size - _n, _tag_type); if (_result == -1) return -1; _n += _result; } while (_n <= _size); // If here, we ran out of buffer space before processing finished. // This indicates the tag was broken, and we have to bail. return -1; case 11: // This integer array should be OK. Skip this tag. return _n + 4 + 4 * EndianFlip(*((int *)(_out_buffer + _n))); default: // If we are here, there is a serious problem. // No tag should have an ID value above 11; return -1 to indicate processing failed. return -1; } } /* This method copies the Entities or TileEntities tag into an empty buffer and checks to make sure it is not broken. * _in_buffer: Pointer to the tag in the XBox file * _out_buffer: Pointer to the empty buffer into which to copy this tag. * _size: size of the tag in bytes. * returns: the length of the empty buffer used. */ int ProcessEntities(unsigned char *_in_buffer, unsigned char *_out_buffer, int _offset, int _size) { int _n = 7; int _i = 0; // Do the memory copy first memcpy(_out_buffer, _in_buffer + _offset, _size); // Now pass the TileEntities tag into the global processor. return ProcessTag(_out_buffer + _n + 8, _size, 9); } /* This looks at the top blocks in every column and makes an attempt at assigning a biome to each column. * Entire chunks will share ONE biome. Assignments are made based on which biome the chunk probably is. * The method considers only the TOP NON-AIR blocks in the chunk in making its decisions. * _in_buffer: pointer to the biomes block to modify. * _block_buffer: pointer to the blocks section corresponding to the biomes buffer above. * _data_buffer: pointer to the data section corresponding to the biomes buffer above. * _flag_no_reset: pointer to a buffer in which to store flags for biome columns that are not to be changed in the second pass. */ void ProcessBiomes(unsigned char *_in_buffer, unsigned char *_block_buffer, unsigned char *_data_buffer) { int n, _index; int _c_max, _max_ID; int max_height = 0; int min_height = 128; // First clear the input buffer and set all biomes to plains by default for (int _i = 0; _i < 256; _i ++) _in_buffer[_i] = 0xFF; for (int _i = 0; _i < 256; _i ++) local_biome_list[_i] = 0; for (int _i = 0; _i < 256; _i ++) _flag_no_reset[_i] = 0; // We want to start at an offset of 128 for this column _block_buffer += 128; _data_buffer += 64; // First pass - get top block and see if it is listed as a known biome for (int _i = 0; _i < 256; _i ++) { // Initial offset for this column n = -1; // Loop backwards through the block column until we find the first non-air block while (_block_buffer[n] == 0 && n > -256) n --; // If no non-air blocks were found, bail now if (n == -256) continue; // Look at the heights ONLY IF this is a DIRT block. if (_block_buffer[n] == 2 || _block_buffer[n] == 3) { if (128 + n > max_height) max_height = 128 + n; if (128 + n < min_height) min_height = 128 + n; } // Now get the index of this data / block pair if (n % 2 == 0) _index = LoNybble(_data_buffer[(n - 1) / 2]); else _index = HiNybble(_data_buffer[(n - 1) / 2]); // Add block to index _index |= _block_buffer[n] << 4; // Update the biome if it is known if (biome_list[_index] != 0xFF) { local_biome_list[biome_list[_index]] ++; _in_buffer[_i] = biome_list[_index]; } // Reset index _index = 0; // Move to next column _block_buffer += 128; _data_buffer += 64; } // Now get the most common biome in this chunk _c_max = -1; for (int _i = 0; _i < 256; _i ++) { if (local_biome_list[_i] > _c_max) { _c_max = local_biome_list[_i]; _max_ID = _i; } } // Default to plains if (_max_ID == 0) _max_ID = 1; // If the maximum height is at least 80, or if the tallest hill is 12 or more blocks higher than than the lowest valley... // ... this is extreme hills if (max_height >= 80 || max_height - min_height >= 12) _max_ID = 3; // If more than half of this chunk consists of water, we set it to ocean if (local_biome_list[254] > 128) _max_ID = 0; // If at least one hundred blocks in this chunk consist of sand, we set it to desert // Also, if at least one of the blocks in this chunk is sand and there is little water and dirt, this is desert if ( (local_biome_list[253] >= 100) || (local_biome_list[253] > 0 && local_biome_list[254] < 64 && local_biome_list[252] == 0 && local_biome_list[1] == 0) ) _max_ID = 2; // If any evidence was found this should be a swamp biome and there was no evidence for a jungle biome... // ... call this a swamp. (Ex: vines but no jungle leaves) if (local_biome_list[6] > 0) _max_ID = 6; if (local_biome_list[21] > 0) _max_ID = 21; // Second pass - assign all to the most common for this chunk. for (int _i = 0; _i < 256; _i ++) { if (_in_buffer[_i] != 30 && _in_buffer[_i] != 21) _in_buffer[_i] = _max_ID; } } /* Anvil format stores blocks in YZX format instead of XZY format. * This method takes an input buffer with blocks stored in XZY order and outputs the data in YZX format into an output buffer. * _in_buffer: the block data stored in XZY format. * _out_buffer: buffer to write the YZX format data. * _y_levels: The number of planes in the y direction to convert. */ bool xyz_flip(unsigned char *_in_buffer, unsigned char *_out_buffer, unsigned char _y_level) { unsigned char x, y, z; int c_ts = _y_level * 16, c_td; bool nair_flag = false; int c_inc = 0x70; // Loop through all y planes for (y = 0; y < 16; y ++) { // Each new plane begins at an index specified only by the plane's y coordinate. c_td = y; // There are 16 rows in this plane for (z = 0; z < 16; z ++) { // There are 16 cols in this plane for (x = 0; x < 16; x ++) { // If we find a non-air block, we set a flag to let us know this section is not empty if (_in_buffer[c_ts] != 0) nair_flag = true; // Move memory, then update coordinates _out_buffer[c_td] = _in_buffer[c_ts]; c_td += 1 << 8; c_ts ++; } // Increment the z counter in the destination integer and reset the x counter c_td += 1 << 4; c_td &= 0xFF; // Skip to the next column in the source block c_ts += c_inc; } } return nair_flag; } /* Anvil format stores blocks in YZX format instead of XZY format. * This method takes an input buffer with data nybbles stored in XZY order and outputs the data in YZX format into an output buffer. * _in_buffer: the block data stored in XZY format. * _out_buffer: buffer to write the YZX format data. * _y_levels: The number of planes in the y direction to convert. */ void xyz_nybbleflip(unsigned char *_in_buffer, unsigned char *_out_buffer, unsigned char _y_level) { unsigned char x, y, z; unsigned char st; int c_ts, c_td; int c_inc = 0x70; // Zero memory first for easier bitwise operation later for (c_ts = 0; c_ts < 0x800; c_ts ++) _out_buffer[c_ts] = 0; // Get the source pointer set c_ts = 16 * _y_level; // Loop through all y planes for (y = 0; y < 16; y ++) { // Each new plane begins at an index specified only by the plane's y coordinate. c_td = y; // There are 16 rows in this plane for (z = 0; z < 16; z ++) { // There are 16 cols in this plane for (x = 0; x < 16; x ++) { // Get the nybble we're interested in if (c_ts % 2 == 0) st = LoNybble(_in_buffer[c_ts / 2]); else st = HiNybble(_in_buffer[c_ts / 2]); // Update the target nybble if (c_td % 2 == 0) _out_buffer[c_td / 2] |= st; else _out_buffer[c_td / 2] |= st << 4; // Increment the pointer and repeat c_td += 1 << 8; c_ts ++; } // Increment the z counter in the destination integer and reset the x counter c_td += 1 << 4; c_td &= 0xFF; // Skip to next column in source block c_ts += c_inc; } } } /* XBox files are packed a little more tightly than simple compression. * The signal 'FF' followed by a number n and a byte B indicates that B is repeated n + 1 times. * If n is less than 3, the signal indicates that the byte 'FF' shall be repeated n + 1 times. * This method unpacks a given input buffer in this format to the output buffer given. * Return value is the amount of the output buffer used. * _input_buffer: the data to expand * _output_buffer: buffer in which to output the expanded data * _input_size: length of the input buffer to expand */ unsigned long ExpandX(unsigned char *_input_buffer, unsigned char *_output_buffer, unsigned long _input_size) { unsigned long retval = 0; unsigned short _limit; unsigned char _repeat; // Begin looping through the input buffer for (unsigned long _i = 0; _i < _input_size; _i ++) { // Check to see if the character at this place is FF if (_input_buffer[_i] == 0xFF) { // If here, increment outer counter and add the repeated bytes. // Note that the (n + 1)th repeat is taken care of after this if statement. _i ++; _repeat = _input_buffer[_i + 1]; _limit = (unsigned short)_input_buffer[_i]; if (_limit >= 3) { for (int _n = 0; _n <= _limit; _n ++) { _output_buffer[retval] = _repeat; retval ++; } } else { _i --; for (int _n = 0; _n <= _limit; _n ++) { _output_buffer[retval] = 0xFF; retval ++; } } // Increment the outer counter again to skip the number of repeats and the repeated byte _i ++; } else { _output_buffer[retval] = _input_buffer[_i]; retval ++; } } // Return the length of outputted content. return retval; } /* The entry point for the console application. * Call this application with the following: * ExpandX _input_directory _output_directory _x_pos _z_pos [Nether|End] * _input_directory: path to the directory containing the packed XBox files. * _output_directory: path to the directory in which to output the converted PC files. * _x_pos: use '-' if this region is negative in the x direction; any other string otherwise. * _z_pos: use '-' if this region is negative in the z direction; any other string otherwise. * [Nether|End]: Optional; use Nether for a Nether chunk and End for an End chunk. DO NOT SET for an overworld chunk. */ int _tmain(int argc, _TCHAR* argv[]) { // Allocate memory xbuffer = new unsigned char[409600]; nbuffer = new unsigned char[409600]; rbuffer = new unsigned char[409600]; ebuffer = new unsigned char[409600]; sbuffer = new unsigned char[409600]; afile_path = new char[200]; aoutput_path = new char[200]; status = new char[300]; biome_list = new unsigned char[4096]; // Load xstart and zstart if (argc < 5) { xstart = 0; zstart = 0; } else { xstart = (*argv[3] == '-' ? 32 : 0); zstart = (*argv[4] == '-' ? 32 : 0); } // If this is the Nether or End, there are less chunks expected if (argc > 5) missing = -943; // Load the Anvil template file into memory input_file_f = fopen("anvil_template.dat", "r"); fseek(input_file_f, 0L, SEEK_END); template_length = ftell(input_file_f); fseek(input_file_f, 0L, SEEK_SET); fread(rbuffer, sizeof(char), template_length, input_file_f); fclose(input_file_f); // Load the section template file into memory input_file_f = fopen("section_template.dat", "r"); fseek(input_file_f, 0L, SEEK_END); section_template_length = ftell(input_file_f); fseek(input_file_f, 0L, SEEK_SET); fread(sbuffer, sizeof(char), section_template_length, input_file_f); fclose(input_file_f); // This assigns biome IDs to specific block types for later reference. populate_biomes(biome_list); // Open the log file log_file_f = fopen("log.txt", "w+b"); // Loop through input directory for (iol = 0; iol < 1024; iol ++) { // Get the input and output paths sprintf(afile_path, "%ls\\%d.dat", argv[1], iol); sprintf(aoutput_path, "%ls\\%d.dat", argv[2], iol); // Next input file input_file_f = fopen(afile_path, "rb"); if (input_file_f != NULL) { // Get the size of the file fseek(input_file_f, 0L, SEEK_END); file_size = ftell(input_file_f); // Read in all data fseek(input_file_f, 0L, SEEK_SET); bytes_read = fread(nbuffer, sizeof(char), file_size, input_file_f); if (bytes_read != file_size) printf("Warning: could not read %d bytes from file %d.\n", file_size, iol); fclose(input_file_f); // Perform the actual expansion file_size = ExpandX(nbuffer, xbuffer, file_size); // Get the start points for the blocks, data, skylight, and blocklight blocks blockstart = BlockStart(xbuffer, file_size); datastart = DataStart(xbuffer, file_size); blocklightstart = BlockLight(xbuffer, file_size); skylightstart = SkyLight(xbuffer, file_size); // If there is no block or data section, we delete this corrupted file and abort conversion of this chunk if (blockstart == -1 || datastart == -1) { remove(aoutput_path); remove(afile_path); sprintf(status, "Warning: chunk missing blocks and/or data; not converted at x=%d, z=%d.\r\n\0", (iol % 32) - xstart, (int)(iol / 32) - zstart); fwrite(status, 1, strlen(status), log_file_f); continue; } // If here, there was no issue with the chunk file, so we open the output file output_file_f = fopen(aoutput_path, "w+b"); // Attempt to correct the biomes ProcessBiomes(rbuffer + 0x28, xbuffer + blockstart, xbuffer + datastart); // Output xpos and zpos into the template xpos = EndianFlip((iol % 32) - xstart); zpos = EndianFlip(iol / 32 - zstart); memcpy(rbuffer + 0x15B, &xpos, 4); memcpy(rbuffer + 0x166, &zpos, 4); // If this is the nether, we replace all bedrock with air and set biome to "hell" // For the end, set biome to "Sky" if (argc > 5) { if (wcscmp(argv[5], L"Nether") == 0) { for (i = blockstart; i < blockstart + 0x8000; i ++) if ((i - blockstart) % 128 >= 3) if (xbuffer[i] == 7) xbuffer[i] = 87; for (i = 0x28; i < 0x128; i ++) rbuffer[i] = 8; } else if (wcscmp(argv[5], L"End") == 0) for (i = 0x28; i < 0x128; i ++) rbuffer[i] = 9; } // Find and process the Entities section zpos = -1; for (int n = file_size - 1; n > 0; n --) { if (*(xbuffer + n) == 's' && *(xbuffer + n - 1) == 'e' && *(xbuffer + n - 2) == 'i' && *(xbuffer + n - 3) == 't' && *(xbuffer + n - 8) == 8) { zpos = 11 + ProcessEntities(xbuffer, ebuffer, n - 14, file_size - n + 14); break; } } // Begin assembling the final file memcpy(nbuffer, rbuffer, 0xB); if (zpos != 10) { memcpy(nbuffer + 0xB, ebuffer + 4, zpos); memcpy(nbuffer + 0xB + zpos, rbuffer + 0x1B, 0x14F); } else { memcpy(nbuffer + 0xB, nbuffer + 0xB, 0x15F); zpos = 0x10; } // Find the tile entity section and process for (int n = file_size - 1; n > 0; n --) { if (*(xbuffer + n) == 's' && *(xbuffer + n - 1) == 'e' && *(xbuffer + n - 2) == 'i' && *(xbuffer + n - 3) == 't' && *(xbuffer + n - 12) == 12) { xpos = 15 + ProcessEntities(xbuffer, ebuffer, n - 14, file_size - n + 14); break; } } // Add the TileEntities section if (xpos != 14) { memcpy(nbuffer + 0x15A + zpos, ebuffer, xpos); memcpy(nbuffer + 0x15A + zpos + xpos, rbuffer + 0x17E, 0x44C); } else { memcpy(nbuffer + 0x15A + zpos, rbuffer + 0x16A, 0x460); xpos = 0x14; } // Use zpos as the total length of the upper template file for now zpos += 0x5A6 + xpos; // Get the number of y planes in the blocks segment y_plane = EndianFlip(*(int *)(xbuffer + blockstart - 4)) / 0x1000; // Reset section count to zero. s_num = 0; h = 0; // Now, flip the XYZ order of all blocks in each segment for (xpos = 0; xpos < y_plane; xpos ++) { // If this is the 8th plane up from the bottom, we advance to the second half of the XBox blocks section. if (xpos == 8) h = 0x8000; // Perform the flip of blocks. If this if statement returns false, the section was empty. // Empty sections are not stored, so if this section is empty we skip it now. if (!xyz_flip(xbuffer + blockstart + h, sbuffer + 0x183D, xpos % 8)) continue; // If here, this section is not empty, so now we flip the data, blocklight, and skylight sections. xyz_nybbleflip(xbuffer + datastart + (h / 2), sbuffer + 0xB, xpos % 8); xyz_nybbleflip(xbuffer + blocklightstart + (h / 2), sbuffer + 0x102B, xpos % 8); xyz_nybbleflip(xbuffer + skylightstart + (h / 2), sbuffer + 0x81A, xpos % 8); // This assigns the Y coordinate of the section. *(sbuffer + 0x182F) = (unsigned char)xpos; // Move this section to the end of the template file memcpy(nbuffer + zpos + 0x283E * s_num, sbuffer, 0x283E); s_num ++; } // Set the final two bytes to zero and set the number of sections correctly for (xpos = 0; xpos < 3; xpos ++) *(nbuffer + zpos + 0x283E * s_num + xpos) = 0; *(nbuffer + zpos - 1) = s_num; // Here we double check the status of this chunk; if it is broken, we remove it if (ProcessTag(nbuffer + 3, zpos + 0x283E * s_num - 1, 10) != zpos + 0x283E * s_num - 1) { remove(aoutput_path); remove(afile_path); sprintf(status, "Warning: broken chunk not converted at x=%d, z=%d.\r\n\0", (iol % 32) - xstart, (int)(iol / 32) - zstart); fwrite(status, 1, strlen(status), log_file_f); broken ++; } else { fwrite(nbuffer, 1, zpos + 0x283E * s_num + 2, output_file_f); } // Reset buffer for next chunk memcpy(nbuffer, rbuffer, template_length); // Close handle to this output file fclose(output_file_f); } else missing ++; } // Write final info and close log file if (missing < 0) missing = 0; sprintf(status, "Chunk conversion complete for region x=%d, z=%d. Chunk details:\r\n\0", xstart == 32 ? -1 : 0, zstart == 32 ? -1 : 0); fwrite(status, 1, strlen(status), log_file_f); sprintf(status, "\tOK: %d\r\n\tMissing: %d\r\n\tBroken: %d\r\n\0", 1024 - missing - broken - (argc > 5 ? 943 : 295), missing, broken); fwrite(status, 1, strlen(status), log_file_f); fclose(log_file_f); // Free buffers delete [409600] xbuffer; delete [409600] nbuffer; delete [409600] rbuffer; delete [409600] ebuffer; delete [409600] sbuffer; delete [200] aoutput_path; delete [200] afile_path; delete [300] status; delete [4096] biome_list; return 0; }