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)
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
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]