Example #1
0
 def _read_tilemap_data(self, data: bytes):
     """Handles the decompressed tile data returned by the BPC_TILEMAP decompressor."""
     tilemap = []
     # The first chunk is not stored, but is always empty
     for i in range(0, self.tiling_width*self.tiling_height):
         tilemap.append(TilemapEntry.from_int(0))
     for i, entry in enumerate(iter_bytes(data, BPC_TILEMAP_BYTELEN)):
         tilemap.append(TilemapEntry.from_int(int.from_bytes(entry, 'little')))
     return tilemap
Example #2
0
    def tiles_to_pil(self, layer: int, palettes: List[List[int]], width_in_tiles=20, single_palette=None) -> Image.Image:
        """
        Convert all individual tiles of the BPC into one PIL image.
        The image contains all tiles next to each other, the image width is tile_width tiles.
        The resulting image has one large palette with all palettes merged together.

        The tiles are exported with the palette of the first placed tile or 0 if tile is not in tilemap,
        for easier editing. The result image contains a palette that consists of all palettes merged together.

        If single_palette is not None, all palettes are exported using the palette no. stored in single_palette.

        The first tile is a NULL tile. It is always empty, even when re-imported.
        """

        # create a dummy tile map containing all the tiles
        dummy_tile_map = []
        for i in range(0, self.layers[layer].number_tiles+1):
            dummy_tile_map.append(TilemapEntry(
                idx=i,
                pal_idx=single_palette if single_palette is not None else self._get_palette_for_tile(layer, i),
                flip_x=False,
                flip_y=False
            ))
        width = width_in_tiles * BPC_TILE_DIM
        height = math.ceil((self.layers[layer].number_tiles+1) / width_in_tiles) * BPC_TILE_DIM

        return to_pil(
            dummy_tile_map, self.layers[layer].tiles, palettes, BPC_TILE_DIM, width, height
        )
Example #3
0
    def tiles_to_pil_separate(self, palette, width_in_tiles=20) -> List[Image.Image]:
        """
        Exports the BPA as an image, where each row of 8x8 tiles is the
        animation set for a single tile. The 16 color palette passed is used to color the image.
        """
        dummy_tile_map = []

        # create a dummy tile map containing all the tiles
        for tile_idx in range(0, self.number_of_tiles * self.number_of_frames):
            dummy_tile_map.append(TilemapEntry(
                idx=tile_idx,
                pal_idx=0,
                flip_x=False,
                flip_y=False,
                ignore_too_large=True
            ))
        width = width_in_tiles * BPA_TILE_DIM
        height = math.ceil(self.number_of_tiles / width_in_tiles) * BPA_TILE_DIM

        images = []
        for frame_start in range(0, self.number_of_tiles * self.number_of_frames, self.number_of_tiles):
            images.append(to_pil(
                dummy_tile_map[frame_start:frame_start+self.number_of_tiles], self.tiles, [palette], BPA_TILE_DIM, width, height
            ))
        return images
Example #4
0
 def re_fill_chunks(self):
     if len(self.chunks) > 400:
         raise ValueError(
             _("A dungeon background or tilemap can not have more than 400 chunks."
               ))
     self.chunks += [[TilemapEntry.from_int(0)
                      for _ in range(0, 9)]] * (400 - len(self.chunks))
Example #5
0
    def tiles_to_pil(self, palette: List[int]) -> Image.Image:
        """
        Exports the BPA as an image, where each row of 8x8 tiles is the
        animation set for a single tile. The 16 color palette passed is used to color the image.
        """
        dummy_tile_map = []
        width_in_tiles = self.number_of_frames
        etr = self.number_of_frames * self.number_of_tiles

        # create a dummy tile map containing all the tiles
        # The tiles in the BPA are stored so, that each tile of the each frame is next
        # to each other. So the second frame of the first tile is at self.number_of_images + 1.
        for tile_idx in range(0, self.number_of_tiles):
            for frame_idx in range(0, self.number_of_frames):
                dummy_tile_map.append(TilemapEntry(
                    idx=frame_idx * self.number_of_tiles + tile_idx,
                    pal_idx=0,
                    flip_x=False,
                    flip_y=False,
                    ignore_too_large=True
                ))
        width = width_in_tiles * BPA_TILE_DIM
        height = math.ceil(etr / width_in_tiles) * BPA_TILE_DIM

        return to_pil(
            dummy_tile_map, self.tiles, [palette], BPA_TILE_DIM, width, height
        )
