Example #1
0
    def __init__(self, rom: NintendoDSRom, config: Pmd2Data, skip_core_patches: bool = False):
        self._rom = rom
        self._config = config
        self._loaded_patches: Dict[str, AbstractPatchHandler] = {}
        # Path to the directories, which contain the ASM files for the handlers.
        self._patch_dirs: Dict[str, str] = {}

        self._arm_patcher = ArmPatcher(self._rom)
        self._created_tmpdirs: List[TemporaryDirectory[Any]] = []

        if not skip_core_patches:
            # Load core patches
            for handler_type in PatchType:
                self.add_manually(handler_type.value(), CORE_PATCHES_BASE_DIR)
Example #2
0
class Patcher:
    def __init__(self,
                 rom: NintendoDSRom,
                 config: Pmd2Data,
                 skip_core_patches=False):
        self._rom = rom
        self._config = config
        self._loaded_patches: Dict[str, AbstractPatchHandler] = {}
        # Path to the directories, which contain the ASM files for the handlers.
        self._patch_dirs: Dict[str, str] = {}

        self._arm_patcher = ArmPatcher(self._rom)
        self._created_tmpdirs: List[TemporaryDirectory] = []

        if not skip_core_patches:
            # Load core patches
            for handler_type in PatchType:
                self.add_manually(handler_type.value(), CORE_PATCHES_BASE_DIR)

    def __del__(self):
        for tmpdir in self._created_tmpdirs:
            tmpdir.cleanup()

    def is_applied(self, name: str):
        if name not in self._loaded_patches:
            raise ValueError(f"The patch '{name}' was not found.")
        return self._loaded_patches[name].is_applied(self._rom, self._config)

    def apply(self, name: str):
        if name not in self._loaded_patches:
            raise ValueError(f"The patch '{name}' was not found.")
        self._loaded_patches[name].apply(partial(self._apply_armips, name),
                                         self._rom, self._config)

    def _apply_armips(self, name: str):
        patch = self._config.asm_patches_constants.patches[name]
        patch_dir_for_version = self._config.asm_patches_constants.patch_dir.filepath
        stub_path_for_version = self._config.asm_patches_constants.patch_dir.stubpath
        self._arm_patcher.apply(
            patch, self._config.binaries,
            os.path.join(self._patch_dirs[name], patch_dir_for_version),
            stub_path_for_version, self._config.game_edition)

    def add_pkg(self, zip_path: str):
        """Loads a skypatch file. Raises PatchPackageError on error."""
        tmpdir = TemporaryDirectory()
        self._created_tmpdirs.append(tmpdir)
        with ZipFile(zip_path, 'r') as zip:
            zip.extractall(tmpdir.name)
            zip_id = id(zip)

        # Load the configuration
        try:
            config_xml = os.path.join(tmpdir.name, 'config.xml')
            PatchPackageConfigMerger(config_xml,
                                     self._config.game_edition).merge(
                                         self._config.asm_patches_constants)
        except FileNotFoundError as ex:
            raise PatchPackageError(
                "config.xml missing in patch package.") from ex
        except ParseError as ex:
            raise PatchPackageError(
                "Syntax error in the config.xml while reading patch package."
            ) from ex

        # Evalulate the module
        try:
            module_name = f"skytemple_files.__patches.p{zip_id}"
            spec = importlib.util.spec_from_file_location(
                module_name, os.path.join(tmpdir.name, 'patch.py'))
            patch = importlib.util.module_from_spec(spec)
            spec.loader.exec_module(patch)
        except FileNotFoundError as ex:
            raise PatchPackageError(
                "patch.py missing in patch package.") from ex
        except SyntaxError as ex:
            raise PatchPackageError(
                "The patch.py of the patch package contains a syntay error."
            ) from ex

        try:
            handler = patch.PatchHandler()
        except AttributeError as ex:
            raise PatchPackageError(
                "The patch.py of the patch package does not contain a 'PatchHandler'."
            ) from ex

        try:
            self.add_manually(handler, tmpdir.name)
        except ValueError as ex:
            raise PatchPackageError(
                "The patch package does not contain an entry for the handler's patch name "
                "in the config.xml.") from ex

    def add_manually(self, handler: AbstractPatchHandler, patch_base_dir: str):
        # Try to find the patch in the config
        if handler.name not in self._config.asm_patches_constants.patches.keys(
        ):
            raise ValueError(
                f"No patch for handler '{handler.name}' found in the configuration."
            )
        self._loaded_patches[handler.name] = handler
        self._patch_dirs[handler.name] = os.path.realpath(patch_base_dir)

    def list(self) -> Generator[AbstractPatchHandler, None, None]:
        for handler in self._loaded_patches.values():
            yield handler
