Beispiel #1
0
 def __init__(self):
     self.minitiles = EbGraphicTileset(num_tiles=896,
                                       tile_width=8,
                                       tile_height=8)
     self.arrangements = [None for i in range(1024)]
     self.collisions = [None for i in range(1024)]
     self.palettes = []
    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 test_from_image_8x8_1bpp(self):
        palette = EbPalette(1, 2)
        palette[0, 0].from_tuple((0xff, 0xff, 0xff))
        palette[0, 1].from_tuple((0x0, 0x0, 0x0))

        arrangement = EbTileArrangement(width=2, height=2)
        arrangement[0, 0].tile = 0
        arrangement[1, 0].tile = 2
        arrangement[0, 1].tile = 1
        arrangement[1, 1].tile = 3

        tileset = EbGraphicTileset(num_tiles=4, tile_width=8, tile_height=8)
        tileset.from_image(self.tile_8x8_2bpp_img,
                           arrangement=arrangement,
                           palette=palette)
        assert_list_equal(tileset[0], [[0] * 8] * 8)
        assert_list_equal(tileset[2], [[1] * 8] * 8)
        assert_list_equal(tileset[1],
                          [[0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0],
                           [0, 0, 1, 1, 1, 1, 0, 0], [0, 0, 1, 1, 1, 1, 0, 0],
                           [0, 0, 1, 1, 1, 1, 0, 0], [0, 0, 1, 1, 1, 1, 0, 0],
                           [0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0]])
        assert_list_equal(tileset[3],
                          [[0, 1, 1, 1, 1, 1, 1, 1], [0, 1, 1, 1, 1, 1, 1, 1],
                           [0, 1, 0, 0, 0, 0, 1, 1], [1, 1, 0, 0, 1, 0, 1, 1],
                           [1, 1, 0, 1, 0, 0, 1, 1], [1, 1, 0, 0, 0, 0, 1, 1],
                           [1, 1, 1, 1, 1, 1, 1, 1], [1, 1, 1, 1, 1, 1, 1, 0]])
    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 test_from_block_1bpp_out_of_bounds(self):
     block = Block()
     block.from_list([
         0b00000011, 0b01110000, 0b01001001, 0b11110000, 0b01001010,
         0b11001000, 0b01110001, 0b00000001, 0b00100000, 0b00110000,
         0b00101000, 0b00101000, 0b01100000, 0b11100000, 0b11000000,
         0b00000001
     ])
     tileset = EbGraphicTileset(num_tiles=3, tile_width=8, tile_height=8)
     tileset.from_block(block, offset=0, bpp=1)
     assert_list_equal(tileset[0],
                       [[0, 0, 0, 0, 0, 0, 1, 1], [0, 1, 1, 1, 0, 0, 0, 0],
                        [0, 1, 0, 0, 1, 0, 0, 1], [1, 1, 1, 1, 0, 0, 0, 0],
                        [0, 1, 0, 0, 1, 0, 1, 0], [1, 1, 0, 0, 1, 0, 0, 0],
                        [0, 1, 1, 1, 0, 0, 0, 1], [0, 0, 0, 0, 0, 0, 0, 1]])
     assert_list_equal(tileset[1],
                       [[0, 0, 1, 0, 0, 0, 0, 0], [0, 0, 1, 1, 0, 0, 0, 0],
                        [0, 0, 1, 0, 1, 0, 0, 0], [0, 0, 1, 0, 1, 0, 0, 0],
                        [0, 1, 1, 0, 0, 0, 0, 0], [1, 1, 1, 0, 0, 0, 0, 0],
                        [1, 1, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 1]])
     assert_list_equal(tileset[2],
                       [[0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0],
                        [0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0],
                        [0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0],
                        [0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0]])
Beispiel #6
0
    def __init__(self):
        super(WindowGraphicsModule, self).__init__()
        self.graphics_1 = EbGraphicTileset(num_tiles=416, tile_width=8, tile_height=8)
        self.graphics_2 = EbGraphicTileset(num_tiles=7, tile_width=8, tile_height=8)

        self.flavor_palettes = [EbPalette(8, 4) for i in range(7)]
        self.flavor_names = dict()
Beispiel #7
0
    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)
Beispiel #8
0
 def test_from_block_4bpp(self):
     block = Block()
     block.from_list(
         [
             0b01010110,
             0b00001011,
             0b11001110,
             0b10010110,
             0b01110001,
             0b00111011,
             0b00001011,
             0b10011110,
             0b00011000,
             0b00000011,
             0b10000001,
             0b11101011,
             0b00000100,
             0b01000101,
             0b01010110,
             0b10001111,
             0b00101100,
             0b10110000,
             0b01010110,
             0b10110010,
             0b01010000,
             0b11000000,
             0b00111000,
             0b10010111,
             0b00101101,
             0b11111100,
             0b01111101,
             0b11101010,
             0b10101111,
             0b10110111,
             0b01100000,
             0b11101110,
         ]
     )
     tileset = EbGraphicTileset(num_tiles=1, tile_width=8, tile_height=8)
     tileset.from_block(block, offset=0, bpp=4)
     assert_list_equal(
         tileset[0],
         [
             [8, 1, 12, 9, 6, 5, 3, 2],
             [11, 5, 8, 14, 1, 7, 15, 0],
             [8, 13, 3, 7, 2, 0, 2, 3],
             [10, 0, 4, 14, 7, 10, 11, 9],
             [8, 8, 12, 9, 13, 12, 2, 6],
             [11, 14, 14, 4, 14, 4, 10, 7],
             [12, 2, 12, 8, 4, 15, 12, 14],
             [10, 13, 12, 1, 10, 11, 11, 2],
         ],
     )
Beispiel #9
0
 def test_to_block_4bpp(self):
     tileset = EbGraphicTileset(num_tiles=1, tile_width=8, tile_height=8)
     tileset.tiles = [None]
     tileset.tiles[0] = [
         [8, 1, 12, 9, 6, 5, 3, 2],
         [11, 5, 8, 14, 1, 7, 15, 0],
         [8, 13, 3, 7, 2, 0, 2, 3],
         [10, 0, 4, 14, 7, 10, 11, 9],
         [8, 8, 12, 9, 13, 12, 2, 6],
         [11, 14, 14, 4, 14, 4, 10, 7],
         [12, 2, 12, 8, 4, 15, 12, 14],
         [10, 13, 12, 1, 10, 11, 11, 2],
     ]
     block = Block()
     block.from_list([0] * 32)
     tileset.to_block(block, 0, 4)
     assert_list_equal(
         block.to_list(),
         [
             0b01010110,
             0b00001011,
             0b11001110,
             0b10010110,
             0b01110001,
             0b00111011,
             0b00001011,
             0b10011110,
             0b00011000,
             0b00000011,
             0b10000001,
             0b11101011,
             0b00000100,
             0b01000101,
             0b01010110,
             0b10001111,
             0b00101100,
             0b10110000,
             0b01010110,
             0b10110010,
             0b01010000,
             0b11000000,
             0b00111000,
             0b10010111,
             0b00101101,
             0b11111100,
             0b01111101,
             0b11101010,
             0b10101111,
             0b10110111,
             0b01100000,
             0b11101110,
         ],
     )
Beispiel #10
0
 def test_from_block_1bpp(self):
     block = Block()
     block.from_list(
         [
             0b00000011,
             0b01110000,
             0b01001001,
             0b11110000,
             0b01001010,
             0b11001000,
             0b01110001,
             0b00000001,
             0b00100000,
             0b00110000,
             0b00101000,
             0b00101000,
             0b01100000,
             0b11100000,
             0b11000000,
             0b00000001,
         ]
     )
     tileset = EbGraphicTileset(num_tiles=2, tile_width=8, tile_height=8)
     tileset.from_block(block, offset=0, bpp=1)
     assert_list_equal(
         tileset[0],
         [
             [0, 0, 0, 0, 0, 0, 1, 1],
             [0, 1, 1, 1, 0, 0, 0, 0],
             [0, 1, 0, 0, 1, 0, 0, 1],
             [1, 1, 1, 1, 0, 0, 0, 0],
             [0, 1, 0, 0, 1, 0, 1, 0],
             [1, 1, 0, 0, 1, 0, 0, 0],
             [0, 1, 1, 1, 0, 0, 0, 1],
             [0, 0, 0, 0, 0, 0, 0, 1],
         ],
     )
     assert_list_equal(
         tileset[1],
         [
             [0, 0, 1, 0, 0, 0, 0, 0],
             [0, 0, 1, 1, 0, 0, 0, 0],
             [0, 0, 1, 0, 1, 0, 0, 0],
             [0, 0, 1, 0, 1, 0, 0, 0],
             [0, 1, 1, 0, 0, 0, 0, 0],
             [1, 1, 1, 0, 0, 0, 0, 0],
             [1, 1, 0, 0, 0, 0, 0, 0],
             [0, 0, 0, 0, 0, 0, 0, 1],
         ],
     )
