def assemble(self, patch_asm: str): with tempfile.TemporaryDirectory() as tmp: shutil.copytree(os.path.join(get_resources_dir(), 'patches', 'asm_patches', 'eos_move_effects'), tmp, dirs_exist_ok=True, symlinks=True) set_rw_permission_folder(tmp) # Write final asm file with open_utf8(os.path.join(tmp, ASM_ENTRYPOINT_FN), 'w') as file: file.write(patch_asm) # Run armips try: prefix = "" # Under Windows, try to load from SkyTemple _resources dir first. if sys.platform.startswith('win') and os.path.exists( os.path.join(get_resources_dir(), 'armips.exe')): prefix = os.path.join(get_resources_dir(), '') exec_name = os.getenv('SKYTEMPLE_ARMIPS_EXEC', f'{prefix}armips') result = subprocess.Popen([exec_name, ASM_ENTRYPOINT_FN], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, cwd=tmp) retcode = result.wait() except FileNotFoundError as ex: raise make_user_err( ArmipsNotInstalledError, _("ARMIPS could not be found. Make sure, that " "'armips' is inside your system's PATH.")) from ex if retcode != 0: raise make_user_err( PatchError, _("ARMIPS reported an error while applying the patch."), str(result.stdout.read(), 'utf-8'), str(result.stderr.read(), 'utf-8') # type: ignore if result.stderr else '') out_bin_path = os.path.join(tmp, OUT_BIN) if not os.path.exists(out_bin_path): raise ValueError( f(_("The armips source file did not create a {OUT_BIN}."))) with open(out_bin_path, 'rb') as file: return file.read()
def load_default(cls, for_version='EoS_EU') -> Pmd2Data: """ Load the default pmd2data.xml, patched with the skytemple.xml and create a Pmd2Data object for the version passed. """ res_dir = os.path.join(get_resources_dir(), 'ppmdu_config') return Pmd2XmlReader([ os.path.join(res_dir, 'pmd2data.xml'), os.path.join(res_dir, 'skytemple.xml') ], for_version).parse()
def load_binaries(edition: str) -> List[Pmd2Binary]: version, region = edition.split("_") if version != GAME_VERSION_EOS: raise ValueError("This game version is not supported.") binaries: Dict[str, _IncompleteBinary] = {} files = glob(os.path.join(SYMBOLS_DIR, '*.yml')) # Make sure the arm and overlay files are read this: These are the main files. # They will contain the address, length and description. files.sort(key=lambda key: -1 if key.startswith('arm') or key.startswith('overlay') else 1) for yml_path in files: with open_utf8(yml_path, 'r') as f: _read(binaries, yaml.safe_load(f), region) with open_utf8( os.path.join(get_resources_dir(), "skytemple_pmdsky_debug_symbols.yml"), 'r') as f: _read(binaries, yaml.safe_load(f), region) return _build(binaries)
import importlib.util from ndspy.rom import NintendoDSRom from skytemple_files.common.ppmdu_config.data import Pmd2Data, Pmd2AsmPatchesConstants from skytemple_files.common.ppmdu_config.xml_reader import Pmd2AsmPatchesConstantsXmlReader from skytemple_files.common.util import get_resources_dir from skytemple_files.patch.arm_patcher import ArmPatcher from skytemple_files.patch.handler.abstract import AbstractPatchHandler from skytemple_files.patch.handler.actor_and_level_loader import ActorAndLevelListLoaderPatchHandler from skytemple_files.patch.handler.disable_tips import DisableTipsPatch from skytemple_files.patch.handler.move_shortcuts import MoveShortcutsPatch from skytemple_files.patch.handler.same_type_partner import SameTypePartnerPatch from skytemple_files.patch.handler.unused_dungeon_chance import UnusedDungeonChancePatch CORE_PATCHES_BASE_DIR = os.path.join(get_resources_dir(), 'patches') class PatchType(Enum): ACTOR_AND_LEVEL_LIST_LOADER = ActorAndLevelListLoaderPatchHandler UNUSED_DUNGEON_CHANCE_PATCH = UnusedDungeonChancePatch MOVE_SHORTCUTS = MoveShortcutsPatch DISABLE_TIPS = DisableTipsPatch SAME_TYPE_PARTNER = SameTypePartnerPatch class PatchPackageError(RuntimeError): pass class Patcher:
def apply(self, patch: Union[Pmd2Patch, Pmd2SimplePatch], binaries: Dict[str, Pmd2Binary], patch_file_dir: str, stub_path: str, game_id: str, parameter_values: Dict[str, Union[int, str]]): with tempfile.TemporaryDirectory() as tmp: try: shutil.copytree(patch_file_dir, tmp, symlinks=True, dirs_exist_ok=True) set_rw_permission_folder(tmp) # Build ASM file to run asm_entrypoint = '' # First read in stub with open(os.path.join(tmp, stub_path)) as fi: asm_entrypoint += fi.read() + '\n' if isinstance(patch, Pmd2SimplePatch): for replace in patch.string_replacements: fn = os.path.join(tmp, replace.filename) game = None for game_candidate in replace.games: if game_candidate.game_id == game_id: game = game_candidate if game is not None: with open_utf8(os.path.join(tmp, fn), 'r') as fi: new_content = replace.regexp.sub( game.replace, fi.read()) with open_utf8(os.path.join(tmp, fn), 'w') as fi: fi.write(new_content) # If it's a simple patch just output and re-import all binaries. for binary_name, binary in binaries.items(): binary_path = os.path.join(tmp, binary_name.split('/')[-1]) # Write binary to tmp dir with open(binary_path, 'wb') as fib: try: fib.write( get_binary_from_rom_ppmdu( self.rom, binary)) except ValueError as err: if binary_name.split( '/')[-1] == 'overlay_0036.bin': continue # We ignore if End's extra overlay is missing. raise err # For simple patches we also output the overlay table as y9.bin: binary_path = os.path.join(tmp, Y9_BIN) # Write binary to tmp dir with open(binary_path, 'wb') as fib: fib.write(self.rom.arm9OverlayTable) # Then include other includes for include in patch.includes: asm_entrypoint += f'.include "{os.path.join(tmp, include.filename)}"\n' # Build binary blocks if isinstance(patch, Pmd2Patch): for open_bin in patch.open_bins: binary = binaries[open_bin.filepath] binary_path = os.path.join( tmp, open_bin.filepath.split('/')[-1]) os.makedirs(os.path.dirname(binary_path), exist_ok=True) # Write binary to tmp dir with open(binary_path, 'wb') as fib: fib.write( get_binary_from_rom_ppmdu(self.rom, binary)) asm_entrypoint += f'.open "{binary_path}", 0x{binary.loadaddress:0x}\n' for include in open_bin.includes: asm_entrypoint += f'.include "{os.path.join(tmp, include.filename)}"\n' asm_entrypoint += '.close\n' # Write final asm file with open_utf8(os.path.join(tmp, ASM_ENTRYPOINT_FN), 'w') as fi: fi.write(asm_entrypoint) # Build parameters for equ parameters = [] for param_name, param_value in parameter_values.items(): parameters += [ '-equ', param_name, f'"{param_value}"' if isinstance( param_value, str) else str(param_value) ] # Run armips try: prefix = "" # Under Windows, try to load from SkyTemple _resources dir first. if sys.platform.startswith('win') and os.path.exists( os.path.join(get_resources_dir(), 'armips.exe')): prefix = os.path.join(get_resources_dir(), '') exec_name = os.getenv('SKYTEMPLE_ARMIPS_EXEC', f'{prefix}armips') cmd_line = [exec_name, ASM_ENTRYPOINT_FN] + parameters if os.getenv('SKYTEMPLE_DEBUG_ARMIPS_OUTPUT', False): print("ARMIPS CMDLINE:") print(cmd_line) result = subprocess.Popen(cmd_line, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, cwd=tmp) retcode = result.wait() except FileNotFoundError as ex: raise make_user_err( ArmipsNotInstalledError, _("ARMIPS could not be found. Make sure, that " "'armips' is inside your system's PATH.")) from ex if os.getenv('SKYTEMPLE_DEBUG_ARMIPS_OUTPUT', False): print("ARMIPS OUTPUT:") if result is not None: print(str(result.stdout.read(), 'utf-8')) # type: ignore print( str(result.stderr.read(), 'utf-8') if result.stderr else '') # type: ignore if retcode != 0: raise make_user_err( PatchError, _("ARMIPS reported an error while applying the patch." ), str(result.stdout.read(), 'utf-8'), str(result.stderr.read(), 'utf-8') # type: ignore if result.stderr else '') # type: ignore # Load the binaries back into the ROM opened_binaries = {} if isinstance(patch, Pmd2SimplePatch): # Read in all binaries again opened_binaries = binaries # Also read in arm9OverlayTable binary_path = os.path.join(tmp, Y9_BIN) with open(binary_path, 'rb') as fib: self.rom.arm9OverlayTable = fib.read() else: # Read opened binaries again for open_bin in patch.open_bins: opened_binaries[open_bin.filepath] = binaries[ open_bin.filepath] for binary_name, binary in opened_binaries.items(): binary_path = os.path.join(tmp, binary_name.split('/')[-1]) with open(binary_path, 'rb') as fib: try: set_binary_in_rom_ppmdu(self.rom, binary, fib.read()) except ValueError as err: if binary_name.split( '/')[-1] == 'overlay_0036.bin': continue # We ignore if End's extra overlay is missing. raise err except (PatchError, ArmipsNotInstalledError): raise except BaseException as ex: raise RuntimeError(f( _("Error while applying the patch: {ex}"))) from ex
# # You should have received a copy of the GNU General Public License # along with SkyTemple. If not, see <https://www.gnu.org/licenses/>. import os from typing import Callable from ndspy.fnt import Folder from ndspy.rom import NintendoDSRom from skytemple_files.common.ppmdu_config.data import Pmd2Data from skytemple_files.common.util import get_resources_dir from skytemple_files.patch.category import PatchCategory from skytemple_files.patch.handler.abstract import AbstractPatchHandler from skytemple_files.common.util import _ OV_FILE_IDX = 36 OV_FILE_PATH = os.path.join(get_resources_dir(), 'patches', 'asm_patches', 'end_asm_mods', 'src', 'overlay_0036.bin') class ExtraSpacePatch(AbstractPatchHandler): @property def name(self) -> str: return 'ExtraSpace' @property def description(self) -> str: return _("This patch adds a new overlay 36 to the game, which is loaded at address 0x023A7080 and provides extra space for patches to place code and data in.") @property def author(self) -> str: return 'End45'
# You should have received a copy of the GNU General Public License # along with SkyTemple. If not, see <https://www.gnu.org/licenses/>. import os import re import warnings from dataclasses import dataclass, field from glob import glob from typing import List, Dict, Optional import yaml from skytemple_files.common.ppmdu_config.data import Pmd2GameEdition, GAME_VERSION_EOS from skytemple_files.common.ppmdu_config.pmdsky_debug.data import Pmd2Binary, Pmd2BinarySymbol, Pmd2BinarySymbolType from skytemple_files.common.util import get_resources_dir, open_utf8 SYMBOLS_DIR = os.path.join(get_resources_dir(), 'pmdsky-debug', 'symbols') OVERLAY_REGEX = re.compile(r"overlay(\d+)") # Aliase for backwards compatibility: ALIAS_MAP = { "DEBUG_SPECIAL_EPISODE_NUMBER": ["DEBUG_SPECIAL_EPISODE_TYPE"], "DUNGEON_DATA_LIST": ["DUNGEON_LIST"], "GUEST_MONSTER_DATA": ["GUEST_POKEMON_DATA"], "GUEST_MONSTER_DATA2": ["GUEST_POKEMON_DATA2"], "SCRIPT_VARS_VALUES": ["GAME_VARS_VALUES"], "GUMMI_BELLY_RESTORE_TABLE": ["GUMMI_BELLY_HEAL"], "IQ_GUMMI_GAIN_TABLE": ["IQ_GUMMI_GAIN"], "IQ_GROUP_SKILLS": ["IQ_GROUPS_SKILLS"], "COMPRESSED_IQ_GROUP_SKILLS": ["COMPRESSED_IQ_GROUPS_SKILLS"], "MEMORY_ALLOCATION_TABLE": ["MEMORY_ALLOC_TABLE"], "SECONDARY_TERRAIN_TYPES": ["SECONDARY_TERRAINS"], "TACTICS_UNLOCK_LEVEL_TABLE": ["TACTICS_UNLOCK_LEVEL"],
def apply(self, patch: Union[Pmd2Patch, Pmd2SimplePatch], binaries: Dict[str, Pmd2Binary], patch_file_dir: str, stub_path: str, game_id: str): try: with tempfile.TemporaryDirectory() as tmp: shutil.copytree(patch_file_dir, tmp, dirs_exist_ok=True) # Build ASM file to run asm_entrypoint = '' # First read in stub with open(os.path.join(tmp, stub_path)) as f: asm_entrypoint += f.read() + '\n' if isinstance(patch, Pmd2SimplePatch): for replace in patch.string_replacements: fn = os.path.join(tmp, replace.filename) game = None for game_candidate in replace.games: if game_candidate.game_id == game_id: game = game_candidate if game is not None: with open(os.path.join(tmp, fn), 'r') as f: new_content = replace.regexp.sub( game.replace, f.read()) with open(os.path.join(tmp, fn), 'w') as f: f.write(new_content) # If it's a simple patch just output and re-import all binaries. for binary_name, binary in binaries.items(): binary_path = os.path.join(tmp, binary_name.split('/')[-1]) # Write binary to tmp dir with open(binary_path, 'wb') as f: try: f.write( get_binary_from_rom_ppmdu( self.rom, binary)) except ValueError as err: if binary_name.split( '/')[-1] == 'overlay_0036.bin': continue # We ignore if End's extra overlay is missing. raise err # For simple patches we also output the overlay table as y9.bin: binary_path = os.path.join(tmp, Y9_BIN) # Write binary to tmp dir with open(binary_path, 'wb') as f: f.write(self.rom.arm9OverlayTable) # Then include other includes for include in patch.includes: asm_entrypoint += f'.include "{os.path.join(tmp, include.filename)}"\n' # Build binary blocks if isinstance(patch, Pmd2Patch): for open_bin in patch.open_bins: binary = binaries[open_bin.filepath] binary_path = os.path.join( tmp, open_bin.filepath.split('/')[-1]) os.makedirs(os.path.dirname(binary_path), exist_ok=True) # Write binary to tmp dir with open(binary_path, 'wb') as f: f.write(get_binary_from_rom_ppmdu( self.rom, binary)) asm_entrypoint += f'.open "{binary_path}", 0x{binary.loadaddress:0x}\n' for include in open_bin.includes: asm_entrypoint += f'.include "{os.path.join(tmp, include.filename)}"\n' asm_entrypoint += '.close\n' # Write final asm file with open_utf8(os.path.join(tmp, ASM_ENTRYPOINT_FN), 'w') as f: f.write(asm_entrypoint) # Run armips original_cwd = os.getcwd() os.chdir(tmp) try: prefix = "" # Under Windows, try to load from SkyTemple _resources dir first. if sys.platform.startswith('win') and os.path.exists( os.path.join(get_resources_dir(), 'armips.exe')): prefix = os.path.join(get_resources_dir(), '') result = subprocess.Popen( [f'{prefix}armips', ASM_ENTRYPOINT_FN], stdout=subprocess.PIPE, stderr=subprocess.STDOUT) retcode = result.wait() except FileNotFoundError as ex: raise ArmipsNotInstalledError( "ARMIPS could not be found. Make sure, that " "'armips' is inside your system's PATH.") from ex finally: # Restore cwd os.chdir(original_cwd) if retcode != 0: raise PatchError( "ARMIPS reported an error while applying the patch.", str(result.stdout.read(), 'utf-8'), str(result.stderr.read(), 'utf-8') if result.stderr else '') # Load the binaries back into the ROM opened_binaries = {} if isinstance(patch, Pmd2SimplePatch): # Read in all binaries again opened_binaries = binaries # Also read in arm9OverlayTable binary_path = os.path.join(tmp, Y9_BIN) with open(binary_path, 'rb') as f: self.rom.arm9OverlayTable = f.read() else: # Read opened binaries again for open_bin in patch.open_bins: opened_binaries[open_bin.filepath] = binaries[ open_bin.filepath] for binary_name, binary in opened_binaries.items(): binary_path = os.path.join(tmp, binary_name.split('/')[-1]) with open(binary_path, 'rb') as f: try: set_binary_in_rom_ppmdu(self.rom, binary, f.read()) except ValueError as err: if binary_name.split( '/')[-1] == 'overlay_0036.bin': continue # We ignore if End's extra overlay is missing. raise err except (PatchError, ArmipsNotInstalledError): raise except BaseException as ex: raise RuntimeError(f"Error while applying the patch: {ex}") from ex
# GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with SkyTemple. If not, see <https://www.gnu.org/licenses/>. import os from typing import Callable from ndspy.fnt import Folder from ndspy.rom import NintendoDSRom from skytemple_files.common.ppmdu_config.data import Pmd2Data from skytemple_files.common.util import get_resources_dir from skytemple_files.patch.handler.abstract import AbstractPatchHandler from skytemple_files.common.util import _ OV_FILE_IDX = 36 OV_FILE_PATH = os.path.join(get_resources_dir(), 'patches', 'asm_patches', 'end_asm_mods', 'src', 'overlay_0036.bin') class ExtraSpacePatch(AbstractPatchHandler): @property def name(self) -> str: return 'ExtraSpace' @property def description(self) -> str: return _( "This patch adds a new overlay 36 to the game, which is loaded at address 0x023A7080 and provides extra space for patches to place code and data in." ) @property