Skip to content

Instantly share code, notes, and snippets.

@jord-nijhuis
Last active August 1, 2018 16:07
Show Gist options
  • Save jord-nijhuis/4769774a0c08ae8e999ed1bb73ae567a to your computer and use it in GitHub Desktop.
Save jord-nijhuis/4769774a0c08ae8e999ed1bb73ae567a to your computer and use it in GitHub Desktop.
Convert MCSharp .lvl savefiles to a format MCEdit understands
#!/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