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]])
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))
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
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 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], ], )
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], ], )
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]])
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_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_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]])
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)
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 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 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], ], )
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)
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
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])
class DeathScreenModule(EbModule): """Extracts the death screen data from EarthBound.""" NAME = "Death Screen" FREE_RANGES = [ (0x21cfaf, 0x21d4f3), # Tileset (0x21d4f4, 0x21d5e7), # Palette (0x21d5e8, 0x21d6e1) # Arrangement ] def __init__(self): super(DeathScreenModule, self).__init__() self.tileset = EbGraphicTileset( num_tiles=NUM_TILES, tile_width=TILE_WIDTH, tile_height=TILE_HEIGHT ) self.arrangement = EbTileArrangement( width=ARRANGEMENT_WIDTH, height=ARRANGEMENT_HEIGHT ) self.palette = EbPalette( num_subpalettes=NUM_SUBPALETTES, subpalette_length=SUBPALETTE_LENGTH ) def read_from_rom(self, rom): with EbCompressibleBlock() as block: # Read the tileset data block.from_compressed_block( block=rom, offset=from_snes_address( read_asm_pointer(rom, TILESET_POINTER) ) ) self.tileset.from_block(block=block, offset=0, bpp=TILESET_BPP) # Read the arrangement data block.from_compressed_block( block=rom, offset=from_snes_address( read_asm_pointer(rom, ARRANGEMENT_POINTER) ) ) self.arrangement.from_block(block=block, offset=0) # Read the palette data block.from_compressed_block( block=rom, offset=from_snes_address( read_asm_pointer(rom, PALETTE_POINTER) ) ) self.palette.from_block(block=block, offset=0) def write_to_rom(self, rom): # Write the tileset data block_size = self.tileset.block_size(bpp=TILESET_BPP) with EbCompressibleBlock(block_size) as block: self.tileset.to_block(block=block, offset=0, bpp=TILESET_BPP) self._write_compressed_block(rom, block, TILESET_POINTER) # Write the tile arrangement data block_size = self.arrangement.block_size() with EbCompressibleBlock(block_size) as block: self.arrangement.to_block(block=block, offset=0) self._write_compressed_block(rom, block, ARRANGEMENT_POINTER) # Write the palette data block_size = self.palette.block_size() with EbCompressibleBlock(block_size) as block: self.palette.to_block(block=block, offset=0) self._write_compressed_block( rom, block, PALETTE_POINTER ) def read_from_project(self, resource_open): with resource_open(DEATH_SCREEN_PATH, "png") as f: image = open_indexed_image(f) self.arrangement.from_image(image, self.tileset, self.palette) with resource_open(DEATH_SCREEN_SUBPALETTES_PATH, "yml") as f: subpalettes = yml_load(f) for subpalette, tiles in subpalettes.items(): for x, y in tiles: self.arrangement[x, y].subpalette = subpalette def write_to_project(self, resource_open): with resource_open(DEATH_SCREEN_PATH, "png") as f: image = self.arrangement.image(self.tileset, self.palette, True) image.save(f) with resource_open(DEATH_SCREEN_SUBPALETTES_PATH, "yml") as f: subpalettes = {} for x in range(ARRANGEMENT_WIDTH): for y in range(ARRANGEMENT_HEIGHT): subpalette = self.arrangement[x, y].subpalette if subpalette not in subpalettes: subpalettes[subpalette] = [] subpalettes[subpalette].append((x, y)) yml_dump(subpalettes, f, None) def upgrade_project( self, old_version, new_version, rom, resource_open_r, resource_open_w, resource_delete): if old_version < 9: self.read_from_rom(rom) self.write_to_project(resource_open_w) @staticmethod def _write_compressed_block(rom, compressed_block, pointer): compressed_block.compress() new_offset = rom.allocate(data=compressed_block) write_asm_pointer( block=rom, offset=pointer, pointer=to_snes_address(new_offset) )
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])
class TitleScreenModule(EbModule): """Extracts the title screen data from EarthBound. This module allows for the editing of the background and characters of the title screen. The slide-in animation for the characters is controlled through assembly, while the rest of the animation works by changing between several palettes (one for each new frame of animation) and keeping the same tileset for each frame. """ NAME = "Title Screen" FREE_RANGES = [ (0x21B211, 0x21C6E4), # Background Tileset (0x21AF7D, 0x21B210), # Background Arrangement (0x21CDE1, 0x21CE07), # Background Palette (0x21AEFD, 0x21AF7C), # Background Animated Palette (0x21C6E5, 0x21CDE0), # Characters Tileset (0x21AE7C, 0x21AE82), # Characters Palette (0x21AE83, 0x21AEFC), # Characters Animated Palette (0x21CE08, 0x21CF9C) # Animation Data ] def __init__(self): super(TitleScreenModule, self).__init__() # Background data (includes the central "B", the copyright # notice and the glow around the letters) self.bg_tileset = EbGraphicTileset(num_tiles=BG_NUM_TILES, tile_width=TILE_WIDTH, tile_height=TILE_HEIGHT) self.bg_arrangement = EbTileArrangement(width=BG_ARRANGEMENT_WIDTH, height=BG_ARRANGEMENT_HEIGHT) self.bg_anim_palette = EbPalette( num_subpalettes=BG_NUM_ANIM_SUBPALETTES, subpalette_length=ANIM_SUBPALETTE_LENGTH) self.bg_palette = EbPalette(num_subpalettes=NUM_SUBPALETTES, subpalette_length=BG_SUBPALETTE_LENGTH) # Characters data (the title screen's animated letters) self.chars_tileset = EbGraphicTileset(num_tiles=CHARS_NUM_TILES, tile_width=TILE_WIDTH, tile_height=TILE_HEIGHT) self.chars_anim_palette = EbPalette( num_subpalettes=CHARS_NUM_ANIM_SUBPALETTES, subpalette_length=ANIM_SUBPALETTE_LENGTH) self.chars_palette = EbPalette( num_subpalettes=NUM_SUBPALETTES, subpalette_length=CHARS_SUBPALETTE_LENGTH) self.chars_layouts = [[] for _ in xrange(NUM_CHARS)] def read_from_rom(self, rom): self.read_background_data_from_rom(rom) self.read_chars_data_from_rom(rom) self.read_chars_layouts_from_rom(rom) # Add the characters palette to the background data. self.bg_palette[0, CHARS_ANIM_SLICE] =\ self.chars_anim_palette.get_subpalette( CHARS_NUM_ANIM_SUBPALETTES - 1 )[0, :] def read_background_data_from_rom(self, rom): with EbCompressibleBlock() as block: # Read the background tileset data self._decompress_block(rom, block, BG_TILESET_POINTER) self.bg_tileset.from_block(block=block, offset=0, bpp=BG_TILESET_BPP) # Read the background tile arrangement data self._decompress_block(rom, block, BG_ARRANGEMENT_POINTER) self.bg_arrangement.from_block(block=block, offset=0) # Read the background palette data # The decompressed data is smaller than the expected value, # so it is extended with black entries. self._decompress_block(rom, block, BG_PALETTE_POINTER) block.from_array(block.to_array() + [0] * (BG_SUBPALETTE_LENGTH * 2 - len(block))) self.bg_palette.from_block(block=block, offset=0) # Read the background animated palette data # Each subpalette corresponds to an animation frame. self._decompress_block(rom, block, BG_ANIM_PALETTE_POINTER) self.bg_anim_palette.from_block(block=block, offset=0) def read_chars_data_from_rom(self, rom): with EbCompressibleBlock() as block: # Read the characters tileset data self._decompress_block(rom, block, CHARS_TILESET_POINTER) self.chars_tileset.from_block(block=block, offset=0, bpp=CHARS_TILESET_BPP) # Read the characters palette data self._decompress_block(rom, block, CHARS_PALETTE_POINTER) self.chars_palette.from_block(block=block, offset=0) # Read the characters animated palette data # Each subpalette corresponds to an animation frame. self._decompress_block(rom, block, CHARS_ANIM_PALETTE_POINTER) self.chars_anim_palette.from_block(block=block, offset=0) def read_chars_layouts_from_rom(self, rom): self.chars_layouts = [[] for _ in xrange(NUM_CHARS)] for char in xrange(NUM_CHARS): # Get the location of a character's data offset = CHARS_LAYOUT_POINTER_OFFSET + rom.read_multi( CHARS_LAYOUT_TABLE + char * 2, 2) # Read entries until a final entry is encountered while True: entry = TitleScreenLayoutEntry() entry.from_block(rom, offset) self.chars_layouts[char].append(entry) offset += 5 if entry.is_final(): break def write_to_rom(self, rom): self.write_background_data_to_rom(rom) self.write_chars_data_to_rom(rom) self.write_chars_layouts_to_rom(rom) def write_background_data_to_rom(self, rom): # Write the background tileset data block_size = self.bg_tileset.block_size(bpp=BG_TILESET_BPP) with EbCompressibleBlock(block_size) as block: self.bg_tileset.to_block(block=block, offset=0, bpp=BG_TILESET_BPP) self._write_compressed_block(rom, block, BG_TILESET_POINTER) # Write the background tile arrangement data block_size = self.bg_arrangement.block_size() with EbCompressibleBlock(block_size) as block: self.bg_arrangement.to_block(block=block, offset=0) self._write_compressed_block(rom, block, BG_ARRANGEMENT_POINTER) # Write the background palette data # There is an additional pointer to this location, so change that one # too block_size = self.bg_palette.block_size() with EbCompressibleBlock(block_size) as block: self.bg_palette.to_block(block=block, offset=0) new_offset = self._write_compressed_block(rom, block, BG_PALETTE_POINTER) write_asm_pointer(block=rom, offset=BG_PALETTE_POINTER_SECONDARY, pointer=to_snes_address(new_offset)) # Write the background animated palette data block_size = self.bg_anim_palette.block_size() with EbCompressibleBlock(block_size) as block: self.bg_anim_palette.to_block(block=block, offset=0) self._write_compressed_block(rom, block, BG_ANIM_PALETTE_POINTER) def write_chars_data_to_rom(self, rom): # Write the characters tileset data block_size = self.chars_tileset.block_size(bpp=CHARS_TILESET_BPP) with EbCompressibleBlock(block_size) as block: self.chars_tileset.to_block(block=block, offset=0, bpp=CHARS_TILESET_BPP) self._write_compressed_block(rom, block, CHARS_TILESET_POINTER) # Write the characters palette data block_size = self.chars_palette.block_size() with EbCompressibleBlock(block_size) as block: self.chars_palette.to_block(block=block, offset=0) self._write_compressed_block(rom, block, CHARS_PALETTE_POINTER) # Write the characters animation palette data block_size = self.chars_anim_palette.block_size() with EbCompressibleBlock(block_size) as block: self.chars_anim_palette.to_block(block=block, offset=0) self._write_compressed_block(rom, block, CHARS_ANIM_PALETTE_POINTER) def write_chars_layouts_to_rom(self, rom): block_size = sum(TitleScreenLayoutEntry.block_size() * len(c) for c in self.chars_layouts) # Ensure the new data is located in only one bank # Spreading it across two banks might make part of it inaccessible. def can_write_to(begin): return begin >> 16 == (begin + block_size) >> 16 with Block(block_size) as block: # Write the character animation data to the ROM offset = 0 for layout in self.chars_layouts: for entry in layout: entry.to_block(block=block, offset=offset) offset += entry.block_size() new_offset = to_snes_address( rom.allocate(data=block, size=block_size, can_write_to=can_write_to)) # Write the offsets to the layouts to the ROM new_bank = new_offset >> 16 new_data_start = new_offset & 0xFFFF data_offset = new_data_start for c, layout in enumerate(self.chars_layouts): rom[CHARS_LAYOUT_TABLE + c * 2:CHARS_LAYOUT_TABLE + c * 2 + 2] = [data_offset & 0xFF, data_offset >> 8] data_offset += len( layout) * TitleScreenLayoutEntry.block_size() # Change the offset for the character layouts # The way this normally works is that EarthBound stores the address # of the bank holding the data (0xE1 by default, hence the 0x210000 # offset); the offsets in the table are then prefixed with that # address. However, reallocating the data may have changed its # bank, so we need to manually set it to the new bank address. rom[CHARS_LAYOUT_BANK:CHARS_LAYOUT_BANK + 2] = [0xA9, new_bank] def read_from_project(self, resource_open): self.read_background_data_from_project(resource_open) self.read_chars_data_from_project(resource_open) def read_background_data_from_project(self, resource_open): # Load the background reference image # The image's arrangement, tileset and palette will be used for the # animation frames with resource_open(BG_REFERENCE_PATH, "png") as f: image = open_indexed_image(f) self.bg_arrangement.from_image(image, self.bg_tileset, self.bg_palette) # Read the background animated frames for frame in xrange(NUM_ANIM_FRAMES): # Create temporary structures used to check consistency between # frames tileset = EbGraphicTileset(BG_NUM_TILES, TILE_WIDTH, TILE_HEIGHT) arrangement = EbTileArrangement(BG_ARRANGEMENT_WIDTH, BG_ARRANGEMENT_HEIGHT) palette = EbPalette(NUM_SUBPALETTES, BG_SUBPALETTE_LENGTH) # Read one frame's image data with resource_open(BG_FRAMES_PATH.format(frame), "png") as f: image = open_indexed_image(f) arrangement.from_image(image, tileset, palette) # Make sure each frame's tileset and arrangement is identical # The background palette is checked only if it isn't the fake # palette used for the first few frames if frame >= CHARS_NUM_ANIM_SUBPALETTES: # Get the background animated subpalette from the background # palette colors = palette[0, BG_ANIM_SLICE] self.bg_anim_palette.subpalettes[ frame - CHARS_NUM_ANIM_SUBPALETTES] = colors palette[0, BG_ANIM_SLICE] = self.bg_palette[0, BG_ANIM_SLICE] if self.bg_palette != palette: log.warn("Palette from background frame {} does not match " "reference.".format(frame)) if self.bg_tileset != tileset: log.warn("Tileset from background frame {} does not match " "reference.".format(frame)) if self.bg_arrangement != arrangement: log.warn("Arrangement from background frame {} does not match " "reference.".format(frame)) def read_chars_data_from_project(self, resource_open): # Read the characters positions with resource_open(CHARS_POSITIONS_PATH, "yml") as f: chars_positions = yml_load(f) # Read the characters animated frames self.chars_tileset = None self.chars_anim_palette = EbPalette(CHARS_NUM_ANIM_SUBPALETTES, ANIM_SUBPALETTE_LENGTH) original_tileset = None for p in xrange(CHARS_NUM_ANIM_SUBPALETTES): # Read one of the animation frames with resource_open(CHARS_FRAMES_PATH.format(p), "png") as f: # Create temporary structures to hold the data image = open_indexed_image(f) arrangement = EbTileArrangement(image.width / TILE_WIDTH, image.height / TILE_HEIGHT) tileset = EbGraphicTileset(CHARS_NUM_TILES, TILE_WIDTH, TILE_HEIGHT) anim_subpalette = EbPalette(NUM_SUBPALETTES, ANIM_SUBPALETTE_LENGTH) arrangement.from_image(image, tileset, anim_subpalette, True) # Add the characters animation subpalette for i in xrange(ANIM_SUBPALETTE_LENGTH): self.chars_anim_palette[p, i] = anim_subpalette[0, i] # Add the characters tileset if not already set, otherwise # ensure that it the current tileset is identical if not self.chars_tileset: original_tileset = tileset self.chars_tileset = EbGraphicTileset(CHARS_NUM_TILES, TILE_WIDTH, TILE_HEIGHT) self.chars_tileset.tiles = [[[0 for _ in xrange(TILE_HEIGHT)] for _ in xrange(TILE_WIDTH)] for _ in xrange(CHARS_NUM_TILES)] unused_tiles = set(xrange(CHARS_NUM_TILES)) # Set the new character layouts self.chars_layouts = [[] for _ in xrange(NUM_CHARS)] for c, data in chars_positions.items(): # Get the data from the YAML file x = int(data['x'] / TILE_WIDTH) y = int(data['y'] / TILE_HEIGHT) width = int(data['width'] / TILE_WIDTH) height = int(data['height'] / TILE_HEIGHT) x_offset = data['top_left_offset']['x'] y_offset = data['top_left_offset']['y'] unknown = data['unknown'] # Generate a list of all tiles must be visited # Where possible, we try to generate a multi tile (4 tiles # stored as one); otherwise, bordering tiles that are # visited will all be single tiles. l = [(i, j) for i in xrange(0, width, 2) for j in xrange(0, height, 2)] if width % 2 == 1: l.extend([(width - 1, j) for j in xrange(1, height, 2)]) if height % 2 == 1: l.extend([(i, height - 1) for i in xrange(1, width, 2)]) # Generate the new reduced tileset for i, j in l: # Put the tile in the new tileset o_tile = arrangement[x + i, y + j].tile n_tile = unused_tiles.pop() self.chars_tileset.tiles[n_tile] = tileset[o_tile] entry = TitleScreenLayoutEntry(i * 8 + x_offset, j * 8 + y_offset, n_tile, 0, unknown) # Create a multi entry if possible to save space if i < width - 1 and j < height - 1: entry.set_single(True) o_tile_r = arrangement[x + i + 1, y + j].tile o_tile_d = arrangement[x + i, y + j + 1].tile o_tile_dr = arrangement[x + i + 1, y + j + 1].tile n_tile_r = n_tile + 1 n_tile_d = n_tile + 16 n_tile_dr = n_tile + 17 unused_tiles.difference_update( (n_tile_r, n_tile_d, n_tile_dr)) self.chars_tileset.tiles[n_tile_r] = \ tileset[o_tile_r] self.chars_tileset.tiles[n_tile_d] = \ tileset[o_tile_d] self.chars_tileset.tiles[n_tile_dr] = \ tileset[o_tile_dr] self.chars_layouts[c].append(entry) self.chars_layouts[c][-1].set_final(True) elif original_tileset != tileset: log.warn("Tileset from characters frame {} does not match " "tileset from characters frame 0.".format(p)) # Read the initial characters palette with resource_open(CHARS_INITIAL_PATH, "png") as f: image = open_indexed_image(f) arrangement = EbTileArrangement(image.width / TILE_WIDTH, image.height / TILE_HEIGHT) tileset = EbGraphicTileset(CHARS_NUM_TILES, TILE_WIDTH, TILE_HEIGHT) self.chars_palette = EbPalette(NUM_SUBPALETTES, ANIM_SUBPALETTE_LENGTH) arrangement.from_image(image, tileset, self.chars_palette) def write_to_project(self, resource_open): self.write_background_data_to_project(resource_open) self.write_chars_data_to_project(resource_open) def write_background_data_to_project(self, resource_open): # Write out the reference background image # This image is used to get the arrangement, tileset and static palette # that will be used by all background images. with resource_open(BG_REFERENCE_PATH, "png") as f: image = self.bg_arrangement.image(self.bg_tileset, self.bg_palette) image.save(f) # Write out the background's animated frames for frame in xrange(NUM_ANIM_FRAMES): palette = EbPalette(NUM_SUBPALETTES, BG_SUBPALETTE_LENGTH) if frame < CHARS_NUM_ANIM_SUBPALETTES: palette[0, CHARS_ANIM_SLICE] = \ self.chars_anim_palette.get_subpalette(frame)[0, :] else: palette[0, :] = self.bg_palette.get_subpalette(0)[0, :] palette[0, BG_ANIM_SLICE] = \ self.bg_anim_palette.get_subpalette( frame - CHARS_NUM_ANIM_SUBPALETTES )[0, :] with resource_open(BG_FRAMES_PATH.format(frame), "png") as f: image = self.bg_arrangement.image(self.bg_tileset, palette) image.save(f) def write_chars_data_to_project(self, resource_open): # Build an arrangement combining every character for convenience chars_positions = {} arrangement = EbTileArrangement(3 * 9, 6) for c, layout in enumerate(self.chars_layouts): top_left = {'x': 128, 'y': 128} for e, entry in enumerate(layout): tile = entry.tile & (CHARS_NUM_TILES - 1) top_left['x'] = min(top_left['x'], int(entry.x)) top_left['y'] = min(top_left['y'], int(entry.y)) x = c * 3 + (entry.x + 16) / 8 y = (entry.y + 24) / 8 arrangement[x, y].tile = tile if not entry.is_single(): arrangement[x + 1, y].tile = tile + 1 arrangement[x, y + 1].tile = tile + 16 arrangement[x + 1, y + 1].tile = tile + 17 chars_positions[c] = { 'x': c * 3 * 8, 'y': 0, 'width': 3 * 8, 'height': 6 * 8, 'top_left_offset': top_left, 'unknown': layout[0].unknown } # Write the characters animation frames for p in xrange(CHARS_NUM_ANIM_SUBPALETTES): with resource_open(CHARS_FRAMES_PATH.format(p), "png") as f: image = arrangement.image( self.chars_tileset, self.chars_anim_palette.get_subpalette(p)) image.save(f) # Write out the initial characters palette with resource_open(CHARS_INITIAL_PATH, "png") as f: image = arrangement.image(self.chars_tileset, self.chars_palette) image.save(f) # Write out the positions of the characters with resource_open(CHARS_POSITIONS_PATH, "yml") as f: yml_dump(chars_positions, f, False) def upgrade_project(self, old_version, new_version, rom, resource_open_r, resource_open_w, resource_delete): if old_version < 9: self.read_from_rom(rom) self.write_to_project(resource_open_w) @staticmethod def _decompress_block(rom, block, pointer): block.from_compressed_block(block=rom, offset=from_snes_address( read_asm_pointer(rom, pointer))) @staticmethod def _write_compressed_block(rom, compressed_block, pointer): compressed_block.compress() new_offset = rom.allocate(data=compressed_block) write_asm_pointer(block=rom, offset=pointer, pointer=to_snes_address(new_offset)) return new_offset
class 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) )