diff --git a/CHANGES.rst b/CHANGES.rst index 086a686..656718c 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,3 +1,38 @@ +0.1.1 (Unreleased) +================== + +Features +-------- + +- Added Game Boy Advance +- ROMPatcher now supports RomPatcher.js + +Fixes +----- + +ROMCleaner +~~~~~~~~~~ + +- Ensure we clear patched files out of cache + +ROMParser +~~~~~~~~~ + +- ROMParser will now filter out RetroAchievements subsets, since they're hacks +- When checking for RAPatch matches, if the check is a list will simply check there's something in the list subset + +ROMPatcher +~~~~~~~~~~ + +- Unquote patch URL before downloading + +General +~~~~~~~ + +- Added known issue for long filenames +- RAPatch checks now includes modern/improved/demoted versions +- Language parsing can now handle languages formatted like "En+De" (and test updated) + 0.1.0 (2024-12-04) ================== diff --git a/README.md b/README.md index f5b0eeb..bd7118a 100644 --- a/README.md +++ b/README.md @@ -22,21 +22,25 @@ ROMSearch offers the ability to: * Moving files to a structured location, including potentially additional files that may be needed * Discord integration so users can see the results of runs in a simple, clean way -To get started, see the [documentation](https://romsearch.readthedocs.io/en/latest/). +To get started, see the [documentation](https://romsearch.readthedocs.io/en/latest/). For known issues and workarounds, +see the [known issues](https://romsearch.readthedocs.io/en/latest/known_issues.html). Currently, ROMSearch is in early development, and so many features may be added over time. At the moment, ROMSearch works for the following consoles: -* Nintendo +* Nintendo (Handheld) * Game Boy * Game Boy Color + * Game Boy Advance +* Nintendo (Home) * Nintendo Entertainment System * Super Nintendo Entertainment System * GameCube -* Sony +* Sony (Handheld) + * PlayStation Portable +* Sony (Home) * PlayStation * PlayStation 2 - * PlayStation Portable but be aware there may be quirks that will only become apparent over time. We encourage users to open [issues](https://github.com/bbtufty/romsearch/issues) as and where they find them. \ No newline at end of file diff --git a/docs/configs/config.rst b/docs/configs/config.rst index 4098724..14e6509 100644 --- a/docs/configs/config.rst +++ b/docs/configs/config.rst @@ -85,8 +85,9 @@ Syntax: :: # ['games', 'applications']. Defaults to 'all_but_games', which will # remove everything except games - rompatcher: # ROMPatcher specific options - xdelta_path: [path_to_xdelta] # OPTIONAL. This is where xdelta is located on your filesystem + rompatcher: # ROMPatcher specific options + xdelta_path: [path_to_xdelta] # OPTIONAL. This is where xdelta is located on your filesystem + rompatcher_js_path: [path_to_rompatcher] # OPTIONAL. This is where RomPatcher.js is located on your filesystem discord: # OPTIONAL. If defined, supply a webhook URL so that ROMSearch can post Discord webhook_url: [webhook_url] # notifications diff --git a/docs/configs/platforms.rst b/docs/configs/platforms.rst index 30804a5..5fc9e52 100644 --- a/docs/configs/platforms.rst +++ b/docs/configs/platforms.rst @@ -15,11 +15,11 @@ Syntax: :: ra_id: [id] # OPTIONAL. The RetroAchievements console ID, from their API_GetConsoleIDs ra_hash_method: ["md5", "custom"] # OPTIONAL. The RetroAchievements hash method. Supports "md5" and "custom" - patch_method: ["xdelta"] # OPTIONAL: Method for patching ROMs. Supports "xdelta", "rompatcher.js" - file_exts: # OPTIONAL: Potential file extensions. ROMPatcher uses this to figure - - [ext] # out the file to patch - patch_file_exts: # OPTIONAL: Potential file extensions. ROMPatcher uses this to figure - - [ext] # out the patch file + patch_method: ["xdelta", "rompatcher_js"] # OPTIONAL: Method for patching ROMs. Supports "xdelta", "rompatcher_js" + file_exts: # OPTIONAL: Potential file extensions. ROMPatcher uses this to figure + - [ext] # out the file to patch + patch_file_exts: # OPTIONAL: Potential file extensions. ROMPatcher uses this to figure + - [ext] # out the patch file Nintendo - Game Boy =================== @@ -31,17 +31,37 @@ Nintendo - Game Boy Color .. literalinclude:: ../../romsearch/configs/platforms/Nintendo - Game Boy Color.yml +Nintendo - Game Boy Advance +=========================== + +.. literalinclude:: ../../romsearch/configs/platforms/Nintendo - Game Boy Advance.yml + Nintendo - GameCube =================== .. literalinclude:: ../../romsearch/configs/platforms/Nintendo - GameCube.yml +Nintendo - Nintendo Entertainment System +======================================== + +.. literalinclude:: ../../romsearch/configs/platforms/Nintendo - Nintendo Entertainment System.yml + Nintendo - Super Nintendo Entertainment System ============================================== .. literalinclude:: ../../romsearch/configs/platforms/Nintendo - Super Nintendo Entertainment System.yml +Sony - PlayStation Portable +=========================== + +.. literalinclude:: ../../romsearch/configs/platforms/Sony - PlayStation Portable.yml + Sony - PlayStation ================== -.. literalinclude:: ../../romsearch/configs/platforms/Sony - PlayStation.yml \ No newline at end of file +.. literalinclude:: ../../romsearch/configs/platforms/Sony - PlayStation.yml + +Sony - PlayStation 2 +==================== + +.. literalinclude:: ../../romsearch/configs/platforms/Sony - PlayStation 2.yml \ No newline at end of file diff --git a/docs/intro.rst b/docs/intro.rst index 9e8e62b..c649ed1 100644 --- a/docs/intro.rst +++ b/docs/intro.rst @@ -38,19 +38,26 @@ everything and then filter from the downloaded files. For more details, see the Currently, ROMSearch is in early development, and so many features may be added over time. At the moment, ROMSearch has the capability for: -* Nintendo +* Nintendo (Handheld) * Game Boy * Game Boy Color + * Game Boy Advance + +* Nintendo (Home) + * GameCube * Nintendo - Nintendo Entertainment System * Nintendo - Super Nintendo Entertainment System -* Sony +* Sony (Handheld) + + * PlayStation Portable + +* Sony (Home) * PlayStation * PlayStation 2 - * PlayStation Portable but be aware there may be quirks that will only become apparent over time. We encourage users to open `issues `_ as and where they find them. Known issues can be found at diff --git a/docs/known_issues.rst b/docs/known_issues.rst index 1c4a6c4..fe90576 100644 --- a/docs/known_issues.rst +++ b/docs/known_issues.rst @@ -2,6 +2,11 @@ Known Issues ############ +* For very long filenames in Windows, there may be an error in moving files due to the length of the filename. + + * Enabling long paths will fix this. In the Registry Editor, set + ``HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\FileSystem\LongPathsEnabled`` to 1. + * In GameFinder, includes/excludes can cause some unexpected behavior since it will also search through duplicate files. For example, having an include of "Crash Bandicoot" for the PS1 will also grab "Crash Bash" and "CTR - Crash Team Racing" since at least one of their duplicates starts with "Crash Bandicoot". diff --git a/docs/modules/rompatcher.rst b/docs/modules/rompatcher.rst index 5127b3d..d4efcc6 100644 --- a/docs/modules/rompatcher.rst +++ b/docs/modules/rompatcher.rst @@ -5,7 +5,7 @@ ROMPatcher If a patch file is found and the ROMPatcher module is selected, then this will download patch files and apply to ROMs. To highlight if a ROM has been patched, the final file will have a (ROMPatched) in the file name. -Currently, ROMSearch only uses ``xdelta`` to patch ROMs. +Currently, ROMSearch can use ``xdelta`` and ``RomPatcher.js`` to patch ROMs. For more details on the ROMPatcher arguments, see the :doc:`config file documentation <../configs/config>`. @@ -13,8 +13,21 @@ xdelta ====== To use ``xdelta``, download the latest xdelta3 release from -`here `_. After unzipping the .exe file, add the path to this file -into your config, under ``xdelta_path`` in the ``rompatcher`` config section. +`https://github.com/jmacd/xdelta-gpl/releases/latest `_. +After unzipping the .exe file, add the path to this file into your config, under ``xdelta_path`` in the ``rompatcher`` +config section. + +RomPatcher.js +============= + +You may need to download ``node.js`` to start with. You can get it at `https://nodejs.org/en `_. +After that, clone the NodePatcher.js repository and install: :: + + git clone https://github.com/marcrobledo/RomPatcher.js.git + cd RomPatcher.js + npm install + +After this, as the RomPatcher.js path in the config, put the path to the ``index.js`` file. API === diff --git a/romsearch/configs/clonelists/retool.yml b/romsearch/configs/clonelists/retool.yml index 984c627..3601ff5 100644 --- a/romsearch/configs/clonelists/retool.yml +++ b/romsearch/configs/clonelists/retool.yml @@ -2,6 +2,7 @@ url: "https://raw.githubusercontent.com/unexpectedpanda/retool-clonelists-metada Nintendo - Game Boy: "Nintendo - Game Boy (No-Intro).json" Nintendo - Game Boy Color: "Nintendo - Game Boy Color (No-Intro).json" +Nintendo - Game Boy Advance: "Nintendo - Game Boy Advance (No-Intro).json" Nintendo - GameCube: "Nintendo - GameCube (Redump).json" Nintendo - Nintendo Entertainment System: "Nintendo - Nintendo Entertainment System (No-Intro).json" Nintendo - Super Nintendo Entertainment System: "Nintendo - Super Nintendo Entertainment System (No-Intro).json" diff --git a/romsearch/configs/dats/no-intro.yml b/romsearch/configs/dats/no-intro.yml index ba9d770..ef745f7 100644 --- a/romsearch/configs/dats/no-intro.yml +++ b/romsearch/configs/dats/no-intro.yml @@ -4,6 +4,9 @@ Nintendo - Game Boy: Nintendo - Game Boy Color: file_mapping: "Nintendo - Game Boy Color (*)" +Nintendo - Game Boy Advance: + file_mapping: "Nintendo - Game Boy Advance (*)" + Nintendo - Nintendo Entertainment System: file_mapping: "Nintendo - Nintendo Entertainment System (Headered) (*)" diff --git a/romsearch/configs/defaults.yml b/romsearch/configs/defaults.yml index 04ceda2..6f3ce70 100644 --- a/romsearch/configs/defaults.yml +++ b/romsearch/configs/defaults.yml @@ -6,6 +6,7 @@ datetime_format: "%Y/%m/%d, %H:%M:%S" platforms: - "Nintendo - Game Boy" - "Nintendo - Game Boy Color" + - "Nintendo - Game Boy Advance" - "Nintendo - GameCube" - "Nintendo - Nintendo Entertainment System" - "Nintendo - Super Nintendo Entertainment System" @@ -234,3 +235,6 @@ ra_patch_checks: - "multi_disc" - "demos" - "preproduction" + - "improved_version" + - "modern_version" + - "demoted_version" diff --git a/romsearch/configs/platforms/Nintendo - Game Boy Advance.yml b/romsearch/configs/platforms/Nintendo - Game Boy Advance.yml new file mode 100644 index 0000000..179ba5c --- /dev/null +++ b/romsearch/configs/platforms/Nintendo - Game Boy Advance.yml @@ -0,0 +1,13 @@ +group: "No-Intro" +dir: "/No-Intro/Nintendo - Game Boy Advance/" +unzip: false + +ra_id: 5 +ra_hash_method: "md5" + +# For the ROM patcher +patch_method: "rompatcher.js" +file_exts: + - ".gba" +patch_file_exts: + - ".bps" diff --git a/romsearch/configs/regex.yml b/romsearch/configs/regex.yml index 92227c4..175e35b 100644 --- a/romsearch/configs/regex.yml +++ b/romsearch/configs/regex.yml @@ -4,7 +4,7 @@ regions: flags: "NOFLAG" languages: - pattern: "\\((([languages])(,\\s?)?)*\\)" + pattern: "\\((([languages])((,|\\+)\\s?)?)*\\)" type: "list" flags: "NOFLAG" @@ -191,6 +191,10 @@ controller_set: culture_publishers: pattern: "\\(Culture Publishers\\)" +dsi: + pattern: "\\(DSI\\)" + group: "improved_version" + dx_pack: pattern: "\\(DX Pack\\)" group: "improved_version" @@ -319,6 +323,10 @@ usb_mic_doukonban: pattern: "\\(USB Mic Doukonban\\)" group: "improved_version" +vivendi: + pattern: "\\(Vivendi\\)" + group: "improved_version" + # BUDGET EDITIONS artdink: @@ -502,6 +510,10 @@ konami_collector_series: pattern: "\\(Konami Collector's Series\\)" group: "modern_version" +mega_man_battle_network_legacy_collection: + pattern: "\\(Mega Man Battle Network Legacy Collection\\)" + group: "modern_version" + mega_man_legacy_collection: pattern: "\\(Mega Man Legacy Collection\\)" group: "modern_version" @@ -530,6 +542,10 @@ ninja_jajamaru_retro: pattern: "\\(Ninja JaJaMaru Retro Collection\\)" group: "modern_version" +pokemon_box: + pattern: "\\(Pokemon Box\\)" + group: "modern_version" + qubyte_classic: pattern: "\\(QUByte Classics\\)" group: "modern_version" diff --git a/romsearch/configs/sample_config.yml b/romsearch/configs/sample_config.yml index 6b9f92c..6381837 100644 --- a/romsearch/configs/sample_config.yml +++ b/romsearch/configs/sample_config.yml @@ -82,6 +82,7 @@ rahasher: rompatcher: xdelta_path: F:/Emulation/xdelta3-3.1.0-x86_64.exe + rompatcher_js_path: F:\Emulation\RomPatcher.js\index.js discord: webhook_url: "https://discord.com/api/webhooks/discord_url" diff --git a/romsearch/gui/gui_config.py b/romsearch/gui/gui_config.py index 72238a6..be442b8 100644 --- a/romsearch/gui/gui_config.py +++ b/romsearch/gui/gui_config.py @@ -173,9 +173,11 @@ def __init__( # Do the same for the directories, but for files instead self.all_files = { "xdelta_path": self.ui.lineEditConfigRomPatcherxdeltaPath, + "rompatcher_js_path": self.ui.lineEditConfigRomPatcherRomPatcherjsPath, } self.all_files_buttons = { "xdelta_path": self.ui.pushButtonConfigRomPatcherxdeltaPath, + "rompatcher_js_path": self.ui.pushButtonConfigRomPatcherRomPatcherjsPath, } for b in self.all_files_buttons: self.all_files_buttons[b].clicked.connect( @@ -194,6 +196,7 @@ def __init__( self.rompatcher_text_fields = { "xdelta_path": self.ui.lineEditConfigRomPatcherxdeltaPath, + "rompatcher_js_path": self.ui.lineEditConfigRomPatcherRomPatcherjsPath, } self.discord_text_fields = { diff --git a/romsearch/gui/layout_about.py b/romsearch/gui/layout_about.py index ffe0360..993886c 100644 --- a/romsearch/gui/layout_about.py +++ b/romsearch/gui/layout_about.py @@ -3,7 +3,7 @@ ################################################################################ ## Form generated from reading UI file 'layout_about.ui' ## -## Created by: Qt User Interface Compiler version 6.7.0 +## Created by: Qt User Interface Compiler version 6.7.3 ## ## WARNING! All changes made in this file will be lost when recompiling UI file! ################################################################################ diff --git a/romsearch/gui/layout_romsearch.py b/romsearch/gui/layout_romsearch.py index 8a3ed65..3ed6150 100644 --- a/romsearch/gui/layout_romsearch.py +++ b/romsearch/gui/layout_romsearch.py @@ -3,7 +3,7 @@ ################################################################################ ## Form generated from reading UI file 'layout_romsearch.ui' ## -## Created by: Qt User Interface Compiler version 6.7.0 +## Created by: Qt User Interface Compiler version 6.7.3 ## ## WARNING! All changes made in this file will be lost when recompiling UI file! ################################################################################ @@ -1277,6 +1277,26 @@ def setupUi(self, RomSearch): self.verticalLayoutConfigRomPatcher.addLayout(self.horizontalLayoutConfigRomPatcherxdelta) + self.labelConfigRomPatcherRomPatcherjs = QLabel(self.tabConfigRomPatcher) + self.labelConfigRomPatcherRomPatcherjs.setObjectName(u"labelConfigRomPatcherRomPatcherjs") + + self.verticalLayoutConfigRomPatcher.addWidget(self.labelConfigRomPatcherRomPatcherjs) + + self.horizontalLayoutConfigRomPatcherRomPatcherjs = QHBoxLayout() + self.horizontalLayoutConfigRomPatcherRomPatcherjs.setObjectName(u"horizontalLayoutConfigRomPatcherRomPatcherjs") + self.lineEditConfigRomPatcherRomPatcherjsPath = QLineEdit(self.tabConfigRomPatcher) + self.lineEditConfigRomPatcherRomPatcherjsPath.setObjectName(u"lineEditConfigRomPatcherRomPatcherjsPath") + + self.horizontalLayoutConfigRomPatcherRomPatcherjs.addWidget(self.lineEditConfigRomPatcherRomPatcherjsPath) + + self.pushButtonConfigRomPatcherRomPatcherjsPath = QPushButton(self.tabConfigRomPatcher) + self.pushButtonConfigRomPatcherRomPatcherjsPath.setObjectName(u"pushButtonConfigRomPatcherRomPatcherjsPath") + + self.horizontalLayoutConfigRomPatcherRomPatcherjs.addWidget(self.pushButtonConfigRomPatcherRomPatcherjsPath) + + + self.verticalLayoutConfigRomPatcher.addLayout(self.horizontalLayoutConfigRomPatcherRomPatcherjs) + self.lineConfigRomPatcherBottom = QFrame(self.tabConfigRomPatcher) self.lineConfigRomPatcherBottom.setObjectName(u"lineConfigRomPatcherBottom") self.lineConfigRomPatcherBottom.setFrameShape(QFrame.Shape.HLine) @@ -1808,6 +1828,9 @@ def retranslateUi(self, RomSearch): self.labelConfigRomPatcherxdelta.setText(QCoreApplication.translate("RomSearch", u"xdelta path", None)) self.lineEditConfigRomPatcherxdeltaPath.setPlaceholderText(QCoreApplication.translate("RomSearch", u"xdelta.exe", None)) self.pushButtonConfigRomPatcherxdeltaPath.setText(QCoreApplication.translate("RomSearch", u"Browse", None)) + self.labelConfigRomPatcherRomPatcherjs.setText(QCoreApplication.translate("RomSearch", u"RomPatcher.js path", None)) + self.lineEditConfigRomPatcherRomPatcherjsPath.setPlaceholderText(QCoreApplication.translate("RomSearch", u"index.js", None)) + self.pushButtonConfigRomPatcherRomPatcherjsPath.setText(QCoreApplication.translate("RomSearch", u"Browse", None)) self.tabWidgetConfig.setTabText(self.tabWidgetConfig.indexOf(self.tabConfigRomPatcher), QCoreApplication.translate("RomSearch", u"ROMPatcher", None)) self.labelConfigDiscordWebhookUrlTitle.setText(QCoreApplication.translate("RomSearch", u"Webhook URL", None)) self.labelConfigDiscordWebhookUrlDescription.setText(QCoreApplication.translate("RomSearch", u"URL for Discord webhooks. Must be set for notifications to be sent", None)) diff --git a/romsearch/gui/layout_romsearch.ui b/romsearch/gui/layout_romsearch.ui index 6f75cd9..bd06f1b 100644 --- a/romsearch/gui/layout_romsearch.ui +++ b/romsearch/gui/layout_romsearch.ui @@ -2219,6 +2219,31 @@ + + + + RomPatcher.js path + + + + + + + + + index.js + + + + + + + Browse + + + + + diff --git a/romsearch/modules/romcleaner.py b/romsearch/modules/romcleaner.py index d9a6b3e..cdba82b 100644 --- a/romsearch/modules/romcleaner.py +++ b/romsearch/modules/romcleaner.py @@ -189,6 +189,9 @@ def clean_roms( g_i_short = os.path.splitext(g_i)[0] + if "(ROMPatched)" in rom_short: + g_i_short += " (ROMPatched)" + if g_i_short == rom_short: # Also keep info on the dictionary stuff to clean from the cache diff --git a/romsearch/modules/romparser.py b/romsearch/modules/romparser.py index f794ad8..5d8a46f 100644 --- a/romsearch/modules/romparser.py +++ b/romsearch/modules/romparser.py @@ -63,6 +63,59 @@ def get_pattern_val(regex, tag, regex_type, pattern_mappings=None): return pattern_val +def is_ra_subset(name): + """Check if a name is a RetroAchievements subset + + Args: + name (str): Name to check + """ + + match_pattern = "\\[Subset.*\\]" + + match = find_pattern(match_pattern, name) + is_subset = False + if match is not None: + is_subset = True + + return is_subset + +def check_match(i, j, checks_passed=None): + """Check if two bools/strings/lists match + + For lists, we simply check if there's any subset that matches + + Args: + i: Input 1 + j: Input 2 + checks_passed: If not None, will inherit this as initial start. + Else, will default to True + """ + + if checks_passed is None: + checks_passed = True + if not isinstance(checks_passed, bool): + raise ValueError("checks_passed should be a boolean value") + + # If we have a bool or string, then they should match + if isinstance(i, bool) or isinstance(i, str): + if not i == j: + checks_passed = False + + # If a list, then check there's at least some overlap + elif isinstance(i, list): + s_i = set(i) + s_j = set(j) + s_k = s_i.intersection(s_j) + + if len(s_k) == 0: + checks_passed = False + + else: + t = type(i) + raise ValueError(f"Do not know how to check against type {t}") + + return checks_passed + class ROMParser: @@ -564,6 +617,10 @@ def get_ra_match( for r in self.ra_hashes: for h in self.ra_hashes[r]["Hashes"]: + # If the RA list is a subset, then skip + if is_ra_subset(r): + continue + # Since these should be exact matches, skip those that have # patches if h["PatchUrl"] is not None: @@ -637,6 +694,10 @@ def get_parsed_match( for r in self.ra_hashes: for h in self.ra_hashes[r]["Hashes"]: + # If the RA list is a subset, then skip + if is_ra_subset(r): + continue + # Are we looking for patch URLs or not? if want_patched_files: if h["PatchUrl"] is None: @@ -699,12 +760,37 @@ def get_parsed_match( ) # Now, make sure all the useful checks pass - if all( - [ - m_parsed[check] == r_parsed[check] - for check in self.ra_patch_checks - ] - ): + ra_checks_passed = True + for check in self.ra_patch_checks: + + # If we've already failed, then just skip + if not ra_checks_passed: + continue + + ra_checks_passed = check_match(m_parsed[check], + r_parsed[check], + checks_passed=ra_checks_passed, + ) + + # After this first pass, also see if any of the regex checks are grouped, + # and double-check the sublevel below. This is because we could have e.g. + # mismatched modern types (like a GameCube version vs a Wii U Virtual Console + # version), which inevitably won't match hashes + if ra_checks_passed: + if check not in self.regex_config: + for r_c in self.regex_config: + + if not ra_checks_passed: + continue + + r_c_group = self.regex_config[r_c].get("group", None) + if r_c_group == check: + ra_checks_passed = check_match(m_parsed[r_c], + r_parsed[r_c], + checks_passed=ra_checks_passed, + ) + + if ra_checks_passed: # If we seem to have multiple patch files defined, # then raise a warning and assume there isn't a patch diff --git a/romsearch/modules/rompatcher.py b/romsearch/modules/rompatcher.py index 51efd91..dd7d352 100644 --- a/romsearch/modules/rompatcher.py +++ b/romsearch/modules/rompatcher.py @@ -1,7 +1,10 @@ +from urllib import parse as urlparse + import glob import os +import re import shutil - +import subprocess import wget import romsearch @@ -15,6 +18,7 @@ ALLOWED_PATCH_METHODS = [ "xdelta", + "rompatcher.js", ] @@ -154,6 +158,7 @@ def run( # Now we have everything we need to patch this ROM patched_file = self.patch_rom( unpatched_file=unpatched_file, + patch_dir=patch_dir, patch_file=patch_file, ) @@ -203,7 +208,8 @@ def download_patch_file( ) ) - patch_file = wget.download(patch_url, out=patch_dir) + # Since the URL can already have the % in, unquote before passing to wget + patch_file = wget.download(urlparse.unquote(patch_url), out=patch_dir) if patch_file.endswith(".zip"): unzip_file(patch_file, patch_dir) @@ -223,12 +229,17 @@ def download_patch_file( return patch_file - def patch_rom(self, unpatched_file, patch_file): + def patch_rom(self, + unpatched_file, + patch_file, + patch_dir, + ): """Patch a ROM Args: unpatched_file (str): ROM file to patch patch_file (str): Patch file to patch + patch_dir (str): Patch directory """ # Get the method we're using to patch things @@ -250,6 +261,17 @@ def patch_rom(self, unpatched_file, patch_file): patch_file=patch_file, out_file=patched_file, ) + elif patch_method == "rompatcher.js": + + rompatcher_js_file = f"{unpatch_file_split[0]} (patched){unpatch_file_split[1]}" + + self.rompatcher_js_patch( + unpatched_file=unpatched_file, + patch_file=patch_file, + rompatcher_js_file=rompatcher_js_file, + out_file=patched_file, + patch_dir=patch_dir, + ) else: raise ValueError( f"Patch method needs to be one of {', '.join(ALLOWED_PATCH_METHODS)}, not {patch_method}" @@ -316,3 +338,93 @@ def xdelta_patch( os.system(cmd) return True + + def rompatcher_js_patch( + self, + unpatched_file, + patch_file, + rompatcher_js_file, + out_file, + patch_dir, + ): + """Patch using RomPatcher.js + + Args: + unpatched_file (str): ROM file to patch + patch_file (str): Patch file to patch + rompatcher_js_file (str): Filename that RomPatcher.js will output + out_file (str): Path for output file + patch_dir (str): Patch directory + """ + + rompatcher_js_path = self.config.get("rompatcher", {}).get("rompatcher_js_path", None) + + if rompatcher_js_path is None: + raise ValueError("Path to RomPatcher.js needs to be defined in user config") + + if not os.path.exists(rompatcher_js_path): + raise ValueError("RomPatcher.js path not found") + + cmd = f'node {rompatcher_js_path} patch "{unpatched_file}" "{patch_file}"' + + self.logger.info( + centred_string( + f"Patching file with RomPatcher.js:", + total_length=self.log_line_length, + ) + ) + self.logger.info( + left_aligned_string( + f"-> Unpatched file: {os.path.basename(unpatched_file)}", + total_length=self.log_line_length, + ) + ) + self.logger.info( + left_aligned_string( + f"-> Patch file: {os.path.basename(patch_file)}", + total_length=self.log_line_length, + ) + ) + + self.logger.info( + left_aligned_string( + f"-> RomPatcher.js file: {os.path.basename(rompatcher_js_file)}", + total_length=self.log_line_length, + ) + ) + + # Change to patch directory so file ends up in a sensible spot + orig_dir = os.getcwd() + os.chdir(patch_dir) + + with subprocess.Popen( + cmd, text=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT + ) as process: + for line in process.stdout: + + # Replace any potential tabs in the line, strip whitespace and skip newline at the end + line = re.sub("\s+", " ", line[:-1]) + line = line.lstrip().rstrip() + + if len(line) == 0: + continue + + # Log each line of the output using the provided logger + self.logger.info( + centred_string(line, total_length=self.log_line_length) + ) + + # Return to working directory + os.chdir(orig_dir) + + self.logger.info( + left_aligned_string( + f"-> Renaming file to: {os.path.basename(out_file)}", + total_length=self.log_line_length, + ) + ) + + shutil.copy(rompatcher_js_file, out_file) + os.remove(rompatcher_js_file) + + return True diff --git a/tests/tests_romparser.py b/tests/tests_romparser.py index ab633bc..e13e173 100644 --- a/tests/tests_romparser.py +++ b/tests/tests_romparser.py @@ -1,6 +1,6 @@ from romsearch import ROMParser -TEST_NAME = "Example Game (USA) (En,De,Fr)" +TEST_NAME = "Example Game (USA) (En,De,Fr,Es+It)" def test_romparser_regions(): @@ -23,7 +23,7 @@ def test_romparser_regions(): def test_romparser_languages(): """Put a filename into ROMParser and check it returns the right languages""" - expected_languages = ["English", "French", "German"] + expected_languages = ["English", "French", "German", "Italian", "Spanish"] test_case = {TEST_NAME: {"priority": 1}}