Example #6
0
    def from_pil(self, pil: Image.Image, force_import=False) -> None:
        """
        Modify the image data in the BGP by importing the passed PIL.
        The passed PIL will be split into separate tiles and the tile's palette index
        is determined by the first pixel value of each tile in the PIL. The PIL
        must have a palette containing the 16 sub-palettes with 16 colors each (256 colors).

        If a pixel in a tile uses a color outside of it's 16 color range, an error is thrown or
        the color is replaced with 0 of the palette (transparent). This is controlled by
        the force_import flag.

        The image must have the size 256x192. For dimension calculating, see the constants of this module.
        """
        self.tiles, self.tilemap, self.palettes = from_pil(
            pil, BGP_PAL_NUMBER_COLORS, BGP_MAX_PAL, BGP_TILE_DIM,
            BGP_RES_WIDTH, BGP_RES_HEIGHT, 1, 1, force_import)

        if len(self.tiles) == 0x3FF:
            raise AttributeError(
                f"Error when importing: max tile count reached.")

        # Add the 0 tile (used to clear bgs)
        self.tiles.insert(0, bytearray(int(BGP_TILE_DIM * BGP_TILE_DIM / 2)))
        # Shift tile indices by 1
        for x in self.tilemap:
            x.idx += 1

        # Fill up the tiles and tilemaps to 1024, which seems to be the required default
        for _ in range(len(self.tiles), BGP_TOTAL_NUMBER_TILES_ACTUALLY):
            self.tiles.append(bytearray(int(BGP_TILE_DIM * BGP_TILE_DIM / 2)))
        for _ in range(len(self.tilemap), BGP_TOTAL_NUMBER_TILES_ACTUALLY):
            self.tilemap.append(TilemapEntry.from_int(u16(0)))
Example #7
0
def output_at_dungeon_tiles(fn: str, common_at: CommonAt, pal: Dpla):
    print("Outputting dungeon AT as image.")
    img_bin = common_at.decompress()
    tiles = list(iter_bytes(img_bin, int(8 * 8)))
    # create a dummy tile map containing all the tiles
    tilemap = []
    for i in range(0, len(tiles)):
        tilemap.append(TilemapEntry(i, False, False, 0, True))
    out_img = to_pil(tilemap,
                     tiles, [pal.get_palette_for_frame(0, 0)],
                     8,
                     int(len(tiles) * 8 / 3),
                     4 * 8,
                     tiling_width=1,
                     tiling_height=1,
                     bpp=8)
    # Alternate stategy:
    img_8bpp = img_bin  #bytes(x for x in iter_bytes_4bit_le(img_bin))
    mod = 16 * 4
    channels = 1
    mode = 'RGB' if channels == 3 else 'P'
    out_img = Image.frombuffer(mode,
                               (int(len(img_8bpp) / mod / channels), mod),
                               bytes(img_8bpp), 'raw', mode, 0, 1)
    if mode == 'P':
        out_img.putpalette(pal.get_palette_for_frame(0, 0))
    os.makedirs(os.path.join(output_dir, 'raw_img'), exist_ok=True)
    with open(os.path.join(output_dir, 'raw_img', fn), 'wb') as f:
        f.write(img_bin)

    out_img.save(os.path.join(output_dir, fn + '.png'))