Beispiel #11
0
 def test_from_block_2bpp(self):
     block = Block()
     block.from_list([
         0b01010101,  # Tile 1
         0b10111010,
         0b01100100,
         0b11001111,
         0b10100000,
         0b10111101,
         0b11100001,
         0b01101011,
         0b10110111,
         0b00000111,
         0b11111010,
         0b01111101,
         0b00110010,
         0b11101100,
         0b00110110,
         0b10111100,
         0b11111001,  # Tile 2
         0b01101010,
         0b10011000,
         0b11111111,
         0b11001011,
         0b00111000,
         0b01000001,
         0b01110001,
         0b11001010,
         0b11000000,
         0b01111010,
         0b11011101,
         0b00011011,
         0b00001111,
         0b00100001,
         0b11110000
     ])
     tileset = EbGraphicTileset(num_tiles=2, tile_width=8, tile_height=8)
     tileset.from_block(block, offset=0, bpp=2)
     assert_list_equal(tileset[0],
                       [[2, 1, 2, 3, 2, 1, 2, 1], [2, 3, 1, 0, 2, 3, 2, 2],
                        [3, 0, 3, 2, 2, 2, 0, 2], [1, 3, 3, 0, 2, 0, 2, 3],
                        [1, 0, 1, 1, 0, 3, 3, 3], [1, 3, 3, 3, 3, 2, 1, 2],
                        [2, 2, 3, 1, 2, 2, 1, 0], [2, 0, 3, 3, 2, 3, 1, 0]])
     assert_list_equal(tileset[1],
                       [[1, 3, 3, 1, 3, 0, 2, 1], [3, 2, 2, 3, 3, 2, 2, 2],
                        [1, 1, 2, 2, 3, 0, 1, 1], [0, 3, 2, 2, 0, 0, 0, 3],
                        [3, 3, 0, 0, 1, 0, 1, 0], [2, 3, 1, 3, 3, 2, 1, 2],
                        [0, 0, 0, 1, 3, 2, 3, 3], [2, 2, 3, 2, 0, 0, 0, 1]])
Beispiel #12
0
    def read_from_rom(self, rom):
        self.bg_table.from_block(block=rom, offset=from_snes_address(BACKGROUND_TABLE_OFFSET))
        self.scroll_table.from_block(block=rom, offset=from_snes_address(SCROLL_TABLE_OFFSET))
        self.distortion_table.from_block(block=rom, offset=from_snes_address(DISTORTION_TABLE_OFFSET))
        self.graphics_pointer_table.from_block(
            block=rom,
            offset=from_snes_address(read_asm_pointer(block=rom,
                                                      offset=GRAPHICS_POINTER_TABLE_ASM_POINTER_OFFSETS[0])))
        self.arrangement_pointer_table.from_block(
            block=rom,
            offset=from_snes_address(read_asm_pointer(block=rom,
                                                      offset=ARRANGEMENT_POINTER_TABLE_ASM_POINTER_OFFSETS[0])))
        self.palette_pointer_table.from_block(
            block=rom,
            offset=from_snes_address(read_asm_pointer(block=rom,
                                                      offset=PALETTE_POINTER_TABLE_ASM_POINTER_OFFSETS[0])))

        self.backgrounds = [None for i in range(self.graphics_pointer_table.num_rows)]
        self.palettes = [None for i in range(self.palette_pointer_table.num_rows)]
        for i in range(self.bg_table.num_rows):
            graphics_id = self.bg_table[i][0]
            color_depth = self.bg_table[i][2]
            if self.backgrounds[graphics_id] is None:
                # Max tiles used in rom: 421 (2bpp) 442 (4bpp)
                tileset = EbGraphicTileset(num_tiles=512, tile_width=8, tile_height=8)
                with EbCompressibleBlock() as compressed_block:
                    compressed_block.from_compressed_block(
                        block=rom,
                        offset=from_snes_address(self.graphics_pointer_table[graphics_id][0]))
                    tileset.from_block(compressed_block, offset=0, bpp=color_depth)

                arrangement = EbTileArrangement(width=32, height=32)
                with EbCompressibleBlock() as compressed_block:
                    compressed_block.from_compressed_block(
                        block=rom,
                        offset=from_snes_address(self.arrangement_pointer_table[graphics_id][0]))
                    arrangement.from_block(block=compressed_block, offset=0)

                self.backgrounds[graphics_id] = (tileset, color_depth, arrangement)
                
            palette_id = self.bg_table[i][1]
            if self.palettes[palette_id] is None:
                palette = EbPalette(num_subpalettes=1, subpalette_length=16)
                palette.from_block(block=rom, offset=from_snes_address(self.palette_pointer_table[palette_id][0]))
                self.palettes[palette_id] = palette
Beispiel #13
0
class EbCreditsFont(object):
    def __init__(self):
        self.tileset = EbGraphicTileset(num_tiles=192, tile_width=8, tile_height=8)
        self.palette = EbPalette(num_subpalettes=2, subpalette_length=4)

    def from_block(self, block, tileset_asm_pointer_offset, palette_offset):
        with EbCompressibleBlock() as compressed_block:
            compressed_block.from_compressed_block(block=block, offset=from_snes_address(
                read_asm_pointer(block=block, offset=tileset_asm_pointer_offset)))
            self.tileset.from_block(block=compressed_block, bpp=2)
        self.palette.from_block(block=block, offset=palette_offset)

    def to_block(self, block, tileset_asm_pointer_offset, palette_offset):
        tileset_block_size = self.tileset.block_size(bpp=2)
        with EbCompressibleBlock(tileset_block_size) as compressed_block:
            self.tileset.to_block(block=compressed_block, offset=0, bpp=2)
            compressed_block.compress()
            tileset_offset = block.allocate(data=compressed_block)
            write_asm_pointer(block=block, offset=tileset_asm_pointer_offset, pointer=to_snes_address(tileset_offset))
        self.palette.to_block(block=block, offset=palette_offset)

    def to_files(self, image_file, image_format="png"):
        image = _CREDITS_PREVIEW_ARRANGEMENT.image(self.tileset, self.palette)
        image.save(image_file, image_format)
        del image

    def from_files(self, image_file, image_format="png"):
        image = open_indexed_image(image_file)
        self.palette.from_image(image)
        self.tileset.from_image(image, _CREDITS_PREVIEW_ARRANGEMENT, self.palette)
        del image
Beispiel #14
0
    def test_from_image_8x16_2bpp(self):
        palette = EbPalette(1, 4)
        palette[0, 0].from_tuple((0xff, 0xff, 0xff))
        palette[0, 1].from_tuple((0x30, 0x00, 0xff))
        palette[0, 2].from_tuple((0xff, 0x00, 0x00))
        palette[0, 3].from_tuple((0x00, 0xff, 0x48))

        arrangement = EbTileArrangement(width=2, height=3)
        arrangement[0, 0].tile = 1
        arrangement[1, 0].tile = 1
        arrangement[0, 1].tile = 3
        arrangement[1, 1].tile = 2
        arrangement[0, 2].tile = 0
        arrangement[1, 2].tile = 4

        tileset = EbGraphicTileset(num_tiles=5, tile_width=8, tile_height=16)
        tileset.from_image(self.tile_8x16_4bpp_img,
                           arrangement=arrangement,
                           palette=palette)

        assert_list_equal(tileset[1], [[2] * 8] * 16)
        assert_list_equal(tileset[3], [[3] * 8] * 16)
        assert_list_equal(tileset[2],
                          [[3] * 8, [3] * 8, [3] * 8, [3] * 8, [3] * 8,
                           [3, 3, 1, 3, 3, 3, 3, 3], [3, 3, 1, 3, 3, 1, 3, 3],
                           [1] * 8, [1, 1, 2, 2, 1, 1, 1, 1],
                           [1, 2, 2, 2, 2, 2, 1, 1], [1, 1, 1, 1, 1, 2, 1, 1],
                           [1, 1, 1, 1, 2, 2, 1, 1], [1, 1, 2, 2, 2, 1, 1, 1],
                           [1] * 8, [1] * 8, [1, 1, 1, 3, 1, 1, 1, 1]])
        assert_list_equal(tileset[0],
                          [[2, 1, 1, 1, 1, 1, 1, 1], [2, 3, 3, 3, 3, 3, 3, 1],
                           [0, 2, 3, 3, 3, 3, 1, 3], [0, 2, 3, 3, 3, 3, 1, 3],
                           [0, 0, 2, 3, 3, 1, 3, 3], [0, 0, 2, 3, 3, 1, 3, 3],
                           [0, 0, 0, 2, 1, 3, 3, 3], [0, 0, 0, 2, 1, 3, 3, 3],
                           [0, 0, 0, 1, 2, 3, 3, 3], [0, 0, 0, 1, 2, 3, 3, 3],
                           [0, 0, 1, 0, 0, 2, 3, 3], [0, 0, 1, 0, 0, 2, 3, 3],
                           [0, 1, 0, 0, 0, 0, 2, 3], [0, 1, 0, 0, 0, 0, 2, 3],
                           [1, 0, 0, 0, 0, 0, 0, 2], [1, 0, 0, 0, 0, 0, 0, 2]])
        assert_list_equal(tileset[4],
                          [[3] * 8, [3] * 8, [3] * 8, [3] * 8, [3] * 8,
                           [3, 2, 3, 3, 3, 2, 3, 3], [3] * 8, [3] * 8,
                           [3, 3, 3, 3, 3, 3, 3, 2], [2, 3, 3, 3, 3, 3, 2, 3],
                           [3, 2, 3, 3, 3, 2, 2, 3], [3, 2, 2, 2, 2, 2, 3, 3],
                           [3] * 8, [3] * 8, [3] * 8, [3] * 8])
