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
Exemple #2
0
    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}
Exemple #3
0
    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'))