Example #3
0
class Patcher:
    def __init__(self, rom: NintendoDSRom, config: Pmd2Data, skip_core_patches: bool = False):
        self._rom = rom
        self._config = config
        self._loaded_patches: Dict[str, AbstractPatchHandler] = {}
        # Path to the directories, which contain the ASM files for the handlers.
        self._patch_dirs: Dict[str, str] = {}

        self._arm_patcher = ArmPatcher(self._rom)
        self._created_tmpdirs: List[TemporaryDirectory[Any]] = []

        if not skip_core_patches:
            # Load core patches
            for handler_type in PatchType:
                self.add_manually(handler_type.value(), CORE_PATCHES_BASE_DIR)

    def __del__(self) -> None:
        for tmpdir in self._created_tmpdirs:
            tmpdir.cleanup()

    def is_applied(self, name: str) -> bool:
        if name not in self._loaded_patches:
            raise ValueError(f(_("The patch '{name}' was not found.")))
        return self._loaded_patches[name].is_applied(self._rom, self._config)

    def apply(self, name: str, config: Optional[Dict[str, Any]] = None) -> None:
        """
        Apply a patch.
        If the patch requires parameters, values for ALL of them must be in the dict `config` (even if default values
        are specified in the XML config).
        """
        if name not in self._loaded_patches:
            raise ValueError(f(_("The patch '{name}' was not found.")))
        patch = self._loaded_patches[name]
        if isinstance(patch, DependantPatch):
            for patch_name in patch.depends_on():
                try:
                    if not self.is_applied(patch_name):
                        raise PatchDependencyError(f(_("The patch '{patch_name}' needs to be applied before you can "
                                                       "apply '{name}'.")))
                except ValueError as err:
                    raise PatchDependencyError(f(_("The patch '{patch_name}' needs to be applied before you can "
                                                   "apply '{name}'. "
                                                   "This patch could not be found."))) from err
        # Check config
        patch_data = self._config.asm_patches_constants.patches[name]
        if patch_data.has_parameters():
            if config is None:
                raise PatchNotConfiguredError(_("No configuration was given."), "*", "No configuration was given.")
            for param in patch_data.parameters.values():
                if param.name not in config:
                    raise PatchNotConfiguredError(_("Missing configuration value."), param.name, "Not given.")
                if param.type == Pmd2PatchParameterType.INTEGER:
                    val = config[param.name]
                    if not isinstance(val, int):
                        raise PatchNotConfiguredError(_("Invalid configuration value."), param.name, "Must be int.")
                    if param.min is not None and val < param.min:
                        raise PatchNotConfiguredError(_("Invalid configuration value."), param.name, _("Must be >= {}.").format(param.min))
                    if param.max is not None and val > param.max:
                        raise PatchNotConfiguredError(_("Invalid configuration value."), param.name, _("Must be <= {}.").format(param.max))
                if param.type == Pmd2PatchParameterType.STRING:
                    val = config[param.name]
                    if not isinstance(val, str):
                        raise PatchNotConfiguredError(_("Invalid configuration value."), param.name, "Must be str.")
                if param.type == Pmd2PatchParameterType.SELECT:
                    val = config[param.name]
                    found = False
                    for option in param.options:  # type: ignore
                        if not isinstance(val, type(option.value)) or option.value != val:
                            continue
                        found = True
                        break
                    if not found:
                        raise PatchNotConfiguredError(_("Invalid configuration value."), param.name, "Must be one of the options.")
            patch.supply_parameters(config)
        patch.apply(
            partial(self._apply_armips, name, patch),
            self._rom, self._config
        )

    def _apply_armips(self, name: str, calling_patch: AbstractPatchHandler) -> None:
        patch = self._config.asm_patches_constants.patches[name]
        patch_dir_for_version = self._config.asm_patches_constants.patch_dir.filepath
        stub_path_for_version = self._config.asm_patches_constants.patch_dir.stubpath
        parameter_values = calling_patch.get_parameters()

        self._arm_patcher.apply(patch, self._config.binaries,
                                os.path.join(self._patch_dirs[name], patch_dir_for_version),
                                stub_path_for_version, self._config.game_edition, parameter_values)

    def add_pkg(self, path: str, is_zipped: bool = True) -> None:
        """Loads a skypatch file. Raises PatchPackageError on error."""
        tmpdir = TemporaryDirectory()
        self._created_tmpdirs.append(tmpdir)
        if is_zipped:
            with ZipFile(path, 'r') as zip:
                zip.extractall(tmpdir.name)
                f_id = id(zip)
        else:
            shutil.copytree(os.path.join(path, '.'), os.path.join(tmpdir.name, '.'), dirs_exist_ok=True)
            f_id = id(tmpdir)

        # Load the configuration
        try:
            config_xml = os.path.join(tmpdir.name, 'config.xml')
            PatchPackageConfigMerger(config_xml, self._config.game_edition).merge(self._config.asm_patches_constants)
        except FileNotFoundError as ex:
            raise PatchPackageError(_("config.xml missing in patch package.")) from ex
        except ParseError as ex:
            raise PatchPackageError(_("Syntax error in the config.xml while reading patch package.")) from ex

        # Evalulate the module
        try:
            module_name = f"skytemple_files.__patches.p{f_id}"
            spec = importlib.util.spec_from_file_location(module_name,
                                                          os.path.join(tmpdir.name, 'patch.py'))
            assert spec is not None
            patch = importlib.util.module_from_spec(spec)
            spec.loader.exec_module(patch)  # type: ignore
        except FileNotFoundError as ex:
            raise PatchPackageError(_("patch.py missing in patch package.")) from ex
        except SyntaxError as ex:
            raise PatchPackageError(_("The patch.py of the patch package contains a syntax error.")) from ex

        try:
            handler = patch.PatchHandler()  # type: ignore
        except AttributeError as ex:
            raise PatchPackageError(_("The patch.py of the patch package does not contain a 'PatchHandler'.")) from ex

        try:
            self.add_manually(handler, tmpdir.name)
        except ValueError as ex:
            raise PatchPackageError(_("The patch package does not contain an entry for the handler's patch name "
                                      "in the config.xml.")) from ex

    def add_manually(self, handler: AbstractPatchHandler, patch_base_dir: str) -> None:
        # Try to find the patch in the config
        if handler.name not in self._config.asm_patches_constants.patches.keys():
            raise ValueError(f(_("No patch for handler '{handler.name}' found in the configuration.")))
        self._loaded_patches[handler.name] = handler
        self._patch_dirs[handler.name] = patch_base_dir

    def list(self) -> Generator[AbstractPatchHandler, None, None]:
        for handler in self._loaded_patches.values():
            yield handler

    def get(self, name: str) -> AbstractPatchHandler:
        return self._loaded_patches[name]