def corrupt341():
    """Corrupt 341: Dungeon tiles Beach cave 2? -- No? Tiles -> Chunk mappings or similar?"""
    img341 = dungeon_bin[341].decompress()
    img341new = bytearray(img341)

    # Decode XOR
    #XOR_ROW_LEN = 7200#18 * 7
    #rows_decoded = []
    #row_before = bytes(XOR_ROW_LEN)
    #for chunk in iter_bytes(img341, XOR_ROW_LEN):
    #    xored = bytearray(a ^ b for (a, b) in zip(chunk, row_before))
    #    row_before = xored
    #    rows_decoded.append(xored)

    dummy_map = [
        TilemapEntry(10, False, False, 0),
        TilemapEntry(10, True, False, 0),
        TilemapEntry(10, False, True, 0),
        TilemapEntry(5, False, False, 0),
        TilemapEntry(5, True, False, 0),
        TilemapEntry(5, False, True, 0),
        TilemapEntry(10, False, False, 6),
        TilemapEntry(10, True, False, 6),
        TilemapEntry(10, False, True, 6)
    ]

    for j in range(1, 300):
        for i, m in enumerate(dummy_map):
            write_u16(img341new, m.to_int(), (j * 18) + 2 * i)

    all_tilemaps = []
    for bytes2 in iter_bytes(img341new, 2):
        all_tilemaps.append(TilemapEntry.from_int(read_u16(bytes2, 0)))

    # Encode XOR
    #rows_encoded = []
    #row_before = bytes(XOR_ROW_LEN)
    #for row in rows_decoded:
    #    xored = bytes(a ^ b for (a, b) in zip(row, row_before))
    #    row_before = row
    #    rows_encoded.append(xored)
    #img341new = bytes(itertools.chain.from_iterable(rows_encoded))
    #assert img341 == img341new

    with open('/tmp/corrupt.bin', 'wb') as f:
        f.write(img341new)
    dungeon_bin[341] = FileType.COMMON_AT.compress(img341new)
Example #9
0
    def to_pil(self, index, palette_index=0) -> Image.Image:
        dummy_tilemap = []
        for i in range(CHUNK_DIM * CHUNK_DIM):
            dummy_tilemap.append(TilemapEntry(i, False, False, palette_index))

        return to_pil(dummy_tilemap, self.sprites[index], self.palettes,
                      TILE_DIM, TILE_DIM * CHUNK_DIM, TILE_DIM * CHUNK_DIM,
                      CHUNK_DIM, CHUNK_DIM)
Example #10
0
    def __init__(self, data: bytes):
        if not isinstance(data, memoryview):
            data = memoryview(data)

        all_tilemaps = []
        for bytes2 in iter_bytes(data, 2):
            all_tilemaps.append(TilemapEntry.from_int(read_u16(bytes2, 0)))
        self.chunks = list(
            chunks(all_tilemaps, DPC_TILING_DIM * DPC_TILING_DIM))
Example #11
0
 def _extract_tilemap(self):
     tilemap_end = self.header.tilemap_data_begin + self.header.tilemap_data_length
     self.tilemap = []
     for i, entry in enumerate(iter_bytes(self.data, BGP_TILEMAP_ENTRY_BYTELEN, self.header.tilemap_data_begin, tilemap_end)):
         # NOTE: There will likely be more than 768 (BGP_TOTAL_NUMBER_TILES) tiles. Why is unknown, but the
         #       rest is just zero padding.
         self.tilemap.append(TilemapEntry.from_int(int.from_bytes(entry, 'little')))
     if len(self.tilemap) < BGP_TOTAL_NUMBER_TILES:
         raise ValueError(f"Invalid BGP image: Too few tiles ({len(self.tilemap)}) in tile mapping."
                          f"Must be at least {BGP_TOTAL_NUMBER_TILES}.")
