Exemplo n.º 1
0
 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]
Exemplo n.º 2
0
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()
Exemplo n.º 3
0
    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")
Exemplo n.º 4
0
    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)
Exemplo n.º 5
0
    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.")
Exemplo n.º 6
0
    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
Exemplo n.º 7
0
    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))
Exemplo n.º 8
0
 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
Exemplo n.º 9
0
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))
Exemplo n.º 10
0
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))
Exemplo n.º 11
0
    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)
Exemplo n.º 12
0
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
Exemplo n.º 13
0
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))
Exemplo n.º 14
0
def check_if_project_too_old(project):
    if project.version < FORMAT_VERSION:
        raise CoilSnakeError(
            "This project must be upgraded before performing this operation.")
Exemplo n.º 15
0
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.")
Exemplo n.º 16
0
    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()