def delete_resource(self, module_name, resource_name): if module_name not in self._resources: raise CoilSnakeError("No such module {}".format(module_name)) if resource_name not in self._resources[module_name]: raise CoilSnakeError("No such resource {} in module {}".format(resource_name, module_name)) fname = os.path.join(self._dir_name, self._resources[module_name][resource_name]) if os.path.isfile(fname): os.remove(fname) del self._resources[module_name][resource_name]
def decompile_script(rom_filename, project_path, 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(rom_filename): raise RuntimeError("Rom \"" + rom_filename + "\" is not a file.") rom = Rom() rom.from_file(rom_filename) if rom.type != "Earthbound": raise CoilSnakeError( "Cannot decompile script of a non-Earthbound rom. A {} rom was supplied." .format(rom.type)) del rom project_ccscript_path = os.path.join(project_path, "ccscript") start_time = time.time() rom_file = open(rom_filename, "rb") try: ccsw = CCScriptWriter(rom_file, project_ccscript_path, False) ccsw.loadDialogue(True) ccsw.processDialogue() ccsw.outputDialogue(True) except Exception as inst: log.exception("Error") else: log.info("Decompiled script to {} in {:.2f}s".format( project_path, time.time() - start_time)) finally: rom_file.close()
def read_from_project(self, resource_open): with resource_open("sprite_group_palettes", "yml") as f: self.palette_table.from_yml_file(f) with resource_open("sprite_groups", "yml") as f: input = yml_load(f) num_groups = len(input) self.groups = [] for i in range(num_groups): group = SpriteGroup(16) group.from_yml_rep(input[i]) palette = EbPalette(1, 16) with resource_open("SpriteGroups/" + str(i).zfill(3), "png") as f2: image = open_indexed_image(f2) group.from_image(image) palette.from_image(image) del image self.groups.append(group) # Assign the palette number to the sprite for j in range(8): if palette.list()[3:] == self.palette_table[j][0].list( )[3:]: group.palette = j break else: raise CoilSnakeError("Sprite Group #" + str(i).zfill(3) + " uses an invalid palette")
def write_to_rom(self, rom): for ips_desc_filename in [s for s in os.listdir(get_ips_directory(rom.type)) if s.lower().endswith(".yml")]: patch_name = ips_desc_filename[:-4] with open(os.path.join(get_ips_directory(rom.type), ips_desc_filename)) as ips_desc_file: ips_desc = yml_load(ips_desc_file) if "Hidden" in ips_desc and ips_desc["Hidden"]: continue elif (ips_desc["Title"] in self.patches) and (self.patches[ips_desc["Title"]].lower() == "enabled"): # First, check that we can apply this ranges = map(lambda y: tuple(map(lambda z: int(z, 0), y[1:-1].split(','))), ips_desc["Ranges"]) for range in ranges: if not rom.is_unallocated(range): raise CoilSnakeError( "Can't apply patch \"{}\" because range ({:#x},{:#x}) is not unallocated".format( ips_desc["Title"], range[0], range[1])) # Now apply the patch ips = IpsPatch() offset = 0 if "Header" in ips_desc: offset = ips_desc["Header"] ips.load(get_ips_filename(rom.type, patch_name), offset) ips.apply(rom) # Mark the used ranges as used for range in ranges: rom.mark_allocated(range)
def _clean(self): """If this is a clean version of one of the variants of the EarthBound ROM, patch it so that it becomes a clean version of the reference ROM. """ # Truncate the ROM if it is has been expanded if len(self) > 0x300000: # Change the ExHiROM bytes in case this is an ExHiROM self[0x00ffd5] = 0x31 self[0x00ffd7] = 0x0c # Truncate the data self.size = 0x300000 self.data = self.data[:0x300000] # Ensure the ROM isn't too small elif len(self) < 0x300000: raise CoilSnakeError("Not a valid clean EarthBound ROM.") # Check if this ROM is already a reference ROM hash = self._calc_hash() if hash == self.REFERENCE_MD5: return # Try to fix the ROM with a patch if it is one of the known alternatives try: patch_filename = self.ALT_MD5[hash] except KeyError: pass # Unknown variant else: patch = IpsPatch() patch.load( os.path.join(ASSET_PATH, "rom-fixes", "Earthbound", patch_filename)) patch.apply(self) self._setup_rom_post_load() return # As a last attempt, try to set the last byte to 0x0, since LunarIPS # likes to add 0xff at the end self[-1] = 0x0 if self._calc_hash() == self.REFERENCE_MD5: return raise CoilSnakeError("Not a valid clean EarthBound ROM.")
def load(self, filename, global_offset=0): self.last_offset_used = 0 try: with open(filename, 'rb') as ips: ips.seek(0) if ips.read(5) != b'PATCH': raise CoilSnakeError("Not an IPS file: " + filename) # Read in the records while True: offset = ips.read(3) if offset == b'EOF': break offset_int = offset[0] << 16 offset_int |= offset[1] << 8 offset_int |= offset[2] offset_int -= global_offset size = ips.read(1)[0] << 8 size |= ips.read(1)[0] if size == 0: # RLE data rle_size = ips.read(1)[0] << 8 rle_size |= ips.read(1)[0] value = ips.read(1)[0] if offset_int >= 0: # This happens if we're trying to write to before the global_offset. # IE: If the IPS was writing to the header self.instructions.append( (b"RLE", (offset_int, rle_size, value))) self.last_offset_used = max( self.last_offset_used, offset_int + rle_size - 1) else: # Record data data = array('B', ips.read(size)) if offset_int >= 0: # This happens if we're trying to write to before the global_offset. # IE: If the IPS was writing to the header self.instructions.append( (b"RECORD", (offset_int, size, data))) self.last_offset_used = max( self.last_offset_used, offset_int + size - 1) except Exception as e: raise CoilSnakeError("Not a valid IPS file: " + filename) from e
def print_keyword(self, f, byte): for item in KEYWORD_BYTE_HEIGHT.items(): keyword, v = item b, h = v if byte == b: f.write('{}\n'.format(keyword)) return MODE_CONTROL else: raise CoilSnakeError('Unknown control byte 0x{:X}'.format(byte))
def apply(self, rom): if self.last_offset_used >= rom.size: raise CoilSnakeError("Your ROM must be expanded such that it is at least {size} ({size:#x}) bytes long" .format(size=self.last_offset_used + 1)) for instruction, args in self.instructions: if instruction == 'RLE': offset, size, value = args for i in range(offset, offset + size): rom[i] = value elif instruction == 'RECORD': offset, size, data = args rom[offset:offset + size] = data
def patch_rom(clean_rom_filename, patched_rom_filename, patch_filename, headered, progress_bar=None): if not os.path.isfile(clean_rom_filename): raise RuntimeError("Clean Rom \"" + clean_rom_filename + "\" is not a file.") if not os.path.isfile(patch_filename): raise RuntimeError("Patch \"" + patch_filename + "\" is not a file.") if clean_rom_filename != patched_rom_filename: copyfile(clean_rom_filename, patched_rom_filename) log.info("Patching ROM {} with patch {}".format(patched_rom_filename, patch_filename)) patching_start_time = time.time() if patch_filename.endswith(".ips"): output_rom = Rom() output_rom.from_file(clean_rom_filename) patch = IpsPatch() elif patch_filename.endswith(".ebp"): output_rom = EbRom() output_rom.from_file(clean_rom_filename) patch = EbpPatch() else: raise CoilSnakeError("Unknown patch format.") # Load the patch and expand the ROM as needed add_header = headered and not isinstance(patch, EbpPatch) extra = int(add_header)*0x200 # 0x200 if a header will be added, 0 otherwise patch.load(patch_filename) if isinstance(patch, EbpPatch): log.info("Patch: {title} by {author}".format(**patch.metadata)) if patch.last_offset_used > len(output_rom) + extra: if patch.last_offset_used < 0x400000 + extra: output_rom.expand(0x400000) elif patch.last_offset_used < 0x600000 + extra: output_rom.expand(0x600000) else: output_rom.expand(patch.last_offset_used) # If the user specified the patch was made for a headered ROM, add a header # to the ROM if add_header: output_rom.add_header() # Apply the patch and write out the patched ROM patch.apply(output_rom) if add_header: # Remove the header that was added, so that we're always dealing with # unheadered ROMs in the end output_rom.data = output_rom.data[0x200:] output_rom.size -= 0x200 output_rom.to_file(patched_rom_filename) log.info("Patched to {} in {:.2f}s".format(patched_rom_filename, time.time() - patching_start_time))
def create_patch(clean_rom, hacked_rom, patch_path, author, description, title, progress_bar=None): """Starts creating the patch in its own thread.""" creating_patch_start_time = time.time() # Prepare the metadata. metadata = json.dumps({ "patcher": "EBPatcher", "author": author, "title": title, "description": description }) # Try to create the patch; if it fails, display an error message. try: if patch_path.endswith(".ebp"): log.info("Creating EBP patch by " + author + " with description \"" + description + "\" called " + title + "...") patch = EbpPatch() patch.create(clean_rom, hacked_rom, patch_path, metadata) elif patch_path.endswith(".ips"): log.info("Creating IPS patch...") patch = IpsPatch() patch.create(clean_rom, hacked_rom, patch_path) else: raise CoilSnakeError("Unknown patch format.") except OSError as e: log.info("There was an error creating the patch: " + e) return # Display a success message. patch_name = "" if patch_path.rfind("/") != -1: patch_name = patch_path[patch_path.rfind("/") + 1:len(patch_path)] else: patch_name = patch_path[patch_path.rfind("\\") + 1:len(patch_path)] log.info("The patch {} was successfully created in {:.2f}s.".format( patch_name, time.time() - creating_patch_start_time))
def load(self, filename): try: self.patch.load(filename) with open(filename, 'rb') as ebp: # Skip to the end of the IPS part of the patch ebp.seek(5 + sum([ 3 + 2 + i[1][1] + (2 if i[0] == 'RLE' else 0) for i in self.patch.instructions ]) + 3) # Load the metadata, if available try: self.metadata = json.loads(ebp.read().decode("utf-8")) except ValueError: self.metadata = None except: raise CoilSnakeError("Not a valid EBP file: " + filename)
def open_indexed_image(f): image = open_image(f) if image.mode != 'P': raise CoilSnakeError("Image does not use an indexed palette: {}".format(f.name)) return image
def check_if_types_match(project, rom): if rom.type != project.romtype: raise CoilSnakeError( "Rom type {} does not match Project type {}".format( rom.type, project.romtype))
def check_if_project_too_old(project): if project.version < FORMAT_VERSION: raise CoilSnakeError( "This project must be upgraded before performing this operation.")
def check_if_project_too_new(project): if project.version > FORMAT_VERSION: raise CoilSnakeError( "This project is not compatible with this version of CoilSnake. Please use this project" + " with a newer version of CoilSnake.")
def create(self, clean_rom, hacked_rom, patch_path): """Creates an IPS patch from the source and target ROMs.""" with Rom() as cr, Rom() as hr: cr.from_file(clean_rom) hr.from_file(hacked_rom) if cr.__len__() > hr.__len__(): if hr.__len__() <= 0x400000 and hr.__len__() > 0x300000: raise CoilSnakeError( "Clean ROM greater in size than hacked ROM. Please use a 3 Megabyte or 4 Megabyte clean ROM." ) if hr.__len__() <= 0x300000: raise CoilSnakeError( "Clean ROM greater in size than hacked ROM. Please use a 3 Megabyte clean ROM." ) # Expand clean ROM as necessary. if cr.__len__() < hr.__len__(): if hr.__len__() == 0x400000: cr.expand(0x400000) elif hr.__len__() == 0x600000: cr.expand(0x600000) else: cr.expand(patch.last_offset_used) # Create the records. i = None records = {} index = 0 # Get the first byte of each ROM so that the loop works correctly. s = cr.__getitem__(index).to_bytes(1, byteorder='big') t = hr.__getitem__(index).to_bytes(1, byteorder='big') index += 1 while index <= cr.__len__() and index <= hr.__len__(): if t == s and i is not None: i = None elif t != s: if i is not None: # Check that the record's size can fit in 2 bytes. if index - 1 - i == 0xFFFF: i = None continue records[i] += t else: i = index - 1 # Check that the offset isn't EOF. If it is, go back one # byte to work around this IPS limitation. if i.to_bytes(3, byteorder='big') != b"EOF": records[i] = t else: i -= 1 records[i] = hr.to_list()[i] if index < cr.__len__() and index < hr.__len__(): s = cr.__getitem__(index).to_bytes(1, byteorder='big') t = hr.__getitem__(index).to_bytes(1, byteorder='big') index += 1 # Write the patch. with open(patch_path, "wb") as pfile: pfile.seek(0) pfile.write(b"PATCH") for r in sorted(records): pfile.write(r.to_bytes(3, byteorder='big')) pfile.write(len(records[r]).to_bytes(2, byteorder='big')) pfile.write(records[r]) pfile.write(b"EOF") pfile.close()