Example #12
0
    def to_pil(self) -> Image.Image:
        tilemap = []
        for i in range(ICON_DIM_IMG_TILES * ICON_DIM_IMG_TILES):
            tilemap.append(
                TilemapEntry(idx=i, pal_idx=0, flip_x=False, flip_y=False))

        return to_pil(
            tilemap,
            list(chunks(self.bitmap, ICON_DIM_TILE * ICON_DIM_TILE // 2)),
            [self._palette],
            ICON_DIM_TILE,
            ICON_DIM_IMG_PX,
            ICON_DIM_IMG_PX,
            bpp=4,
        )
Example #13
0
    def get(self) -> Image.Image:
        decompressed_data = self.decompress()
        w = TILE_DIM * self.entry_data.width
        h = TILE_DIM * self.entry_data.height

        # Create a virtual tilemap
        tilemap = []
        for i in range(int((w * h) / (TILE_DIM * TILE_DIM))):
            tilemap.append(
                TilemapEntry(idx=i, pal_idx=0, flip_x=False, flip_y=False))

        return to_pil(
            tilemap,
            list(grouper(int(TILE_DIM * TILE_DIM / 2), decompressed_data)),
            [self.pal], TILE_DIM, w, h)
Example #14
0
def output_at_water_tiles(fn: str, common_at: CommonAt, pal: Dpla):
    print("Outputting water AT as image.")
    img_bin = common_at.decompress()
    tiles = list(iter_bytes(img_bin, int(8 * 8 / 2)))
    # create a dummy tile map containing all the tiles
    tilemap = []
    for i in range(0, len(tiles)):
        tilemap.append(TilemapEntry(i, False, False, 0, True))
    out_img = to_pil(tilemap,
                     tiles, [pal.get_palette_for_frame(0, 0)],
                     8,
                     int(len(tiles) * 8 / 3),
                     4 * 8,
                     tiling_width=1,
                     tiling_height=1)
    os.makedirs(os.path.join(output_dir, 'raw_img'), exist_ok=True)
    with open(os.path.join(output_dir, 'raw_img', fn), 'wb') as f:
        f.write(img_bin)

    out_img.save(os.path.join(output_dir, fn + '.png'))
Example #15
0
    def import_tile_mappings(
            self, layer: int, tile_mappings: List[TilemapEntry],
            contains_null_chunk=False, correct_tile_ids=True
    ):
        """
        Replace the tile mappings of the specified layer.
        If contains_null_tile is False, the null chunk is added to the list, at the beginning.

        If correct_tile_ids is True, then the tile id of tile_mappings is also increased by one. Use this,
        if you previously used import_tiles with contains_null_tile=False
        """
        nb_tiles_in_chunk = self.tiling_width * self.tiling_height
        if correct_tile_ids:
            for entry in tile_mappings:
                if not contains_null_chunk:
                    entry.idx += 1
        if not contains_null_chunk:
            tile_mappings = [TilemapEntry.from_int(0) for _ in range(0, nb_tiles_in_chunk)] + tile_mappings
        self.layers[layer].tilemap = tile_mappings
        self.layers[layer].chunk_tilemap_len = int(len(tile_mappings) / self.tiling_width / self.tiling_height)
Example #16
0
    def import_tile_mappings(self,
                             tile_mappings: List[List[TilemapEntry]],
                             contains_null_chunk=False,
                             correct_tile_ids=True):
        """
        Replace the tile mappings of the specified layer.
        If contains_null_tile is False, the null chunk is added to the list, at the beginning.

        If correct_tile_ids is True, then the tile id of tile_mappings is also increased by one. Use this,
        if you previously used import_tiles with contains_null_tile=False
        """
        if correct_tile_ids:
            for chunk in tile_mappings:
                for entry in chunk:
                    if not contains_null_chunk:
                        entry.idx += 1
        if not contains_null_chunk:
            tile_mappings = [[TilemapEntry.from_int(0)
                              for _ in range(0, 9)]] + tile_mappings
        self.chunks = tile_mappings
        self.re_fill_chunks()
Example #17
0
    def add_upper_layer(self):
        """Add an upper layer. Silently does nothing when it already exists."""
        if self.number_of_layers == 2:
            return
        self.number_of_layers = 2
        if len(self.layers) < 2:
            self.layers.append(self.layers[0])
        else:
            self.layers[1] = self.layers[0]

        tilemap = []
        # The first chunk is not stored, but is always empty
        for i in range(0, self.tiling_width * self.tiling_height):
            tilemap.append(TilemapEntry.from_int(0))
        self.layers[0] = BpcLayer(
            number_tiles=1,
            tilemap_len=1,
            bpas=[0, 0, 0, 0],
            # The first tile is not stored, but is always empty
            tiles=[bytearray(int(BPC_TILE_DIM * BPC_TILE_DIM / 2))],
            tilemap=tilemap
        )
Example #18
0
    def add_upper_layer(self) -> None:
        """Add an upper layer. Silently does nothing when it already exists."""
        if self.number_of_layers == 2:
            return
        self.number_of_layers = 2
        if len(self.layers) < 2:
            self.layers.append(self.layers[0])
        else:
            self.layers[1] = self.layers[0]

        tilemap = []
        # The first chunk is not stored, but is always empty
        for i in range(0, self.tiling_width * self.tiling_height):
            tilemap.append(TilemapEntry.from_int(u16(0)))
        self.layers[0] = BpcLayer(
            u16(1),
            [u16(0), u16(0), u16(0), u16(0)],
            u16(1),
            # The first tile is not stored, but is always empty
            [bytearray(int(BPC_TILE_DIM * BPC_TILE_DIM / 2))],
            tilemap  # type: ignore
        )
Example #19
0
    def tiles_to_pil(self,
                     palettes: Sequence[Sequence[int]],
                     width_in_tiles=20,
                     palette_index=0) -> Image.Image:
        """
        Convert all individual tiles of the DPCI into one PIL image.
        The image contains all tiles next to each other, the image width is tile_width tiles.
        The resulting image has one large palette with all palettes merged together.

        palettes is a list of 16 16 color palettes.
        The tiles are exported with the first palette in the list of palettes.
        The result image contains a palette that consists of all palettes merged together.
        """
        # create a dummy tile map containing all the tiles
        tilemap = []
        for i in range(0, len(self.tiles)):
            tilemap.append(TilemapEntry(i, False, False, palette_index, True))

        width = width_in_tiles * DPCI_TILE_DIM
        height = math.ceil((len(self.tiles)) / width_in_tiles) * DPCI_TILE_DIM

        return to_pil(tilemap, self.tiles, palettes, DPCI_TILE_DIM, width,
                      height)
Example #20
0
 def on_add_chunk_clicked(self, *args):
     m = self.builder.get_object(f'icon_view_chunk').get_model()
     m.append([len(self.edited_mappings)])
     for i in range(len(self.edited_mappings),
                    len(self.edited_mappings) + 9):
         self.edited_mappings.append(TilemapEntry.from_int(0))
Example #21
0
    def __init__(self,
                 parent_window,
                 incoming_mappings: List[TilemapEntry],
                 tile_graphics: AbstractTileGraphicsProvider,
                 palettes: AbstractTilePalettesProvider,
                 pal_ani_durations: int,
                 animated_tile_graphics: List[
                     Optional[AbstractTileGraphicsProvider]] = None,
                 animated_tile_durations=0):
        path = os.path.abspath(os.path.dirname(__file__))

        self.builder = make_builder(os.path.join(path, 'chunk_editor.glade'))

        self.dialog: Gtk.Dialog = self.builder.get_object(
            'map_bg_chunk_editor')
        self.dialog.set_attached_to(parent_window)
        self.dialog.set_transient_for(parent_window)

        self.tile_graphics = tile_graphics
        self.animated_tile_graphics = animated_tile_graphics
        self.palettes = palettes
        self.animated_tile_durations = animated_tile_durations
        self.pal_ani_durations = pal_ani_durations

        self.current_tile_id = 0
        self.current_tile_drawer: DrawerTiled = None

        self.switching_tile = False

        self.edited_mappings = []
        for mapping in incoming_mappings:
            self.edited_mappings.append(TilemapEntry.from_int(
                mapping.to_int()))

        self.tile_surfaces = []
        # For each palette
        for pal in range(0, len(self.palettes.get())):
            all_bpc_tiles_for_current_pal = self.tile_graphics.get_pil(
                self.palettes.get(), pal)
            tiles_current_pal = []
            self.tile_surfaces.append(tiles_current_pal)

            has_pal_ani = self.palettes.is_palette_affected_by_animation(pal)
            len_pal_ani = self.palettes.animation_length(
            ) if has_pal_ani else 1

            # BPC tiles
            # For each tile...
            for tile_idx in range(0, self.tile_graphics.count()):
                # For each frame of palette animation...
                pal_ani_tile = []
                tiles_current_pal.append(pal_ani_tile)
                for pal_ani in range(0, len_pal_ani):
                    # Switch out the palette with that from the palette animation
                    if has_pal_ani:
                        pal_for_frame = itertools.chain.from_iterable(
                            self.palettes.apply_palette_animations(pal_ani))
                        all_bpc_tiles_for_current_pal.putpalette(pal_for_frame)
                    pal_ani_tile.append([
                        pil_to_cairo_surface(
                            all_bpc_tiles_for_current_pal.crop(
                                (0, tile_idx * TILE_DIM, TILE_DIM,
                                 tile_idx * TILE_DIM +
                                 TILE_DIM)).convert('RGBA'))
                    ])
            # BPA tiles
            # For each BPA...
            if self.animated_tile_graphics is not None:
                for ani_tile_g in self.animated_tile_graphics:
                    if ani_tile_g is not None:
                        all_bpa_tiles_for_current_pal = ani_tile_g.get_pil(
                            self.palettes.get(), pal)
                        # For each tile...
                        for tile_idx in range(0, ani_tile_g.count()):
                            pal_ani_tile = []
                            tiles_current_pal.append(pal_ani_tile)
                            # For each frame of palette animation...
                            for pal_ani in range(0, len_pal_ani):
                                bpa_ani_tile = []
                                pal_ani_tile.append(bpa_ani_tile)
                                # For each frame of BPA animation...
                                for frame in all_bpa_tiles_for_current_pal:
                                    # Switch out the palette with that from the palette animation
                                    if has_pal_ani:
                                        pal_for_frame = itertools.chain.from_iterable(
                                            self.palettes.
                                            apply_palette_animations(pal_ani))
                                        all_bpc_tiles_for_current_pal.putpalette(
                                            pal_for_frame)
                                    bpa_ani_tile.append(
                                        pil_to_cairo_surface(
                                            frame.crop(
                                                (0, tile_idx * TILE_DIM,
                                                 TILE_DIM,
                                                 tile_idx * TILE_DIM +
                                                 TILE_DIM)).convert('RGBA')))

            self.builder.connect_signals(self)

            self.dummy_tile_map = []
            self.current_tile_picker_palette = 0
            for i in range(0, self.tile_graphics.count()):
                self.dummy_tile_map.append(
                    TilemapEntry(idx=i,
                                 pal_idx=self.current_tile_picker_palette,
                                 flip_x=False,
                                 flip_y=False))

            if self.animated_tile_graphics:
                self.bpa_starts_cursor = len(self.dummy_tile_map)
                self.bpa_starts = [None, None, None, None]
                for i, ani_tile_g in enumerate(self.animated_tile_graphics):
                    if ani_tile_g is not None:
                        self.bpa_starts[i] = self.bpa_starts_cursor
                        self.current_tile_picker_palette = 0
                        for j in range(0, ani_tile_g.count()):
                            self.dummy_tile_map.append(
                                TilemapEntry(
                                    idx=self.bpa_starts_cursor + j,
                                    pal_idx=self.current_tile_picker_palette,
                                    flip_x=False,
                                    flip_y=False))
                        self.bpa_starts_cursor += ani_tile_g.count()