Beispiel #15
0
 def test_from_block_4bpp(self):
     block = Block()
     block.from_list([
         0b01010110, 0b00001011, 0b11001110, 0b10010110, 0b01110001,
         0b00111011, 0b00001011, 0b10011110, 0b00011000, 0b00000011,
         0b10000001, 0b11101011, 0b00000100, 0b01000101, 0b01010110,
         0b10001111, 0b00101100, 0b10110000, 0b01010110, 0b10110010,
         0b01010000, 0b11000000, 0b00111000, 0b10010111, 0b00101101,
         0b11111100, 0b01111101, 0b11101010, 0b10101111, 0b10110111,
         0b01100000, 0b11101110
     ])
     tileset = EbGraphicTileset(num_tiles=1, tile_width=8, tile_height=8)
     tileset.from_block(block, offset=0, bpp=4)
     assert_list_equal(
         tileset[0],
         [[8, 1, 12, 9, 6, 5, 3, 2], [11, 5, 8, 14, 1, 7, 15, 0],
          [8, 13, 3, 7, 2, 0, 2, 3], [10, 0, 4, 14, 7, 10, 11, 9],
          [8, 8, 12, 9, 13, 12, 2, 6], [11, 14, 14, 4, 14, 4, 10, 7],
          [12, 2, 12, 8, 4, 15, 12, 14], [10, 13, 12, 1, 10, 11, 11, 2]])
Beispiel #16
0
    def test_to_image_single_subpalette(self):
        palette = EbPalette(1, 2)
        tileset = EbGraphicTileset(num_tiles=6, tile_width=8, tile_height=8)
        arrangement = EbTileArrangement(width=6, height=1)
        arrangement.from_image(self.tile_8x8_2bpp_2_img,
                               tileset=tileset,
                               palette=palette)

        new_image = arrangement.image(tileset, palette)
        assert_images_equal(self.tile_8x8_2bpp_2_img, new_image)
Beispiel #17
0
    def test_from_image_8x8_1bpp(self):
        palette = EbPalette(1, 2)
        palette[0, 0].from_tuple((0xFF, 0xFF, 0xFF))
        palette[0, 1].from_tuple((0x0, 0x0, 0x0))

        arrangement = EbTileArrangement(width=2, height=2)
        arrangement[0, 0].tile = 0
        arrangement[1, 0].tile = 2
        arrangement[0, 1].tile = 1
        arrangement[1, 1].tile = 3

        tileset = EbGraphicTileset(num_tiles=4, tile_width=8, tile_height=8)
        tileset.from_image(self.tile_8x8_2bpp_img, arrangement=arrangement, palette=palette)
        assert_list_equal(tileset[0], [[0] * 8] * 8)
        assert_list_equal(tileset[2], [[1] * 8] * 8)
        assert_list_equal(
            tileset[1],
            [
                [0, 0, 0, 0, 0, 0, 0, 0],
                [0, 0, 0, 0, 0, 0, 0, 0],
                [0, 0, 1, 1, 1, 1, 0, 0],
                [0, 0, 1, 1, 1, 1, 0, 0],
                [0, 0, 1, 1, 1, 1, 0, 0],
                [0, 0, 1, 1, 1, 1, 0, 0],
                [0, 0, 0, 0, 0, 0, 0, 0],
                [0, 0, 0, 0, 0, 0, 0, 0],
            ],
        )
        assert_list_equal(
            tileset[3],
            [
                [0, 1, 1, 1, 1, 1, 1, 1],
                [0, 1, 1, 1, 1, 1, 1, 1],
                [0, 1, 0, 0, 0, 0, 1, 1],
                [1, 1, 0, 0, 1, 0, 1, 1],
                [1, 1, 0, 1, 0, 0, 1, 1],
                [1, 1, 0, 0, 0, 0, 1, 1],
                [1, 1, 1, 1, 1, 1, 1, 1],
                [1, 1, 1, 1, 1, 1, 1, 0],
            ],
        )
    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)
                )
Beispiel #19
0
    def __init__(self, frames, unknown, graphics_data_size=None):
        self.graphics_data_size = graphics_data_size
        self.frames = frames
        self.unknown = unknown

        if graphics_data_size:
            num_tiles = EbGraphicTileset.tiles_from_parameters(
                graphics_data_size, TILE_WIDTH, TILE_HEIGHT, TILE_BPP)
        else:  # Make tileset with maximum number of tiles possible for each frame of animation
            num_tiles = SCREEN_WIDTH_TILES * SCREEN_HEIGHT_TILES * frames

        # Animations are 2 bpp, so the palette will have 4 colors
        self.palette = EbPalette(num_subpalettes=1, subpalette_length=4)
        self.graphics = EbGraphicTileset(num_tiles=num_tiles,
                                         tile_width=TILE_WIDTH,
                                         tile_height=TILE_HEIGHT)
        self.arrangements = [
            EbTileArrangement(width=SCREEN_WIDTH_TILES,
                              height=SCREEN_HEIGHT_TILES)
            for i in range(self.frames)
        ]
Beispiel #20
0
class EbFont(object):
    def __init__(self, num_characters=96, tile_width=16, tile_height=8):
        self.num_characters = num_characters
        self.tileset = EbGraphicTileset(num_tiles=num_characters, tile_width=tile_width, tile_height=tile_height)
        self.character_widths = None

    def from_block(self, block, tileset_offset, character_widths_offset):
        self.tileset.from_block(block=block, offset=tileset_offset, bpp=1)
        self.character_widths = block[character_widths_offset:character_widths_offset + self.num_characters].to_list()

    def to_block(self, block, tileset_offset, character_widths_offset):
        self.tileset.to_block(block=block, offset=tileset_offset, bpp=1)
        block[character_widths_offset:character_widths_offset + self.num_characters] = self.character_widths

    def to_files(self, image_file, widths_file, image_format="png", widths_format="yml"):
        image = _FONT_IMAGE_ARRANGEMENT.image(self.tileset, _FONT_IMAGE_PALETTE)
        image.save(image_file, image_format)
        del image

        character_widths_dict = dict(enumerate(self.character_widths))
        if widths_format == "yml":
            yml_dump(character_widths_dict, widths_file, default_flow_style=False)

    def from_files(self, image_file, widths_file, image_format="png", widths_format="yml"):
        image = open_indexed_image(image_file)
        self.tileset.from_image(image, _FONT_IMAGE_ARRANGEMENT, _FONT_IMAGE_PALETTE)
        del image

        if widths_format == "yml":
            widths_dict = yml_load(widths_file)
            self.character_widths = map(lambda i: widths_dict[i], range(self.tileset.num_tiles_maximum))
Beispiel #21
0
    def test_add_tile(self):
        tileset = EbGraphicTileset(num_tiles=5, tile_width=8, tile_height=8)
        tile = [
            array('B', [5, 7, 6, 4, 1, 5, 3, 3]),
            array('B', [5, 8, 3, 7, 5, 7, 1, 1]),
            array('B', [6, 1, 1, 5, 4, 0, 1, 5]),
            array('B', [2, 1, 4, 5, 4, 1, 6, 4]),
            array('B', [4, 6, 6, 2, 1, 0, 4, 4]),
            array('B', [6, 1, 0, 7, 5, 8, 1, 8]),
            array('B', [8, 0, 4, 0, 2, 8, 0, 8]),
            array('B', [0, 6, 8, 6, 0, 5, 4, 0])
        ]
        tile1_id, tile1_vflip, tile1_hflip = tileset.add_tile(tile)

        tile2_id, tile2_vflip, tile2_hflip = tileset.add_tile(tile)
        assert_equal(tile2_id, tile1_id)
        assert_equal(tile2_vflip, tile1_vflip)
        assert_equal(tile2_hflip, tile1_hflip)

        tile.reverse()
        tile2_id, tile2_vflip, tile2_hflip = tileset.add_tile(tile)
        assert_equal(tile2_id, tile1_id)
        assert_equal(tile2_vflip, not tile1_vflip)
        assert_equal(tile2_hflip, tile1_hflip)

        for row in tile:
            row.reverse()
        tile2_id, tile2_vflip, tile2_hflip = tileset.add_tile(tile)
        assert_equal(tile2_id, tile1_id)
        assert_equal(tile2_vflip, not tile1_vflip)
        assert_equal(tile2_hflip, not tile1_hflip)

        tile.reverse()
        tile2_id, tile2_vflip, tile2_hflip = tileset.add_tile(tile)
        assert_equal(tile2_id, tile1_id)
        assert_equal(tile2_vflip, tile1_vflip)
        assert_equal(tile2_hflip, not tile1_hflip)

        tile = [
            array('B', [1, 2, 3, 4, 1, 5, 3, 3]),
            array('B', [5, 8, 3, 7, 5, 7, 1, 1]),
            array('B', [6, 1, 1, 5, 4, 0, 1, 5]),
            array('B', [2, 1, 4, 5, 4, 1, 6, 4]),
            array('B', [4, 6, 6, 2, 1, 0, 4, 4]),
            array('B', [6, 1, 0, 7, 5, 8, 1, 8]),
            array('B', [8, 0, 4, 0, 2, 8, 0, 8]),
            array('B', [0, 6, 8, 6, 0, 5, 4, 0])
        ]
        tile2_id, tile2_vflip, tile2_hflip = tileset.add_tile(tile)
        assert_not_equal(tile2_id, tile1_id)
    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
        )
