class TitleScreenModule(EbModule):
    """Extracts the title screen data from EarthBound.

    This module allows for the editing of the background and characters
    of the title screen. The slide-in animation for the characters is
    controlled through assembly, while the rest of the animation works
    by changing between several palettes (one for each new frame of
    animation) and keeping the same tileset for each frame.
    """

    NAME = "Title Screen"
    FREE_RANGES = [
        (0x21B211, 0x21C6E4),  # Background Tileset
        (0x21AF7D, 0x21B210),  # Background Arrangement
        (0x21CDE1, 0x21CE07),  # Background Palette
        (0x21AEFD, 0x21AF7C),  # Background Animated Palette
        (0x21C6E5, 0x21CDE0),  # Characters Tileset
        (0x21AE7C, 0x21AE82),  # Characters Palette
        (0x21AE83, 0x21AEFC),  # Characters Animated Palette
        (0x21CE08, 0x21CF9C)  # Animation Data
    ]

    def __init__(self):
        super(TitleScreenModule, self).__init__()

        # Background data (includes the central "B", the copyright
        # notice and the glow around the letters)
        self.bg_tileset = EbGraphicTileset(num_tiles=BG_NUM_TILES,
                                           tile_width=TILE_WIDTH,
                                           tile_height=TILE_HEIGHT)
        self.bg_arrangement = EbTileArrangement(width=BG_ARRANGEMENT_WIDTH,
                                                height=BG_ARRANGEMENT_HEIGHT)
        self.bg_anim_palette = EbPalette(
            num_subpalettes=BG_NUM_ANIM_SUBPALETTES,
            subpalette_length=ANIM_SUBPALETTE_LENGTH)
        self.bg_palette = EbPalette(num_subpalettes=NUM_SUBPALETTES,
                                    subpalette_length=BG_SUBPALETTE_LENGTH)

        # Characters data (the title screen's animated letters)
        self.chars_tileset = EbGraphicTileset(num_tiles=CHARS_NUM_TILES,
                                              tile_width=TILE_WIDTH,
                                              tile_height=TILE_HEIGHT)
        self.chars_anim_palette = EbPalette(
            num_subpalettes=CHARS_NUM_ANIM_SUBPALETTES,
            subpalette_length=ANIM_SUBPALETTE_LENGTH)
        self.chars_palette = EbPalette(
            num_subpalettes=NUM_SUBPALETTES,
            subpalette_length=CHARS_SUBPALETTE_LENGTH)
        self.chars_layouts = [[] for _ in xrange(NUM_CHARS)]

    def read_from_rom(self, rom):
        self.read_background_data_from_rom(rom)
        self.read_chars_data_from_rom(rom)
        self.read_chars_layouts_from_rom(rom)

        # Add the characters palette to the background data.
        self.bg_palette[0, CHARS_ANIM_SLICE] =\
            self.chars_anim_palette.get_subpalette(
                CHARS_NUM_ANIM_SUBPALETTES - 1
            )[0, :]

    def read_background_data_from_rom(self, rom):
        with EbCompressibleBlock() as block:
            # Read the background tileset data
            self._decompress_block(rom, block, BG_TILESET_POINTER)
            self.bg_tileset.from_block(block=block,
                                       offset=0,
                                       bpp=BG_TILESET_BPP)

            # Read the background tile arrangement data
            self._decompress_block(rom, block, BG_ARRANGEMENT_POINTER)
            self.bg_arrangement.from_block(block=block, offset=0)

            # Read the background palette data
            # The decompressed data is smaller than the expected value,
            # so it is extended with black entries.
            self._decompress_block(rom, block, BG_PALETTE_POINTER)
            block.from_array(block.to_array() + [0] *
                             (BG_SUBPALETTE_LENGTH * 2 - len(block)))
            self.bg_palette.from_block(block=block, offset=0)

            # Read the background animated palette data
            # Each subpalette corresponds to an animation frame.
            self._decompress_block(rom, block, BG_ANIM_PALETTE_POINTER)
            self.bg_anim_palette.from_block(block=block, offset=0)

    def read_chars_data_from_rom(self, rom):
        with EbCompressibleBlock() as block:
            # Read the characters tileset data
            self._decompress_block(rom, block, CHARS_TILESET_POINTER)
            self.chars_tileset.from_block(block=block,
                                          offset=0,
                                          bpp=CHARS_TILESET_BPP)

            # Read the characters palette data
            self._decompress_block(rom, block, CHARS_PALETTE_POINTER)
            self.chars_palette.from_block(block=block, offset=0)

            # Read the characters animated palette data
            # Each subpalette corresponds to an animation frame.
            self._decompress_block(rom, block, CHARS_ANIM_PALETTE_POINTER)
            self.chars_anim_palette.from_block(block=block, offset=0)

    def read_chars_layouts_from_rom(self, rom):
        self.chars_layouts = [[] for _ in xrange(NUM_CHARS)]
        for char in xrange(NUM_CHARS):
            # Get the location of a character's data
            offset = CHARS_LAYOUT_POINTER_OFFSET + rom.read_multi(
                CHARS_LAYOUT_TABLE + char * 2, 2)

            # Read entries until a final entry is encountered
            while True:
                entry = TitleScreenLayoutEntry()
                entry.from_block(rom, offset)
                self.chars_layouts[char].append(entry)
                offset += 5
                if entry.is_final():
                    break

    def write_to_rom(self, rom):
        self.write_background_data_to_rom(rom)
        self.write_chars_data_to_rom(rom)
        self.write_chars_layouts_to_rom(rom)

    def write_background_data_to_rom(self, rom):
        # Write the background tileset data
        block_size = self.bg_tileset.block_size(bpp=BG_TILESET_BPP)
        with EbCompressibleBlock(block_size) as block:
            self.bg_tileset.to_block(block=block, offset=0, bpp=BG_TILESET_BPP)
            self._write_compressed_block(rom, block, BG_TILESET_POINTER)

        # Write the background tile arrangement data
        block_size = self.bg_arrangement.block_size()
        with EbCompressibleBlock(block_size) as block:
            self.bg_arrangement.to_block(block=block, offset=0)
            self._write_compressed_block(rom, block, BG_ARRANGEMENT_POINTER)

        # Write the background palette data
        # There is an additional pointer to this location, so change that one
        # too
        block_size = self.bg_palette.block_size()
        with EbCompressibleBlock(block_size) as block:
            self.bg_palette.to_block(block=block, offset=0)
            new_offset = self._write_compressed_block(rom, block,
                                                      BG_PALETTE_POINTER)
            write_asm_pointer(block=rom,
                              offset=BG_PALETTE_POINTER_SECONDARY,
                              pointer=to_snes_address(new_offset))

        # Write the background animated palette data
        block_size = self.bg_anim_palette.block_size()
        with EbCompressibleBlock(block_size) as block:
            self.bg_anim_palette.to_block(block=block, offset=0)
            self._write_compressed_block(rom, block, BG_ANIM_PALETTE_POINTER)

    def write_chars_data_to_rom(self, rom):
        # Write the characters tileset data
        block_size = self.chars_tileset.block_size(bpp=CHARS_TILESET_BPP)
        with EbCompressibleBlock(block_size) as block:
            self.chars_tileset.to_block(block=block,
                                        offset=0,
                                        bpp=CHARS_TILESET_BPP)
            self._write_compressed_block(rom, block, CHARS_TILESET_POINTER)

        # Write the characters palette data
        block_size = self.chars_palette.block_size()
        with EbCompressibleBlock(block_size) as block:
            self.chars_palette.to_block(block=block, offset=0)
            self._write_compressed_block(rom, block, CHARS_PALETTE_POINTER)

        # Write the characters animation palette data
        block_size = self.chars_anim_palette.block_size()
        with EbCompressibleBlock(block_size) as block:
            self.chars_anim_palette.to_block(block=block, offset=0)
            self._write_compressed_block(rom, block,
                                         CHARS_ANIM_PALETTE_POINTER)

    def write_chars_layouts_to_rom(self, rom):
        block_size = sum(TitleScreenLayoutEntry.block_size() * len(c)
                         for c in self.chars_layouts)

        # Ensure the new data is located in only one bank
        # Spreading it across two banks might make part of it inaccessible.
        def can_write_to(begin):
            return begin >> 16 == (begin + block_size) >> 16

        with Block(block_size) as block:
            # Write the character animation data to the ROM
            offset = 0
            for layout in self.chars_layouts:
                for entry in layout:
                    entry.to_block(block=block, offset=offset)
                    offset += entry.block_size()
            new_offset = to_snes_address(
                rom.allocate(data=block,
                             size=block_size,
                             can_write_to=can_write_to))

            # Write the offsets to the layouts to the ROM
            new_bank = new_offset >> 16
            new_data_start = new_offset & 0xFFFF
            data_offset = new_data_start
            for c, layout in enumerate(self.chars_layouts):
                rom[CHARS_LAYOUT_TABLE + c * 2:CHARS_LAYOUT_TABLE + c * 2 +
                    2] = [data_offset & 0xFF, data_offset >> 8]
                data_offset += len(
                    layout) * TitleScreenLayoutEntry.block_size()

            # Change the offset for the character layouts
            # The way this normally works is that EarthBound stores the address
            # of the bank holding the data (0xE1 by default, hence the 0x210000
            # offset); the offsets in the table are then prefixed with that
            # address. However, reallocating the data may have changed its
            # bank, so we need to manually set it to the new bank address.
            rom[CHARS_LAYOUT_BANK:CHARS_LAYOUT_BANK + 2] = [0xA9, new_bank]

    def read_from_project(self, resource_open):
        self.read_background_data_from_project(resource_open)
        self.read_chars_data_from_project(resource_open)

    def read_background_data_from_project(self, resource_open):
        # Load the background reference image
        # The image's arrangement, tileset and palette will be used for the
        # animation frames
        with resource_open(BG_REFERENCE_PATH, "png") as f:
            image = open_indexed_image(f)
            self.bg_arrangement.from_image(image, self.bg_tileset,
                                           self.bg_palette)

        # Read the background animated frames
        for frame in xrange(NUM_ANIM_FRAMES):
            # Create temporary structures used to check consistency between
            # frames
            tileset = EbGraphicTileset(BG_NUM_TILES, TILE_WIDTH, TILE_HEIGHT)
            arrangement = EbTileArrangement(BG_ARRANGEMENT_WIDTH,
                                            BG_ARRANGEMENT_HEIGHT)
            palette = EbPalette(NUM_SUBPALETTES, BG_SUBPALETTE_LENGTH)

            # Read one frame's image data
            with resource_open(BG_FRAMES_PATH.format(frame), "png") as f:
                image = open_indexed_image(f)
                arrangement.from_image(image, tileset, palette)

            # Make sure each frame's tileset and arrangement is identical
            # The background palette is checked only if it isn't the fake
            # palette used for the first few frames
            if frame >= CHARS_NUM_ANIM_SUBPALETTES:
                # Get the background animated subpalette from the background
                # palette
                colors = palette[0, BG_ANIM_SLICE]
                self.bg_anim_palette.subpalettes[
                    frame - CHARS_NUM_ANIM_SUBPALETTES] = colors
                palette[0, BG_ANIM_SLICE] = self.bg_palette[0, BG_ANIM_SLICE]
                if self.bg_palette != palette:
                    log.warn("Palette from background frame {} does not match "
                             "reference.".format(frame))
            if self.bg_tileset != tileset:
                log.warn("Tileset from background frame {} does not match "
                         "reference.".format(frame))
            if self.bg_arrangement != arrangement:
                log.warn("Arrangement from background frame {} does not match "
                         "reference.".format(frame))

    def read_chars_data_from_project(self, resource_open):
        # Read the characters positions
        with resource_open(CHARS_POSITIONS_PATH, "yml") as f:
            chars_positions = yml_load(f)

        # Read the characters animated frames
        self.chars_tileset = None
        self.chars_anim_palette = EbPalette(CHARS_NUM_ANIM_SUBPALETTES,
                                            ANIM_SUBPALETTE_LENGTH)
        original_tileset = None
        for p in xrange(CHARS_NUM_ANIM_SUBPALETTES):
            # Read one of the animation frames
            with resource_open(CHARS_FRAMES_PATH.format(p), "png") as f:
                # Create temporary structures to hold the data
                image = open_indexed_image(f)
                arrangement = EbTileArrangement(image.width / TILE_WIDTH,
                                                image.height / TILE_HEIGHT)
                tileset = EbGraphicTileset(CHARS_NUM_TILES, TILE_WIDTH,
                                           TILE_HEIGHT)
                anim_subpalette = EbPalette(NUM_SUBPALETTES,
                                            ANIM_SUBPALETTE_LENGTH)
                arrangement.from_image(image, tileset, anim_subpalette, True)

            # Add the characters animation subpalette
            for i in xrange(ANIM_SUBPALETTE_LENGTH):
                self.chars_anim_palette[p, i] = anim_subpalette[0, i]

            # Add the characters tileset if not already set, otherwise
            # ensure that it the current tileset is identical
            if not self.chars_tileset:
                original_tileset = tileset
                self.chars_tileset = EbGraphicTileset(CHARS_NUM_TILES,
                                                      TILE_WIDTH, TILE_HEIGHT)
                self.chars_tileset.tiles = [[[0 for _ in xrange(TILE_HEIGHT)]
                                             for _ in xrange(TILE_WIDTH)]
                                            for _ in xrange(CHARS_NUM_TILES)]
                unused_tiles = set(xrange(CHARS_NUM_TILES))

                # Set the new character layouts
                self.chars_layouts = [[] for _ in xrange(NUM_CHARS)]
                for c, data in chars_positions.items():
                    # Get the data from the YAML file
                    x = int(data['x'] / TILE_WIDTH)
                    y = int(data['y'] / TILE_HEIGHT)
                    width = int(data['width'] / TILE_WIDTH)
                    height = int(data['height'] / TILE_HEIGHT)
                    x_offset = data['top_left_offset']['x']
                    y_offset = data['top_left_offset']['y']
                    unknown = data['unknown']

                    # Generate a list of all tiles must be visited
                    # Where possible, we try to generate a multi tile (4 tiles
                    # stored as one); otherwise, bordering tiles that are
                    # visited will all be single tiles.
                    l = [(i, j) for i in xrange(0, width, 2)
                         for j in xrange(0, height, 2)]
                    if width % 2 == 1:
                        l.extend([(width - 1, j)
                                  for j in xrange(1, height, 2)])
                    if height % 2 == 1:
                        l.extend([(i, height - 1)
                                  for i in xrange(1, width, 2)])

                    # Generate the new reduced tileset
                    for i, j in l:
                        # Put the tile in the new tileset
                        o_tile = arrangement[x + i, y + j].tile
                        n_tile = unused_tiles.pop()
                        self.chars_tileset.tiles[n_tile] = tileset[o_tile]

                        entry = TitleScreenLayoutEntry(i * 8 + x_offset,
                                                       j * 8 + y_offset,
                                                       n_tile, 0, unknown)

                        # Create a multi entry if possible to save space
                        if i < width - 1 and j < height - 1:
                            entry.set_single(True)
                            o_tile_r = arrangement[x + i + 1, y + j].tile
                            o_tile_d = arrangement[x + i, y + j + 1].tile
                            o_tile_dr = arrangement[x + i + 1, y + j + 1].tile
                            n_tile_r = n_tile + 1
                            n_tile_d = n_tile + 16
                            n_tile_dr = n_tile + 17
                            unused_tiles.difference_update(
                                (n_tile_r, n_tile_d, n_tile_dr))
                            self.chars_tileset.tiles[n_tile_r] = \
                                tileset[o_tile_r]
                            self.chars_tileset.tiles[n_tile_d] = \
                                tileset[o_tile_d]
                            self.chars_tileset.tiles[n_tile_dr] = \
                                tileset[o_tile_dr]

                        self.chars_layouts[c].append(entry)
                    self.chars_layouts[c][-1].set_final(True)

            elif original_tileset != tileset:
                log.warn("Tileset from characters frame {} does not match "
                         "tileset from characters frame 0.".format(p))

        # Read the initial characters palette
        with resource_open(CHARS_INITIAL_PATH, "png") as f:
            image = open_indexed_image(f)
            arrangement = EbTileArrangement(image.width / TILE_WIDTH,
                                            image.height / TILE_HEIGHT)
            tileset = EbGraphicTileset(CHARS_NUM_TILES, TILE_WIDTH,
                                       TILE_HEIGHT)
            self.chars_palette = EbPalette(NUM_SUBPALETTES,
                                           ANIM_SUBPALETTE_LENGTH)
            arrangement.from_image(image, tileset, self.chars_palette)

    def write_to_project(self, resource_open):
        self.write_background_data_to_project(resource_open)
        self.write_chars_data_to_project(resource_open)

    def write_background_data_to_project(self, resource_open):
        # Write out the reference background image
        # This image is used to get the arrangement, tileset and static palette
        # that will be used by all background images.
        with resource_open(BG_REFERENCE_PATH, "png") as f:
            image = self.bg_arrangement.image(self.bg_tileset, self.bg_palette)
            image.save(f)

        # Write out the background's animated frames
        for frame in xrange(NUM_ANIM_FRAMES):
            palette = EbPalette(NUM_SUBPALETTES, BG_SUBPALETTE_LENGTH)
            if frame < CHARS_NUM_ANIM_SUBPALETTES:
                palette[0, CHARS_ANIM_SLICE] = \
                    self.chars_anim_palette.get_subpalette(frame)[0, :]
            else:
                palette[0, :] = self.bg_palette.get_subpalette(0)[0, :]
                palette[0, BG_ANIM_SLICE] = \
                    self.bg_anim_palette.get_subpalette(
                        frame - CHARS_NUM_ANIM_SUBPALETTES
                    )[0, :]
            with resource_open(BG_FRAMES_PATH.format(frame), "png") as f:
                image = self.bg_arrangement.image(self.bg_tileset, palette)
                image.save(f)

    def write_chars_data_to_project(self, resource_open):
        # Build an arrangement combining every character for convenience
        chars_positions = {}
        arrangement = EbTileArrangement(3 * 9, 6)
        for c, layout in enumerate(self.chars_layouts):
            top_left = {'x': 128, 'y': 128}
            for e, entry in enumerate(layout):
                tile = entry.tile & (CHARS_NUM_TILES - 1)
                top_left['x'] = min(top_left['x'], int(entry.x))
                top_left['y'] = min(top_left['y'], int(entry.y))
                x = c * 3 + (entry.x + 16) / 8
                y = (entry.y + 24) / 8
                arrangement[x, y].tile = tile
                if not entry.is_single():
                    arrangement[x + 1, y].tile = tile + 1
                    arrangement[x, y + 1].tile = tile + 16
                    arrangement[x + 1, y + 1].tile = tile + 17
            chars_positions[c] = {
                'x': c * 3 * 8,
                'y': 0,
                'width': 3 * 8,
                'height': 6 * 8,
                'top_left_offset': top_left,
                'unknown': layout[0].unknown
            }

        # Write the characters animation frames
        for p in xrange(CHARS_NUM_ANIM_SUBPALETTES):
            with resource_open(CHARS_FRAMES_PATH.format(p), "png") as f:
                image = arrangement.image(
                    self.chars_tileset,
                    self.chars_anim_palette.get_subpalette(p))
                image.save(f)

        # Write out the initial characters palette
        with resource_open(CHARS_INITIAL_PATH, "png") as f:
            image = arrangement.image(self.chars_tileset, self.chars_palette)
            image.save(f)

        # Write out the positions of the characters
        with resource_open(CHARS_POSITIONS_PATH, "yml") as f:
            yml_dump(chars_positions, f, False)

    def upgrade_project(self, old_version, new_version, rom, resource_open_r,
                        resource_open_w, resource_delete):
        if old_version < 9:
            self.read_from_rom(rom)
            self.write_to_project(resource_open_w)

    @staticmethod
    def _decompress_block(rom, block, pointer):
        block.from_compressed_block(block=rom,
                                    offset=from_snes_address(
                                        read_asm_pointer(rom, pointer)))

    @staticmethod
    def _write_compressed_block(rom, compressed_block, pointer):
        compressed_block.compress()
        new_offset = rom.allocate(data=compressed_block)
        write_asm_pointer(block=rom,
                          offset=pointer,
                          pointer=to_snes_address(new_offset))
        return new_offset
