def update_tile_viewer(values): try: # Getting the tile from the object group based on the text in the selected list item tile_index = int( re.search('^(\d+):', values['-TILE LIST-'][0]).group(1)) tile_dict = objectgroup['objects'][tile_index] t = Tile.from_dict(tile_dict) img_data = np.array_split( [region_plane_colors[v] for v in t.region_plane], t.size[2]) # Get / generate and cache tile height map, and apply shading to the image if values['-HEIGHTMAP-']: if not '_height' in tile_dict: tile_dict['_height'] = t.get_height_map() zr = range(0, t.size[2]) for x in range(0, t.size[0]): for z in zr: img_data[z][x] = [ min( 255, v * (0.25 + 1.25 * tile_dict['_height'][z * t.size[0] + x] / t.size[1])) for v in img_data[z][x] ] # Draw boundaries if values['-BOUNDARIES-']: for b in t.boundaries: img_data[b.z][b.x] = boundaries_color # Convert array to Pillow image img = Image.fromarray(np.array(img_data, dtype=np.uint8), 'RGB') # Get image scale from slider scale = values['-SCALE-'] scaled_size = (int(t.size[0] * scale), int(t.size[2] * scale)) # Update UI with text and image window['-TOUT-'].update(values['-TILE LIST-'][0]) window['-IMAGE-'].update( data=ImageTk.PhotoImage(img.resize(scaled_size, Image.NEAREST))) except: if values['-TILE LIST-'][0]: window['-TOUT-'].update('ERROR while loading tile!') else: pass
def convert(self, dict_format=True): """Returns a Dungeons object group, or a list of tiles, based on the Java Edition world.""" with open(self.world_dir + '/objectgroup.json') as json_file: tiles = [ Tile.from_dict(t) for t in json.load(json_file)['objects'] ] world = JavaWorldReader(self.world_dir) air_blocks = ['minecraft:air', 'minecraft:cave_air'] player_heads = ['minecraft:player_head', 'minecraft:player_wall_head'] # Apologies for the confusing variable names below. Let me explain what they mean: # ax, ay, az are absolute coordinates. These are the world coordinates of the block in Java edition. # tx, ty, tz are tile coordinates. These are relative to the tile's position. # cx and cz are chunk coordinates. Chunks hold 16x256x16 blocks. # yi and zi are iterable ranges for the Y and Z axes. for tile in tiles: # Creating these ranges here is faster than doing it for each slice/column of the tile zi = range(tile.size[2]) yi = range(min(256, tile.size[1])) doors = [] # For each slice of the tile along the X axis... for tx in range(tile.size[0]): ax = tx + tile.pos[0] cx = ax // 16 # For each column of the slice along the Z axis... for tz in zi: az = tz + tile.pos[2] cz = az // 16 chunk = world.chunk(cx, cz) if chunk is None: print( f'Warning: Missing chunk at {cx},{cz}. Blocks in this chunk will be ignored.' ) continue # TODO: Handle boundaries differently. With the current implemenation, # boundaries that go outside of the tile (most of the vanilla ones do...) # will lose the parts that are outside of the tile. current_boundary = None # For each block in the column along the Y axis... for ty in yi: ay = ty + tile.pos[1] # Get the block from the Java world chunk java_block = chunk.get_block(ax % 16, ay, az % 16) namespaced_id = java_block.namespace + ':' + java_block.id # There's no reason to keep going if the block is just air if namespaced_id in air_blocks: continue # Handle blocks that are used for special things in this converter, like tile doors and boundaries if namespaced_id == 'minecraft:structure_block': entity = find_tile_entity(chunk, ax, ay, az) if entity is None: continue if entity['name'].value.startswith('door:'): door = Door(pos=[ tx + entity['posX'].value, ty + entity['posY'].value, tz + entity['posZ'].value ], size=[ entity['sizeX'].value, entity['sizeY'].value, entity['sizeZ'].value ]) if len(entity['name'].value) > 5: door.name = entity['name'].value[5:] if len(entity['metadata'].value) > 2: try: door_info = json.loads( entity['metadata'].value) if 'tags' in door_info: door.tags = door_info['tags'] except: print( f'Warning: Invalid JSON in structure block metadata at {ax},{ay},{az}' ) tile.doors.append(door) elif entity['name'].value.startswith('region:'): tile_region = Region( # Note: This is a Tile.Region, not an anvil.Region pos=[ tx - entity['posX'].value, ty - entity['posY'].value, tz - entity['posZ'].value ], size=[ entity['sizeX'].value, entity['sizeY'].value, entity['sizeZ'].value ]) if len(entity['name'].value) > 7: tile_region.name = entity['name'].value[7:] if len(entity['metadata'].value) > 2: try: region_info = json.loads( entity['metadata'].value) if 'tags' in region_info: tile_region.tags = region_info[ 'tags'] if 'type' in region_info: tile_region.type = region_info[ 'type'] except: print( f'Warning: Invalid JSON in structure block metadata at {ax},{ay},{az}' ) tile.regions.append(tile_region) continue if namespaced_id in player_heads: tile_region = Region( [tx, ty, tz] ) # Note: This is a Tile.Region, not an anvil.Region tile_region.name = 'playerstart' tile_region.tags = 'playerstart' tile_region.type = 'trigger' tile.regions.append(tile_region) continue if namespaced_id == self.boundary_block: # Check if this block is connected to the last boundary found in this column if current_boundary is None or current_boundary.y + current_boundary.h != ty: current_boundary = Boundary(tx, ty, tz, 1) tile.boundaries.append(current_boundary) else: current_boundary.h += 1 continue # Mapped blocks have both a Java namespaced ID + state and a Dungeons ID + data value mapped_block = find_java_block(java_block) if mapped_block is None: props = {} for prop in java_block.properties: props[prop] = java_block.properties[prop].value print( f'Warning: {java_block}{json.dumps(props)} is not mapped to anything. It will be replaced by air.' ) continue # Check if the block has a data value if len(mapped_block['dungeons']) > 1: tile.set_block( tx, ty, tz, block_id=mapped_block['dungeons'][0], block_data=mapped_block['dungeons'][1]) else: tile.set_block( tx, ty, tz, block_id=mapped_block['dungeons'][0]) if dict_format: return {'objects': [t.dict() for t in tiles]} else: return {'objects': tiles}
def convert(self): """Creates a Java Edition world in the world directory from the object group.""" # TODO: Converting to a Java world should be done one region or maybe even # one sub-region at a time. Right now, all regions are kept # in memory until the conversion process is done, which means the memory # usage can be massive for bigger object groups. # anvil-parser doesn't actually support loading a region from a file and # then editing it and writing it to a file again. Regions loaded from a # file are read-only, and the regions that can be edited start out empty. region_cache = {} block_cache = {} def get_region(rx, rz): if f'{rx}x{rz}' in region_cache: return region_cache[f'{rx}x{rz}'] else: region_cache[f'{rx}x{rz}'] = anvil.EmptyRegion(rx, rz) return region_cache[f'{rx}x{rz}'] structure_block = anvil.Block('minecraft', 'structure_block') player_head = anvil.Block('minecraft', 'player_head') def find_room_for_structure_block(area, get_block): xi = range(area[0]) zi = range(area[1]) # Blocks that will break if a stucture block is placed on top of them breakable_blocks = [0x3c, 0xc6] # Check the area and blocks above it for y in range(49): for x in xi: for z in zi: if get_block(x, y, z) == 0 and not get_block( x, y - 1, z) in breakable_blocks: return (x, y, z) # Check blocks below the area for y in range(-1, -49, -1): for x in xi: for z in zi: if get_block(x, y, z) == 0 and not get_block( x, y - 1, z) in breakable_blocks: return (x, y, z) # No room found :( return None if isinstance(self.objectgroup, dict): og = self.objectgroup else: # If objectgroup is a file path, parse the json file with open(self.objectgroup) as json_file: og = json.load(json_file) for tile_dict in og['objects']: if isinstance(tile_dict, Tile): tile = tile_dict else: tile = Tile.from_dict(tile_dict) zi = range(tile.size[2]) yi = range(min(256, tile.size[1])) # For each slice of the tile along the X axis... for tx in range(tile.size[0]): ax = tx + tile.pos[0] rx = ax // 512 # For each column of the slice along the Z axis... for tz in zi: az = tz + tile.pos[2] rz = az // 512 region = get_region(rx, rz) # For each block in the column along the Y axis... for ty in yi: ay = ty + tile.pos[1] # Skip this block if it's outside of the world bounds if ay < 0 or ay >= 256: continue bidx = tile.get_block_index(tx, ty, tz) # If the block is just air, we don't need to do anything if tile.blocks[bidx] == 0: continue # Get the Java block from the cache if it's there bcid = tile.blocks[bidx] << 4 | tile.block_data[bidx] if bcid in block_cache: java_block = block_cache[bcid] else: # If not, find it and add it to the cache to speed things up later mapped_block = find_dungeons_block( tile.blocks[bidx], tile.block_data[bidx]) if mapped_block is None: print( f'Warning: {tile.blocks[bidx]}:{tile.block_data[bidx]} is not mapped to anything. It will be replaced by air.' ) continue if len(mapped_block['java']) > 1: java_block = anvil.Block( *mapped_block['java'][0].split(':', 1), mapped_block['java'][1]) else: java_block = anvil.Block( *mapped_block['java'][0].split(':', 1)) block_cache[bcid] = java_block # Once we have the Java block, add it to the region region.set_block(java_block, ax, ay, az) # TODO: Block post-processing to fix fences, walls, stairs, and more converter_blocks = [] # Add the tile doors to the world for door in tile.doors: def get_block(x, y, z): tx = x + door.pos[0] ty = y + door.pos[1] tz = z + door.pos[2] if f'{tx},{ty},{tz}' in converter_blocks: return -1 if tx >= 0 and tx < tile.size[ 0] and ty >= 0 and ty < tile.size[ 1] and tz >= 0 and tz < tile.size[2]: return tile.get_block_id(tx, ty, tz) else: return 0 pos = find_room_for_structure_block(door.size[::2], get_block) if pos is None: if hasattr(door, 'name'): print( f'Warning: No room to place structure block for door: {door.name}' ) else: print( f'Warning: No room to place structure block for unnamed door.' ) else: tpos = [p + d for p, d in zip(pos, door.pos)] if tpos[0] >= 0 and tpos[0] < tile.size[0] and tpos[ 1] >= 0 and tpos[1] < tile.size[1] and tpos[ 2] >= 0 and tpos[2] < tile.size[2]: apos = [p + t for p, t in zip(tpos, tile.pos)] region = get_region(apos[0] // 512, apos[2] // 512) region.set_block(structure_block, *apos) metadata = door.dict() metadata.pop('name', None) metadata.pop('pos', None) metadata.pop('size', None) if hasattr(door, 'name'): tile_entity = structure_block_entity( *apos, 'SAVE', f'door:{door.name}', json.dumps(metadata), *[-v for v in pos], *door.size) else: tile_entity = structure_block_entity( *apos, 'SAVE', 'door:', json.dumps(metadata), *[-v for v in pos], *door.size) region.chunks[apos[2] // 16 % 32 * 32 + apos[0] // 16 % 32].tile_entities.append(tile_entity) converter_blocks.append( f'{tpos[0]},{tpos[1]},{tpos[2]}') if self.region_structure_blocks: # Add the tile regions to the world for tile_region in tile.regions: # playerstart regions just use a player head instead of a structure block if self.playerstart_to_player_head and hasattr( tile_region, 'tags') and tile_region.tags == 'playerstart': ax = tile.pos[0] + tile_region.pos[0] ay = tile.pos[1] + tile_region.pos[1] az = tile.pos[2] + tile_region.pos[2] rx = ax // 512 rz = az // 512 region = get_region(rx, rz) region.set_block(player_head, ax, ay, az) tile_entity = TAG_Compound() tile_entity.tags.extend([ TAG_String(name='id', value='minecraft:skull'), TAG_Byte(name='keepPacked', value=0), TAG_Int(name='x', value=ax), TAG_Int(name='y', value=ay), TAG_Int(name='z', value=az) ]) region.chunks[az // 16 % 32 * 32 + ax // 16 % 32].tile_entities.append(tile_entity) converter_blocks.append( f'{tile_region.pos[0]},{tile_region.pos[1]},{tile_region.pos[2]}' ) elif tile_region.size[0] <= 48 and tile_region.size[ 1] <= 48 and tile_region.size[2] <= 48: def get_block(x, y, z): tx = x + tile_region.pos[0] ty = y + tile_region.pos[1] tz = z + tile_region.pos[2] if f'{tx},{ty},{tz}' in converter_blocks: return -1 if tx >= 0 and tx < tile.size[ 0] and ty >= 0 and ty < tile.size[ 1] and tz >= 0 and tz < tile.size[2]: return tile.get_block_id(tx, ty, tz) else: return 0 pos = find_room_for_structure_block( tile_region.size[::2], get_block) if pos is None: if hasattr(tile_region, 'name'): print( f'Warning: No room to place structure block for region: {tile_region.name}' ) else: print( f'Warning: No room to place structure block for unnamed region.' ) else: tpos = [ p + d for p, d in zip(pos, tile_region.pos) ] if tpos[0] >= 0 and tpos[0] < tile.size[ 0] and tpos[1] >= 0 and tpos[ 1] < tile.size[1] and tpos[ 2] >= 0 and tpos[2] < tile.size[2]: apos = [p + t for p, t in zip(tpos, tile.pos)] region = get_region(apos[0] // 512, apos[2] // 512) region.set_block(structure_block, *apos) metadata = tile_region.dict() metadata.pop('name', None) metadata.pop('pos', None) metadata.pop('size', None) if hasattr(tile_region, 'name'): tile_entity = structure_block_entity( *apos, 'SAVE', f'region:{tile_region.name}', json.dumps(metadata), *[-v for v in pos], *tile_region.size) else: tile_entity = structure_block_entity( *apos, 'SAVE', 'region:', json.dumps(metadata), *[-v for v in pos], *tile_region.size) region.chunks[ apos[2] // 16 % 32 * 32 + apos[0] // 16 % 32].tile_entities.append(tile_entity) converter_blocks.append( f'{tpos[0]},{tpos[1]},{tpos[2]}') # Add the tile boundaries to the world for boundary in tile.boundaries: ax = tile.pos[0] + boundary.x az = tile.pos[2] + boundary.z rx = ax // 512 rz = az // 512 region = get_region(rx, rz) for by in range(boundary.h): ay = tile.pos[1] + boundary.y + by region.set_block(self.boundary_block, ax, ay, az) # Write regions to files os.makedirs(os.path.join(self.world_dir, 'region'), exist_ok=True) for k in region_cache: region_cache[k].save( os.path.join( self.world_dir, f'region/r.{region_cache[k].x}.{region_cache[k].z}.mca')) # For convenience, write the object group to objectgroup.json in the world # directory, so JavaWorldToObjectGroup can convert the world back to an # object group without any changes. og_copy = json.loads(json.dumps(og)) # faster than copy.deepcopy for tile in og_copy['objects']: tile.pop('blocks', None) tile.pop('boundaries', None) tile.pop('doors', None) tile.pop('height-plane', None) if self.region_structure_blocks and 'regions' in tile: # Keep only regions that are too big turn into structure blocks tile['regions'] = [ r for r in tile['regions'] if r['size'][0] > 48 or r['size'][1] > 48 or r['size'][2] > 48 ] with open(os.path.join(self.world_dir, 'objectgroup.json'), 'w') as out_file: out_file.write(stringify(og_copy)) # Create level.dat file level = NBTFile('level_template.dat', 'rb') level['Data']['LevelName'].value = self.level_name level['Data']['LastPlayed'].value = int(time.time() * 1000) # Place the player spawn above the center of the first tile. # This could probably be made a bit smarter, since the center of the tile # might still be above the void. For now, this faster solution will have to do. level['Data']['SpawnX'].value = int(og['objects'][0]['pos'][0] + og['objects'][0]['size'][0] * 0.5) level['Data']['SpawnY'].value = min( 255, og['objects'][0]['pos'][1] + og['objects'][0]['size'][1]) level['Data']['SpawnZ'].value = int(og['objects'][0]['pos'][2] + og['objects'][0]['size'][2] * 0.5) level.write_file(os.path.join(self.world_dir, 'level.dat'))