Beispiel #23
0
    def test_init(self):
        tileset = EbGraphicTileset(num_tiles=1, tile_width=8, tile_height=16)
        assert_equal(tileset.num_tiles_maximum, 1)
        assert_equal(tileset.tile_width, 8)
        assert_equal(tileset.tile_height, 16)

        assert_raises(InvalidArgumentError, EbGraphicTileset, 0, 8, 8)
        assert_raises(InvalidArgumentError, EbGraphicTileset, -1, 8, 8)
        assert_raises(InvalidArgumentError, EbGraphicTileset, 1, 0, 8)
        assert_raises(InvalidArgumentError, EbGraphicTileset, 1, -1, 8)
        assert_raises(InvalidArgumentError, EbGraphicTileset, 1, 1, 8)
        assert_raises(InvalidArgumentError, EbGraphicTileset, 1, 8, 0)
        assert_raises(InvalidArgumentError, EbGraphicTileset, 1, 8, -1)
Beispiel #24
0
    def test_add_tile(self):
        tileset = EbGraphicTileset(num_tiles=5, tile_width=8, tile_height=8)
        tile = [
            array("B", [5, 7, 6, 4, 1, 5, 3, 3]),
            array("B", [5, 8, 3, 7, 5, 7, 1, 1]),
            array("B", [6, 1, 1, 5, 4, 0, 1, 5]),
            array("B", [2, 1, 4, 5, 4, 1, 6, 4]),
            array("B", [4, 6, 6, 2, 1, 0, 4, 4]),
            array("B", [6, 1, 0, 7, 5, 8, 1, 8]),
            array("B", [8, 0, 4, 0, 2, 8, 0, 8]),
            array("B", [0, 6, 8, 6, 0, 5, 4, 0]),
        ]
        tile1_id, tile1_vflip, tile1_hflip = tileset.add_tile(tile)

        tile2_id, tile2_vflip, tile2_hflip = tileset.add_tile(tile)
        assert_equal(tile2_id, tile1_id)
        assert_equal(tile2_vflip, tile1_vflip)
        assert_equal(tile2_hflip, tile1_hflip)

        tile.reverse()
        tile2_id, tile2_vflip, tile2_hflip = tileset.add_tile(tile)
        assert_equal(tile2_id, tile1_id)
        assert_equal(tile2_vflip, not tile1_vflip)
        assert_equal(tile2_hflip, tile1_hflip)

        for row in tile:
            row.reverse()
        tile2_id, tile2_vflip, tile2_hflip = tileset.add_tile(tile)
        assert_equal(tile2_id, tile1_id)
        assert_equal(tile2_vflip, not tile1_vflip)
        assert_equal(tile2_hflip, not tile1_hflip)

        tile.reverse()
        tile2_id, tile2_vflip, tile2_hflip = tileset.add_tile(tile)
        assert_equal(tile2_id, tile1_id)
        assert_equal(tile2_vflip, tile1_vflip)
        assert_equal(tile2_hflip, not tile1_hflip)

        tile = [
            array("B", [1, 2, 3, 4, 1, 5, 3, 3]),
            array("B", [5, 8, 3, 7, 5, 7, 1, 1]),
            array("B", [6, 1, 1, 5, 4, 0, 1, 5]),
            array("B", [2, 1, 4, 5, 4, 1, 6, 4]),
            array("B", [4, 6, 6, 2, 1, 0, 4, 4]),
            array("B", [6, 1, 0, 7, 5, 8, 1, 8]),
            array("B", [8, 0, 4, 0, 2, 8, 0, 8]),
            array("B", [0, 6, 8, 6, 0, 5, 4, 0]),
        ]
        tile2_id, tile2_vflip, tile2_hflip = tileset.add_tile(tile)
        assert_not_equal(tile2_id, tile1_id)
Beispiel #25
0
 def test_to_block_2bpp(self):
     tileset = EbGraphicTileset(num_tiles=2, tile_width=8, tile_height=8)
     tileset.tiles = [None, None]
     tileset.tiles[0] = [[2, 1, 2, 3, 2, 1, 2, 1], [2, 3, 1, 0, 2, 3, 2, 2],
                         [3, 0, 3, 2, 2, 2, 0, 2], [1, 3, 3, 0, 2, 0, 2, 3],
                         [1, 0, 1, 1, 0, 3, 3, 3], [1, 3, 3, 3, 3, 2, 1, 2],
                         [2, 2, 3, 1, 2, 2, 1, 0], [2, 0, 3, 3, 2, 3, 1, 0]]
     tileset.tiles[1] = [[1, 3, 3, 1, 3, 0, 2, 1], [3, 2, 2, 3, 3, 2, 2, 2],
                         [1, 1, 2, 2, 3, 0, 1, 1], [0, 3, 2, 2, 0, 0, 0, 3],
                         [3, 3, 0, 0, 1, 0, 1, 0], [2, 3, 1, 3, 3, 2, 1, 2],
                         [0, 0, 0, 1, 3, 2, 3, 3], [2, 2, 3, 2, 0, 0, 0, 1]]
     block = Block()
     block.from_list([0] * 32)
     tileset.to_block(block, 0, 2)
     assert_list_equal(
         block.to_list(),
         [
             0b01010101,  # Tile 1
             0b10111010,
             0b01100100,
             0b11001111,
             0b10100000,
             0b10111101,
             0b11100001,
             0b01101011,
             0b10110111,
             0b00000111,
             0b11111010,
             0b01111101,
             0b00110010,
             0b11101100,
             0b00110110,
             0b10111100,
             0b11111001,  # Tile 2
             0b01101010,
             0b10011000,
             0b11111111,
             0b11001011,
             0b00111000,
             0b01000001,
             0b01110001,
             0b11001010,
             0b11000000,
             0b01111010,
             0b11011101,
             0b00011011,
             0b00001111,
             0b00100001,
             0b11110000
         ])
Beispiel #26
0
class SoundStoneModule(EbModule):
    NAME = "Sound Stone"
    FREE_RANGES = [(0x0EDD5D, 0x0EF805)]  # Sound stone graphics

    def __init__(self):
        super(SoundStoneModule, self).__init__()
        self.tileset = EbGraphicTileset(num_tiles=352,
                                        tile_width=8,
                                        tile_height=8)
        self.palette = EbPalette(num_subpalettes=6, subpalette_length=16)

    def read_from_rom(self, rom):
        graphics_offset = from_snes_address(
            read_asm_pointer(block=rom, offset=GRAPHICS_ASM_POINTER_OFFSET))
        with EbCompressibleBlock() as compressed_block:
            compressed_block.from_compressed_block(block=rom,
                                                   offset=graphics_offset)
            self.tileset.from_block(block=compressed_block, bpp=4)
        self.palette.from_block(block=rom, offset=PALETTE_OFFSET)

    def write_to_rom(self, rom):
        tileset_block_size = self.tileset.block_size(bpp=4)
        with EbCompressibleBlock(tileset_block_size) as compressed_block:
            self.tileset.to_block(block=compressed_block, offset=0, bpp=4)
            compressed_block.compress()
            tileset_offset = rom.allocate(data=compressed_block)
            write_asm_pointer(block=rom,
                              offset=GRAPHICS_ASM_POINTER_OFFSET,
                              pointer=to_snes_address(tileset_offset))
        self.palette.to_block(block=rom, offset=PALETTE_OFFSET)

    def read_from_project(self, resource_open):
        with resource_open("Logos/SoundStone", "png") as image_file:
            image = open_indexed_image(image_file)
            self.palette.from_image(image)
            self.tileset.from_image(image, SOUND_STONE_ARRANGEMENT,
                                    self.palette)

    def write_to_project(self, resource_open):
        image = SOUND_STONE_ARRANGEMENT.image(self.tileset, self.palette)
        with resource_open("Logos/SoundStone", "png") as image_file:
            image.save(image_file, "png")

    def upgrade_project(self, old_version, new_version, rom, resource_open_r,
                        resource_open_w, resource_delete):
        if old_version < 8:
            self.read_from_rom(rom)
            self.write_to_project(resource_open_w)
Beispiel #27
0
    def test_from_image_single_subpalette(self):
        palette = EbPalette(1, 2)
        tileset = EbGraphicTileset(num_tiles=6, tile_width=8, tile_height=8)
        arrangement = EbTileArrangement(width=6, height=1)
        arrangement.from_image(self.tile_8x8_2bpp_2_img,
                               tileset=tileset,
                               palette=palette)

        assert_equal(palette[0, 0], EbColor(0, 0, 0))
        assert_equal(palette[0, 1], EbColor(0xf8, 0xf8, 0xf8))

        item = arrangement[0, 0]
        assert_equal(item.subpalette, 0)

        assert_equal(arrangement[1, 0].tile, item.tile)
        assert_equal(arrangement[1, 0].is_horizontally_flipped,
                     not item.is_horizontally_flipped)
        assert_equal(arrangement[1, 0].is_vertically_flipped,
                     item.is_vertically_flipped)
        assert_equal(arrangement[1, 0].subpalette, 0)

        assert_equal(arrangement[2, 0].tile, item.tile)
        assert_equal(arrangement[2, 0].is_horizontally_flipped,
                     item.is_horizontally_flipped)
        assert_equal(arrangement[2, 0].is_vertically_flipped,
                     not item.is_vertically_flipped)
        assert_equal(arrangement[2, 0].subpalette, 0)

        assert_equal(arrangement[3, 0].tile, item.tile)
        assert_equal(arrangement[3, 0].is_horizontally_flipped,
                     not item.is_horizontally_flipped)
        assert_equal(arrangement[2, 0].is_vertically_flipped,
                     not item.is_vertically_flipped)
        assert_equal(arrangement[3, 0].subpalette, 0)

        assert_not_equal(arrangement[4, 0].tile, item.tile)
        assert_equal(arrangement[4, 0].subpalette, 0)

        assert_equal(arrangement[5, 0].tile, item.tile)
        assert_equal(arrangement[5, 0].is_horizontally_flipped,
                     item.is_horizontally_flipped)
        assert_equal(arrangement[5, 0].is_vertically_flipped,
                     item.is_vertically_flipped)
        assert_equal(arrangement[5, 0].subpalette, 0)