class TitleScreenModule(EbModule):
    """Extracts the title screen data from EarthBound.

    This module allows for the editing of the background and characters
    of the title screen. The slide-in animation for the characters is
    controlled through assembly, while the rest of the animation works
    by changing between several palettes (one for each new frame of
    animation) and keeping the same tileset for each frame.
    """

    NAME = "Title Screen"
    FREE_RANGES = [
        (0x21B211, 0x21C6E4),  # Background Tileset
        (0x21AF7D, 0x21B210),  # Background Arrangement
        (0x21CDE1, 0x21CE07),  # Background Palette
        (0x21AEFD, 0x21AF7C),  # Background Animated Palette

        (0x21C6E5, 0x21CDE0),  # Characters Tileset
        (0x21AE7C, 0x21AE82),  # Characters Palette
        (0x21AE83, 0x21AEFC),  # Characters Animated Palette

        (0x21CE08, 0x21CF9C)  # Animation Data
    ]

    def __init__(self):
        super(TitleScreenModule, self).__init__()

        # Background data (includes the central "B", the copyright
        # notice and the glow around the letters)
        self.bg_tileset = EbGraphicTileset(
            num_tiles=BG_NUM_TILES, tile_width=TILE_WIDTH,
            tile_height=TILE_HEIGHT
        )
        self.bg_arrangement = EbTileArrangement(
            width=BG_ARRANGEMENT_WIDTH, height=BG_ARRANGEMENT_HEIGHT
        )
        self.bg_anim_palette = EbPalette(
            num_subpalettes=BG_NUM_ANIM_SUBPALETTES,
            subpalette_length=ANIM_SUBPALETTE_LENGTH
        )
        self.bg_palette = EbPalette(
            num_subpalettes=NUM_SUBPALETTES,
            subpalette_length=BG_SUBPALETTE_LENGTH
        )

        # Characters data (the title screen's animated letters)
        self.chars_tileset = EbGraphicTileset(
            num_tiles=CHARS_NUM_TILES, tile_width=TILE_WIDTH,
            tile_height=TILE_HEIGHT
        )
        self.chars_anim_palette = EbPalette(
            num_subpalettes=CHARS_NUM_ANIM_SUBPALETTES,
            subpalette_length=ANIM_SUBPALETTE_LENGTH
        )
        self.chars_palette = EbPalette(
            num_subpalettes=NUM_SUBPALETTES,
            subpalette_length=CHARS_SUBPALETTE_LENGTH
        )
        self.chars_layouts = [[] for _ in range(NUM_CHARS)]

    def read_from_rom(self, rom):
        self.read_background_data_from_rom(rom)
        self.read_chars_data_from_rom(rom)
        self.read_chars_layouts_from_rom(rom)

        # Add the characters palette to the background data.
        self.bg_palette[0, CHARS_ANIM_SLICE] =\
            self.chars_anim_palette.get_subpalette(
                CHARS_NUM_ANIM_SUBPALETTES - 1
            )[0, :]

    def read_background_data_from_rom(self, rom):
        with EbCompressibleBlock() as block:
            # Read the background tileset data
            self._decompress_block(rom, block, BG_TILESET_POINTER)
            self.bg_tileset.from_block(
                block=block, offset=0, bpp=BG_TILESET_BPP
            )

            # Read the background tile arrangement data
            self._decompress_block(rom, block, BG_ARRANGEMENT_POINTER)
            self.bg_arrangement.from_block(block=block, offset=0)

            # Read the background palette data
            # The decompressed data is smaller than the expected value,
            # so it is extended with black entries.
            self._decompress_block(rom, block, BG_PALETTE_POINTER)
            block.from_array(
                block.to_array() + [0]*(BG_SUBPALETTE_LENGTH*2 - len(block))
            )
            self.bg_palette.from_block(block=block, offset=0)

            # Read the background animated palette data
            # Each subpalette corresponds to an animation frame.
            self._decompress_block(rom, block, BG_ANIM_PALETTE_POINTER)
            self.bg_anim_palette.from_block(block=block, offset=0)

    def read_chars_data_from_rom(self, rom):
        with EbCompressibleBlock() as block:
            # Read the characters tileset data
            self._decompress_block(rom, block, CHARS_TILESET_POINTER)
            self.chars_tileset.from_block(
                block=block, offset=0, bpp=CHARS_TILESET_BPP
            )

            # Read the characters palette data
            self._decompress_block(rom, block, CHARS_PALETTE_POINTER)
            self.chars_palette.from_block(block=block, offset=0)

            # Read the characters animated palette data
            # Each subpalette corresponds to an animation frame.
            self._decompress_block(rom, block, CHARS_ANIM_PALETTE_POINTER)
            self.chars_anim_palette.from_block(block=block, offset=0)

    def read_chars_layouts_from_rom(self, rom):
        lda_instruction = rom[CHARS_LAYOUT_BANK]
        chars_layout_pointer_offset = CHARS_LAYOUT_POINTER_OFFSET_DEFAULT

        # Check if we are dealing with the modified Rom,
        # If we are, we need to recalculate the offset to the
        # character layouts
        if lda_instruction == 0xA9:
            bank = rom[CHARS_LAYOUT_BANK + 1]
            chars_layout_pointer_offset = from_snes_address(bank << 16)

        self.chars_layouts = [[] for _ in range(NUM_CHARS)]
        for char in range(NUM_CHARS):
            # Get the location of a character's data
            offset = chars_layout_pointer_offset + rom.read_multi(
                CHARS_LAYOUT_TABLE + char*2, 2
            )

            # Read entries until a final entry is encountered
            while True:
                entry = TitleScreenLayoutEntry()
                entry.from_block(rom, offset)
                self.chars_layouts[char].append(entry)
                offset += 5
                if entry.is_final():
                    break

    def write_to_rom(self, rom):
        self.write_background_data_to_rom(rom)
        self.write_chars_data_to_rom(rom)
        self.write_chars_layouts_to_rom(rom)

    def write_background_data_to_rom(self, rom):
        # Write the background tileset data
        block_size = self.bg_tileset.block_size(bpp=BG_TILESET_BPP)
        with EbCompressibleBlock(block_size) as block:
            self.bg_tileset.to_block(block=block, offset=0, bpp=BG_TILESET_BPP)
            self._write_compressed_block(rom, block, BG_TILESET_POINTER)

        # Write the background tile arrangement data
        block_size = self.bg_arrangement.block_size()
        with EbCompressibleBlock(block_size) as block:
            self.bg_arrangement.to_block(block=block, offset=0)
            self._write_compressed_block(rom, block, BG_ARRANGEMENT_POINTER)

        # Write the background palette data
        # There is an additional pointer to this location, so change that one
        # too
        block_size = self.bg_palette.block_size()
        with EbCompressibleBlock(block_size) as block:
            self.bg_palette.to_block(block=block, offset=0)
            new_offset = self._write_compressed_block(
                rom, block, BG_PALETTE_POINTER
            )
            write_asm_pointer(
                block=rom, offset=BG_PALETTE_POINTER_SECONDARY,
                pointer=to_snes_address(new_offset)
            )

        # Write the background animated palette data
        block_size = self.bg_anim_palette.block_size()
        with EbCompressibleBlock(block_size) as block:
            self.bg_anim_palette.to_block(block=block, offset=0)
            self._write_compressed_block(rom, block, BG_ANIM_PALETTE_POINTER)

    def write_chars_data_to_rom(self, rom):
        # Write the characters tileset data
        block_size = self.chars_tileset.block_size(bpp=CHARS_TILESET_BPP)
        with EbCompressibleBlock(block_size) as block:
            self.chars_tileset.to_block(
                block=block, offset=0, bpp=CHARS_TILESET_BPP
            )
            self._write_compressed_block(rom, block, CHARS_TILESET_POINTER)

        # Write the characters palette data
        block_size = self.chars_palette.block_size()
        with EbCompressibleBlock(block_size) as block:
            self.chars_palette.to_block(block=block, offset=0)
            self._write_compressed_block(rom, block, CHARS_PALETTE_POINTER)

        # Write the characters animation palette data
        block_size = self.chars_anim_palette.block_size()
        with EbCompressibleBlock(block_size) as block:
            self.chars_anim_palette.to_block(block=block, offset=0)
            self._write_compressed_block(
                rom, block, CHARS_ANIM_PALETTE_POINTER
            )

    def write_chars_layouts_to_rom(self, rom):
        block_size = sum(
            TitleScreenLayoutEntry.block_size()*len(c)
            for c in self.chars_layouts
        )

        # Ensure the new data is located in only one bank
        # Spreading it across two banks might make part of it inaccessible.
        def can_write_to(begin):
            return begin >> 16 == (begin + block_size) >> 16

        with Block(block_size) as block:
            # Write the character animation data to the ROM
            offset = 0
            for layout in self.chars_layouts:
                for entry in layout:
                    entry.to_block(block=block, offset=offset)
                    offset += entry.block_size()
            new_offset = to_snes_address(rom.allocate(
                data=block,
                size=block_size,
                can_write_to=can_write_to
            ))

            # Write the offsets to the layouts to the ROM
            new_bank = new_offset >> 16
            new_data_start = new_offset & 0xFFFF
            data_offset = new_data_start
            for c, layout in enumerate(self.chars_layouts):
                rom[CHARS_LAYOUT_TABLE + c*2:CHARS_LAYOUT_TABLE + c*2 + 2] = [
                    data_offset & 0xFF, data_offset >> 8
                ]
                data_offset += len(layout)*TitleScreenLayoutEntry.block_size()

            # Change the offset for the character layouts
            # The way this normally works is that EarthBound stores the address
            # of the bank holding the data (0xE1 by default, hence the 0x210000
            # offset); the offsets in the table are then prefixed with that
            # address. However, reallocating the data may have changed its
            # bank, so we need to manually set it to the new bank address.

            # In order to change the offset, we are replacing a LDA instruction 
            # which addresses a direct page (0xA5) with a LDA instruction
            # that treats its operand as the constant to load (0xA9)
            # See https://wiki.superfamicom.org/snes/show/65816+Reference#instructions.
            rom[CHARS_LAYOUT_BANK:CHARS_LAYOUT_BANK + 2] = [0xA9, new_bank]

    def read_from_project(self, resource_open):
        self.read_background_data_from_project(resource_open)
        self.read_chars_data_from_project(resource_open)

    def read_background_data_from_project(self, resource_open):
        # Load the background reference image
        # The image's arrangement, tileset and palette will be used for the
        # animation frames
        with resource_open(BG_REFERENCE_PATH, "png") as f:
            image = open_indexed_image(f)
            self.bg_arrangement.from_image(
                image, self.bg_tileset, self.bg_palette
            )

        # Read the background animated frames
        for frame in range(NUM_ANIM_FRAMES):
            # Create temporary structures used to check consistency between
            # frames
            tileset = EbGraphicTileset(BG_NUM_TILES, TILE_WIDTH, TILE_HEIGHT)
            arrangement = EbTileArrangement(
                BG_ARRANGEMENT_WIDTH, BG_ARRANGEMENT_HEIGHT
            )
            palette = EbPalette(NUM_SUBPALETTES, BG_SUBPALETTE_LENGTH)

            # Read one frame's image data
            with resource_open(BG_FRAMES_PATH.format(frame), "png") as f:
                image = open_indexed_image(f)
                arrangement.from_image(image, tileset, palette)

            # Make sure each frame's tileset and arrangement is identical
            # The background palette is checked only if it isn't the fake
            # palette used for the first few frames
            if frame >= CHARS_NUM_ANIM_SUBPALETTES:
                # Get the background animated subpalette from the background
                # palette
                colors = palette[0, BG_ANIM_SLICE]
                self.bg_anim_palette.subpalettes[
                    frame - CHARS_NUM_ANIM_SUBPALETTES
                ] = colors
                palette[0, BG_ANIM_SLICE] = self.bg_palette[
                    0, BG_ANIM_SLICE
                ]
                if self.bg_palette != palette:
                    log.warn(
                        "Palette from background frame {} does not match "
                        "reference.".format(frame)
                    )
            if self.bg_tileset != tileset:
                log.warn(
                    "Tileset from background frame {} does not match "
                    "reference.".format(frame)
                )
            if self.bg_arrangement != arrangement:
                log.warn(
                    "Arrangement from background frame {} does not match "
                    "reference.".format(frame)
                )

    def read_chars_data_from_project(self, resource_open):
        # Read the characters positions
        with resource_open(CHARS_POSITIONS_PATH, "yml", True) as f:
            chars_positions = yml_load(f)

        # Read the characters animated frames
        self.chars_tileset = None
        self.chars_anim_palette = EbPalette(
            CHARS_NUM_ANIM_SUBPALETTES, ANIM_SUBPALETTE_LENGTH
        )
        original_tileset = None
        for p in range(CHARS_NUM_ANIM_SUBPALETTES):
            # Read one of the animation frames
            with resource_open(CHARS_FRAMES_PATH.format(p), "png") as f:
                # Create temporary structures to hold the data
                image = open_indexed_image(f)
                arrangement = EbTileArrangement(
                    image.width // TILE_WIDTH, image.height // TILE_HEIGHT
                )
                tileset = EbGraphicTileset(
                    CHARS_NUM_TILES, TILE_WIDTH, TILE_HEIGHT
                )
                anim_subpalette = EbPalette(
                    NUM_SUBPALETTES, ANIM_SUBPALETTE_LENGTH
                )
                arrangement.from_image(image, tileset, anim_subpalette, True)

            # Add the characters animation subpalette
            for i in range(ANIM_SUBPALETTE_LENGTH):
                self.chars_anim_palette[p, i] = anim_subpalette[0, i]

            # Add the characters tileset if not already set, otherwise
            # ensure that it the current tileset is identical
            if not self.chars_tileset:
                original_tileset = tileset
                self.chars_tileset = EbGraphicTileset(
                    CHARS_NUM_TILES, TILE_WIDTH, TILE_HEIGHT
                )
                self.chars_tileset.tiles = [
                    [[0 for _ in range(TILE_HEIGHT)]
                        for _ in range(TILE_WIDTH)]
                    for _ in range(CHARS_NUM_TILES)
                ]
                unused_tiles = set(range(CHARS_NUM_TILES))

                # Set the new character layouts
                self.chars_layouts = [[] for _ in range(NUM_CHARS)]
                for c, data in chars_positions.items():
                    # Get the data from the YAML file
                    x = int(data['x'] // TILE_WIDTH)
                    y = int(data['y'] // TILE_HEIGHT)
                    width = int(data['width'] // TILE_WIDTH)
                    height = int(data['height'] // TILE_HEIGHT)
                    x_offset = data['top_left_offset']['x']
                    y_offset = data['top_left_offset']['y']
                    unknown = data['unknown']

                    # Generate a list of all tiles must be visited
                    # Where possible, we try to generate a multi tile (4 tiles
                    # stored as one); otherwise, bordering tiles that are
                    # visited will all be single tiles.
                    l = [
                        (i, j) for i in range(0, width, 2)
                        for j in range(0, height, 2)
                    ]
                    if width % 2 == 1:
                        l.extend([(width-1, j) for j in range(1, height, 2)])
                    if height % 2 == 1:
                        l.extend([(i, height-1) for i in range(1, width, 2)])

                    # Generate the new reduced tileset
                    for i, j in l:
                        # Put the tile in the new tileset
                        o_tile = arrangement[x + i, y + j].tile
                        n_tile = unused_tiles.pop()
                        self.chars_tileset.tiles[n_tile] = tileset[o_tile]

                        entry = TitleScreenLayoutEntry(
                            i*8 + x_offset, j*8 + y_offset, n_tile, 0, unknown
                        )

                        # Create a multi entry if possible to save space
                        if i < width - 1 and j < height - 1:
                            entry.set_single(True)
                            o_tile_r = arrangement[x+i+1, y+j].tile
                            o_tile_d = arrangement[x+i, y+j+1].tile
                            o_tile_dr = arrangement[x+i+1, y+j+1].tile
                            n_tile_r = n_tile + 1
                            n_tile_d = n_tile + 16
                            n_tile_dr = n_tile + 17
                            unused_tiles.difference_update(
                                (n_tile_r, n_tile_d, n_tile_dr)
                            )
                            self.chars_tileset.tiles[n_tile_r] = \
                                tileset[o_tile_r]
                            self.chars_tileset.tiles[n_tile_d] = \
                                tileset[o_tile_d]
                            self.chars_tileset.tiles[n_tile_dr] = \
                                tileset[o_tile_dr]

                        self.chars_layouts[c].append(entry)
                    self.chars_layouts[c][-1].set_final(True)

            elif original_tileset != tileset:
                log.warn(
                    "Tileset from characters frame {} does not match "
                    "tileset from characters frame 0.".format(p)
                )

        # Read the initial characters palette
        with resource_open(CHARS_INITIAL_PATH, "png") as f:
            image = open_indexed_image(f)
            arrangement = EbTileArrangement(
                image.width // TILE_WIDTH, image.height // TILE_HEIGHT
            )
            tileset = EbGraphicTileset(
                CHARS_NUM_TILES, TILE_WIDTH, TILE_HEIGHT
            )
            self.chars_palette = EbPalette(
                NUM_SUBPALETTES, ANIM_SUBPALETTE_LENGTH
            )
            arrangement.from_image(image, tileset, self.chars_palette)

    def write_to_project(self, resource_open):
        self.write_background_data_to_project(resource_open)
        self.write_chars_data_to_project(resource_open)

    def write_background_data_to_project(self, resource_open):
        # Write out the reference background image
        # This image is used to get the arrangement, tileset and static palette
        # that will be used by all background images.
        with resource_open(
            BG_REFERENCE_PATH, "png"
        ) as f:
            image = self.bg_arrangement.image(self.bg_tileset, self.bg_palette)
            image.save(f)

        # Write out the background's animated frames
        for frame in range(NUM_ANIM_FRAMES):
            palette = EbPalette(NUM_SUBPALETTES, BG_SUBPALETTE_LENGTH)
            if frame < CHARS_NUM_ANIM_SUBPALETTES:
                palette[0, CHARS_ANIM_SLICE] = \
                    self.chars_anim_palette.get_subpalette(frame)[0, :]
            else:
                palette[0, :] = self.bg_palette.get_subpalette(0)[0, :]
                palette[0, BG_ANIM_SLICE] = \
                    self.bg_anim_palette.get_subpalette(
                        frame - CHARS_NUM_ANIM_SUBPALETTES
                    )[0, :]
            with resource_open(BG_FRAMES_PATH.format(frame), "png") as f:
                image = self.bg_arrangement.image(self.bg_tileset, palette)
                image.save(f)

    def write_chars_data_to_project(self, resource_open):
        # Build an arrangement combining every character for convenience
        chars_positions = {}
        arrangement = EbTileArrangement(3*9, 6)
        for c, layout in enumerate(self.chars_layouts):
            top_left = {'x': 128, 'y': 128}
            for e, entry in enumerate(layout):
                tile = entry.tile & (CHARS_NUM_TILES - 1)
                top_left['x'] = min(top_left['x'], int(entry.x))
                top_left['y'] = min(top_left['y'], int(entry.y))
                x = c*3 + (entry.x + 16) // 8
                y = (entry.y + 24) // 8
                arrangement[x, y].tile = tile
                if not entry.is_single():
                    arrangement[x+1, y].tile = tile + 1
                    arrangement[x, y+1].tile = tile + 16
                    arrangement[x+1, y+1].tile = tile + 17
            chars_positions[c] = {
                'x': c*3*8,
                'y': 0,
                'width': 3*8,
                'height': 6*8,
                'top_left_offset': top_left,
                'unknown': layout[0].unknown
            }

        # Write the characters animation frames
        for p in range(CHARS_NUM_ANIM_SUBPALETTES):
            with resource_open(CHARS_FRAMES_PATH.format(p), "png") as f:
                image = arrangement.image(
                    self.chars_tileset,
                    self.chars_anim_palette.get_subpalette(p)
                )
                image.save(f)

        # Write out the initial characters palette
        with resource_open(CHARS_INITIAL_PATH, "png") as f:
            image = arrangement.image(
                self.chars_tileset,
                self.chars_palette
            )
            image.save(f)

        # Write out the positions of the characters
        with resource_open(CHARS_POSITIONS_PATH, "yml", True) as f:
            yml_dump(chars_positions, f, False)

    def upgrade_project(
            self, old_version, new_version, rom, resource_open_r,
            resource_open_w, resource_delete):
        if old_version < 9:
            self.read_from_rom(rom)
            self.write_to_project(resource_open_w)

    @staticmethod
    def _decompress_block(rom, block, pointer):
        block.from_compressed_block(
            block=rom,
            offset=from_snes_address(read_asm_pointer(rom, pointer))
        )

    @staticmethod
    def _write_compressed_block(rom, compressed_block, pointer):
        compressed_block.compress()
        new_offset = rom.allocate(data=compressed_block)
        write_asm_pointer(
            block=rom, offset=pointer, pointer=to_snes_address(new_offset)
        )
        return new_offset
class DeathScreenModule(EbModule):
    """Extracts the death screen data from EarthBound."""

    NAME = "Death Screen"
    FREE_RANGES = [
        (0x21cfaf, 0x21d4f3),  # Tileset
        (0x21d4f4, 0x21d5e7),  # Palette
        (0x21d5e8, 0x21d6e1)  # Arrangement
    ]

    def __init__(self):
        super(DeathScreenModule, self).__init__()

        self.tileset = EbGraphicTileset(
            num_tiles=NUM_TILES, tile_width=TILE_WIDTH, tile_height=TILE_HEIGHT
        )
        self.arrangement = EbTileArrangement(
            width=ARRANGEMENT_WIDTH, height=ARRANGEMENT_HEIGHT
        )
        self.palette = EbPalette(
            num_subpalettes=NUM_SUBPALETTES,
            subpalette_length=SUBPALETTE_LENGTH
        )

    def read_from_rom(self, rom):
        with EbCompressibleBlock() as block:
            # Read the tileset data
            block.from_compressed_block(
                block=rom, offset=from_snes_address(
                    read_asm_pointer(rom, TILESET_POINTER)
                )
            )
            self.tileset.from_block(block=block, offset=0, bpp=TILESET_BPP)

            # Read the arrangement data
            block.from_compressed_block(
                block=rom, offset=from_snes_address(
                    read_asm_pointer(rom, ARRANGEMENT_POINTER)
                )
            )
            self.arrangement.from_block(block=block, offset=0)

            # Read the palette data
            block.from_compressed_block(
                block=rom, offset=from_snes_address(
                    read_asm_pointer(rom, PALETTE_POINTER)
                )
            )
            self.palette.from_block(block=block, offset=0)

    def write_to_rom(self, rom):
        # Write the tileset data
        block_size = self.tileset.block_size(bpp=TILESET_BPP)
        with EbCompressibleBlock(block_size) as block:
            self.tileset.to_block(block=block, offset=0, bpp=TILESET_BPP)
            self._write_compressed_block(rom, block, TILESET_POINTER)

        # Write the tile arrangement data
        block_size = self.arrangement.block_size()
        with EbCompressibleBlock(block_size) as block:
            self.arrangement.to_block(block=block, offset=0)
            self._write_compressed_block(rom, block, ARRANGEMENT_POINTER)

        # Write the palette data
        block_size = self.palette.block_size()
        with EbCompressibleBlock(block_size) as block:
            self.palette.to_block(block=block, offset=0)
            self._write_compressed_block(
                rom, block, PALETTE_POINTER
            )

    def read_from_project(self, resource_open):
        with resource_open(DEATH_SCREEN_PATH, "png") as f:
            image = open_indexed_image(f)
            self.arrangement.from_image(image, self.tileset, self.palette)
        with resource_open(DEATH_SCREEN_SUBPALETTES_PATH, "yml", True) as f:
            subpalettes = yml_load(f)
            for subpalette, tiles in subpalettes.items():
                for x, y in tiles:
                    self.arrangement[x, y].subpalette = subpalette

    def write_to_project(self, resource_open):
        with resource_open(DEATH_SCREEN_PATH, "png") as f:
            image = self.arrangement.image(self.tileset, self.palette, True)
            image.save(f)
        with resource_open(DEATH_SCREEN_SUBPALETTES_PATH, "yml", True) as f:
            subpalettes = {}
            for x in range(ARRANGEMENT_WIDTH):
                for y in range(ARRANGEMENT_HEIGHT):
                    subpalette = self.arrangement[x, y].subpalette
                    if subpalette not in subpalettes:
                        subpalettes[subpalette] = []
                    subpalettes[subpalette].append((x, y))
            yml_dump(subpalettes, f, None)

    def upgrade_project(
            self, old_version, new_version, rom, resource_open_r,
            resource_open_w, resource_delete):
        if old_version < 9:
            self.read_from_rom(rom)
            self.write_to_project(resource_open_w)

    @staticmethod
    def _write_compressed_block(rom, compressed_block, pointer):
        compressed_block.compress()
        new_offset = rom.allocate(data=compressed_block)
        write_asm_pointer(
            block=rom, offset=pointer, pointer=to_snes_address(new_offset)
        )
class DeathScreenModule(EbModule):
    """Extracts the death screen data from EarthBound."""

    NAME = "Death Screen"
    FREE_RANGES = [
        (0x21cfaf, 0x21d4f3),  # Tileset
        (0x21d4f4, 0x21d5e7),  # Palette
        (0x21d5e8, 0x21d6e1)  # Arrangement
    ]

    def __init__(self):
        super(DeathScreenModule, self).__init__()

        self.tileset = EbGraphicTileset(
            num_tiles=NUM_TILES, tile_width=TILE_WIDTH, tile_height=TILE_HEIGHT
        )
        self.arrangement = EbTileArrangement(
            width=ARRANGEMENT_WIDTH, height=ARRANGEMENT_HEIGHT
        )
        self.palette = EbPalette(
            num_subpalettes=NUM_SUBPALETTES,
            subpalette_length=SUBPALETTE_LENGTH
        )

    def read_from_rom(self, rom):
        with EbCompressibleBlock() as block:
            # Read the tileset data
            block.from_compressed_block(
                block=rom, offset=from_snes_address(
                    read_asm_pointer(rom, TILESET_POINTER)
                )
            )
            self.tileset.from_block(block=block, offset=0, bpp=TILESET_BPP)

            # Read the arrangement data
            block.from_compressed_block(
                block=rom, offset=from_snes_address(
                    read_asm_pointer(rom, ARRANGEMENT_POINTER)
                )
            )
            self.arrangement.from_block(block=block, offset=0)

            # Read the palette data
            block.from_compressed_block(
                block=rom, offset=from_snes_address(
                    read_asm_pointer(rom, PALETTE_POINTER)
                )
            )
            self.palette.from_block(block=block, offset=0)

    def write_to_rom(self, rom):
        # Write the tileset data
        block_size = self.tileset.block_size(bpp=TILESET_BPP)
        with EbCompressibleBlock(block_size) as block:
            self.tileset.to_block(block=block, offset=0, bpp=TILESET_BPP)
            self._write_compressed_block(rom, block, TILESET_POINTER)

        # Write the tile arrangement data
        block_size = self.arrangement.block_size()
        with EbCompressibleBlock(block_size) as block:
            self.arrangement.to_block(block=block, offset=0)
            self._write_compressed_block(rom, block, ARRANGEMENT_POINTER)

        # Write the palette data
        block_size = self.palette.block_size()
        with EbCompressibleBlock(block_size) as block:
            self.palette.to_block(block=block, offset=0)
            self._write_compressed_block(
                rom, block, PALETTE_POINTER
            )

    def read_from_project(self, resource_open):
        with resource_open(DEATH_SCREEN_PATH, "png") as f:
            image = open_indexed_image(f)
            self.arrangement.from_image(image, self.tileset, self.palette)
        with resource_open(DEATH_SCREEN_SUBPALETTES_PATH, "yml") as f:
            subpalettes = yml_load(f)
            for subpalette, tiles in subpalettes.items():
                for x, y in tiles:
                    self.arrangement[x, y].subpalette = subpalette

    def write_to_project(self, resource_open):
        with resource_open(DEATH_SCREEN_PATH, "png") as f:
            image = self.arrangement.image(self.tileset, self.palette, True)
            image.save(f)
        with resource_open(DEATH_SCREEN_SUBPALETTES_PATH, "yml") as f:
            subpalettes = {}
            for x in range(ARRANGEMENT_WIDTH):
                for y in range(ARRANGEMENT_HEIGHT):
                    subpalette = self.arrangement[x, y].subpalette
                    if subpalette not in subpalettes:
                        subpalettes[subpalette] = []
                    subpalettes[subpalette].append((x, y))
            yml_dump(subpalettes, f, None)

    def upgrade_project(
            self, old_version, new_version, rom, resource_open_r,
            resource_open_w, resource_delete):
        if old_version < 9:
            self.read_from_rom(rom)
            self.write_to_project(resource_open_w)

    @staticmethod
    def _write_compressed_block(rom, compressed_block, pointer):
        compressed_block.compress()
        new_offset = rom.allocate(data=compressed_block)
        write_asm_pointer(
            block=rom, offset=pointer, pointer=to_snes_address(new_offset)
        )