diff --git a/coilsnake/assets/ips/Earthbound/battle_animation_expand.ips b/coilsnake/assets/ips/Earthbound/battle_animation_expand.ips new file mode 100644 index 00000000..8632aa78 Binary files /dev/null and b/coilsnake/assets/ips/Earthbound/battle_animation_expand.ips differ diff --git a/coilsnake/assets/ips/Earthbound/battle_animation_expand.yml b/coilsnake/assets/ips/Earthbound/battle_animation_expand.yml new file mode 100644 index 00000000..9080d3c4 --- /dev/null +++ b/coilsnake/assets/ips/Earthbound/battle_animation_expand.yml @@ -0,0 +1,6 @@ +Title: Modify battle animation functions to play expanded animations +Author: Jeffman (IPS format conversion by SupremeKirb) +Source: https://github.com/jeffman/EbPsiAnimationEditor/blob/master/additional-animations.md +Hidden: True +Header: 0x200 +Ranges: [] \ No newline at end of file diff --git a/coilsnake/assets/modulelist.txt b/coilsnake/assets/modulelist.txt index b0487247..d5cdfae0 100644 --- a/coilsnake/assets/modulelist.txt +++ b/coilsnake/assets/modulelist.txt @@ -17,6 +17,7 @@ eb.DeathScreenModule eb.SoundStoneModule eb.CastModule eb.AnimationModule +eb.BattleAnimationModule eb.MapModule eb.MapEnemyModule eb.MapMusicModule diff --git a/coilsnake/assets/structures/eb.yml b/coilsnake/assets/structures/eb.yml index 28be8488..98c6179c 100644 --- a/coilsnake/assets/structures/eb.yml +++ b/coilsnake/assets/structures/eb.yml @@ -414,16 +414,23 @@ type: pointer - name: Frame duration (1/60th seconds) size: 1 - - name: Palette duration (1/60th seconds) + - name: Palette cycle duration (1/60th seconds) + size: 1 + - name: Palette cycle lower index + size: 1 + - name: Palette cycle upper index size: 1 - - name: unknown - size: 2 - type: bytearray - name: Frame count size: 1 - - name: unknown 2 - size: 2 - type: bytearray + - name: Targetting + values: + - single + - row + - all + - random + size: 1 + - name: Delay before enemy colour change + size: 1 - name: Duration of enemy colour change size: 1 - name: Enemy colour @@ -436,7 +443,7 @@ 13431935: &id091 entries: - name: Palette - size: 16 + size: 8 type: palette name: PSI_ANIM_PALETTES offset: 13431935 diff --git a/coilsnake/model/eb/graphics.py b/coilsnake/model/eb/graphics.py index 37820b6d..210e2514 100644 --- a/coilsnake/model/eb/graphics.py +++ b/coilsnake/model/eb/graphics.py @@ -209,7 +209,6 @@ def __eq__(self, other): def __getitem__(self, key): return self.tiles[key] - class EbTileArrangementItem(EqualityMixin, StringRepresentationMixin): def __init__(self, tile=0, subpalette=0, is_vertically_flipped=False, is_horizontally_flipped=False, is_priority=False): @@ -244,6 +243,17 @@ def to_block(self, block, offset=0): | self.tile), 2) +class EbOneByteTileArrangementItem(EbTileArrangementItem): + def check_validity(self): + if self.tile < 0 or self.tile > 0xFF: + raise InvalidArgumentError("Invalid tile[{}]".format(self.tile)) + + def from_block(self, block, offset=0): + self.tile = block[offset] + + def to_block(self, block, offset=0): + self.check_validity() + block[offset] = self.tile class EbTileArrangement(EqualityMixin): """A class representing an image formed by an arrangement of tile-based graphics with a certain palette.""" @@ -387,7 +397,28 @@ def __getitem__(self, key): x, y, self.width, self.height)) return self.arrangement[y][x] +class EbOneByteTileArrangement(EbTileArrangement): + """A class representing a tilemap that consists only of a single byte with a 0-255 tile ID per tile. + It's expected that other data, such as palette, priority, etc, is appended later.""" + def __init__(self, width, height): + super().__init__(width, height) + self.arrangement = [[EbOneByteTileArrangementItem() for x in range(self.width)] for y in range(self.height)] + def from_block(self, block, offset=0): + for row in self.arrangement: + for item in row: + item.from_block(block, offset) + offset += 1 + + def to_block(self, block, offset=0): + for row in self.arrangement: + for item in row: + item.to_block(block, offset) + offset += 1 + + def block_size(self): + return sum([len(x) for x in self.arrangement]) + class EbCompressedGraphic(object): def __init__(self, num_tiles, tile_width, tile_height, bpp, arrangement_width, arrangement_height, num_palettes, num_subpalettes, subpalette_length, compressed_palettes=True): diff --git a/coilsnake/modules/common/GenericModule.py b/coilsnake/modules/common/GenericModule.py index 01f9d0c0..c28d292a 100644 --- a/coilsnake/modules/common/GenericModule.py +++ b/coilsnake/modules/common/GenericModule.py @@ -27,5 +27,5 @@ def read_from_project(self, resource_open): def write_to_project(self, resource_open): pass - def upgrade_project(self, old_version, new_version, rom, resource_open_r, resource_open_w, resource_delete): + def upgrade_project(self, old_version, new_version, rom, old_compiled_rom, resource_open_r, resource_open_w, resource_delete): pass diff --git a/coilsnake/modules/common/PatchModule.py b/coilsnake/modules/common/PatchModule.py index 60c9890f..084b9c33 100644 --- a/coilsnake/modules/common/PatchModule.py +++ b/coilsnake/modules/common/PatchModule.py @@ -84,7 +84,7 @@ def read_from_project(self, resource_open): with resource_open("patches", "yml", True) as f: self.patches = yml_load(f) - def upgrade_project(self, old_version, new_version, rom, resource_open_r, resource_open_w, resource_delete): + def upgrade_project(self, old_version, new_version, rom, old_compiled_rom, resource_open_r, resource_open_w, resource_delete): if old_version == 1: self.read_from_rom(rom) self.write_to_project(resource_open_w) diff --git a/coilsnake/modules/common/UsedRangeModule.py b/coilsnake/modules/common/UsedRangeModule.py index ee9129fc..38881cdd 100644 --- a/coilsnake/modules/common/UsedRangeModule.py +++ b/coilsnake/modules/common/UsedRangeModule.py @@ -53,7 +53,7 @@ def __init__(self): super(UsedRangeModule, self).__init__() self.ranges = [] - def upgrade_project(self, old_version, new_version, rom, resource_open_r, resource_open_w, resource_delete): + def upgrade_project(self, old_version, new_version, rom, old_compiled_rom, resource_open_r, resource_open_w, resource_delete): """ Upgrades a project's used ranges module to the latest version. """ @@ -66,6 +66,7 @@ def upgrade_project(self, old_version, new_version, rom, resource_open_r, resour old_version + 1, new_version, rom, + old_compiled_rom, resource_open_r, resource_open_w, resource_delete) diff --git a/coilsnake/modules/eb/AnimationModule.py b/coilsnake/modules/eb/AnimationModule.py index 50b975cb..e6f942ee 100644 --- a/coilsnake/modules/eb/AnimationModule.py +++ b/coilsnake/modules/eb/AnimationModule.py @@ -188,7 +188,7 @@ def write_to_project(self, resource_open): with resource_open('Animations/animations', 'yml', True) as f: yml_dump(animation_data, f, default_flow_style=False) - def upgrade_project(self, old_version, new_version, rom, resource_open_r, resource_open_w, resource_delete): + def upgrade_project(self, old_version, new_version, rom, old_compiled_rom, resource_open_r, resource_open_w, resource_delete): if old_version < 10: self.read_from_rom(rom) self.write_to_project(resource_open_w) diff --git a/coilsnake/modules/eb/BattleAnimationModule.py b/coilsnake/modules/eb/BattleAnimationModule.py new file mode 100644 index 00000000..475295cd --- /dev/null +++ b/coilsnake/modules/eb/BattleAnimationModule.py @@ -0,0 +1,429 @@ +from coilsnake.exceptions.common.exceptions import CoilSnakeTraceableError, CoilSnakeUserError +from coilsnake.model.common.ips import IpsPatch +from coilsnake.model.eb.blocks import EbCompressibleBlock +from coilsnake.model.eb.graphics import EbGraphicTileset, EbOneByteTileArrangement, EbTileArrangement, EbOneByteTileArrangementItem +from coilsnake.model.eb.palettes import EbPalette +from coilsnake.model.eb.table import eb_table_from_offset +from coilsnake.modules.common.PatchModule import get_ips_filename +from coilsnake.modules.eb.EbModule import EbModule +from coilsnake.util.eb.pointer import from_snes_address, to_snes_address, AsmPointerReference, XlPointerReference +from coilsnake.util.common.image import open_indexed_image +from coilsnake.util.common.yml import yml_load, yml_dump + +import logging + +log = logging.getLogger(__name__) + +DEFAULT_ANIMATION_COUNT = 34 + +BATTLE_ANIMATION_TABLE_DEFAULT_ADDRESS = 0xCCF04D +BATTLE_ANIMATION_PALETTES_DEFAULT_ADDRESS = 0xCCF47F +BATTLE_ANIMATION_ARRANGEMENT_PTRS_DEFAULT_ADDRESS = 0xCCF58F +BATTLE_ANIMATION_TILESET_BANK_DEFAULT_ADDRESS = 0xCC0000 + +BATTLE_ANIMATION_TABLE_REFERENCES = ( + AsmPointerReference(0x02E34F), + XlPointerReference(0x02E153), + XlPointerReference(0x02E1BA), + XlPointerReference(0x02E509), +) +BATTLE_ANIMATION_PALETTE_TABLE_REFERENCES = ( + AsmPointerReference(0x02E2F4), +) +BATTLE_ANIMATION_ARRANGEMENT_PTRS_REFERENCES = ( + AsmPointerReference(0x02E461), +) +BATTLE_ANIMATION_TILESET_BANK_REFERENCES = ( + # 32-bit pointer to the bank which the compressed tilesets are in; then the game adds the short tileset pointer after + AsmPointerReference(0x02E13F), + AsmPointerReference(0x02E1A6), +) + + +# Common EbTileArrangement used to render the tileset to an image +TILESET_IMAGE_ARRANGEMENT = EbTileArrangement(16, 16) +tile_id_to_write = 0 +for tile_row in TILESET_IMAGE_ARRANGEMENT.arrangement: + for tile in tile_row: + tile.tile = tile_id_to_write + tile_id_to_write += 1 +del tile_id_to_write + +# Common EbPalette used to render the tileset to an image +# Grayscale +TILESET_IMAGE_PALETTE = EbPalette(1, 4, (0, 0, 0, 85, 85, 85, 170, 170, 170, 255, 255, 255)) + +# Helper function for reading pointers from code +def read_pointer_ref_or_default(ref, rom, default_addr): + addr = ref.read(rom) + if not addr: + log.warning("Failed to validate code-read pointer reference starting at offset ${:06X}; defaulting to vanilla address ${:06X}".format(ref.offset, default_addr)) + return default_addr + return addr + +class BattleAnimation: + def __init__(self, + tileset_pointer_short, frame_duration, palette_cycle_duration, + palette_cycle_lower_index, palette_cycle_upper_index, + frame_count, targetting, enemy_color_delay, + enemy_color_duration, enemy_color + ): + self.tileset_pointer_short = tileset_pointer_short + self.frame_duration = frame_duration + self.palette_cycle_duration = palette_cycle_duration + self.palette_cycle_lower_index = palette_cycle_lower_index + self.palette_cycle_upper_index = palette_cycle_upper_index + self.frame_count = frame_count + self.targetting = targetting + self.enemy_color_delay = enemy_color_delay + self.enemy_color_duration = enemy_color_duration + self.enemy_color = enemy_color + + self.palette = EbPalette(num_subpalettes=1, subpalette_length=4) + self.tileset = EbGraphicTileset(num_tiles=256) + + if self.frame_count is None: + # Happens when reading from project, because we don't know the frame count yet + self.arrangements = [] + else: + self.arrangements = [EbOneByteTileArrangement(width=32, height=32) for _ in range(self.frame_count)] + + def arrangements_from_block(self, block, offset): + with EbCompressibleBlock() as compressed_block: + compressed_block.from_compressed_block(block, offset) + next_offset = 0 + for frame in self.arrangements: + frame.from_block(compressed_block, next_offset) + next_offset += frame.block_size() + + def arrangements_to_block(self): + block = EbCompressibleBlock(32*32*len(self.arrangements)) + next_offset = 0 + for frame in self.arrangements: + frame.to_block(block, next_offset) + next_offset += 32*32 + block.compress() + return block + + def arrangements_from_map(self, map_file): + self.arrangements = [] + self.frame_count = 0 + raw: str = map_file.read() + text_frames = raw.split("\n\n") + if text_frames and not text_frames[-1]: + # The last split is from the trailing \n\n gap + text_frames.pop() + if not text_frames: + raise CoilSnakeUserError("Battle animation has no frames") + + for i, text_frame in enumerate(text_frames): + arrangement = EbOneByteTileArrangement(32, 32) + rows = text_frame.split("\n") + if len(rows) != arrangement.height: + raise CoilSnakeUserError("Frame {} has {} rows of tiles (expected {})".format(i, len(rows), arrangement.height)) + for j, row in enumerate(rows): + tiles = row.split() + if len(tiles) != arrangement.width: + raise CoilSnakeUserError("Frame {} row {} has {} tiles (expected {})".format(i, j, len(tiles), arrangement.width)) + for k, tile in enumerate(tiles): + arrangement.arrangement[j][k] = EbOneByteTileArrangementItem(int(tile, 16)) + self.arrangements.append(arrangement) + self.frame_count += 1 + + if self.frame_count > 64: + raise Exception("Frame count cannot exceed 64 frames.") + + +class BattleAnimationModule(EbModule): + """Extracts battle animations from EarthBound""" + NAME = "Battle Animations" + + # Animations config, arrangements, arrangement pointers, tilesets, and palettes + FREE_RANGES = [(0x0C2E19, 0x0CF616)] + + def __init__(self): + super(BattleAnimationModule, self).__init__() + self.battle_animation_table = eb_table_from_offset(BATTLE_ANIMATION_TABLE_DEFAULT_ADDRESS, + hidden_columns=["Short pointer to tileset", "Frame count"]) + self.palette_table = eb_table_from_offset(BATTLE_ANIMATION_PALETTES_DEFAULT_ADDRESS) + self.arrangement_ptr_table = eb_table_from_offset(BATTLE_ANIMATION_ARRANGEMENT_PTRS_DEFAULT_ADDRESS) + self.battle_animations: list[BattleAnimation] = [] + + self.tilesets: list[EbGraphicTileset] = [] + + def read_from_rom(self, rom): + # Before this module was added, PSI animations were expanded manually + # and applied to a base ROM for future compilations. + # That means we need to read the code to find pointers to potentially relocated + # tables, and do some guesswork (fancy programmers call it "heuristics") to find the length. + config_ptr = read_pointer_ref_or_default(BATTLE_ANIMATION_TABLE_REFERENCES[0], rom, BATTLE_ANIMATION_TABLE_DEFAULT_ADDRESS) + palettes_ptr = read_pointer_ref_or_default(BATTLE_ANIMATION_PALETTE_TABLE_REFERENCES[0], rom, BATTLE_ANIMATION_PALETTES_DEFAULT_ADDRESS) + arrangements_ptrs_ptr = read_pointer_ref_or_default(BATTLE_ANIMATION_ARRANGEMENT_PTRS_REFERENCES[0], rom, BATTLE_ANIMATION_ARRANGEMENT_PTRS_DEFAULT_ADDRESS) + tilesets_bank_ptr = read_pointer_ref_or_default(BATTLE_ANIMATION_TILESET_BANK_REFERENCES[0], rom, BATTLE_ANIMATION_TILESET_BANK_DEFAULT_ADDRESS) + + log.info("Found battle animation pointers:\n Config table: ${:06X}\n Palettes: ${:06X}\n Arrangement ptrs: ${:06X}\n Tileset bank: ${:06X}".format( + config_ptr, palettes_ptr, arrangements_ptrs_ptr, tilesets_bank_ptr + )) + + # Now we need to guess the length of things. + guessed_length = 0 + while True: + # Keep going until any of these happen: + # - Arrangement ptr is invalid, null, or goes over bank boundary + # - Config table goes over bank boundary + # - Decompressed arrangement data is not a multiple of the frame size + # Other things which could be added: + # - Check for data overlap + + # Check arrangement pointer table + arrangement_ptr_ptr = arrangements_ptrs_ptr + guessed_length*self.arrangement_ptr_table.schema.size + arrangement_ptr = rom.read_multi(from_snes_address(arrangement_ptr_ptr), 4) + if arrangement_ptr_ptr & 0xFF0000 != arrangements_ptrs_ptr & 0xFF0000: + # Bank cross + log.info("Battle animation count stopping at {} due to arrangement pointer table bank cross".format(guessed_length)) + break + if arrangement_ptr == 0 or arrangement_ptr & 0xFF000000: + # Null or upper byte is set which is invalid + log.info("Battle animation count stopping at {} due to invalid arrangement pointer ${:06X}".format(guessed_length, arrangement_ptr)) + break + + # Check config table + cfg_table_entry = config_ptr + guessed_length*self.battle_animation_table.schema.size + if cfg_table_entry & 0xFF0000 != config_ptr & 0xFF0000: + # Bank cross + log.info("Battle animation count stopping at {} due to config table bank cross".format(guessed_length)) + break + # Check decomp'd arrangement data is a multiple of the frame size + # Can't do framecount because Rockin G has 10 unused frames and the Switch Online ROM trims off a couple frames on Counter-PSI Unit + with EbCompressibleBlock() as arrangement_block: + arrangement_block.from_compressed_block(rom, from_snes_address(arrangement_ptr)) + if arrangement_block.size % 1024: # Sizeof a frame of decomp'd data + log.info("Battle animation count stopping at {} due to arrangement data not being a multiple of frame size".format(guessed_length)) + break + + guessed_length += 1 + + # Recreate the tables at the correct size + self.battle_animation_table.recreate(num_rows=guessed_length) + self.palette_table.recreate(num_rows=guessed_length) + self.arrangement_ptr_table.recreate(num_rows=guessed_length) + + # Actually dump the data + self.battle_animation_table.from_block( + rom, offset=from_snes_address(config_ptr) + ) + self.palette_table.from_block( + rom, offset=from_snes_address(palettes_ptr) + ) + self.arrangement_ptr_table.from_block( + rom, offset=from_snes_address(arrangements_ptrs_ptr) + ) + + # Translate to internal format + known_tilesets = {} # Maps short addresses to indexes in self.tilesets + + for index in range(self.battle_animation_table.num_rows): + row = self.battle_animation_table[index] + battle_animation = BattleAnimation(*row) + + battle_animation.palette = self.palette_table[index][0] + + arrangement_ptr = from_snes_address(self.arrangement_ptr_table[index][0]) + battle_animation.arrangements_from_block(rom, arrangement_ptr) + + tileset_ptr = from_snes_address(battle_animation.tileset_pointer_short | tilesets_bank_ptr) + # Keep track of tilesets we've already seen and skip duplicating them + # Not as crucial here as it is when writing ... + if tileset_ptr not in known_tilesets: + with EbCompressibleBlock() as compressed_block: + tileset = EbGraphicTileset(256) + compressed_block.from_compressed_block(rom, tileset_ptr) + tileset.from_block(compressed_block) + known_tilesets[tileset_ptr] = len(self.tilesets) + self.tilesets.append(tileset) + + battle_animation.tileset = self.tilesets[known_tilesets[tileset_ptr]] + + self.battle_animations.append(battle_animation) + + def write_to_rom(self, rom): + # If necessary, apply the battle animation expansion patch. + # The range this patch covers is not currently marked as free, so it's OK to use. + # But if that ever changes then this may break. + # (Ranges: 3F98D - 3F98F, 3FE00 - 3FE20) + if len(self.battle_animations) > DEFAULT_ANIMATION_COUNT: + # The patch causes battle animations #34 and up, when called via script (or the function at C3F981), + # to require being called starting at ID 55 (56 in CCScript) instead of ID 34. + # This is because battle animations and the HDMA-based enemy-attack animations share the same ID space. + log.info("Applying battle animation expansion patch") + patch = IpsPatch() + patch.load(get_ips_filename(rom.type, "battle_animation_expand")) + patch.apply(rom) + + # Write palette table + palette_table_offset = rom.allocate(size=self.palette_table.size) + self.palette_table.to_block(rom, palette_table_offset) + # Relocate references to palette table + for reference in BATTLE_ANIMATION_PALETTE_TABLE_REFERENCES: + if reference.validate_structure(rom): + reference.write(rom, to_snes_address(palette_table_offset)) + else: + log.warning("Palette table relocation at %#x failed structure check - skipping...", reference.offset) + + # Write compressed arrangements + # The arrangements for different animations can be in different banks. Lucky us + for id, animation in enumerate(self.battle_animations): + block = animation.arrangements_to_block() + arrangement_offset = rom.allocate(size=block.size) + # Write to ROM + rom.to_block(block, arrangement_offset) + # Reconstruct table of arrangement pointers + self.arrangement_ptr_table[id] = [to_snes_address(arrangement_offset)] + + # Write table of arrangement pointers + arrangement_ptr_table_offset = rom.allocate(size=self.arrangement_ptr_table.size) + self.arrangement_ptr_table.to_block(rom, arrangement_ptr_table_offset) + # Relocate references to table of arrangement pointers + for reference in BATTLE_ANIMATION_ARRANGEMENT_PTRS_REFERENCES: + if reference.validate_structure(rom): + reference.write(rom, to_snes_address(arrangement_ptr_table_offset)) + else: + log.warning("Arrangement pointer table relocation at %#x failed structure check - skipping...", reference.offset) + + tilesets_compressed: list[EbCompressibleBlock] = [] + for tileset in self.tilesets: + with EbCompressibleBlock(tileset.block_size()) as compressed_block: + tileset.to_block(compressed_block) + compressed_block.compress() + tilesets_compressed.append(compressed_block) + compressed_total_size = sum(i.size for i in tilesets_compressed) + + current_tileset_offset = rom.allocate(size=compressed_total_size) + tileset_short_ptrs = [] + tilesets_bank_only = current_tileset_offset & 0xFF0000 + + # Write the compressed tilesets + for compressed in tilesets_compressed: + rom.to_block(compressed, current_tileset_offset) + tileset_short_ptrs.append(current_tileset_offset & 0xFFFF) + current_tileset_offset += compressed.size + # Repoint them + for reference in BATTLE_ANIMATION_TILESET_BANK_REFERENCES: + if reference.validate_structure(rom): + reference.write(rom, to_snes_address(tilesets_bank_only)) + else: + log.warning("Tileset bank relocation at %#x failed structure check - skipping...", reference.offset) + + + # Write the config table. Fill in the final field: the short pointer to the tileset + for id, animation in enumerate(self.battle_animations): + # This is slower, but better than keeping an index into the self.tilesets list in the + # BattleAnimation object, because that keeps it more separate. + self.battle_animation_table[id][0] = tileset_short_ptrs[self.tilesets.index(animation.tileset)] + battle_animation_table_offset = rom.allocate(size=self.battle_animation_table.size) + self.battle_animation_table.to_block(rom, battle_animation_table_offset) + # And repoint + for reference in BATTLE_ANIMATION_TABLE_REFERENCES: + if reference.validate_structure(rom): + reference.write(rom, to_snes_address(battle_animation_table_offset)) + else: + log.warning("Battle animation table relocation at %#x failed structure check - skipping...", reference.offset) + + def read_from_project(self, resource_open): + with resource_open("BattleAnimations/battle_animations", "yml", True) as f: + yml_rep = yml_load(f) + num_rows = len(yml_rep) + self.battle_animation_table.recreate(num_rows=num_rows) + self.battle_animation_table.from_yml_rep(yml_rep) + + # Recreate the palette and arrangement tables too + self.palette_table.recreate(num_rows=self.battle_animation_table.num_rows) + self.arrangement_ptr_table.recreate(num_rows=self.battle_animation_table.num_rows) + + # For trimming data + known_tilesets = {} # Key: ID from yaml | val: Index into self.tilesets + tilesets_max_ID_used = [] + + for animation_id in range(self.battle_animation_table.num_rows): + try: + data = self.battle_animation_table[animation_id] + animation = BattleAnimation(*data) + self.battle_animations.append(animation) + # Frame count and short pointer to tileset still need to be filled in + # We'll fill in frame count here after we read the map files, + # and tileset pointer after we figure out where it's going in the ROM + + # Get palette + animation.palette = EbPalette(1, 4) + animation.palette.from_yml_rep(yml_rep[animation_id]["Palette"]) + self.palette_table[animation_id] = [animation.palette] + + # Create tileset if this is the first time we've seen it. + # This is nicer than globbing and prevents unused tilesets from being compiled + tileset_id = int(yml_rep[animation_id]["Tileset"]) + if tileset_id not in known_tilesets: + with resource_open("BattleAnimations/Tilesets/{:02d}".format(tileset_id), "png") as tileset_f: + tileset_image = open_indexed_image(tileset_f) + tileset = EbGraphicTileset(256) + tileset.from_image(tileset_image, TILESET_IMAGE_ARRANGEMENT, TILESET_IMAGE_PALETTE) + known_tilesets[tileset_id] = len(self.tilesets) + self.tilesets.append(tileset) + tilesets_max_ID_used.append(0) # Populate later + animation.tileset = self.tilesets[known_tilesets[tileset_id]] + + with resource_open("BattleAnimations/Arrangements/{:02d}".format(animation_id), "map", True) as map_f: + animation.arrangements_from_map(map_f) # Frame count is filled in now + # Put frame count in the table also + self.battle_animation_table[animation_id][5] = animation.frame_count + # Will fill in the arrangement pointers when we actually have the arrangements in the ROM + + # Find max tile ID used + tilesets_max_ID_used[known_tilesets[tileset_id]] = max( + tilesets_max_ID_used[known_tilesets[tileset_id]], + max(max(max(tile.tile for tile in row) for row in arrangement.arrangement) for arrangement in animation.arrangements) + ) + + except Exception as e: + message = "Encountered an error while reading battle animation #{}.".format(animation_id) + raise CoilSnakeTraceableError(message, e) + + # Trim tilesets past max ID + for index, tileset in enumerate(self.tilesets): + tileset.num_tiles_maximum = tilesets_max_ID_used[index]+1 + tileset.tiles = tileset.tiles[:tileset.num_tiles_maximum] + + def write_to_project(self, resource_open): + battle_animation_yml = self.battle_animation_table.to_yml_rep() + for i, animation in enumerate(self.battle_animations): + # Add extra fields to the battle animation yml + battle_animation_yml[i]["Tileset"] = self.tilesets.index(animation.tileset) + battle_animation_yml[i]["Palette"] = animation.palette.yml_rep() + + # Write arrangements (tilemaps) + # This is called ".map" just like the overworld map but it is a little different. + # - Tile indexes are 2-digit instead of 3-digit (we can only have 256 tiles) + # - The data is arranged into a series of rectangles representing a frame each + with resource_open("BattleAnimations/Arrangements/{:02d}".format(i), "map", True) as f: + for frame in animation.arrangements: + for row in range(frame.height): + for col in range(frame.width): + f.write(hex(frame[col, row].tile)[2:].zfill(2)) + f.write(" ") + f.write("\n") + f.write("\n") + + with resource_open("BattleAnimations/battle_animations", "yml", True) as f: + yml_dump(battle_animation_yml, f, False) + + # Write tileset images + for i, tileset in enumerate(self.tilesets): + with resource_open("BattleAnimations/Tilesets/{:02d}".format(i), "png") as f: + image = TILESET_IMAGE_ARRANGEMENT.image(tileset, TILESET_IMAGE_PALETTE, True) + image.save(f, "png") + + def upgrade_project(self, old_version, new_version, rom, old_compiled_rom, resource_open_r, resource_open_w, resource_delete): + if old_version < 14: + self.read_from_rom(old_compiled_rom) + # We use the old compiled ROM because, in CoilSnake projects prior to the addition of this module, + # typically battle animation repointing happens via CCScript at compile-time. + self.write_to_project(resource_open_w) \ No newline at end of file diff --git a/coilsnake/modules/eb/CastModule.py b/coilsnake/modules/eb/CastModule.py index bb6c1292..bbdcbfad 100644 --- a/coilsnake/modules/eb/CastModule.py +++ b/coilsnake/modules/eb/CastModule.py @@ -270,7 +270,7 @@ def write_gfx_to_project(self, obj, resource_open): with resource_open(obj.path(), '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): + def upgrade_project(self, old_version, new_version, rom, old_compiled_rom, resource_open_r, resource_open_w, resource_delete): if old_version < 10: self.read_from_rom(rom) self.write_to_project(resource_open_w) diff --git a/coilsnake/modules/eb/CharacterSubstitutionsModule.py b/coilsnake/modules/eb/CharacterSubstitutionsModule.py index 8219e9b4..217f3a34 100644 --- a/coilsnake/modules/eb/CharacterSubstitutionsModule.py +++ b/coilsnake/modules/eb/CharacterSubstitutionsModule.py @@ -35,6 +35,6 @@ def write_to_project(self, resource_open): with resource_open(self.FILE, 'yml', True) as f: f.write(MODULE_COMMENT) - def upgrade_project(self, old_version, new_version, rom, resource_open_r, resource_open_w, resource_delete): + def upgrade_project(self, old_version, new_version, rom, old_compiled_rom, resource_open_r, resource_open_w, resource_delete): if old_version <= 7: self.write_to_project(resource_open_w) \ No newline at end of file diff --git a/coilsnake/modules/eb/CompressedGraphicsModule.py b/coilsnake/modules/eb/CompressedGraphicsModule.py index 0f170b56..b4928505 100644 --- a/coilsnake/modules/eb/CompressedGraphicsModule.py +++ b/coilsnake/modules/eb/CompressedGraphicsModule.py @@ -287,7 +287,7 @@ def write_gas_station_to_project(self, resource_open): with resource_open(GAS_STATION_INFO.name + "3", "png") as image_file: images[2].save(image_file, "png") - def upgrade_project(self, old_version, new_version, rom, resource_open_r, resource_open_w, resource_delete): + def upgrade_project(self, old_version, new_version, rom, old_compiled_rom, resource_open_r, resource_open_w, resource_delete): if old_version == new_version: return elif old_version <= 2: @@ -300,6 +300,6 @@ def upgrade_project(self, old_version, new_version, rom, resource_open_r, resour self.read_gas_station_from_rom(rom) self.write_gas_station_to_project(resource_open_w) - self.upgrade_project(3, new_version, rom, resource_open_r, resource_open_w, resource_delete) + self.upgrade_project(3, new_version, rom, old_compiled_rom, resource_open_r, resource_open_w, resource_delete) else: - self.upgrade_project(old_version + 1, new_version, rom, resource_open_r, resource_open_w, resource_delete) + self.upgrade_project(old_version + 1, new_version, rom, old_compiled_rom, resource_open_r, resource_open_w, resource_delete) diff --git a/coilsnake/modules/eb/DeathScreenModule.py b/coilsnake/modules/eb/DeathScreenModule.py index 75d3f45b..ca50be49 100644 --- a/coilsnake/modules/eb/DeathScreenModule.py +++ b/coilsnake/modules/eb/DeathScreenModule.py @@ -138,8 +138,8 @@ def write_to_project(self, resource_open): yml_dump(subpalettes, f, None) def upgrade_project( - self, old_version, new_version, rom, resource_open_r, - resource_open_w, resource_delete): + self, old_version, new_version, rom, old_compiled_rom, + resource_open_r, resource_open_w, resource_delete): # version 1-8: no death screen support # version 9: Only supported Ness' death screen (DeathScreen.png) # version 10+: Supports Ness and Jeff (DeathScreen_*.png) diff --git a/coilsnake/modules/eb/EnemyModule.py b/coilsnake/modules/eb/EnemyModule.py index 8489714e..d4db7807 100644 --- a/coilsnake/modules/eb/EnemyModule.py +++ b/coilsnake/modules/eb/EnemyModule.py @@ -238,7 +238,7 @@ def read_from_project(self, resource_open): group = [EnemyGroupTableEntry.from_yml_rep(x) for x in enemy_group] self.enemy_groups.append(group) - def upgrade_project(self, old_version, new_version, rom, resource_open_r, resource_open_w, resource_delete): + def upgrade_project(self, old_version, new_version, rom, old_compiled_rom, resource_open_r, resource_open_w, resource_delete): if old_version == new_version: return elif old_version == 3: @@ -274,7 +274,7 @@ def upgrade_project(self, old_version, new_version, rom, resource_open_r, resour 6: "64/128", 7: "128/128"}) self.upgrade_project( - old_version + 1, new_version, rom, resource_open_r, resource_open_w, resource_delete) + old_version + 1, new_version, rom, old_compiled_rom, resource_open_r, resource_open_w, resource_delete) else: self.upgrade_project( - old_version + 1, new_version, rom, resource_open_r, resource_open_w, resource_delete) + old_version + 1, new_version, rom, old_compiled_rom, resource_open_r, resource_open_w, resource_delete) diff --git a/coilsnake/modules/eb/FontModule.py b/coilsnake/modules/eb/FontModule.py index 80c845a9..5aa3be3f 100644 --- a/coilsnake/modules/eb/FontModule.py +++ b/coilsnake/modules/eb/FontModule.py @@ -101,7 +101,7 @@ def read_credits_font_from_project(self, resource_open): with resource_open("Fonts/credits", "png") as image_file: self.credits_font.from_files(image_file, "png") - def upgrade_project(self, old_version, new_version, rom, resource_open_r, resource_open_w, resource_delete): + def upgrade_project(self, old_version, new_version, rom, old_compiled_rom, resource_open_r, resource_open_w, resource_delete): if old_version == new_version: return elif old_version == 5: @@ -139,12 +139,12 @@ def upgrade_project(self, old_version, new_version, rom, resource_open_r, resour with resource_open_w(widths_resource_name, "yml", True) as widths_file: yml_dump(widths_dict, widths_file, default_flow_style=False) - self.upgrade_project(6, new_version, rom, resource_open_r, resource_open_w, resource_delete) + self.upgrade_project(6, new_version, rom, old_compiled_rom, resource_open_r, resource_open_w, resource_delete) elif old_version <= 2: # The credits font was a new feature in version 3 self.read_credits_font_from_rom(rom) self.write_credits_font_to_project(resource_open_w) - self.upgrade_project(3, new_version, rom, resource_open_r, resource_open_w, resource_delete) + self.upgrade_project(3, new_version, rom, old_compiled_rom, resource_open_r, resource_open_w, resource_delete) else: - self.upgrade_project(old_version + 1, new_version, rom, resource_open_r, resource_open_w, resource_delete) + self.upgrade_project(old_version + 1, new_version, rom, old_compiled_rom, resource_open_r, resource_open_w, resource_delete) diff --git a/coilsnake/modules/eb/MapEventModule.py b/coilsnake/modules/eb/MapEventModule.py index 55e67af5..37132a7e 100644 --- a/coilsnake/modules/eb/MapEventModule.py +++ b/coilsnake/modules/eb/MapEventModule.py @@ -54,7 +54,7 @@ def read_from_project(self, resource_open): with resource_open("map_changes", "yml", True) as f: self.pointer_table.from_yml_file(f) - def upgrade_project(self, old_version, new_version, rom, resource_open_r, resource_open_w, resource_delete): + def upgrade_project(self, old_version, new_version, rom, old_compiled_rom, resource_open_r, resource_open_w, resource_delete): if old_version == new_version: return elif old_version < 5: @@ -78,8 +78,8 @@ def upgrade_project(self, old_version, new_version, rom, resource_open_r, resour convert_values_to_hex_repr_in_yml_file("map_changes", resource_open_r, resource_open_w, ["Event Flag"], default_flow_style=None) - self.upgrade_project(5, new_version, rom, resource_open_r, resource_open_w, resource_delete) + self.upgrade_project(5, new_version, rom, old_compiled_rom, resource_open_r, resource_open_w, resource_delete) else: - self.upgrade_project(old_version + 1, new_version, rom, resource_open_r, resource_open_w, resource_delete) + self.upgrade_project(old_version + 1, new_version, rom, old_compiled_rom, resource_open_r, resource_open_w, resource_delete) diff --git a/coilsnake/modules/eb/MapModule.py b/coilsnake/modules/eb/MapModule.py index cb181cc7..700e85b5 100644 --- a/coilsnake/modules/eb/MapModule.py +++ b/coilsnake/modules/eb/MapModule.py @@ -204,7 +204,7 @@ def read_from_project(self, resource_open): townmap_x, townmap_y] - def upgrade_project(self, old_version, new_version, rom, resource_open_r, resource_open_w, resource_delete): + def upgrade_project(self, old_version, new_version, rom, old_compiled_rom, resource_open_r, resource_open_w, resource_delete): if old_version == new_version: return elif old_version <= 2: @@ -226,6 +226,6 @@ def upgrade_project(self, old_version, new_version, rom, resource_open_r, resour with resource_open_w("map_sectors", 'yml', True) as f: yaml.dump(data, f, Dumper=yaml.CSafeDumper, default_flow_style=False) - self.upgrade_project(3, new_version, rom, resource_open_r, resource_open_w, resource_delete) + self.upgrade_project(3, new_version, rom, old_compiled_rom, resource_open_r, resource_open_w, resource_delete) else: - self.upgrade_project(old_version + 1, new_version, rom, resource_open_r, resource_open_w, resource_delete) + self.upgrade_project(old_version + 1, new_version, rom, old_compiled_rom, resource_open_r, resource_open_w, resource_delete) diff --git a/coilsnake/modules/eb/MiscTablesModule.py b/coilsnake/modules/eb/MiscTablesModule.py index d922c229..cb5f1f21 100644 --- a/coilsnake/modules/eb/MiscTablesModule.py +++ b/coilsnake/modules/eb/MiscTablesModule.py @@ -56,7 +56,7 @@ def write_to_project(self, resource_open): with resource_open(table.name.lower(), "yml", True) as f: table.to_yml_file(f) - def upgrade_project(self, old_version, new_version, rom, resource_open_r, resource_open_w, resource_delete): + def upgrade_project(self, old_version, new_version, rom, old_compiled_rom, resource_open_r, resource_open_w, resource_delete): if old_version == new_version: return elif old_version == 3: @@ -100,6 +100,7 @@ def upgrade_project(self, old_version, new_version, rom, resource_open_r, resour self.upgrade_project(old_version=old_version + 1, new_version=new_version, rom=rom, + old_compiled_rom=old_compiled_rom, resource_open_r=resource_open_r, resource_open_w=resource_open_w, resource_delete=resource_delete) @@ -127,6 +128,7 @@ def upgrade_project(self, old_version, new_version, rom, resource_open_r, resour self.upgrade_project(old_version=old_version + 1, new_version=new_version, rom=rom, + old_compiled_rom=old_compiled_rom, resource_open_r=resource_open_r, resource_open_w=resource_open_w, resource_delete=resource_delete) @@ -149,8 +151,9 @@ def upgrade_project(self, old_version, new_version, rom, resource_open_r, resour self.upgrade_project(old_version=old_version + 1, new_version=new_version, rom=rom, + old_compiled_rom=old_compiled_rom, resource_open_r=resource_open_r, resource_open_w=resource_open_w, resource_delete=resource_delete) else: - self.upgrade_project(old_version + 1, new_version, rom, resource_open_r, resource_open_w, resource_delete) + self.upgrade_project(old_version + 1, new_version, rom, old_compiled_rom, resource_open_r, resource_open_w, resource_delete) diff --git a/coilsnake/modules/eb/MiscTextModule.py b/coilsnake/modules/eb/MiscTextModule.py index 11fc1399..343bf185 100644 --- a/coilsnake/modules/eb/MiscTextModule.py +++ b/coilsnake/modules/eb/MiscTextModule.py @@ -218,7 +218,7 @@ def write_to_project(self, resource_open): with resource_open("text_misc", "yml", True) as f: yml_dump(self.data, f, default_flow_style=False) - def upgrade_project(self, old_version, new_version, rom, resource_open_r, resource_open_w, resource_delete): + def upgrade_project(self, old_version, new_version, rom, old_compiled_rom, resource_open_r, resource_open_w, resource_delete): if old_version == new_version: return elif old_version == 4: @@ -229,10 +229,10 @@ def upgrade_project(self, old_version, new_version, rom, resource_open_r, resour self.write_to_project(resource_open_w) - self.upgrade_project(old_version + 1, new_version, rom, resource_open_r, resource_open_w, resource_delete) + self.upgrade_project(old_version + 1, new_version, rom, old_compiled_rom, resource_open_r, resource_open_w, resource_delete) elif old_version <= 2: self.read_from_rom(rom) self.write_to_project(resource_open_w) - self.upgrade_project(3, new_version, rom, resource_open_r, resource_open_w, resource_delete) + self.upgrade_project(3, new_version, rom, old_compiled_rom, resource_open_r, resource_open_w, resource_delete) else: - self.upgrade_project(old_version + 1, new_version, rom, resource_open_r, resource_open_w, resource_delete) + self.upgrade_project(old_version + 1, new_version, rom, old_compiled_rom, resource_open_r, resource_open_w, resource_delete) diff --git a/coilsnake/modules/eb/MusicModule.py b/coilsnake/modules/eb/MusicModule.py index 5c416edb..91813390 100644 --- a/coilsnake/modules/eb/MusicModule.py +++ b/coilsnake/modules/eb/MusicModule.py @@ -287,7 +287,7 @@ def get_patch(self, rom): ips.load(get_ips_filename(rom.type, 'gas_station_pack_fix'), 0) return ips - def upgrade_project(self, old_version, new_version, rom, resource_open_r, resource_open_w, resource_delete): + def upgrade_project(self, old_version, new_version, rom, old_compiled_rom, resource_open_r, resource_open_w, resource_delete): if old_version == new_version: return if old_version == 11: @@ -298,6 +298,7 @@ def upgrade_project(self, old_version, new_version, rom, resource_open_r, resour 11 if old_version < 11 else old_version + 1, new_version, rom, + old_compiled_rom, resource_open_r, resource_open_w, resource_delete) diff --git a/coilsnake/modules/eb/SoundStoneModule.py b/coilsnake/modules/eb/SoundStoneModule.py index fc019c68..7165b8d8 100644 --- a/coilsnake/modules/eb/SoundStoneModule.py +++ b/coilsnake/modules/eb/SoundStoneModule.py @@ -80,7 +80,7 @@ def write_to_project(self, resource_open): 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): + def upgrade_project(self, old_version, new_version, rom, old_compiled_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) \ No newline at end of file diff --git a/coilsnake/modules/eb/SpriteGroupModule.py b/coilsnake/modules/eb/SpriteGroupModule.py index 5111b67d..d22cec76 100644 --- a/coilsnake/modules/eb/SpriteGroupModule.py +++ b/coilsnake/modules/eb/SpriteGroupModule.py @@ -120,7 +120,7 @@ def write_to_rom(self, rom): self.group_pointer_table.to_block(block=rom, offset=new_table_offset) self.palette_table.to_block(block=rom, offset=from_snes_address(PALETTE_TABLE_OFFSET)) - def upgrade_project(self, old_version, new_version, rom, resource_open_r, resource_open_w, resource_delete): + def upgrade_project(self, old_version, new_version, rom, old_compiled_rom, resource_open_r, resource_open_w, resource_delete): if old_version == new_version: return elif old_version == 4: @@ -136,7 +136,7 @@ def upgrade_project(self, old_version, new_version, rom, resource_open_r, resour del entry["Collision Settings"] with resource_open_w("sprite_groups", "yml", True) as f: yml_dump(data, f) - self.upgrade_project(old_version + 1, new_version, rom, resource_open_r, resource_open_w, resource_delete) + self.upgrade_project(old_version + 1, new_version, rom, old_compiled_rom, resource_open_r, resource_open_w, resource_delete) elif old_version == 2: replace_field_in_yml(resource_name="sprite_groups", resource_open_r=resource_open_r, @@ -149,6 +149,6 @@ def upgrade_project(self, old_version, new_version, rom, resource_open_r, resour resource_open_w=resource_open_w, key="Unknown B", new_key="Collision Settings") - self.upgrade_project(old_version + 1, new_version, rom, resource_open_r, resource_open_w, resource_delete) + self.upgrade_project(old_version + 1, new_version, rom, old_compiled_rom, resource_open_r, resource_open_w, resource_delete) else: - self.upgrade_project(old_version + 1, new_version, rom, resource_open_r, resource_open_w, resource_delete) + self.upgrade_project(old_version + 1, new_version, rom, old_compiled_rom, resource_open_r, resource_open_w, resource_delete) diff --git a/coilsnake/modules/eb/StaffModule.py b/coilsnake/modules/eb/StaffModule.py index b5c2e567..dc838b6d 100644 --- a/coilsnake/modules/eb/StaffModule.py +++ b/coilsnake/modules/eb/StaffModule.py @@ -266,7 +266,7 @@ def write_to_project(self, resource_open): else: mode = self.print_char(f, byte, mode, invbig, invsmall) - def upgrade_project(self, old_version, new_version, rom, resource_open_r, resource_open_w, resource_delete): + def upgrade_project(self, old_version, new_version, rom, old_compiled_rom, resource_open_r, resource_open_w, resource_delete): if old_version < 10: self.read_from_rom(rom) self.write_to_project(resource_open_w) diff --git a/coilsnake/modules/eb/SwirlModule.py b/coilsnake/modules/eb/SwirlModule.py index 0dbbf4e7..5382b8c0 100644 --- a/coilsnake/modules/eb/SwirlModule.py +++ b/coilsnake/modules/eb/SwirlModule.py @@ -144,7 +144,7 @@ def write_to_project(self, resource_open): with resource_open("Swirls/swirls", "yml", True) as f: yml_dump(swirl_data, f, default_flow_style=False) - def upgrade_project(self, old_version, new_version, rom, resource_open_r, resource_open_w, resource_delete): + def upgrade_project(self, old_version, new_version, rom, old_compiled_rom, resource_open_r, resource_open_w, resource_delete): if old_version == new_version: return elif old_version <= 6: @@ -154,6 +154,7 @@ def upgrade_project(self, old_version, new_version, rom, resource_open_r, resour 7, new_version, rom, + old_compiled_rom, resource_open_r, resource_open_w, resource_delete) @@ -162,6 +163,7 @@ def upgrade_project(self, old_version, new_version, rom, resource_open_r, resour old_version + 1, new_version, rom, + old_compiled_rom, resource_open_r, resource_open_w, resource_delete) \ No newline at end of file diff --git a/coilsnake/modules/eb/TilesetModule.py b/coilsnake/modules/eb/TilesetModule.py index 5d7c0afd..f522b9c0 100644 --- a/coilsnake/modules/eb/TilesetModule.py +++ b/coilsnake/modules/eb/TilesetModule.py @@ -209,7 +209,7 @@ def read_from_project(self, resource_open): entry = yml_rep[map_tileset][palette_id] palette.settings_from_yml_rep(entry) - def upgrade_project(self, old_version, new_version, rom, resource_open_r, resource_open_w, resource_delete): + def upgrade_project(self, old_version, new_version, rom, old_compiled_rom, resource_open_r, resource_open_w, resource_delete): if old_version == new_version: return elif old_version <= 6: @@ -227,7 +227,7 @@ def upgrade_project(self, old_version, new_version, rom, resource_open_r, resour self.write_map_palette_settings(yml_rep, resource_open_w) self.upgrade_project( - 7, new_version, rom, resource_open_r, resource_open_w, resource_delete) + 7, new_version, rom, old_compiled_rom, resource_open_r, resource_open_w, resource_delete) else: self.upgrade_project( - old_version + 1, new_version, rom, resource_open_r, resource_open_w, resource_delete) + old_version + 1, new_version, rom, old_compiled_rom, resource_open_r, resource_open_w, resource_delete) diff --git a/coilsnake/modules/eb/TitleScreenModule.py b/coilsnake/modules/eb/TitleScreenModule.py index bdd64bda..697aa53c 100644 --- a/coilsnake/modules/eb/TitleScreenModule.py +++ b/coilsnake/modules/eb/TitleScreenModule.py @@ -599,8 +599,8 @@ def write_chars_data_to_project(self, resource_open): yml_dump(chars_positions, f, False) def upgrade_project( - self, old_version, new_version, rom, resource_open_r, - resource_open_w, resource_delete): + self, old_version, new_version, rom, old_compiled_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) diff --git a/coilsnake/modules/eb/TownMapIconModule.py b/coilsnake/modules/eb/TownMapIconModule.py index 8f03b027..2467a30e 100644 --- a/coilsnake/modules/eb/TownMapIconModule.py +++ b/coilsnake/modules/eb/TownMapIconModule.py @@ -37,7 +37,7 @@ def write_to_project(self, resource_open): with resource_open("TownMaps/icon_positions", "yml", True) as f: self.table.to_yml_file(f) - def upgrade_project(self, old_version, new_version, rom, resource_open_r, resource_open_w, resource_delete): + def upgrade_project(self, old_version, new_version, rom, old_compiled_rom, resource_open_r, resource_open_w, resource_delete): if old_version == new_version: return elif old_version == 4: @@ -54,10 +54,10 @@ def upgrade_project(self, old_version, new_version, rom, resource_open_r, resour convert_values_to_hex_repr_in_yml_file("TownMaps/icon_positions", resource_open_r, resource_open_w, ["Event Flag"]) - self.upgrade_project(5, new_version, rom, resource_open_r, resource_open_w, resource_delete) + self.upgrade_project(5, new_version, rom, old_compiled_rom, resource_open_r, resource_open_w, resource_delete) elif old_version <= 2: self.read_from_rom(rom) self.write_to_project(resource_open_w) - self.upgrade_project(new_version, new_version, rom, resource_open_r, resource_open_w, resource_delete) + self.upgrade_project(new_version, new_version, rom, old_compiled_rom, resource_open_r, resource_open_w, resource_delete) else: - self.upgrade_project(old_version + 1, new_version, rom, resource_open_r, resource_open_w, resource_delete) + self.upgrade_project(old_version + 1, new_version, rom, old_compiled_rom, resource_open_r, resource_open_w, resource_delete) diff --git a/coilsnake/ui/cli.py b/coilsnake/ui/cli.py index fea1659c..8b5d910d 100755 --- a/coilsnake/ui/cli.py +++ b/coilsnake/ui/cli.py @@ -33,6 +33,7 @@ def main(): help="upgrade a project which was created by an older version of CoilSnake") upgrade_parser.add_argument("base_rom") upgrade_parser.add_argument("project_directory") + upgrade_parser.add_argument("--existing_rom", "-e", required=False, help="a compiled ROM of this project, if you have one. Otherwise uses the base ROM") upgrade_parser.set_defaults(func=_upgrade) decomp_script_parser = subparsers.add_parser("scriptdump", help="Decompile a ROM's script to an already existing project.") @@ -97,7 +98,9 @@ def _decompile(args): def _upgrade(args): + existing_rom = args.existing_rom if args.existing_rom else args.base_rom upgrade_project(base_rom_filename=args.base_rom, + old_compiled_rom_filename=existing_rom, project_path=args.project_directory) def _scriptdump(args): diff --git a/coilsnake/ui/common.py b/coilsnake/ui/common.py index e7b78c56..f69c0f60 100644 --- a/coilsnake/ui/common.py +++ b/coilsnake/ui/common.py @@ -40,11 +40,13 @@ def setup_logging(quiet=False, verbose=False, stream=None): logging.root.addHandler(handler) -def upgrade_project(project_path, base_rom_filename, progress_bar=None): +def upgrade_project(project_path, base_rom_filename, old_compiled_rom_filename, progress_bar=None): if not os.path.isdir(project_path): raise RuntimeError("Project directory \"" + project_path + "\" is not a directory.") if not os.path.isfile(base_rom_filename): raise RuntimeError("Base Rom \"" + base_rom_filename + "\" is not a file.") + if not os.path.isfile(old_compiled_rom_filename): + raise RuntimeError("Old compiled Rom \"" + old_compiled_rom_filename + "\" is not a file.") modules = load_modules() @@ -67,6 +69,10 @@ def upgrade_project(project_path, base_rom_filename, progress_bar=None): rom = Rom() rom.from_file(base_rom_filename) check_if_types_match(project=project, rom=rom) + + old_rom = Rom() + old_rom.from_file(old_compiled_rom_filename) + check_if_types_match(project=project, rom=old_rom) compatible_modules = [(name, clazz) for name, clazz in modules if clazz.is_compatible_with_romtype(rom.type)] tick_amount = 1.0/len(compatible_modules) @@ -75,7 +81,7 @@ def upgrade_project(project_path, base_rom_filename, progress_bar=None): log.info("Upgrading {}...".format(module_class.NAME)) start_time = time.time() with module_class() as module: - module.upgrade_project(project.version, FORMAT_VERSION, rom, + module.upgrade_project(project.version, FORMAT_VERSION, rom, old_rom, lambda x, y, astext=False : project.get_resource(module_name, x, y, 'rt' if astext else 'rb', 'utf-8' if astext else None), lambda x, y, astext=False: project.get_resource(module_name, x, y, diff --git a/coilsnake/ui/gui.py b/coilsnake/ui/gui.py index 319c18b5..766c6e59 100644 --- a/coilsnake/ui/gui.py +++ b/coilsnake/ui/gui.py @@ -25,7 +25,7 @@ patch_rom, create_patch from coilsnake.ui.gui_preferences import CoilSnakePreferences from coilsnake.ui.gui_util import browse_for_patch, browse_for_rom, browse_for_project, open_folder, set_entry_text, \ - find_system_java_exe + find_system_java_exe, ROM_FILETYPES from coilsnake.ui.information import coilsnake_about from coilsnake.ui.widgets import ThreadSafeConsole, CoilSnakeGuiProgressBar from coilsnake.util.common.project import PROJECT_FILENAME @@ -323,6 +323,27 @@ def do_upgrade(self, rom_entry, project_entry): icon='warning') if confirm != "yes": return + + has_old = tkinter.messagebox.askyesnocancel("Do you have expanded battle animations?", + "Does this project have expanded battle animations via Jeffman's PSI Animation Editor?\n\n" + + "If it does, select \"Yes\" and then provide the ROM that was produced after compiling.\n" + + "After the upgrade, remove the relevant entries in used_ranges.yml and delete the CCScript file that repoints the data.\n\n" + + "If you don't have expanded animations, or don't mind losing them in the upgrade, select \"No\".") + if has_old is True: + old_rom = tkinter.filedialog.askopenfilename(parent=self.root, + initialdir=os.path.expanduser("~"), + title="Select existing compiled ROM", + filetypes=ROM_FILETYPES) + if not old_rom: + # User cancalled the file picker + return + elif has_old is False: + # Use the same as the base ROM if the user selects "No" + old_rom = rom + else: + # User selected "Cancel" when prompted for a modified base ROM + return + self.save_default_tab() @@ -331,12 +352,12 @@ def do_upgrade(self, rom_entry, project_entry): self.disable_all_components() self.progress_bar.clear() - thread = Thread(target=self._do_upgrade_help, args=(rom, project)) + thread = Thread(target=self._do_upgrade_help, args=(rom, old_rom, project)) thread.start() - def _do_upgrade_help(self, rom, project): + def _do_upgrade_help(self, rom, old_rom, project): try: - upgrade_project(project_path=project, base_rom_filename=rom, progress_bar=self.progress_bar) + upgrade_project(project_path=project, base_rom_filename=rom, old_compiled_rom_filename=old_rom, progress_bar=self.progress_bar) except Exception as inst: log.debug(format_exc()) log.error(inst) @@ -713,6 +734,7 @@ def upgrade_tmp(): set_entry_text(entry=rom_entry, text=self.preferences["default upgrade rom"]) + if self.preferences["default upgrade project"]: set_entry_text(entry=project_entry, text=self.preferences["default upgrade project"]) diff --git a/coilsnake/util/eb/pointer.py b/coilsnake/util/eb/pointer.py index c0d11e8a..b9f91b31 100644 --- a/coilsnake/util/eb/pointer.py +++ b/coilsnake/util/eb/pointer.py @@ -40,10 +40,10 @@ def write_xl_pointer(block, offset, pointer): class AsmPointerReference(object): POINTER_FORMAT = re.compile( - rb'''[\xa9\xa2\xa0].. # Match LDA_i / LDX_i / LDY_i - [\x85\x86\x84](.) # Match STA_d / STX_d / STY_d - [\xa9\xa2\xa0].. # Match LDA_i / LDX_i / LDY_i - [\x85\x86\x84](.) # Match STA_d / STX_d / STY_d + rb'''[\xa9\xa2\xa0].. # Match LDA_i / LDX_i / LDY_i + [\x85\x86\x84](.) # Match STA_d / STX_d / STY_d + [\xa9\xa2\xa0].\x00 # Match LDA_i / LDX_i / LDY_i with upper byte zero + [\x85\x86\x84](.) # Match STA_d / STX_d / STY_d ''', re.VERBOSE | re.DOTALL) def __init__(self, offset): @@ -67,6 +67,13 @@ def validate_structure(self, rom): def write(self, rom, address): log.info("Writing pointer at " + hex(self.offset)) write_asm_pointer(rom, self.offset, address) + + def read(self, rom): + # Get the address from the code at this point + if self.validate_structure(rom): + return (rom.read_multi(self.offset+6, 2) << 16) | rom.read_multi(self.offset+1, 2) + else: + return None class XlPointerReference(object): def __init__(self, offset): @@ -80,3 +87,10 @@ def validate_structure(self, rom): def write(self, rom, address): log.info("Writing xl pointer at " + hex(self.offset)) write_xl_pointer(rom, self.offset, address) + + def read(self, rom): + # Get the address from the code at this point + if self.validate_structure(rom): + return rom.read_multi(self.offset+1, 3) + else: + return None