Beispiel #28
0
    def test_from_image_2_subpalettes(self):
        palette = EbPalette(2, 4)
        tileset = EbGraphicTileset(num_tiles=4, tile_width=8, tile_height=8)
        arrangement = EbTileArrangement(width=4, height=1)
        arrangement.from_image(image=self.tile_8x8_2bpp_3_img,
                               tileset=tileset,
                               palette=palette)

        img_palette = self.tile_8x8_2bpp_3_img.getpalette()
        self.tile_8x8_2bpp_3_img.putpalette([x & 0xf8 for x in img_palette])
        before_image_rgb = self.tile_8x8_2bpp_3_img.convert("RGB")
        after_image_rgb = arrangement.image(tileset, palette).convert("RGB")
        assert_images_equal(before_image_rgb, after_image_rgb)

        assert_set_equal({palette[1, i]
                          for i in range(4)}, {
                              EbColor(24, 0, 248),
                              EbColor(0, 248, 24),
                              EbColor(152, 0, 248),
                              EbColor(248, 144, 0)
                          })
        assert_set_equal({palette[0, i]
                          for i in range(4)}, {
                              EbColor(24, 0, 248),
                              EbColor(0, 248, 24),
                              EbColor(216, 248, 0),
                              EbColor(152, 0, 248)
                          })

        assert_equal(arrangement[0, 0].tile, 0)
        assert_equal(arrangement[0, 0].subpalette, 0)
        assert_equal({tileset[0][0][i]
                      for i in [-1, -2, -3, -4]}, {0, 1, 2, 3})

        assert_equal(arrangement[1, 0].tile, 1)
        assert_equal(arrangement[1, 0].subpalette, 1)
        assert_equal({tileset[1][0][i]
                      for i in [-1, -2, -3, -4]}, {0, 1, 2, 3})

        assert_equal(arrangement[2, 0].tile, 2)
        assert_equal(arrangement[2, 0].subpalette, 0)

        assert_equal(arrangement[3, 0].tile, 3)
        assert_equal(arrangement[3, 0].subpalette, 1)
Beispiel #29
0
    def read_from_project(self, resource_open):
        with resource_open("bg_data_table", "yml", True) as f:
            self.bg_table.from_yml_file(f)
        with resource_open("bg_scrolling_table", "yml", True) as f:
            self.scroll_table.from_yml_file(f)
        with resource_open("bg_distortion_table", "yml", True) as f:
            self.distortion_table.from_yml_file(f)

        self.backgrounds = []
        self.palettes = []
        for i in range(self.bg_table.num_rows):
            new_color_depth = self.bg_table[i][2]
            with resource_open("BattleBGs/" + str(i).zfill(3), "png") as f:
                image = open_indexed_image(f)

                new_palette = EbPalette(num_subpalettes=1,
                                        subpalette_length=16)
                new_tileset = EbGraphicTileset(num_tiles=512,
                                               tile_width=8,
                                               tile_height=8)
                new_arrangement = EbTileArrangement(width=32, height=32)

                new_arrangement.from_image(image, new_tileset, new_palette)

                for j, (tileset, color_depth,
                        arrangement) in enumerate(self.backgrounds):
                    if (color_depth == new_color_depth) \
                            and (tileset == new_tileset) \
                            and (arrangement == new_arrangement):
                        self.bg_table[i][0] = j
                        break
                else:
                    self.bg_table[i][0] = len(self.backgrounds)
                    self.backgrounds.append(
                        (new_tileset, new_color_depth, new_arrangement))

                for j, palette in enumerate(self.palettes):
                    if palette == new_palette:
                        self.bg_table[i][1] = j
                        break
                else:
                    self.bg_table[i][1] = len(self.palettes)
                    self.palettes.append(new_palette)
Beispiel #30
0
 def test_to_block_1bpp(self):
     tileset = EbGraphicTileset(num_tiles=2, tile_width=8, tile_height=8)
     tileset.tiles = [None, None]
     tileset.tiles[0] = [
         [0, 0, 0, 0, 0, 0, 1, 1],
         [0, 1, 1, 1, 0, 0, 0, 0],
         [0, 1, 0, 0, 1, 0, 0, 1],
         [1, 1, 1, 1, 0, 0, 0, 0],
         [0, 1, 0, 0, 1, 0, 1, 0],
         [1, 1, 0, 0, 1, 0, 0, 0],
         [0, 1, 1, 1, 0, 0, 0, 1],
         [0, 0, 0, 0, 0, 0, 0, 1],
     ]
     tileset.tiles[1] = [
         [0, 0, 1, 0, 0, 0, 0, 0],
         [0, 0, 1, 1, 0, 0, 0, 0],
         [0, 0, 1, 0, 1, 0, 0, 0],
         [0, 0, 1, 0, 1, 0, 0, 0],
         [0, 1, 1, 0, 0, 0, 0, 0],
         [1, 1, 1, 0, 0, 0, 0, 0],
         [1, 1, 0, 0, 0, 0, 0, 0],
         [0, 0, 0, 0, 0, 0, 0, 1],
     ]
     block = Block()
     block.from_list([0] * 16)
     tileset.to_block(block, 0, 1)
     assert_list_equal(
         block.to_list(),
         [
             0b00000011,
             0b01110000,
             0b01001001,
             0b11110000,
             0b01001010,
             0b11001000,
             0b01110001,
             0b00000001,
             0b00100000,
             0b00110000,
             0b00101000,
             0b00101000,
             0b01100000,
             0b11100000,
             0b11000000,
             0b00000001,
         ],
     )
Beispiel #31
0
 def test_to_block_1bpp(self):
     tileset = EbGraphicTileset(num_tiles=2, tile_width=8, tile_height=8)
     tileset.tiles = [None, None]
     tileset.tiles[0] = [[0, 0, 0, 0, 0, 0, 1, 1], [0, 1, 1, 1, 0, 0, 0, 0],
                         [0, 1, 0, 0, 1, 0, 0, 1], [1, 1, 1, 1, 0, 0, 0, 0],
                         [0, 1, 0, 0, 1, 0, 1, 0], [1, 1, 0, 0, 1, 0, 0, 0],
                         [0, 1, 1, 1, 0, 0, 0, 1], [0, 0, 0, 0, 0, 0, 0, 1]]
     tileset.tiles[1] = [[0, 0, 1, 0, 0, 0, 0, 0], [0, 0, 1, 1, 0, 0, 0, 0],
                         [0, 0, 1, 0, 1, 0, 0, 0], [0, 0, 1, 0, 1, 0, 0, 0],
                         [0, 1, 1, 0, 0, 0, 0, 0], [1, 1, 1, 0, 0, 0, 0, 0],
                         [1, 1, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 1]]
     block = Block()
     block.from_list([0] * 16)
     tileset.to_block(block, 0, 1)
     assert_list_equal(block.to_list(), [
         0b00000011, 0b01110000, 0b01001001, 0b11110000, 0b01001010,
         0b11001000, 0b01110001, 0b00000001, 0b00100000, 0b00110000,
         0b00101000, 0b00101000, 0b01100000, 0b11100000, 0b11000000,
         0b00000001
     ])
class SoundStoneModule(EbModule):
    NAME = "Sound Stone"
    FREE_RANGES = [(0x0EDD5D, 0x0EF805)]  # Sound stone graphics

    def __init__(self):
        super(SoundStoneModule, self).__init__()
        self.tileset = EbGraphicTileset(num_tiles=352, tile_width=8, tile_height=8)
        self.palette = EbPalette(num_subpalettes=6, subpalette_length=16)

    def read_from_rom(self, rom):
        graphics_offset = from_snes_address(read_asm_pointer(
            block=rom, offset=GRAPHICS_ASM_POINTER_OFFSET))
        with EbCompressibleBlock() as compressed_block:
            compressed_block.from_compressed_block(block=rom, offset=graphics_offset)
            self.tileset.from_block(block=compressed_block, bpp=4)
        self.palette.from_block(block=rom, offset=PALETTE_OFFSET)

    def write_to_rom(self, rom):
        tileset_block_size = self.tileset.block_size(bpp=4)
        with EbCompressibleBlock(tileset_block_size) as compressed_block:
            self.tileset.to_block(block=compressed_block, offset=0, bpp=4)
            compressed_block.compress()
            tileset_offset = rom.allocate(data=compressed_block)
            write_asm_pointer(block=rom, offset=GRAPHICS_ASM_POINTER_OFFSET, pointer=to_snes_address(tileset_offset))
        self.palette.to_block(block=rom, offset=PALETTE_OFFSET)

    def read_from_project(self, resource_open):
        with resource_open("Logos/SoundStone", "png") as image_file:
            image = open_indexed_image(image_file)
            self.palette.from_image(image)
            self.tileset.from_image(image, SOUND_STONE_ARRANGEMENT, self.palette)

    def write_to_project(self, resource_open):
        image = SOUND_STONE_ARRANGEMENT.image(self.tileset, self.palette)
        with resource_open("Logos/SoundStone", "png") as image_file:
            image.save(image_file, "png")

    def upgrade_project(self, old_version, new_version, rom, resource_open_r, resource_open_w, resource_delete):
        if old_version < 8:
            self.read_from_rom(rom)
            self.write_to_project(resource_open_w)
