Last active
August 1, 2018 16:07
-
-
Save jord-nijhuis/4769774a0c08ae8e999ed1bb73ae567a to your computer and use it in GitHub Desktop.
Convert MCSharp .lvl savefiles to a format MCEdit understands
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env python | |
import gzip | |
import os | |
import struct | |
import argparse | |
""" | |
This program converts save files from MCSharp (*.lvl) files to *.raw.lvl-files that MCEdit can understand. | |
The main use case for this is to further convert said saves to a Minecraft Release World. | |
""" | |
""" | |
The header of a MCSharp .lvl file is 18 bytes. | |
See https://github.com/Voziv/MCSharp/blob/master/MCSharp/World/Map.cs | |
""" | |
header_size = 18 | |
""" | |
Maps all the MCSharp blocks to vanilla blocks. Each key is a MCSharp block and each value a vanilla block | |
See https://minecraft.gamepedia.com/Java_Edition_data_values/Classic for the existing blocks and | |
https://github.com/Voziv/MCSharp/blob/master/MCSharp/World/Block.cs for the blocks that MCSharp uses | |
""" | |
conversions = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, | |
29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 45, 4, 1, 1, 2, 3, 5, | |
6, 12, 13, 14, 15, 16, 18, 19, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 37, 38, 39, | |
40, 41, 42, 43, 44, 46, 47, 48, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 20, 49, 45, 4, 1, | |
0, 8, 0xff, 0xff, 0xff, 5, 5, 10, 49, 20, 36, 2, 3, 5, 6, 10, 12, 13, 14, 15, 16, 17, 18, 19, 21, 22, 23, | |
24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 37, 38, 39, 40, 41, 42, 43, 44, 46, 47, 48, 0xff, 0xff, | |
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, | |
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, | |
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0, 5, 0, 0, 0, 49, 20, 36, 36, 45, 4, 1, 1, 2, 3, | |
5, 6, 12, 13, 14, 15, 16, 18, 19, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 37, 38, 39, | |
40, 41, 42, 44, 43, 46, 47, 48, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff] | |
def get_argument_parser(): | |
parser = argparse.ArgumentParser(description='Removes the custom blocks from the MCSharp level-format and saves it ' | |
'in a format that MCEdit understands.') | |
parser.add_argument( | |
'--path', | |
'-p', | |
default=".", | |
help="The directory in which to look for .lvl-files. Default is the working directory" | |
) | |
parser.add_argument( | |
'--recursive', | |
'-r', | |
action='store_true', | |
help="Also search for .lvl-files in subdirectories of the input-directory" | |
) | |
parser.add_argument( | |
'--output', | |
'-o', | |
default=".", | |
help="Store the output in the selected folder, default is to store it next to the original file" | |
) | |
return parser | |
def get_levels(path, recursive): | |
""" | |
:param path: The path to look for .lvl-files | |
:param recursive: Whether to look into subfolders | |
:return: A list of paths to .lvl-files | |
""" | |
if recursive: | |
files = [] | |
for root, directories, filenames in os.walk(path): | |
files.extend([os.path.join(root, file) for file in filenames]) | |
else: | |
files = [file for file in os.listdir(path) if os.path.isfile(file)] | |
# Filter out any fils that do not match .lvl (and are .raw.lvl => already converted) | |
return [file for file in files if file[-3:] == "lvl" and file[-7:] != "raw.lvl"] | |
def get_level_size(level_data): | |
""" | |
Read the size of the level from the header | |
See https://github.com/Voziv/MCSharp/blob/master/MCSharp/World/Map.cs for more information on the map format | |
:param level_data: The level data | |
:return: A tuple containing the size in the following order: 1) width 2) length 3) height | |
""" | |
level_data.seek(2) | |
width = struct.unpack("H", level_data.read(2))[0] | |
length = struct.unpack("H", level_data.read(2))[0] | |
height = struct.unpack("H", level_data.read(2))[0] | |
return width, length, height | |
def get_header(level_data): | |
""" | |
Returns the first bytes that contain the header | |
See https://github.com/Voziv/MCSharp/blob/master/MCSharp/World/Map.cs for more information on the map format | |
:param level_data: The level data | |
:return: The header | |
""" | |
level_data.seek(0) | |
return level_data.read(header_size) | |
def get_blocks(level_data): | |
""" | |
Returns all the blocks that are stored in the map | |
See https://github.com/Voziv/MCSharp/blob/master/MCSharp/World/Map.cs for more information on the map format | |
:param level_data: The level data | |
:return: A list containing all the blocks, each element is a block type | |
""" | |
level_data.seek(header_size) | |
size = get_level_size(level_data) | |
length = size[0] * size[1] * size[2] | |
return struct.unpack("B" * length, level_data.read(length)) | |
def map_custom_blocks(blocks): | |
""" | |
This method maps the custom blocks back to normal blocks | |
See https://minecraft.gamepedia.com/Java_Edition_data_values/Classic for the existing blocks and | |
https://github.com/Voziv/MCSharp/blob/master/MCSharp/World/Block.cs for the blocks that MCSharp uses | |
:param blocks: The blocks to map | |
:return: A list containing all the blocks where each element is a vanilla block | |
""" | |
return [conversions[block] for block in blocks] | |
def save_level_raw(header, blocks, file): | |
""" | |
Saves the header and blocks to the given file | |
This saves the level in a raw format; no GZIP is used | |
:param header: The header to save | |
:param blocks: The blocks to save | |
:param file: The file to save the headers and blocks to | |
""" | |
with open(file, "wb") as level_data: | |
level_data.write(header) | |
level_data.write(struct.pack("B" * len(blocks), *blocks)) | |
def convert_level(level_path, output): | |
""" | |
Convert this level to a vanilla Minecraft Classic level | |
:param level_path: The MCSharp level | |
:param output: The output directory | |
""" | |
print("Opening {0}".format(level_path)) | |
# Open the level through GZIP | |
with gzip.open(level_path) as level_data: | |
size = get_level_size(level_data) | |
print("\tSize: w={}, h={}, l={}".format(size[0], size[1], size[2])) | |
header = get_header(level_data) | |
print("\tConverting blocks") | |
blocks = map_custom_blocks(get_blocks(level_data)) | |
output_level_path = "{name} {width} {length} {height}.raw.lvl".format( | |
name=level_path[:-4] if output is None else "{}/{}.raw.lvl".format( | |
output, | |
os.path.basename(level_path) | |
), | |
width=size[0], | |
length=size[1], | |
height=size[2] | |
) | |
print("\tSaving converted level to {}".format(output_level_path)) | |
if os.path.isfile(output_level_path): | |
print("\tWARNING: FILE ALREADY EXISTS, OVERWRITING") | |
save_level_raw(header, blocks, output_level_path) | |
print("\tDone") | |
def main(): | |
args = get_argument_parser().parse_args() | |
if not args.output is None and not os.path.isdir(args.output): | |
os.makedirs(args.output) | |
for level_path in get_levels(args.path, args.recursive): | |
convert_level(level_path, args.output) | |
if __name__ == "__main__": | |
main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment