Beispiel #1
0
    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()
Beispiel #2
0
 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()
Beispiel #3
0
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)
Beispiel #4
0
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
Beispiel #6
0
#
#  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'
Beispiel #7
0
#  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"],
Beispiel #8
0
    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
Beispiel #9
0
#  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