Beispiel #33
0
 def test_block_size(self):
     assert_equal(
         EbGraphicTileset(num_tiles=2, tile_width=8,
                          tile_height=8).block_size(2), 32)
     assert_equal(
         EbGraphicTileset(num_tiles=10, tile_width=8,
                          tile_height=8).block_size(2), 160)
     assert_equal(
         EbGraphicTileset(num_tiles=10, tile_width=8,
                          tile_height=8).block_size(4), 320)
     assert_equal(
         EbGraphicTileset(num_tiles=10, tile_width=8,
                          tile_height=8).block_size(8), 640)
     assert_equal(
         EbGraphicTileset(num_tiles=10, tile_width=8,
                          tile_height=8).block_size(1), 80)
     assert_equal(
         EbGraphicTileset(num_tiles=10, tile_width=8,
                          tile_height=16).block_size(1), 160)
     assert_equal(
         EbGraphicTileset(num_tiles=10, tile_width=16,
                          tile_height=16).block_size(1), 320)
Beispiel #34
0
 def test_to_block_4bpp(self):
     tileset = EbGraphicTileset(num_tiles=1, tile_width=8, tile_height=8)
     tileset.tiles = [None]
     tileset.tiles[0] = [[8, 1, 12, 9, 6, 5, 3, 2],
                         [11, 5, 8, 14, 1, 7, 15, 0],
                         [8, 13, 3, 7, 2, 0, 2, 3],
                         [10, 0, 4, 14, 7, 10, 11, 9],
                         [8, 8, 12, 9, 13, 12, 2, 6],
                         [11, 14, 14, 4, 14, 4, 10, 7],
                         [12, 2, 12, 8, 4, 15, 12, 14],
                         [10, 13, 12, 1, 10, 11, 11, 2]]
     block = Block()
     block.from_list([0] * 32)
     tileset.to_block(block, 0, 4)
     assert_list_equal(block.to_list(), [
         0b01010110, 0b00001011, 0b11001110, 0b10010110, 0b01110001,
         0b00111011, 0b00001011, 0b10011110, 0b00011000, 0b00000011,
         0b10000001, 0b11101011, 0b00000100, 0b01000101, 0b01010110,
         0b10001111, 0b00101100, 0b10110000, 0b01010110, 0b10110010,
         0b01010000, 0b11000000, 0b00111000, 0b10010111, 0b00101101,
         0b11111100, 0b01111101, 0b11101010, 0b10101111, 0b10110111,
         0b01100000, 0b11101110
     ])
Beispiel #35
0
 def __init__(self):
     self.minitiles = EbGraphicTileset(num_tiles=896, tile_width=8, tile_height=8)
     self.arrangements = [None for i in range(1024)]
     self.collisions = [None for i in range(1024)]
     self.palettes = []
 def __init__(self):
     super(SoundStoneModule, self).__init__()
     self.tileset = EbGraphicTileset(num_tiles=352, tile_width=8, tile_height=8)
     self.palette = EbPalette(num_subpalettes=6, subpalette_length=16)
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
    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)
Beispiel #39
0
 def test_from_block_2bpp(self):
     block = Block()
     block.from_list(
         [
             0b01010101,  # Tile 1
             0b10111010,
             0b01100100,
             0b11001111,
             0b10100000,
             0b10111101,
             0b11100001,
             0b01101011,
             0b10110111,
             0b00000111,
             0b11111010,
             0b01111101,
             0b00110010,
             0b11101100,
             0b00110110,
             0b10111100,
             0b11111001,  # Tile 2
             0b01101010,
             0b10011000,
             0b11111111,
             0b11001011,
             0b00111000,
             0b01000001,
             0b01110001,
             0b11001010,
             0b11000000,
             0b01111010,
             0b11011101,
             0b00011011,
             0b00001111,
             0b00100001,
             0b11110000,
         ]
     )
     tileset = EbGraphicTileset(num_tiles=2, tile_width=8, tile_height=8)
     tileset.from_block(block, offset=0, bpp=2)
     assert_list_equal(
         tileset[0],
         [
             [2, 1, 2, 3, 2, 1, 2, 1],
             [2, 3, 1, 0, 2, 3, 2, 2],
             [3, 0, 3, 2, 2, 2, 0, 2],
             [1, 3, 3, 0, 2, 0, 2, 3],
             [1, 0, 1, 1, 0, 3, 3, 3],
             [1, 3, 3, 3, 3, 2, 1, 2],
             [2, 2, 3, 1, 2, 2, 1, 0],
             [2, 0, 3, 3, 2, 3, 1, 0],
         ],
     )
     assert_list_equal(
         tileset[1],
         [
             [1, 3, 3, 1, 3, 0, 2, 1],
             [3, 2, 2, 3, 3, 2, 2, 2],
             [1, 1, 2, 2, 3, 0, 1, 1],
             [0, 3, 2, 2, 0, 0, 0, 3],
             [3, 3, 0, 0, 1, 0, 1, 0],
             [2, 3, 1, 3, 3, 2, 1, 2],
             [0, 0, 0, 1, 3, 2, 3, 3],
             [2, 2, 3, 2, 0, 0, 0, 1],
         ],
     )
Beispiel #40
0
    def test_from_image_8x16_2bpp(self):
        palette = EbPalette(1, 4)
        palette[0, 0].from_tuple((0xFF, 0xFF, 0xFF))
        palette[0, 1].from_tuple((0x30, 0x00, 0xFF))
        palette[0, 2].from_tuple((0xFF, 0x00, 0x00))
        palette[0, 3].from_tuple((0x00, 0xFF, 0x48))

        arrangement = EbTileArrangement(width=2, height=3)
        arrangement[0, 0].tile = 1
        arrangement[1, 0].tile = 1
        arrangement[0, 1].tile = 3
        arrangement[1, 1].tile = 2
        arrangement[0, 2].tile = 0
        arrangement[1, 2].tile = 4

        tileset = EbGraphicTileset(num_tiles=5, tile_width=8, tile_height=16)
        tileset.from_image(self.tile_8x16_4bpp_img, arrangement=arrangement, palette=palette)

        assert_list_equal(tileset[1], [[2] * 8] * 16)
        assert_list_equal(tileset[3], [[3] * 8] * 16)
        assert_list_equal(
            tileset[2],
            [
                [3] * 8,
                [3] * 8,
                [3] * 8,
                [3] * 8,
                [3] * 8,
                [3, 3, 1, 3, 3, 3, 3, 3],
                [3, 3, 1, 3, 3, 1, 3, 3],
                [1] * 8,
                [1, 1, 2, 2, 1, 1, 1, 1],
                [1, 2, 2, 2, 2, 2, 1, 1],
                [1, 1, 1, 1, 1, 2, 1, 1],
                [1, 1, 1, 1, 2, 2, 1, 1],
                [1, 1, 2, 2, 2, 1, 1, 1],
                [1] * 8,
                [1] * 8,
                [1, 1, 1, 3, 1, 1, 1, 1],
            ],
        )
        assert_list_equal(
            tileset[0],
            [
                [2, 1, 1, 1, 1, 1, 1, 1],
                [2, 3, 3, 3, 3, 3, 3, 1],
                [0, 2, 3, 3, 3, 3, 1, 3],
                [0, 2, 3, 3, 3, 3, 1, 3],
                [0, 0, 2, 3, 3, 1, 3, 3],
                [0, 0, 2, 3, 3, 1, 3, 3],
                [0, 0, 0, 2, 1, 3, 3, 3],
                [0, 0, 0, 2, 1, 3, 3, 3],
                [0, 0, 0, 1, 2, 3, 3, 3],
                [0, 0, 0, 1, 2, 3, 3, 3],
                [0, 0, 1, 0, 0, 2, 3, 3],
                [0, 0, 1, 0, 0, 2, 3, 3],
                [0, 1, 0, 0, 0, 0, 2, 3],
                [0, 1, 0, 0, 0, 0, 2, 3],
                [1, 0, 0, 0, 0, 0, 0, 2],
                [1, 0, 0, 0, 0, 0, 0, 2],
            ],
        )
        assert_list_equal(
            tileset[4],
            [
                [3] * 8,
                [3] * 8,
                [3] * 8,
                [3] * 8,
                [3] * 8,
                [3, 2, 3, 3, 3, 2, 3, 3],
                [3] * 8,
                [3] * 8,
                [3, 3, 3, 3, 3, 3, 3, 2],
                [2, 3, 3, 3, 3, 3, 2, 3],
                [3, 2, 3, 3, 3, 2, 2, 3],
                [3, 2, 2, 2, 2, 2, 3, 3],
                [3] * 8,
                [3] * 8,
                [3] * 8,
                [3] * 8,
            ],
        )
Beispiel #41
0
 def test_from_block_invalid(self):
     tileset = EbGraphicTileset(num_tiles=1, tile_width=8, tile_height=16)
     block = Block()
     assert_raises(NotImplementedError, tileset.from_block, block, 0, 32)
     assert_raises(NotImplementedError, tileset.from_block, block, 0, 2)
Beispiel #42
0
 def __init__(self, num_characters=96, tile_width=16, tile_height=8):
     self.num_characters = num_characters
     self.tileset = EbGraphicTileset(num_tiles=num_characters, tile_width=tile_width, tile_height=tile_height)
     self.character_widths = None
