def read_gfx_from_project(self, obj, resource_open): with resource_open(obj.path(), 'png') as image_file: image = open_indexed_image(image_file) palette = EbPalette(num_subpalettes=1, subpalette_length=4) palette.from_image(image) obj.palettes[0] = palette obj.from_image(image, obj.cast_arrangement())
def read_from_project(self, resource_open): with resource_open("sprite_group_palettes", "yml", True) as f: self.palette_table.from_yml_file(f) with resource_open("sprite_groups", "yml", True) as f: input = yml_load(f) num_groups = len(input) self.groups = [] for i in range(num_groups): group = SpriteGroup(16) group.from_yml_rep(input[i]) palette = EbPalette(1, 16) with resource_open("SpriteGroups/" + str(i).zfill(3), "png") as f2: image = open_indexed_image(f2) group.from_image(image) palette.from_image(image) del image self.groups.append(group) # Assign the palette number to the sprite for j in range(8): if palette.list() == self.palette_table[j][0].list(): group.palette = j break else: raise CoilSnakeError("Sprite Group #" + str(i).zfill(3) + " uses an invalid palette")
def read_from_project(self, resource_open): with resource_open("sprite_group_palettes", "yml") as f: self.palette_table.from_yml_file(f) with resource_open("sprite_groups", "yml") as f: input = yml_load(f) num_groups = len(input) self.groups = [] for i in range(num_groups): group = SpriteGroup(16) group.from_yml_rep(input[i]) palette = EbPalette(1, 16) with resource_open("SpriteGroups/" + str(i).zfill(3), "png") as f2: image = open_indexed_image(f2) group.from_image(image) palette.from_image(image) del image self.groups.append(group) # Assign the palette number to the sprite for j in range(8): if palette.list()[3:] == self.palette_table[j][0].list( )[3:]: group.palette = j break else: raise CoilSnakeError("Sprite Group #" + str(i).zfill(3) + " uses an invalid palette")
def from_yml_rep(cls, yml_rep): palette = EbPalette(num_subpalettes=1, subpalette_length=(cls.size / 2)) try: palette.from_yml_rep(yml_rep) except InvalidYmlRepresentationError as e: raise TableEntryInvalidYmlRepresentationError(e.message) return palette
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 from_yml_rep(cls, yml_rep): palette = EbPalette(num_subpalettes=1, subpalette_length=(cls.size // 2)) try: palette.from_yml_rep(yml_rep) except InvalidYmlRepresentationError as e: raise TableEntryInvalidYmlRepresentationError(str(e)) return palette
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 __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, num_tiles, tile_width, tile_height, bpp, arrangement_width, arrangement_height, num_palettes, num_subpalettes, subpalette_length, compressed_palettes=True): self.bpp = bpp self.compressed_palettes = compressed_palettes self.graphics = EbGraphicTileset(num_tiles=num_tiles, tile_width=tile_width, tile_height=tile_height) if arrangement_width and arrangement_height: self.arrangement = EbTileArrangement(width=arrangement_width, height=arrangement_height) else: self.arrangement = None self.palettes = [ EbPalette(num_subpalettes=num_subpalettes, subpalette_length=subpalette_length) for x in range(num_palettes) ]
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 __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): self.enemy_config_table.from_block(block=rom, offset=from_snes_address(ENEMY_CONFIGURATION_TABLE_DEFAULT_OFFSET)) self.enemy_group_bg_table.from_block(block=rom, offset=from_snes_address(ENEMY_GROUP_BACKGROUND_TABLE_DEFAULT_OFFSET)) # Read the sprites log.debug("Reading battle sprites") self.graphics_pointer_table.from_block( rom, from_snes_address(read_asm_pointer(block=rom, offset=GRAPHICS_POINTER_TABLE_ASM_POINTER_OFFSET))) self.battle_sprites = [] for i in range(self.graphics_pointer_table.num_rows): with EbCompressibleBlock() as compressed_block: compressed_block.from_compressed_block( block=rom, offset=from_snes_address(self.graphics_pointer_table[i][0])) sprite = EbBattleSprite() sprite.from_block(block=compressed_block, offset=0, size=self.graphics_pointer_table[i][1]) self.battle_sprites.append(sprite) # Determine how many palettes there are num_palettes = 0 for i in range(self.enemy_config_table.num_rows): num_palettes = max(num_palettes, self.enemy_config_table[i][14]) num_palettes += 1 # Read the palettes log.debug("Reading palettes") palettes_offset = from_snes_address(read_asm_pointer(block=rom, offset=PALETTES_ASM_POINTER_OFFSET)) self.palettes = [] for i in range(num_palettes): palette = EbPalette(num_subpalettes=1, subpalette_length=16) palette.from_block(block=rom, offset=palettes_offset) self.palettes.append(palette) palettes_offset += palette.block_size() # Read the groups log.debug("Reading enemy groups") self.enemy_group_table.from_block(rom, from_snes_address(ENEMY_GROUP_TABLE_DEFAULT_OFFSET)) self.enemy_groups = [] for i in range(self.enemy_group_table.num_rows): group = [] group_offset = from_snes_address(self.enemy_group_table[i][0]) while rom[group_offset] != 0xff: group.append(EnemyGroupTableEntry.from_block(block=rom, offset=group_offset)) group_offset += EnemyGroupTableEntry.size self.enemy_groups.append(group)
def test_8x8_2colors_1subpaletteof2(self): return eb_palette = EbPalette(num_subpalettes=1, subpalette_length=2) setup_eb_palette_from_image(eb_palette, self.tile_8x8_2bpp_img, 8, 8) assert_equal(eb_palette.num_subpalettes, 1) assert_equal(eb_palette.subpalette_length, 2) assert_equal(eb_palette[0, 0].tuple(), (0xf8, 0xf8, 0xf8)) assert_equal(eb_palette[0, 1].tuple(), (0, 0, 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
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)
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 __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) ]
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
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 test_extra_colors(self): return eb_palette = EbPalette(num_subpalettes=3, subpalette_length=4) setup_eb_palette_from_image(eb_palette, self.tile_8x8_2bpp_img, 8, 8) assert_equal(eb_palette.num_subpalettes, 3) assert_equal(eb_palette.subpalette_length, 4) assert_equal(eb_palette[0, 0].tuple(), (0xf8, 0xf8, 0xf8)) assert_equal(eb_palette[0, 1].tuple(), (0, 0, 0)) assert_equal(eb_palette[0, 2].tuple(), (0, 0, 0)) assert_equal(eb_palette[0, 3].tuple(), (0, 0, 0)) assert_equal(eb_palette[1, 0].tuple(), (0, 0, 0)) assert_equal(eb_palette[1, 1].tuple(), (0, 0, 0)) assert_equal(eb_palette[1, 2].tuple(), (0, 0, 0)) assert_equal(eb_palette[1, 3].tuple(), (0, 0, 0)) assert_equal(eb_palette[2, 0].tuple(), (0, 0, 0)) assert_equal(eb_palette[2, 1].tuple(), (0, 0, 0)) assert_equal(eb_palette[2, 2].tuple(), (0, 0, 0)) assert_equal(eb_palette[2, 3].tuple(), (0, 0, 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])
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)
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)
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)
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)
def test_8x8_5colors_2subpalettesof4(self): return eb_palette = EbPalette(num_subpalettes=2, subpalette_length=4) setup_eb_palette_from_image(eb_palette, self.tile_8x8_2bpp_3_img, 8, 8) assert_equal(eb_palette.num_subpalettes, 2) assert_equal(eb_palette.subpalette_length, 4) assert_set_equal({eb_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({eb_palette[0, i] for i in range(4)}, { EbColor(24, 0, 248), EbColor(0, 248, 24), EbColor(216, 248, 0), EbColor(152, 0, 248) })
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)
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)
class TitleScreenModule(EbModule): """Extracts the title screen data from EarthBound. This module allows for the editing of the background and characters of the title screen. The slide-in animation for the characters is controlled through assembly, while the rest of the animation works by changing between several palettes (one for each new frame of animation) and keeping the same tileset for each frame. """ NAME = "Title Screen" FREE_RANGES = [ (0x21B211, 0x21C6E4), # Background Tileset (0x21AF7D, 0x21B210), # Background Arrangement (0x21CDE1, 0x21CE07), # Background Palette (0x21AEFD, 0x21AF7C), # Background Animated Palette (0x21C6E5, 0x21CDE0), # Characters Tileset (0x21AE7C, 0x21AE82), # Characters Palette (0x21AE83, 0x21AEFC), # Characters Animated Palette (0x21CE08, 0x21CF9C) # Animation Data ] def __init__(self): super(TitleScreenModule, self).__init__() # Background data (includes the central "B", the copyright # notice and the glow around the letters) self.bg_tileset = EbGraphicTileset( num_tiles=BG_NUM_TILES, tile_width=TILE_WIDTH, tile_height=TILE_HEIGHT ) self.bg_arrangement = EbTileArrangement( width=BG_ARRANGEMENT_WIDTH, height=BG_ARRANGEMENT_HEIGHT ) self.bg_anim_palette = EbPalette( num_subpalettes=BG_NUM_ANIM_SUBPALETTES, subpalette_length=ANIM_SUBPALETTE_LENGTH ) self.bg_palette = EbPalette( num_subpalettes=NUM_SUBPALETTES, subpalette_length=BG_SUBPALETTE_LENGTH ) # Characters data (the title screen's animated letters) self.chars_tileset = EbGraphicTileset( num_tiles=CHARS_NUM_TILES, tile_width=TILE_WIDTH, tile_height=TILE_HEIGHT ) self.chars_anim_palette = EbPalette( num_subpalettes=CHARS_NUM_ANIM_SUBPALETTES, subpalette_length=ANIM_SUBPALETTE_LENGTH ) self.chars_palette = EbPalette( num_subpalettes=NUM_SUBPALETTES, subpalette_length=CHARS_SUBPALETTE_LENGTH ) self.chars_layouts = [[] for _ in range(NUM_CHARS)] def read_from_rom(self, rom): self.read_background_data_from_rom(rom) self.read_chars_data_from_rom(rom) self.read_chars_layouts_from_rom(rom) # Add the characters palette to the background data. self.bg_palette[0, CHARS_ANIM_SLICE] =\ self.chars_anim_palette.get_subpalette( CHARS_NUM_ANIM_SUBPALETTES - 1 )[0, :] def read_background_data_from_rom(self, rom): with EbCompressibleBlock() as block: # Read the background tileset data self._decompress_block(rom, block, BG_TILESET_POINTER) self.bg_tileset.from_block( block=block, offset=0, bpp=BG_TILESET_BPP ) # Read the background tile arrangement data self._decompress_block(rom, block, BG_ARRANGEMENT_POINTER) self.bg_arrangement.from_block(block=block, offset=0) # Read the background palette data # The decompressed data is smaller than the expected value, # so it is extended with black entries. self._decompress_block(rom, block, BG_PALETTE_POINTER) block.from_array( block.to_array() + [0]*(BG_SUBPALETTE_LENGTH*2 - len(block)) ) self.bg_palette.from_block(block=block, offset=0) # Read the background animated palette data # Each subpalette corresponds to an animation frame. self._decompress_block(rom, block, BG_ANIM_PALETTE_POINTER) self.bg_anim_palette.from_block(block=block, offset=0) def read_chars_data_from_rom(self, rom): with EbCompressibleBlock() as block: # Read the characters tileset data self._decompress_block(rom, block, CHARS_TILESET_POINTER) self.chars_tileset.from_block( block=block, offset=0, bpp=CHARS_TILESET_BPP ) # Read the characters palette data self._decompress_block(rom, block, CHARS_PALETTE_POINTER) self.chars_palette.from_block(block=block, offset=0) # Read the characters animated palette data # Each subpalette corresponds to an animation frame. self._decompress_block(rom, block, CHARS_ANIM_PALETTE_POINTER) self.chars_anim_palette.from_block(block=block, offset=0) def read_chars_layouts_from_rom(self, rom): lda_instruction = rom[CHARS_LAYOUT_BANK] chars_layout_pointer_offset = CHARS_LAYOUT_POINTER_OFFSET_DEFAULT # Check if we are dealing with the modified Rom, # If we are, we need to recalculate the offset to the # character layouts if lda_instruction == 0xA9: bank = rom[CHARS_LAYOUT_BANK + 1] chars_layout_pointer_offset = from_snes_address(bank << 16) self.chars_layouts = [[] for _ in range(NUM_CHARS)] for char in range(NUM_CHARS): # Get the location of a character's data offset = chars_layout_pointer_offset + rom.read_multi( CHARS_LAYOUT_TABLE + char*2, 2 ) # Read entries until a final entry is encountered while True: entry = TitleScreenLayoutEntry() entry.from_block(rom, offset) self.chars_layouts[char].append(entry) offset += 5 if entry.is_final(): break def write_to_rom(self, rom): self.write_background_data_to_rom(rom) self.write_chars_data_to_rom(rom) self.write_chars_layouts_to_rom(rom) def write_background_data_to_rom(self, rom): # Write the background tileset data block_size = self.bg_tileset.block_size(bpp=BG_TILESET_BPP) with EbCompressibleBlock(block_size) as block: self.bg_tileset.to_block(block=block, offset=0, bpp=BG_TILESET_BPP) self._write_compressed_block(rom, block, BG_TILESET_POINTER) # Write the background tile arrangement data block_size = self.bg_arrangement.block_size() with EbCompressibleBlock(block_size) as block: self.bg_arrangement.to_block(block=block, offset=0) self._write_compressed_block(rom, block, BG_ARRANGEMENT_POINTER) # Write the background palette data # There is an additional pointer to this location, so change that one # too block_size = self.bg_palette.block_size() with EbCompressibleBlock(block_size) as block: self.bg_palette.to_block(block=block, offset=0) new_offset = self._write_compressed_block( rom, block, BG_PALETTE_POINTER ) write_asm_pointer( block=rom, offset=BG_PALETTE_POINTER_SECONDARY, pointer=to_snes_address(new_offset) ) # Write the background animated palette data block_size = self.bg_anim_palette.block_size() with EbCompressibleBlock(block_size) as block: self.bg_anim_palette.to_block(block=block, offset=0) self._write_compressed_block(rom, block, BG_ANIM_PALETTE_POINTER) def write_chars_data_to_rom(self, rom): # Write the characters tileset data block_size = self.chars_tileset.block_size(bpp=CHARS_TILESET_BPP) with EbCompressibleBlock(block_size) as block: self.chars_tileset.to_block( block=block, offset=0, bpp=CHARS_TILESET_BPP ) self._write_compressed_block(rom, block, CHARS_TILESET_POINTER) # Write the characters palette data block_size = self.chars_palette.block_size() with EbCompressibleBlock(block_size) as block: self.chars_palette.to_block(block=block, offset=0) self._write_compressed_block(rom, block, CHARS_PALETTE_POINTER) # Write the characters animation palette data block_size = self.chars_anim_palette.block_size() with EbCompressibleBlock(block_size) as block: self.chars_anim_palette.to_block(block=block, offset=0) self._write_compressed_block( rom, block, CHARS_ANIM_PALETTE_POINTER ) def write_chars_layouts_to_rom(self, rom): block_size = sum( TitleScreenLayoutEntry.block_size()*len(c) for c in self.chars_layouts ) # Ensure the new data is located in only one bank # Spreading it across two banks might make part of it inaccessible. def can_write_to(begin): return begin >> 16 == (begin + block_size) >> 16 with Block(block_size) as block: # Write the character animation data to the ROM offset = 0 for layout in self.chars_layouts: for entry in layout: entry.to_block(block=block, offset=offset) offset += entry.block_size() new_offset = to_snes_address(rom.allocate( data=block, size=block_size, can_write_to=can_write_to )) # Write the offsets to the layouts to the ROM new_bank = new_offset >> 16 new_data_start = new_offset & 0xFFFF data_offset = new_data_start for c, layout in enumerate(self.chars_layouts): rom[CHARS_LAYOUT_TABLE + c*2:CHARS_LAYOUT_TABLE + c*2 + 2] = [ data_offset & 0xFF, data_offset >> 8 ] data_offset += len(layout)*TitleScreenLayoutEntry.block_size() # Change the offset for the character layouts # The way this normally works is that EarthBound stores the address # of the bank holding the data (0xE1 by default, hence the 0x210000 # offset); the offsets in the table are then prefixed with that # address. However, reallocating the data may have changed its # bank, so we need to manually set it to the new bank address. # In order to change the offset, we are replacing a LDA instruction # which addresses a direct page (0xA5) with a LDA instruction # that treats its operand as the constant to load (0xA9) # See https://wiki.superfamicom.org/snes/show/65816+Reference#instructions. rom[CHARS_LAYOUT_BANK:CHARS_LAYOUT_BANK + 2] = [0xA9, new_bank] def read_from_project(self, resource_open): self.read_background_data_from_project(resource_open) self.read_chars_data_from_project(resource_open) def read_background_data_from_project(self, resource_open): # Load the background reference image # The image's arrangement, tileset and palette will be used for the # animation frames with resource_open(BG_REFERENCE_PATH, "png") as f: image = open_indexed_image(f) self.bg_arrangement.from_image( image, self.bg_tileset, self.bg_palette ) # Read the background animated frames for frame in range(NUM_ANIM_FRAMES): # Create temporary structures used to check consistency between # frames tileset = EbGraphicTileset(BG_NUM_TILES, TILE_WIDTH, TILE_HEIGHT) arrangement = EbTileArrangement( BG_ARRANGEMENT_WIDTH, BG_ARRANGEMENT_HEIGHT ) palette = EbPalette(NUM_SUBPALETTES, BG_SUBPALETTE_LENGTH) # Read one frame's image data with resource_open(BG_FRAMES_PATH.format(frame), "png") as f: image = open_indexed_image(f) arrangement.from_image(image, tileset, palette) # Make sure each frame's tileset and arrangement is identical # The background palette is checked only if it isn't the fake # palette used for the first few frames if frame >= CHARS_NUM_ANIM_SUBPALETTES: # Get the background animated subpalette from the background # palette colors = palette[0, BG_ANIM_SLICE] self.bg_anim_palette.subpalettes[ frame - CHARS_NUM_ANIM_SUBPALETTES ] = colors palette[0, BG_ANIM_SLICE] = self.bg_palette[ 0, BG_ANIM_SLICE ] if self.bg_palette != palette: log.warn( "Palette from background frame {} does not match " "reference.".format(frame) ) if self.bg_tileset != tileset: log.warn( "Tileset from background frame {} does not match " "reference.".format(frame) ) if self.bg_arrangement != arrangement: log.warn( "Arrangement from background frame {} does not match " "reference.".format(frame) ) def read_chars_data_from_project(self, resource_open): # Read the characters positions with resource_open(CHARS_POSITIONS_PATH, "yml", True) as f: chars_positions = yml_load(f) # Read the characters animated frames self.chars_tileset = None self.chars_anim_palette = EbPalette( CHARS_NUM_ANIM_SUBPALETTES, ANIM_SUBPALETTE_LENGTH ) original_tileset = None for p in range(CHARS_NUM_ANIM_SUBPALETTES): # Read one of the animation frames with resource_open(CHARS_FRAMES_PATH.format(p), "png") as f: # Create temporary structures to hold the data image = open_indexed_image(f) arrangement = EbTileArrangement( image.width // TILE_WIDTH, image.height // TILE_HEIGHT ) tileset = EbGraphicTileset( CHARS_NUM_TILES, TILE_WIDTH, TILE_HEIGHT ) anim_subpalette = EbPalette( NUM_SUBPALETTES, ANIM_SUBPALETTE_LENGTH ) arrangement.from_image(image, tileset, anim_subpalette, True) # Add the characters animation subpalette for i in range(ANIM_SUBPALETTE_LENGTH): self.chars_anim_palette[p, i] = anim_subpalette[0, i] # Add the characters tileset if not already set, otherwise # ensure that it the current tileset is identical if not self.chars_tileset: original_tileset = tileset self.chars_tileset = EbGraphicTileset( CHARS_NUM_TILES, TILE_WIDTH, TILE_HEIGHT ) self.chars_tileset.tiles = [ [[0 for _ in range(TILE_HEIGHT)] for _ in range(TILE_WIDTH)] for _ in range(CHARS_NUM_TILES) ] unused_tiles = set(range(CHARS_NUM_TILES)) # Set the new character layouts self.chars_layouts = [[] for _ in range(NUM_CHARS)] for c, data in chars_positions.items(): # Get the data from the YAML file x = int(data['x'] // TILE_WIDTH) y = int(data['y'] // TILE_HEIGHT) width = int(data['width'] // TILE_WIDTH) height = int(data['height'] // TILE_HEIGHT) x_offset = data['top_left_offset']['x'] y_offset = data['top_left_offset']['y'] unknown = data['unknown'] # Generate a list of all tiles must be visited # Where possible, we try to generate a multi tile (4 tiles # stored as one); otherwise, bordering tiles that are # visited will all be single tiles. l = [ (i, j) for i in range(0, width, 2) for j in range(0, height, 2) ] if width % 2 == 1: l.extend([(width-1, j) for j in range(1, height, 2)]) if height % 2 == 1: l.extend([(i, height-1) for i in range(1, width, 2)]) # Generate the new reduced tileset for i, j in l: # Put the tile in the new tileset o_tile = arrangement[x + i, y + j].tile n_tile = unused_tiles.pop() self.chars_tileset.tiles[n_tile] = tileset[o_tile] entry = TitleScreenLayoutEntry( i*8 + x_offset, j*8 + y_offset, n_tile, 0, unknown ) # Create a multi entry if possible to save space if i < width - 1 and j < height - 1: entry.set_single(True) o_tile_r = arrangement[x+i+1, y+j].tile o_tile_d = arrangement[x+i, y+j+1].tile o_tile_dr = arrangement[x+i+1, y+j+1].tile n_tile_r = n_tile + 1 n_tile_d = n_tile + 16 n_tile_dr = n_tile + 17 unused_tiles.difference_update( (n_tile_r, n_tile_d, n_tile_dr) ) self.chars_tileset.tiles[n_tile_r] = \ tileset[o_tile_r] self.chars_tileset.tiles[n_tile_d] = \ tileset[o_tile_d] self.chars_tileset.tiles[n_tile_dr] = \ tileset[o_tile_dr] self.chars_layouts[c].append(entry) self.chars_layouts[c][-1].set_final(True) elif original_tileset != tileset: log.warn( "Tileset from characters frame {} does not match " "tileset from characters frame 0.".format(p) ) # Read the initial characters palette with resource_open(CHARS_INITIAL_PATH, "png") as f: image = open_indexed_image(f) arrangement = EbTileArrangement( image.width // TILE_WIDTH, image.height // TILE_HEIGHT ) tileset = EbGraphicTileset( CHARS_NUM_TILES, TILE_WIDTH, TILE_HEIGHT ) self.chars_palette = EbPalette( NUM_SUBPALETTES, ANIM_SUBPALETTE_LENGTH ) arrangement.from_image(image, tileset, self.chars_palette) def write_to_project(self, resource_open): self.write_background_data_to_project(resource_open) self.write_chars_data_to_project(resource_open) def write_background_data_to_project(self, resource_open): # Write out the reference background image # This image is used to get the arrangement, tileset and static palette # that will be used by all background images. with resource_open( BG_REFERENCE_PATH, "png" ) as f: image = self.bg_arrangement.image(self.bg_tileset, self.bg_palette) image.save(f) # Write out the background's animated frames for frame in range(NUM_ANIM_FRAMES): palette = EbPalette(NUM_SUBPALETTES, BG_SUBPALETTE_LENGTH) if frame < CHARS_NUM_ANIM_SUBPALETTES: palette[0, CHARS_ANIM_SLICE] = \ self.chars_anim_palette.get_subpalette(frame)[0, :] else: palette[0, :] = self.bg_palette.get_subpalette(0)[0, :] palette[0, BG_ANIM_SLICE] = \ self.bg_anim_palette.get_subpalette( frame - CHARS_NUM_ANIM_SUBPALETTES )[0, :] with resource_open(BG_FRAMES_PATH.format(frame), "png") as f: image = self.bg_arrangement.image(self.bg_tileset, palette) image.save(f) def write_chars_data_to_project(self, resource_open): # Build an arrangement combining every character for convenience chars_positions = {} arrangement = EbTileArrangement(3*9, 6) for c, layout in enumerate(self.chars_layouts): top_left = {'x': 128, 'y': 128} for e, entry in enumerate(layout): tile = entry.tile & (CHARS_NUM_TILES - 1) top_left['x'] = min(top_left['x'], int(entry.x)) top_left['y'] = min(top_left['y'], int(entry.y)) x = c*3 + (entry.x + 16) // 8 y = (entry.y + 24) // 8 arrangement[x, y].tile = tile if not entry.is_single(): arrangement[x+1, y].tile = tile + 1 arrangement[x, y+1].tile = tile + 16 arrangement[x+1, y+1].tile = tile + 17 chars_positions[c] = { 'x': c*3*8, 'y': 0, 'width': 3*8, 'height': 6*8, 'top_left_offset': top_left, 'unknown': layout[0].unknown } # Write the characters animation frames for p in range(CHARS_NUM_ANIM_SUBPALETTES): with resource_open(CHARS_FRAMES_PATH.format(p), "png") as f: image = arrangement.image( self.chars_tileset, self.chars_anim_palette.get_subpalette(p) ) image.save(f) # Write out the initial characters palette with resource_open(CHARS_INITIAL_PATH, "png") as f: image = arrangement.image( self.chars_tileset, self.chars_palette ) image.save(f) # Write out the positions of the characters with resource_open(CHARS_POSITIONS_PATH, "yml", True) as f: yml_dump(chars_positions, f, False) def upgrade_project( self, old_version, new_version, rom, resource_open_r, resource_open_w, resource_delete): if old_version < 9: self.read_from_rom(rom) self.write_to_project(resource_open_w) @staticmethod def _decompress_block(rom, block, pointer): block.from_compressed_block( block=rom, offset=from_snes_address(read_asm_pointer(rom, pointer)) ) @staticmethod def _write_compressed_block(rom, compressed_block, pointer): compressed_block.compress() new_offset = rom.allocate(data=compressed_block) write_asm_pointer( block=rom, offset=pointer, pointer=to_snes_address(new_offset) ) return new_offset
class DeathScreenModule(EbModule): """Extracts the death screen data from EarthBound.""" NAME = "Death Screen" FREE_RANGES = [ (0x21cfaf, 0x21d4f3), # Tileset (0x21d4f4, 0x21d5e7), # Palette (0x21d5e8, 0x21d6e1) # Arrangement ] def __init__(self): super(DeathScreenModule, self).__init__() self.tileset = EbGraphicTileset( num_tiles=NUM_TILES, tile_width=TILE_WIDTH, tile_height=TILE_HEIGHT ) self.arrangement = EbTileArrangement( width=ARRANGEMENT_WIDTH, height=ARRANGEMENT_HEIGHT ) self.palette = EbPalette( num_subpalettes=NUM_SUBPALETTES, subpalette_length=SUBPALETTE_LENGTH ) def read_from_rom(self, rom): with EbCompressibleBlock() as block: # Read the tileset data block.from_compressed_block( block=rom, offset=from_snes_address( read_asm_pointer(rom, TILESET_POINTER) ) ) self.tileset.from_block(block=block, offset=0, bpp=TILESET_BPP) # Read the arrangement data block.from_compressed_block( block=rom, offset=from_snes_address( read_asm_pointer(rom, ARRANGEMENT_POINTER) ) ) self.arrangement.from_block(block=block, offset=0) # Read the palette data block.from_compressed_block( block=rom, offset=from_snes_address( read_asm_pointer(rom, PALETTE_POINTER) ) ) self.palette.from_block(block=block, offset=0) def write_to_rom(self, rom): # Write the tileset data block_size = self.tileset.block_size(bpp=TILESET_BPP) with EbCompressibleBlock(block_size) as block: self.tileset.to_block(block=block, offset=0, bpp=TILESET_BPP) self._write_compressed_block(rom, block, TILESET_POINTER) # Write the tile arrangement data block_size = self.arrangement.block_size() with EbCompressibleBlock(block_size) as block: self.arrangement.to_block(block=block, offset=0) self._write_compressed_block(rom, block, ARRANGEMENT_POINTER) # Write the palette data block_size = self.palette.block_size() with EbCompressibleBlock(block_size) as block: self.palette.to_block(block=block, offset=0) self._write_compressed_block( rom, block, PALETTE_POINTER ) def read_from_project(self, resource_open): with resource_open(DEATH_SCREEN_PATH, "png") as f: image = open_indexed_image(f) self.arrangement.from_image(image, self.tileset, self.palette) with resource_open(DEATH_SCREEN_SUBPALETTES_PATH, "yml", True) as f: subpalettes = yml_load(f) for subpalette, tiles in subpalettes.items(): for x, y in tiles: self.arrangement[x, y].subpalette = subpalette def write_to_project(self, resource_open): with resource_open(DEATH_SCREEN_PATH, "png") as f: image = self.arrangement.image(self.tileset, self.palette, True) image.save(f) with resource_open(DEATH_SCREEN_SUBPALETTES_PATH, "yml", True) as f: subpalettes = {} for x in range(ARRANGEMENT_WIDTH): for y in range(ARRANGEMENT_HEIGHT): subpalette = self.arrangement[x, y].subpalette if subpalette not in subpalettes: subpalettes[subpalette] = [] subpalettes[subpalette].append((x, y)) yml_dump(subpalettes, f, None) def upgrade_project( self, old_version, new_version, rom, resource_open_r, resource_open_w, resource_delete): if old_version < 9: self.read_from_rom(rom) self.write_to_project(resource_open_w) @staticmethod def _write_compressed_block(rom, compressed_block, pointer): compressed_block.compress() new_offset = rom.allocate(data=compressed_block) write_asm_pointer( block=rom, offset=pointer, pointer=to_snes_address(new_offset) )
def setup(self): super(TestEbPalette, self).setup() self.palette = EbPalette(2, 3)
def read_from_project(self, resource_open): with resource_open("enemy_configuration_table", "yml") as f: self.enemy_config_table.from_yml_file(f) # Read the sprites and palettes self.battle_sprites = [] self.palettes = [] sprite_hashes = dict() num_sprites = 0 palette_hashes = dict() num_palettes = 0 for i in range(self.enemy_config_table.num_rows): battle_sprite = EbBattleSprite() palette = EbPalette(num_subpalettes=1, subpalette_length=16) try: with resource_open("BattleSprites/" + str(i).zfill(3), "png") as f: image = open_indexed_image(f) battle_sprite.from_image(image) palette.from_image(image) del image except IOError: # No battle sprite self.enemy_config_table[i][4] = 0 self.enemy_config_table[i][14] = 0 continue sprite_hash = battle_sprite.hash() try: self.enemy_config_table[i][4] = sprite_hashes[sprite_hash] + 1 except KeyError: self.enemy_config_table[i][4] = num_sprites + 1 sprite_hashes[sprite_hash] = num_sprites self.battle_sprites.append(battle_sprite) num_sprites += 1 palette_hash = palette.hash() try: self.enemy_config_table[i][14] = palette_hashes[palette_hash] except KeyError: self.enemy_config_table[i][14] = num_palettes palette_hashes[palette_hash] = num_palettes self.palettes.append(palette) num_palettes += 1 # Read the groups with resource_open("enemy_groups", "yml") as f: self.enemy_group_table.from_yml_file(f) with resource_open("enemy_groups", "yml") as f: self.enemy_group_bg_table.from_yml_file(f) with resource_open("enemy_groups", "yml") as f: self.enemy_groups = [] enemy_groups_yml_rep = yml_load(f) for entry in enemy_groups_yml_rep.itervalues(): enemy_group = entry["Enemies"] if type(enemy_group) == dict: enemy_group = [enemy_group[x] for x in sorted(enemy_group.keys())] group = [EnemyGroupTableEntry.from_yml_rep(x) for x in enemy_group] self.enemy_groups.append(group)
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 TestEbPalette(BaseTestCase, TilesetImageTestCase): def setup(self): super(TestEbPalette, self).setup() self.palette = EbPalette(2, 3) def test_init(self): assert_equal(self.palette.num_subpalettes, 2) assert_equal(self.palette.subpalette_length, 3) def test_init_invalid(self): assert_raises(InvalidArgumentError, EbPalette, 0, 1) assert_raises(InvalidArgumentError, EbPalette, -1, 1) assert_raises(InvalidArgumentError, EbPalette, 1, 0) assert_raises(InvalidArgumentError, EbPalette, 1, -1) assert_raises(InvalidArgumentError, EbPalette, 0, 0) assert_raises(InvalidArgumentError, EbPalette, -1, -1) def test_num_colors(self): assert_equal(self.palette.num_colors(), 6) def test_getitem(self): assert_equal(self.palette[0, 0].tuple(), (0, 0, 0)) assert_equal(self.palette[0, 1].tuple(), (0, 0, 0)) self.palette[0, 1].from_tuple((8, 16, 32)) assert_equal(self.palette[0, 0].tuple(), (0, 0, 0)) assert_equal(self.palette[0, 1].tuple(), (8, 16, 32)) def test_getitem_invalid(self): assert_raises(InvalidArgumentError, self.palette.__getitem__, (-1, 0)) assert_raises(InvalidArgumentError, self.palette.__getitem__, (0, -1)) assert_raises(InvalidArgumentError, self.palette.__getitem__, (-1, -1)) assert_raises(InvalidArgumentError, self.palette.__getitem__, (2, 0)) assert_raises(InvalidArgumentError, self.palette.__getitem__, (0, 3)) assert_raises(InvalidArgumentError, self.palette.__getitem__, (2, 3)) def test_from_list(self): self.palette.from_list([ 0, 0, 0, 40, 8, 72, 80, 88, 48, 248, 0, 0, 80, 56, 40, 16, 136, 136 ]) assert_equal(self.palette[0, 0].tuple(), (0, 0, 0)) assert_equal(self.palette[0, 1].tuple(), (40, 8, 72)) assert_equal(self.palette[0, 2].tuple(), (80, 88, 48)) assert_equal(self.palette[1, 0].tuple(), (248, 0, 0)) assert_equal(self.palette[1, 1].tuple(), (80, 56, 40)) assert_equal(self.palette[1, 2].tuple(), (16, 136, 136)) def test_to_list(self): self.palette[0, 0].from_tuple((0, 0, 0)) self.palette[0, 1].from_tuple((40, 8, 72)) self.palette[0, 2].from_tuple((80, 88, 48)) self.palette[1, 0].from_tuple((248, 0, 0)) self.palette[1, 1].from_tuple((80, 56, 40)) self.palette[1, 2].from_tuple((16, 136, 136)) assert_list_equal(self.palette.list(), [0, 0, 0, 40, 8, 72, 80, 88, 48, 248, 0, 0, 80, 56, 40, 16, 136, 136]) def test_from_block(self): block = Block() block.from_list([0, 0, 37, 36, 106, 25, 31, 0, 234, 20, 34, 70]) self.palette.from_block(block) assert_list_equal(self.palette.list(), [ 0, 0, 0, 40, 8, 72, 80, 88, 48, 248, 0, 0, 80, 56, 40, 16, 136, 136 ]) def test_to_block(self): self.palette.from_list([ 0, 0, 0, 40, 8, 72, 80, 88, 48, 248, 0, 0, 80, 56, 40, 16, 136, 136 ]) block = Block() block.from_list([0xff] * 50) self.palette.to_block(block, offset=1) assert_list_equal(block[0:14].to_list(), [0xff, 0, 0, 37, 36, 106, 25, 31, 0, 234, 20, 34, 70, 0xff]) def test_from_image(self): self.palette.from_image(self.tile_image_01_img) assert_list_equal(self.palette.list(), [0x00, 0x00, 0x00, 0x08, 0x00, 0xf8, 0xf8, 0x00, 0x00, 0x00, 0xf8, 0x18, 0xc0, 0xf8, 0x00, 0xf8, 0xf8, 0xf8]) def test_to_image(self): image = Image.new('P', (10, 10)) self.palette[0, 0].from_tuple((0, 0, 0)) self.palette[0, 1].from_tuple((40, 8, 72)) self.palette[0, 2].from_tuple((80, 88, 48)) self.palette[1, 0].from_tuple((248, 0, 0)) self.palette[1, 1].from_tuple((80, 56, 40)) self.palette[1, 2].from_tuple((16, 136, 136)) self.palette.to_image(image) assert_list_equal(image.getpalette()[0:(self.palette.num_colors() * 3)], [0, 0, 0, 40, 8, 72, 80, 88, 48, 248, 0, 0, 80, 56, 40, 16, 136, 136]) del image def test_add_colors_to_subpalette_single_color(self): self.palette.add_colors_to_subpalette([EbColor(r=64, g=32, b=16)]) assert_equal(self.palette[1, 0].tuple(), (64, 32, 16)) assert_true(self.palette[1, 0].used) for i in range(self.palette.num_subpalettes): for j in range(self.palette.subpalette_length): if (i, j) not in [(1, 0)]: assert_false(self.palette[i, j].used) def test_add_colors_to_subpalette_shared_colors(self): self.palette.add_colors_to_subpalette([EbColor(r=64, g=32, b=16)]) self.palette.add_colors_to_subpalette([EbColor(r=64, g=32, b=16), EbColor(r=128, g=0, b=16)]) assert_equal(self.palette[1, 0].tuple(), (64, 32, 16)) assert_true(self.palette[1, 0].used) assert_equal(self.palette[1, 1].tuple(), (128, 0, 16)) assert_true(self.palette[1, 1].used) for i in range(self.palette.num_subpalettes): for j in range(self.palette.subpalette_length): if (i, j) not in [(1, 0), (1, 1)]: assert_false(self.palette[i, j].used) def test_add_colors_to_subpalette_shared_colors2(self): self.palette.add_colors_to_subpalette([EbColor(r=64, g=32, b=16)]) self.palette.add_colors_to_subpalette([EbColor(r=64, g=32, b=16), EbColor(r=128, g=0, b=16)]) self.palette.add_colors_to_subpalette([EbColor(r=64, g=32, b=16), EbColor(r=16, g=32, b=64), EbColor(r=32, g=32, b=32)]) assert_equal(self.palette[1, 0].tuple(), (64, 32, 16)) assert_true(self.palette[1, 0].used) assert_equal(self.palette[1, 1].tuple(), (128, 0, 16)) assert_true(self.palette[1, 1].used) assert_false(self.palette[1, 2].used) assert_equal(set([self.palette[0, x].tuple() for x in range(self.palette.subpalette_length)]), set([(64, 32, 16), (16, 32, 64), (32, 32, 32)])) assert_equal([self.palette[0, x].used for x in range(self.palette.subpalette_length)].count(True), 3)
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)
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 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)
class TestEbPalette(BaseTestCase, TilesetImageTestCase): def setup(self): super(TestEbPalette, self).setup() self.palette = EbPalette(2, 3) def test_init(self): assert_equal(self.palette.num_subpalettes, 2) assert_equal(self.palette.subpalette_length, 3) def test_init_invalid(self): assert_raises(InvalidArgumentError, EbPalette, 0, 1) assert_raises(InvalidArgumentError, EbPalette, -1, 1) assert_raises(InvalidArgumentError, EbPalette, 1, 0) assert_raises(InvalidArgumentError, EbPalette, 1, -1) assert_raises(InvalidArgumentError, EbPalette, 0, 0) assert_raises(InvalidArgumentError, EbPalette, -1, -1) def test_num_colors(self): assert_equal(self.palette.num_colors(), 6) def test_getitem(self): assert_equal(self.palette[0, 0].tuple(), (0, 0, 0)) assert_equal(self.palette[0, 1].tuple(), (0, 0, 0)) self.palette[0, 1].from_tuple((8, 16, 32)) assert_equal(self.palette[0, 0].tuple(), (0, 0, 0)) assert_equal(self.palette[0, 1].tuple(), (8, 16, 32)) def test_getitem_invalid(self): assert_raises(InvalidArgumentError, self.palette.__getitem__, (-1, 0)) assert_raises(InvalidArgumentError, self.palette.__getitem__, (0, -1)) assert_raises(InvalidArgumentError, self.palette.__getitem__, (-1, -1)) assert_raises(InvalidArgumentError, self.palette.__getitem__, (2, 0)) assert_raises(InvalidArgumentError, self.palette.__getitem__, (0, 3)) assert_raises(InvalidArgumentError, self.palette.__getitem__, (2, 3)) def test_from_list(self): self.palette.from_list([ 0, 0, 0, 40, 8, 72, 80, 88, 48, 248, 0, 0, 80, 56, 40, 16, 136, 136 ]) assert_equal(self.palette[0, 0].tuple(), (0, 0, 0)) assert_equal(self.palette[0, 1].tuple(), (40, 8, 72)) assert_equal(self.palette[0, 2].tuple(), (80, 88, 48)) assert_equal(self.palette[1, 0].tuple(), (248, 0, 0)) assert_equal(self.palette[1, 1].tuple(), (80, 56, 40)) assert_equal(self.palette[1, 2].tuple(), (16, 136, 136)) def test_to_list(self): self.palette[0, 0].from_tuple((0, 0, 0)) self.palette[0, 1].from_tuple((40, 8, 72)) self.palette[0, 2].from_tuple((80, 88, 48)) self.palette[1, 0].from_tuple((248, 0, 0)) self.palette[1, 1].from_tuple((80, 56, 40)) self.palette[1, 2].from_tuple((16, 136, 136)) assert_list_equal(self.palette.list(), [ 0, 0, 0, 40, 8, 72, 80, 88, 48, 248, 0, 0, 80, 56, 40, 16, 136, 136 ]) def test_from_block(self): block = Block() block.from_list([0, 0, 37, 36, 106, 25, 31, 0, 234, 20, 34, 70]) self.palette.from_block(block) assert_list_equal(self.palette.list(), [ 0, 0, 0, 40, 8, 72, 80, 88, 48, 248, 0, 0, 80, 56, 40, 16, 136, 136 ]) def test_to_block(self): self.palette.from_list([ 0, 0, 0, 40, 8, 72, 80, 88, 48, 248, 0, 0, 80, 56, 40, 16, 136, 136 ]) block = Block() block.from_list([0xff] * 50) self.palette.to_block(block, offset=1) assert_list_equal( block[0:14].to_list(), [0xff, 0, 0, 37, 36, 106, 25, 31, 0, 234, 20, 34, 70, 0xff]) def test_from_image(self): self.palette.from_image(self.tile_image_01_img) assert_list_equal(self.palette.list(), [ 0x00, 0x00, 0x00, 0x08, 0x00, 0xf8, 0xf8, 0x00, 0x00, 0x00, 0xf8, 0x18, 0xc0, 0xf8, 0x00, 0xf8, 0xf8, 0xf8 ]) def test_to_image(self): image = Image.new('P', (10, 10)) self.palette[0, 0].from_tuple((0, 0, 0)) self.palette[0, 1].from_tuple((40, 8, 72)) self.palette[0, 2].from_tuple((80, 88, 48)) self.palette[1, 0].from_tuple((248, 0, 0)) self.palette[1, 1].from_tuple((80, 56, 40)) self.palette[1, 2].from_tuple((16, 136, 136)) self.palette.to_image(image) assert_list_equal( image.getpalette()[0:(self.palette.num_colors() * 3)], [ 0, 0, 0, 40, 8, 72, 80, 88, 48, 248, 0, 0, 80, 56, 40, 16, 136, 136 ]) del image def test_add_colors_to_subpalette_single_color(self): self.palette.add_colors_to_subpalette([EbColor(r=64, g=32, b=16)]) assert_equal(self.palette[1, 0].tuple(), (64, 32, 16)) assert_true(self.palette[1, 0].used) for i in range(self.palette.num_subpalettes): for j in range(self.palette.subpalette_length): if (i, j) not in [(1, 0)]: assert_false(self.palette[i, j].used) def test_add_colors_to_subpalette_shared_colors(self): self.palette.add_colors_to_subpalette([EbColor(r=64, g=32, b=16)]) self.palette.add_colors_to_subpalette( [EbColor(r=64, g=32, b=16), EbColor(r=128, g=0, b=16)]) assert_equal(self.palette[1, 0].tuple(), (64, 32, 16)) assert_true(self.palette[1, 0].used) assert_equal(self.palette[1, 1].tuple(), (128, 0, 16)) assert_true(self.palette[1, 1].used) for i in range(self.palette.num_subpalettes): for j in range(self.palette.subpalette_length): if (i, j) not in [(1, 0), (1, 1)]: assert_false(self.palette[i, j].used) def test_add_colors_to_subpalette_shared_colors2(self): self.palette.add_colors_to_subpalette([EbColor(r=64, g=32, b=16)]) self.palette.add_colors_to_subpalette( [EbColor(r=64, g=32, b=16), EbColor(r=128, g=0, b=16)]) self.palette.add_colors_to_subpalette([ EbColor(r=64, g=32, b=16), EbColor(r=16, g=32, b=64), EbColor(r=32, g=32, b=32) ]) assert_equal(self.palette[1, 0].tuple(), (64, 32, 16)) assert_true(self.palette[1, 0].used) assert_equal(self.palette[1, 1].tuple(), (128, 0, 16)) assert_true(self.palette[1, 1].used) assert_false(self.palette[1, 2].used) assert_equal( set([ self.palette[0, x].tuple() for x in range(self.palette.subpalette_length) ]), set([(64, 32, 16), (16, 32, 64), (32, 32, 32)])) assert_equal([ self.palette[0, x].used for x in range(self.palette.subpalette_length) ].count(True), 3)
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
def from_block(cls, block, offset): palette = EbPalette(num_subpalettes=1, subpalette_length=(cls.size / 2)) palette.from_block(block, offset) return palette
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
from coilsnake.model.eb.blocks import EbCompressibleBlock from coilsnake.model.eb.graphics import EbTileArrangement, EbGraphicTileset from coilsnake.model.eb.palettes import EbPalette from coilsnake.util.common.image import open_indexed_image from coilsnake.util.common.yml import yml_load, yml_dump from coilsnake.util.eb.pointer import from_snes_address, read_asm_pointer, write_asm_pointer, to_snes_address FONT_IMAGE_PALETTE = EbPalette(1, 2) FONT_IMAGE_PALETTE[0, 0].from_tuple((255, 255, 255)) FONT_IMAGE_PALETTE[0, 1].from_tuple((0, 0, 0)) FONT_IMAGE_ARRANGEMENT_WIDTH = 16 _FONT_IMAGE_ARRANGEMENT_96 = EbTileArrangement(width=FONT_IMAGE_ARRANGEMENT_WIDTH, height=6) _FONT_IMAGE_ARRANGEMENT_128 = EbTileArrangement(width=FONT_IMAGE_ARRANGEMENT_WIDTH, height=8) for y in range(_FONT_IMAGE_ARRANGEMENT_96.height): for x in range(_FONT_IMAGE_ARRANGEMENT_96.width): _FONT_IMAGE_ARRANGEMENT_96[x, y].tile = y * _FONT_IMAGE_ARRANGEMENT_96.width + x for y in range(_FONT_IMAGE_ARRANGEMENT_128.height): for x in range(_FONT_IMAGE_ARRANGEMENT_128.width): _FONT_IMAGE_ARRANGEMENT_128[x, y].tile = y * _FONT_IMAGE_ARRANGEMENT_128.width + x 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)