Beispiel #43
0
 def __init__(self):
     self.tileset = EbGraphicTileset(num_tiles=192, tile_width=8, tile_height=8)
     self.palette = EbPalette(num_subpalettes=2, subpalette_length=4)
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
Beispiel #45
0
 def test_to_block_2bpp(self):
     tileset = EbGraphicTileset(num_tiles=2, tile_width=8, tile_height=8)
     tileset.tiles = [None, None]
     tileset.tiles[0] = [
         [2, 1, 2, 3, 2, 1, 2, 1],
         [2, 3, 1, 0, 2, 3, 2, 2],
         [3, 0, 3, 2, 2, 2, 0, 2],
         [1, 3, 3, 0, 2, 0, 2, 3],
         [1, 0, 1, 1, 0, 3, 3, 3],
         [1, 3, 3, 3, 3, 2, 1, 2],
         [2, 2, 3, 1, 2, 2, 1, 0],
         [2, 0, 3, 3, 2, 3, 1, 0],
     ]
     tileset.tiles[1] = [
         [1, 3, 3, 1, 3, 0, 2, 1],
         [3, 2, 2, 3, 3, 2, 2, 2],
         [1, 1, 2, 2, 3, 0, 1, 1],
         [0, 3, 2, 2, 0, 0, 0, 3],
         [3, 3, 0, 0, 1, 0, 1, 0],
         [2, 3, 1, 3, 3, 2, 1, 2],
         [0, 0, 0, 1, 3, 2, 3, 3],
         [2, 2, 3, 2, 0, 0, 0, 1],
     ]
     block = Block()
     block.from_list([0] * 32)
     tileset.to_block(block, 0, 2)
     assert_list_equal(
         block.to_list(),
         [
             0b01010101,  # Tile 1
             0b10111010,
             0b01100100,
             0b11001111,
             0b10100000,
             0b10111101,
             0b11100001,
             0b01101011,
             0b10110111,
             0b00000111,
             0b11111010,
             0b01111101,
             0b00110010,
             0b11101100,
             0b00110110,
             0b10111100,
             0b11111001,  # Tile 2
             0b01101010,
             0b10011000,
             0b11111111,
             0b11001011,
             0b00111000,
             0b01000001,
             0b01110001,
             0b11001010,
             0b11000000,
             0b01111010,
             0b11011101,
             0b00011011,
             0b00001111,
             0b00100001,
             0b11110000,
         ],
     )
Beispiel #46
0
class EbTileset(object):
    def __init__(self):
        self.minitiles = EbGraphicTileset(num_tiles=896, tile_width=8, tile_height=8)
        self.arrangements = [None for i in range(1024)]
        self.collisions = [None for i in range(1024)]
        self.palettes = []

    def from_block(self, block, minitiles_offset, arrangements_offset, collisions_offset):
        self.minitiles_from_block(block, minitiles_offset)
        self.arrangements_from_block(block, arrangements_offset)
        self.collisions_from_block(block, collisions_offset)

    def minitiles_from_block(self, block, offset):
        with EbCompressibleBlock() as compressed_block:
            compressed_block.from_compressed_block(block=block, offset=offset)
            self.minitiles.from_block(block=compressed_block, bpp=4)

    def arrangements_from_block(self, block, offset):
        with EbCompressibleBlock() as compressed_block:
            compressed_block.from_compressed_block(block=block, offset=offset)
            num_arrangements = len(compressed_block) / 32

            j = 0
            for i in range(num_arrangements):
                arrangement = [[0 for x in range(4)] for y in range(4)]
                for y in range(4):
                    for x in range(4):
                        arrangement[y][x] = compressed_block.read_multi(key=j, size=2)
                        j += 2
                self.arrangements[i] = arrangement

    def collisions_from_block(self, block, offset):
        for i, arrangement in enumerate(self.arrangements):
            if arrangement is not None:
                collision_offset = 0x180000 | block.read_multi(key=offset + i * 2, size=2)
                self.collisions[i] = block[collision_offset:collision_offset + 16]

    def minitiles_to_block(self, block):
        with EbCompressibleBlock(self.minitiles.block_size(bpp=4)) as compressed_block:
            self.minitiles.to_block(block=compressed_block, offset=0, bpp=4)
            compressed_block.compress()
            return block.allocate(data=compressed_block)

    def arrangements_to_block(self, block):
        with EbCompressibleBlock(1024 * 16 * 2) as compressed_block:
            i = 0
            for arrangement in self.arrangements:
                for y in range(4):
                    for x in range(4):
                        compressed_block.write_multi(key=i, item=arrangement[y][x], size=2)
                        i += 2
            compressed_block.compress()
            return block.allocate(data=compressed_block)

    def add_palette(self, map_tileset, map_palette, palette):
        self.palettes.append((map_tileset, map_palette, palette))

    def has_map_tileset(self, map_tileset):
        for mt, mp, p in self.palettes:
            if mt == map_tileset:
                return True
        return False

    def get_palettes_by_map_tileset(self, map_tileset):
        return [(mp, p) for (mt, mp, p) in self.palettes if mt == map_tileset]

    def minitile_string_rep(self, n):
        if n >= 896:
            return "0000000000000000000000000000000000000000000000000000000000000000"
        else:
            s = str()
            tile = self.minitiles[n]
            for y in xrange(8):
                for x in xrange(8):
                    s += CHARACTERS[tile[y][x]]
            return s

    def minitile_from_string(self, n, string_rep):
        if n < 896:
            minitile = [[0] * self.minitiles.tile_width for x in range(self.minitiles.tile_height)]
            i = 0
            for y in xrange(8):
                for x in xrange(8):
                    minitile[y][x] = int(string_rep[i], 32)
                    i += 1
            self.minitiles.tiles[n] = minitile

    def arrangement_collision_string_rep(self, n):
        arrangement = self.arrangements[n]
        if arrangement is None:
            return "000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
        else:
            s = str()
            collision = self.collisions[n]
            for y in xrange(4):
                for x in xrange(4):
                    s += "{:04x}{:02x}".format(arrangement[y][x], collision[y*4 + x])
            return s

    def arrangement_collision_from_string(self, n, string_rep):
        i = 0
        arrangement = [[0 for x in range(4)] for y in range(4)]
        collision = [0] * 16
        for y in xrange(4):
            for x in xrange(4):
                arrangement[y][x] = int(string_rep[i:i + 4], 16)
                collision[y * 4 + x] = int(string_rep[i + 4: i + 6], 16)
                i += 6
        self.arrangements[n] = arrangement
        self.collisions[n] = collision

    def to_file(self, f):
        for i in range(512):
            print >> f, self.minitile_string_rep(i)
            print >> f, self.minitile_string_rep(i ^ 512)
            print >> f
        print >> f

        for map_tileset, map_palette, palette in self.palettes:
            f.write(CHARACTERS[map_tileset])
            f.write(CHARACTERS[map_palette])
            print >> f, str(palette)
        print >> f
        print >> f

        for i in range(1024):
            print >> f, self.arrangement_collision_string_rep(i)

    def from_file(self, f):
        self.minitiles.tiles = [None] * 896
        for i in range(512):
            self.minitile_from_string(i, f.readline()[:-1])
            self.minitile_from_string(i ^ 512, f.readline()[:-1])
            f.readline()
        f.readline()

        while True:
            line = f.readline()
            if line == "\n":
                break
            map_tileset = int(line[0], 32)
            map_palette = int(line[1], 32)
            palette = EbMapPalette()
            palette.from_string(line[2:-1])
            self.add_palette(map_tileset, map_palette, palette)
        f.readline()

        for i in range(1024):
            self.arrangement_collision_from_string(i, f.readline()[:-1])
Beispiel #47
0
class EbFont(object):
    def __init__(self, num_characters=96, tile_width=16, tile_height=8):
        self.num_characters = num_characters
        self.tileset = EbGraphicTileset(num_tiles=num_characters, tile_width=tile_width, tile_height=tile_height)
        self.character_widths = None

    def from_block(self, block, tileset_offset, character_widths_offset):
        self.tileset.from_block(block=block, offset=tileset_offset, bpp=1)
        for i in range(96, self.num_characters):
            self.tileset.clear_tile(i, color=1)
        self.character_widths = block[character_widths_offset:character_widths_offset + self.num_characters].to_list()

    def to_block(self, block):
        tileset_offset = block.allocate(size=self.tileset.block_size(bpp=1))
        self.tileset.to_block(block=block, offset=tileset_offset, bpp=1)

        character_widths_offset = block.allocate(size=self.num_characters)
        block[character_widths_offset:character_widths_offset + self.num_characters] = self.character_widths

        return tileset_offset, character_widths_offset

    def to_files(self, image_file, widths_file, image_format="png", widths_format="yml"):
        if self.num_characters == 96:
            image = _FONT_IMAGE_ARRANGEMENT_96.image(self.tileset, FONT_IMAGE_PALETTE)
        elif self.num_characters == 128:
            image = _FONT_IMAGE_ARRANGEMENT_128.image(self.tileset, FONT_IMAGE_PALETTE)
        image.save(image_file, image_format)
        del image

        character_widths_dict = dict(enumerate(self.character_widths))
        if widths_format == "yml":
            yml_dump(character_widths_dict, widths_file, default_flow_style=False)

    def from_files(self, image_file, widths_file, image_format="png", widths_format="yml"):
        image = open_indexed_image(image_file)

        if self.num_characters == 96:
            self.tileset.from_image(image, _FONT_IMAGE_ARRANGEMENT_96, FONT_IMAGE_PALETTE)
        elif self.num_characters == 128:
            self.tileset.from_image(image, _FONT_IMAGE_ARRANGEMENT_128, FONT_IMAGE_PALETTE)
        del image

        if widths_format == "yml":
            widths_dict = yml_load(widths_file)
            self.character_widths = [widths_dict[i] for i in range(self.tileset.num_tiles_maximum)]

    def image_size(self):
        if self.num_characters == 96:
            arr = _FONT_IMAGE_ARRANGEMENT_96
        elif self.num_characters == 128:
            arr = _FONT_IMAGE_ARRANGEMENT_128

        return arr.width * self.tileset.tile_width, arr.height * self.tileset.tile_height
    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)
Beispiel #49
0
class Animation:
    def __init__(self, frames, unknown, graphics_data_size=None):
        self.graphics_data_size = graphics_data_size
        self.frames = frames
        self.unknown = unknown

        if graphics_data_size:
            num_tiles = EbGraphicTileset.tiles_from_parameters(
                graphics_data_size, TILE_WIDTH, TILE_HEIGHT, TILE_BPP)
        else:  # Make tileset with maximum number of tiles possible for each frame of animation
            num_tiles = SCREEN_WIDTH_TILES * SCREEN_HEIGHT_TILES * frames

        # Animations are 2 bpp, so the palette will have 4 colors
        self.palette = EbPalette(num_subpalettes=1, subpalette_length=4)
        self.graphics = EbGraphicTileset(num_tiles=num_tiles,
                                         tile_width=TILE_WIDTH,
                                         tile_height=TILE_HEIGHT)
        self.arrangements = [
            EbTileArrangement(width=SCREEN_WIDTH_TILES,
                              height=SCREEN_HEIGHT_TILES)
            for i in range(self.frames)
        ]

    def from_block(self, block, offset):
        with EbCompressibleBlock() as compressed_block:
            compressed_block.from_compressed_block(block=block, offset=offset)
            self.graphics.from_block(block=compressed_block,
                                     offset=0,
                                     bpp=TILE_BPP)
            next_offset = self.graphics_data_size
            self.palette.from_block(block=compressed_block, offset=next_offset)
            next_offset += self.palette.block_size()

            for arrangement in self.arrangements:
                arrangement.from_block(block=compressed_block,
                                       offset=next_offset)
                next_offset += arrangement.block_size()

    def to_block(self, block):
        self.graphics_data_size = self.graphics.block_size(bpp=TILE_BPP,
                                                           trimmed=True)
        total_block_size = (self.graphics_data_size +
                            self.palette.block_size() +
                            sum(arrangement.block_size()
                                for arrangement in self.arrangements))

        with EbCompressibleBlock(total_block_size) as compressed_block:
            self.graphics.to_block(block=compressed_block,
                                   offset=0,
                                   bpp=TILE_BPP)
            next_offset = self.graphics_data_size
            self.palette.to_block(block=compressed_block, offset=next_offset)
            next_offset += self.palette.block_size()

            for arrangement in self.arrangements:
                arrangement.to_block(block=compressed_block,
                                     offset=next_offset)
                next_offset += arrangement.block_size()

            compressed_block.compress()
            return block.allocate(data=compressed_block)

    def images(self, arrangements=None):
        return [
            arrangement.image(self.graphics, self.palette)
            for arrangement in self.arrangements
        ]

    def add_frame_from_image(self, image, frame_id):
        self.arrangements[frame_id].from_image(image,
                                               self.graphics,
                                               self.palette,
                                               is_animation=True)
class WindowGraphicsModule(EbModule):
    NAME = "Window Graphics"
    FREE_RANGES = [(0x200000, 0x20079f)]  # Graphics

    def __init__(self):
        super(WindowGraphicsModule, self).__init__()
        self.graphics_1 = EbGraphicTileset(num_tiles=416, tile_width=8, tile_height=8)
        self.graphics_2 = EbGraphicTileset(num_tiles=7, tile_width=8, tile_height=8)

        self.flavor_palettes = [EbPalette(8, 4) for i in range(7)]
        self.flavor_names = dict()

    def read_from_rom(self, rom):
        with EbCompressibleBlock() as compressed_block:
            compressed_block.from_compressed_block(
                block=rom,
                offset=from_snes_address(read_asm_pointer(rom, GRAPHICS_1_ASM_POINTER_OFFSET)))
            self.graphics_1.from_block(block=compressed_block, bpp=2)

        with EbCompressibleBlock() as compressed_block:
            compressed_block.from_compressed_block(
                block=rom,
                offset=from_snes_address(read_asm_pointer(rom, GRAPHICS_2_ASM_POINTER_OFFSET)))
            self.graphics_2.from_block(block=compressed_block, bpp=2)

        # Read palettes
        offset = FLAVOR_PALETTES_OFFSET
        for palette in self.flavor_palettes:
            palette.from_block(block=rom, offset=offset)
            offset += 64

        # Read names
        for asm_pointer_offset in FLAVOR_NAME_ASM_POINTER_OFFSETS:
            self.flavor_names[asm_pointer_offset] = FLAVOR_NAME_ENTRY.from_block(
                block=rom,
                offset=from_snes_address(read_asm_pointer(block=rom, offset=asm_pointer_offset)))

    def write_to_rom(self, rom):
        graphics_1_block_size = self.graphics_1.block_size(bpp=2)
        with EbCompressibleBlock(graphics_1_block_size) as compressed_block:
            self.graphics_1.to_block(block=compressed_block, offset=0, bpp=2)
            compressed_block.compress()
            graphics_1_offset = rom.allocate(data=compressed_block)
            write_asm_pointer(block=rom, offset=GRAPHICS_1_ASM_POINTER_OFFSET,
                              pointer=to_snes_address(graphics_1_offset))

        graphics_2_block_size = self.graphics_2.block_size(bpp=2)
        with EbCompressibleBlock(graphics_2_block_size) as compressed_block:
            self.graphics_2.to_block(block=compressed_block, offset=0, bpp=2)
            compressed_block.compress()
            graphics_2_offset = rom.allocate(data=compressed_block)
            write_asm_pointer(block=rom, offset=GRAPHICS_2_ASM_POINTER_OFFSET,
                              pointer=to_snes_address(graphics_2_offset))

        # Write palettes
        offset = FLAVOR_PALETTES_OFFSET
        for palette in self.flavor_palettes:
            palette.to_block(block=rom, offset=offset)
            offset += 64

        # Write names
        for asm_pointer_offset in FLAVOR_NAME_ASM_POINTER_OFFSETS:
            name = self.flavor_names[asm_pointer_offset]
            offset = rom.allocate(size=FLAVOR_NAME_ENTRY.size)
            FLAVOR_NAME_ENTRY.to_block(block=rom, offset=offset, value=name)
            write_asm_pointer(block=rom, offset=asm_pointer_offset, pointer=to_snes_address(offset))

    def write_to_project(self, resource_open):
        for i, palette in enumerate(self.flavor_palettes):
            with resource_open("WindowGraphics/Windows1_" + str(i), "png") as image_file:
                image = ARRANGEMENT_1.image(tileset=self.graphics_1, palette=palette)
                image.save(image_file, "png")
            with resource_open("WindowGraphics/Windows2_" + str(i), "png") as image_file:
                image = ARRANGEMENT_2.image(tileset=self.graphics_2, palette=palette.get_subpalette(7))
                image.save(image_file, "png")

        # Write names
        with resource_open("WindowGraphics/flavor_names", "txt", True) as f:
            for asm_pointer_offset in FLAVOR_NAME_ASM_POINTER_OFFSETS:
                print(self.flavor_names[asm_pointer_offset], file=f)

    def read_from_project(self, resource_open):
        # Read graphics. Just use the first of each image.
        with resource_open("WindowGraphics/Windows1_0", "png") as image_file:
            image = open_indexed_image(image_file)
            self.graphics_1.from_image(image=image,
                                       arrangement=ARRANGEMENT_1,
                                       palette=self.flavor_palettes[0])

        with resource_open("WindowGraphics/Windows2_0", "png") as image_file:
            image = open_indexed_image(image_file)
            self.graphics_2.from_image(image=image,
                                       arrangement=ARRANGEMENT_2,
                                       palette=self.flavor_palettes[0].get_subpalette(7))

        # Read pals from Windows1 of each flavor.
        # Read subpal 7 from Windows2 of each flavor.
        for i, palette in enumerate(self.flavor_palettes):
            # Read all the palette data from Windows1
            with resource_open("WindowGraphics/Windows1_" + str(i), "png") as image_file:
                image = open_indexed_image(image_file)
                palette.from_image(image=image)

            with resource_open("WindowGraphics/Windows2_" + str(i), "png") as image_file:
                image = open_indexed_image(image_file)
                palette_data = image.getpalette()
                m = 0
                for k in range(4):
                    palette[7, k].from_tuple((palette_data[m], palette_data[m + 1], palette_data[m + 2]))
                    m += 3

        # Read names
        with resource_open("WindowGraphics/flavor_names", "txt", True) as f:
            for asm_pointer_offset in FLAVOR_NAME_ASM_POINTER_OFFSETS:
                name = f.readline()[:-1]
                self.flavor_names[asm_pointer_offset] = FLAVOR_NAME_ENTRY.from_yml_rep(name)
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)
        )