From 50b4601d72cd0692e8bfac596e522903078a07c6 Mon Sep 17 00:00:00 2001 From: Lesserkuma Date: Thu, 21 Apr 2022 11:39:01 +0200 Subject: [PATCH] 3.8 --- CHANGES.md | 14 + FlashGBX/FlashGBX.py | 3 +- FlashGBX/FlashGBX_CLI.py | 191 +++++++---- FlashGBX/FlashGBX_GUI.py | 158 ++++++--- FlashGBX/Flashcart.py | 33 +- FlashGBX/GBMemory.py | 13 +- FlashGBX/Mapper.py | 121 ++++--- FlashGBX/PocketCamera.py | 2 +- FlashGBX/RomFileAGB.py | 14 +- FlashGBX/RomFileDMG.py | 193 +++++++++-- FlashGBX/Util.py | 62 +++- FlashGBX/config/fc_AGB_Generic_0_90.txt | 2 +- FlashGBX/config/fc_AGB_Generic_AAA_A9.txt | 2 +- FlashGBX/config/fc_AGB_Generic_AAA_AA.txt | 2 +- FlashGBX/config/fc_AGB_MSP55LV100G.txt | 4 +- FlashGBX/config/fc_AGB_MSP55LV128M.txt | 6 +- FlashGBX/config/fc_DMG_29LV016T.txt | 49 +++ FlashGBX/config/fc_DMG_AT49F040_AUDIO.txt | 3 - FlashGBX/config/fc_DMG_XploderGB.txt | 55 +++ FlashGBX/config/fc_DMG_iG_512KB.txt | 5 +- FlashGBX/fw_GBxCartRW_v1_3.py | 44 +-- FlashGBX/hw_GBxCartRW.py | 400 +++++++++++++++++----- FlashGBX/hw_GBxCartRW_ofw.py | 48 ++- FlashGBX/res/config.zip | Bin 144164 -> 145188 bytes FlashGBX/res/fw_GBxCart_RW_Mini_v1_0.zip | Bin 0 -> 4774 bytes FlashGBX/res/fw_GBxCart_RW_XMAS_v1_0.zip | Bin 0 -> 8093 bytes FlashGBX/res/fw_GBxCart_RW_v1_1_v1_2.zip | Bin 0 -> 8108 bytes FlashGBX/res/fw_GBxCart_RW_v1_4.zip | Bin 22952 -> 24489 bytes FlashGBX/res/fw_GBxCart_RW_v1_4a.zip | Bin 22952 -> 25001 bytes README.md | 19 +- setup.py | 2 +- 31 files changed, 1077 insertions(+), 368 deletions(-) create mode 100644 FlashGBX/config/fc_DMG_29LV016T.txt create mode 100644 FlashGBX/config/fc_DMG_XploderGB.txt create mode 100644 FlashGBX/res/fw_GBxCart_RW_Mini_v1_0.zip create mode 100644 FlashGBX/res/fw_GBxCart_RW_XMAS_v1_0.zip create mode 100644 FlashGBX/res/fw_GBxCart_RW_v1_1_v1_2.zip diff --git a/CHANGES.md b/CHANGES.md index 103f705..fd80833 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,4 +1,18 @@ # Release notes +### v3.8 (released 2022-04-21) +- Added support for AGB-E05-01 with MSP55LV100G *(thanks EmperorOfTigers)* +- Added support for DV15 with MSP55LV100G *(thanks zvxr)* +- Added support for SD007_T40_64BALL_TSOP28 with 29LV016T *(thanks Wkr)* +- Confirmed support for AGB-E05-03H with M29W128GH *(thanks Wkr)* +- The integrated firmware updater can now also update the firmware of insideGadgets GBxCart RW v1.1, v1.2, XMAS v1.0 and Mini v1.0 device revisions +- Added experimental support for writing ISX files to Game Boy flash cartridges +- Bundles GBxCart RW v1.4 firmware version R34+L6 (changes the way Game Boy cartridges are read and enables support for some more flash cartridges) +- Added support for the BLAZE Xploder GB unlicensed cheat cartridge (requires firmware version L6+) +- Added support for the unlicensed Sachen mapper (requires firmware version L6+) +- Added support for insideGadgets 128 KB flash cartridges *(thanks AlexiG)* +- Added support for insideGadgets 256 KB flash cartridges *(thanks AlexiG)* +- Minor bug fixes and improvements + ### v3.7 (released 2022-03-31) - Updated the Game Boy Advance lookup database for save types, ROM sizes and checksums (improves support for Classic NES Series, NES Classics and Famicom Mini cartridges) - When writing new ROMs to Nintendo Power GB Memory Cartridges (DMG-MMSA-JPN), hidden sector data will now be auto-generated if it’s not provided by the user in form of a .map file diff --git a/FlashGBX/FlashGBX.py b/FlashGBX/FlashGBX.py index b71f5f1..c4f6512 100644 --- a/FlashGBX/FlashGBX.py +++ b/FlashGBX/FlashGBX.py @@ -127,11 +127,10 @@ def main(portableMode=False): ap_cli2.add_argument("--dmg-romsize", choices=["auto", "32kb", "64kb", "128kb", "256kb", "512kb", "1mb", "2mb", "4mb", "8mb", "16mb", "32mb"], type=str.lower, default="auto", help="set size of Game Boy cartridge ROM data") ap_cli2.add_argument("--dmg-mbc", choices=["auto", "1", "2", "3", "5", "6", "7"], type=str.lower, default="auto", help="set memory bank controller type of Game Boy cartridge") ap_cli2.add_argument("--dmg-savesize", choices=["auto", "4k", "16k", "64k", "256k", "512k", "1m", "eeprom2k", "eeprom4k", "tama5", "4m"], type=str.lower, default="auto", help="set size of Game Boy cartridge save data") - ap_cli2.add_argument("--agb-romsize", choices=["auto", "4mb", "8mb", "16mb", "32mb", "64mb", "128mb", "256mb"], type=str.lower, default="auto", help="set size of Game Boy Advance cartridge ROM data") + ap_cli2.add_argument("--agb-romsize", choices=["auto", "1mb", "2mb", "4mb", "8mb", "16mb", "32mb", "64mb", "128mb", "256mb"], type=str.lower, default="auto", help="set size of Game Boy Advance cartridge ROM data") ap_cli2.add_argument("--agb-savetype", choices=["auto", "eeprom4k", "eeprom64k", "sram256k", "flash512k", "flash1m", "dacs8m", "sram512k", "sram1m"], type=str.lower, default="auto", help="set type of Game Boy Advance cartridge save data") ap_cli2.add_argument("--store-rtc", action="store_true", help="store RTC register values if supported") ap_cli2.add_argument("--ignore-bad-header", action="store_true", help="don’t stop if invalid data found in cartridge header data") - #ap_cli2.add_argument("--fast-read-mode", action="store_true", help="enable experimental fast read mode for GBxCart RW v1.3") ap_cli2.add_argument("--flashcart-type", type=str, default="autodetect", help="name of flash cart; see txt files in config directory") ap_cli2.add_argument("--prefer-chip-erase", action="store_true", help="prefer full chip erase over sector erase when both available") ap_cli2.add_argument("--reversed-sectors", action="store_true", help="use reversed flash sectors if possible") diff --git a/FlashGBX/FlashGBX_CLI.py b/FlashGBX/FlashGBX_CLI.py index 70a6ab7..8ca8c6f 100644 --- a/FlashGBX/FlashGBX_CLI.py +++ b/FlashGBX/FlashGBX_CLI.py @@ -149,7 +149,7 @@ class FlashGBX_CLI(): print("\n{:s}Couldn’t read cartridge header. Please try again.{:s}\n".format(ANSI.RED, ANSI.RESET)) self.DisconnectDevice() return - elif bad_read and not args.ignore_bad_header: + elif bad_read and not args.ignore_bad_header and ("mapper_raw" in header and header["mapper_raw"] != 0x203): print("\n{:s}Invalid data was detected which usually means that the cartridge couldn’t be read correctly. Please make sure you selected the correct mode and that the cartridge contacts are clean. This check can be disabled with the command line switch “--ignore-bad-header”.{:s}\n".format(ANSI.RED, ANSI.RESET)) print("Cartridge Information:") print(s_header) @@ -227,6 +227,8 @@ class FlashGBX_CLI(): print("\033[KPlease wait while the flash chip is being unlocked... (Elapsed time: {:s})".format(Util.formatProgressTime(elapsed)), end="\r") elif args["action"] == "SECTOR_ERASE": print("\033[KErasing flash sector at address 0x{:X}...".format(args["sector_pos"]), end="\r") + elif args["action"] == "UPDATE_RTC": + print("\nUpdating Real Time Clock...") elif args["action"] == "ABORTING": print("\nStopping...") elif args["action"] == "FINISHED": @@ -244,7 +246,7 @@ class FlashGBX_CLI(): return elif args["action"] == "PROGRESS": # pv style progress status - prog_str = "{:s}/{:s} {:s} [{:s}KB/s] [{:s}] {:s}% ETA {:s} ".format(Util.formatFileSize(size=pos).replace(" ", "").replace("Bytes", "B").replace("Byte", "B").rjust(8), Util.formatFileSize(size=size).replace(" ", "").replace("Bytes", "B"), Util.formatProgressTimeShort(elapsed), "{:.2f}".format(speed).rjust(6), "%PROG_BAR%", "{:d}".format(int(pos/size*100)).rjust(3), Util.formatProgressTimeShort(left)) + prog_str = "{:s}/{:s} {:s} [{:s}KB/s] [{:s}] {:s}% ETA {:s} ".format(Util.formatFileSize(size=pos).replace(" ", "").replace("Bytes", "B").replace("Byte", "B").rjust(8), Util.formatFileSize(size=size).replace(" ", "").replace("Bytes", "B"), Util.formatProgressTimeShort(elapsed), "{:.2f}".format(speed).rjust(6), "%PROG_BAR%", "{:d}".format(int(pos/size*100)).rjust(3), Util.formatProgressTimeShort(left)) prog_width = shutil.get_terminal_size((80, 20))[0] - (len(prog_str) - 10) progress = min(1, max(0, pos/size)) whole_width = math.floor(progress * prog_width) @@ -272,23 +274,36 @@ class FlashGBX_CLI(): elif self.CONN.INFO["last_action"] == 1: # Backup ROM self.CONN.INFO["last_action"] = 0 if self.CONN.GetMode() == "DMG": - print("CRC32: {:04X}".format(self.CONN.INFO["file_crc32"])) + print("CRC32: {:08x}".format(self.CONN.INFO["file_crc32"])) print("SHA-1: {:s}\n".format(self.CONN.INFO["file_sha1"])) if self.CONN.INFO["rom_checksum"] == self.CONN.INFO["rom_checksum_calc"]: print("{:s}The ROM backup is complete and the checksum was verified successfully!{:s}".format(ANSI.GREEN, ANSI.RESET)) - elif ("DMG-MMSA-JPN" in self.ARGS["argparsed"].flashcart_type) or ("features_raw" in self.CONN.INFO and self.CONN.INFO["features_raw"] in (0x105, 0x202)): + elif ("DMG-MMSA-JPN" in self.ARGS["argparsed"].flashcart_type) or ("mapper_raw" in self.CONN.INFO and self.CONN.INFO["mapper_raw"] in (0x105, 0x202)): print("The ROM backup is complete!") else: - print("{:s}The ROM was dumped, but the checksum is not correct. This may indicate a bad dump, however this can be normal for some reproduction prototypes, unlicensed games, patched games and intentional overdumps.{:s}".format(ANSI.YELLOW, ANSI.RESET)) + msg = "The ROM was dumped, but the checksum is not correct." + if self.CONN.INFO["loop_detected"] is not False: + msg += "\nA data loop was detected in the ROM backup at position 0x{:X} ({:s}). This may indicate a bad dump or overdump.".format(self.CONN.INFO["loop_detected"], Util.formatFileSize(self.CONN.INFO["loop_detected"], asInt=True)) + else: + msg += "\nThis may indicate a bad dump, however this can be normal for some reproduction cartridges, unlicensed games, prototypes, patched games and intentional overdumps." + print("{:s}{:s}{:s}".format(ANSI.YELLOW, msg, ANSI.RESET)) elif self.CONN.GetMode() == "AGB": - print("CRC32: {:04X}".format(self.CONN.INFO["file_crc32"])) + print("CRC32: {:08x}".format(self.CONN.INFO["file_crc32"])) print("SHA-1: {:s}\n".format(self.CONN.INFO["file_sha1"])) if Util.AGB_Global_CRC32 == self.CONN.INFO["rom_checksum_calc"]: print("{:s}The ROM backup is complete and the checksum was verified successfully!{:s}".format(ANSI.GREEN, ANSI.RESET)) elif Util.AGB_Global_CRC32 == 0: - print("The ROM backup is complete! As there is no known checksum for this ROM in the database, verification was skipped.") + msg = "The ROM backup is complete! As there is no known checksum for this ROM in the database, verification was skipped." + if self.CONN.INFO["loop_detected"] is not False: + msg += "\nNOTE: A data loop was detected in the ROM backup at position 0x{:X} ({:s}). This may indicate a bad dump or overdump.".format(self.CONN.INFO["loop_detected"], Util.formatFileSize(self.CONN.INFO["loop_detected"], asInt=True)) + print("{:s}{:s}{:s}".format(ANSI.YELLOW, msg, ANSI.RESET)) else: - print("{:s}The ROM backup is complete, but the checksum doesn’t match the known database entry. This may indicate a bad dump, however this can be normal for some reproduction cartridges, unlicensed games, prototypes, patched games and intentional overdumps.{:s}".format(ANSI.YELLOW, ANSI.RESET)) + msg = "The ROM backup is complete, but the checksum doesn’t match the known database entry." + if self.CONN.INFO["loop_detected"] is not False: + msg += "\nA data loop was detected in the ROM backup at position 0x{:X} ({:s}). This may indicate a bad dump or overdump.".format(self.CONN.INFO["loop_detected"], Util.formatFileSize(self.CONN.INFO["loop_detected"], asInt=True)) + else: + msg += "\nThis may indicate a bad dump, however this can be normal for some reproduction cartridges, unlicensed games, prototypes, patched games and intentional overdumps." + print("{:s}{:s}{:s}".format(ANSI.YELLOW, msg, ANSI.RESET)) elif self.CONN.INFO["last_action"] == 2: # Backup RAM self.CONN.INFO["last_action"] = 0 @@ -376,6 +391,10 @@ class FlashGBX_CLI(): self.CONN = None return False + if dev.FW_UPDATE_REQ: + print("{:s}A firmware update for your {:s} device is required to fully use this software.\nPlease visit the official website at {:s} for updates.\n{:s}Current firmware version: {:s}{:s}".format(ANSI.RED, dev.GetFullName(), dev.GetOfficialWebsite(), ANSI.YELLOW, dev.GetFirmwareVersion(), ANSI.RESET)) + time.sleep(5) + self.CONN = dev return True @@ -409,7 +428,7 @@ class FlashGBX_CLI(): if data['has_rtc']: if 'rtc_buffer' in data: try: - if data['features_raw'] == 0x10: # MBC3 + if data['mapper_raw'] == 0x10: # MBC3 rtc_s = data["rtc_buffer"][0x00] rtc_m = data["rtc_buffer"][0x04] rtc_h = data["rtc_buffer"][0x08] @@ -420,7 +439,7 @@ class FlashGBX_CLI(): s += "Invalid state" else: s += "{:d} days, {:02d}:{:02d}:{:02d}".format(rtc_d, rtc_h, rtc_m, rtc_s) - elif data['features_raw'] == 0xFE: # HuC-3 + elif data['mapper_raw'] == 0xFE: # HuC-3 rtc_buffer = struct.unpack(" 2) and ((self.CONN.GetMode() == "DMG") or ("dacs_8m" in header and header["dacs_8m"] is not True)): - msg_cart_type_s = "Cartridge Type: Unknown flash cartridge – Please submit the displayed information along with a picture of the cartridge’s circuit board." - if ("[ 0/90]" in flash_id): msg_cart_type_s += " For ROM writing, you can give the option called “Generic Flash Cartridge (0/90)” a try." - elif ("[ AAA/AA]" in flash_id): msg_cart_type_s += " For ROM writing, you can give the option called “Generic Flash Cartridge (AAA/AA)” a try." - elif ("[ AAA/A9]" in flash_id): msg_cart_type_s += " For ROM writing, you can give the option called “Generic Flash Cartridge (AAA/A9)” a try." - elif ("[WR / AAA/AA]" in flash_id): msg_cart_type_s += " For ROM writing, you can give the option called “Generic Flash Cartridge (WR/AAA/AA)” a try." - elif ("[WR / AAA/A9]" in flash_id): msg_cart_type_s += " For ROM writing, you can give the option called “Generic Flash Cartridge (WR/AAA/A9)” a try." - elif ("[WR / 555/AA]" in flash_id): msg_cart_type_s += " For ROM writing, you can give the option called “Generic Flash Cartridge (WR/555/AA)” a try." - elif ("[WR / 555/A9]" in flash_id): msg_cart_type_s += " For ROM writing, you can give the option called “Generic Flash Cartridge (WR/555/A9)” a try." - elif ("[AUDIO/ AAA/AA]" in flash_id): msg_cart_type_s += " For ROM writing, you can give the option called “Generic Flash Cartridge (AUDIO/AAA/AA)” a try." - elif ("[AUDIO/ 555/AA]" in flash_id): msg_cart_type_s += " For ROM writing, you can give the option called “Generic Flash Cartridge (AUDIO/555/AA)” a try." + msg_cart_type_s = "Cartridge Type: Unknown flash cartridge – Please submit the displayed information along with a picture of the cartridge’s circuit board." + if ("[ 0/90]" in flash_id): + try_this = "Generic Flash Cartridge (0/90)" + elif ("[ AAA/AA]" in flash_id): + try_this = "Generic Flash Cartridge (AAA/AA)" + elif ("[ AAA/A9]" in flash_id): + try_this = "Generic Flash Cartridge (AAA/A9)" + elif ("[WR / AAA/AA]" in flash_id): + try_this = "Generic Flash Cartridge (WR/AAA/AA)" + elif ("[WR / AAA/A9]" in flash_id): + try_this = "Generic Flash Cartridge (WR/AAA/A9)" + elif ("[WR / 555/AA]" in flash_id): + try_this = "Generic Flash Cartridge (WR/555/AA)" + elif ("[WR / 555/A9]" in flash_id): + try_this = "Generic Flash Cartridge (WR/555/A9)" + elif ("[AUDIO/ AAA/AA]" in flash_id): + try_this = "Generic Flash Cartridge (AUDIO/AAA/AA)" + elif ("[AUDIO/ 555/AA]" in flash_id): + try_this = "Generic Flash Cartridge (AUDIO/555/AA)" + if try_this is not None: + msg_cart_type_s += " For ROM writing, you can give the option called “{:s}” a try at your own risk.".format(try_this) msg_cart_type_s += "\n" else: msg_cart_type_s = "Cartridge Type: Generic ROM Cartridge (not rewritable or not auto-detectable)\n" @@ -696,11 +726,11 @@ class FlashGBX_CLI(): if self.CONN.GetMode() == "DMG": if args.dmg_mbc == "auto": try: - mbc = header["features_raw"] - if mbc == 0: mbc = 5 + mbc = header["mapper_raw"] + if mbc == 0: mbc = 0x19 except: print("{:s}Couldn’t determine MBC type, will try to use MBC5. It can also be manually set with the “--dmg-mbc” command line switch.{:s}".format(ANSI.YELLOW, ANSI.RESET)) - mbc = 5 + mbc = 0x19 else: mbc = int(args.dmg_mbc) if mbc == 2: mbc = 0x06 @@ -733,7 +763,7 @@ class FlashGBX_CLI(): if args.agb_romsize == "auto": rom_size = header["rom_size"] else: - sizes = [ "auto", "4mb", "8mb", "16mb", "32mb", "64mb", "128mb", "256mb" ] + sizes = [ "auto", "1mb", "2mb", "4mb", "8mb", "16mb", "32mb", "64mb", "128mb", "256mb" ] rom_size = Util.AGB_Header_ROM_Sizes_Map[sizes.index(args.agb_romsize) - 1] path = header["game_title"].strip().encode('ascii', 'ignore').decode('ascii') @@ -806,6 +836,7 @@ class FlashGBX_CLI(): return cart_type = 0 + mbc = 0 for i in range(0, len(carts)): if not "names" in carts[i]: continue if carts[i]["type"] != mode: continue @@ -840,23 +871,31 @@ class FlashGBX_CLI(): elif os.path.getsize(path) < 0x400: print("{:s}ROM files smaller than 1 KB are not supported.{:s}".format(ANSI.RED, ANSI.RESET)) return - with open(path, "rb") as file: buffer = bytearray(file.read()) + #with open(path, "rb") as file: buffer = bytearray(file.read()) + + with open(path, "rb") as file: + ext = os.path.splitext(path)[1] + if ext.lower() == ".isx": + buffer = bytearray(file.read()) + buffer = Util.isx2bin(buffer) + else: + buffer = bytearray(file.read(0x1000)) + rom_size = os.stat(path).st_size + if "flash_size" in carts[cart_type]: + if rom_size > carts[cart_type]['flash_size']: + msg = "The selected flash cartridge type seems to support ROMs that are up to {:s} in size, but the file you selected is {:s}.".format(Util.formatFileSize(carts[cart_type]['flash_size']), Util.formatFileSize(os.path.getsize(path), roundUp=True)) + msg += " You can still give it a try, but it’s possible that it’s too large which may cause the ROM writing to fail." + print("{:s}{:s}{:s}".format(ANSI.YELLOW, msg, ANSI.RESET)) + answer = input("Do you want to continue? [y/N]: ").strip().lower() + print("") + if answer != "y": + print("Canceled.") + return + except (PermissionError, FileNotFoundError): print("{:s}Couldn’t access file path “{:s}”.{:s}".format(ANSI.RED, args.path, ANSI.RESET)) return - rom_size = len(buffer) - if "flash_size" in carts[cart_type]: - if rom_size > carts[cart_type]['flash_size']: - msg = "The selected flash cartridge type seems to support ROMs that are up to {:s} in size, but the file you selected is {:s}.".format(Util.formatFileSize(carts[cart_type]['flash_size']), Util.formatFileSize(os.path.getsize(path))) - msg += " It’s possible that it’s too large which may cause the ROM writing to fail." - print("{:s}{:s}{:s}".format(ANSI.YELLOW, msg, ANSI.RESET)) - answer = input("Do you want to continue? [y/N]: ").strip().lower() - print("") - if answer != "y": - print("Canceled.") - return - override_voltage = False if args.force_5v is True: override_voltage = 5 @@ -874,26 +913,24 @@ class FlashGBX_CLI(): if not prefer_chip_erase and 'chip_erase' in carts[cart_type]['commands'] and 'sector_erase' in carts[cart_type]['commands']: print("This flash cartridge supports both Sector Erase and Full Chip Erase methods. You can use the “--prefer-chip-erase” command line switch if necessary.") + if "mbc" in carts[cart_type]: + mbc = carts[cart_type]["mbc"] + verify_write = args.no_verify_write is False fix_header = False - try: - if self.CONN.GetMode() == "DMG": - hdr = RomFileDMG(path).GetHeader() - elif self.CONN.GetMode() == "AGB": - hdr = RomFileAGB(path).GetHeader() - if not hdr["logo_correct"]: - print("{:s}WARNING: The ROM file you selected will not boot on actual hardware due to invalid logo data.{:s}".format(ANSI.YELLOW, ANSI.RESET)) - if not hdr["header_checksum_correct"]: - print("{:s}WARNING: The ROM file you selected will not boot on actual hardware due to an invalid header checksum (expected 0x{:02X} instead of 0x{:02X}).{:s}".format(ANSI.YELLOW, hdr["header_checksum_calc"], hdr["header_checksum"], ANSI.RESET)) - answer = input("Fix the header checksum before continuing? [Y/n]: ").strip().lower() - print("") - if answer != "n": - fix_header = True - - except: - print("{:s}The selected file could not be read.{:s}".format(ANSI.RED, ANSI.RESET)) - return + if self.CONN.GetMode() == "DMG": + hdr = RomFileDMG(buffer).GetHeader() + elif self.CONN.GetMode() == "AGB": + hdr = RomFileAGB(buffer).GetHeader() + if not hdr["logo_correct"] and mbc != 0x203: + print("{:s}WARNING: The ROM file you selected will not boot on actual hardware due to invalid logo data.{:s}".format(ANSI.YELLOW, ANSI.RESET)) + if not hdr["header_checksum_correct"] and mbc != 0x203: + print("{:s}WARNING: The ROM file you selected will not boot on actual hardware due to an invalid header checksum (expected 0x{:02X} instead of 0x{:02X}).{:s}".format(ANSI.YELLOW, hdr["header_checksum_calc"], hdr["header_checksum"], ANSI.RESET)) + answer = input("Fix the header checksum before continuing? [Y/n]: ").strip().lower() + print("") + if answer != "n": + fix_header = True print("") v = carts[cart_type]["voltage"] @@ -901,7 +938,13 @@ class FlashGBX_CLI(): print("The following ROM file will now be written to the flash cartridge at {:s}V:\n{:s}".format(str(v), os.path.abspath(path))) print("") - self.CONN.TransferData(args={ 'mode':4, 'path':path, 'cart_type':cart_type, 'override_voltage':override_voltage, 'start_addr':0, 'buffer':buffer, 'prefer_chip_erase':prefer_chip_erase, 'reverse_sectors':reverse_sectors, 'fast_read_mode':True, 'verify_write':verify_write, 'fix_header':fix_header }, signal=self.PROGRESS.SetProgress) + if len(buffer) > 0x1000: + args = { "mode":4, "path":"", "buffer":buffer, "cart_type":cart_type, "override_voltage":override_voltage, "prefer_chip_erase":prefer_chip_erase, "reverse_sectors":reverse_sectors, "fast_read_mode":True, "verify_write":verify_write, "fix_header":fix_header } + else: + args = { "mode":4, "path":path, "cart_type":cart_type, "override_voltage":override_voltage, "prefer_chip_erase":prefer_chip_erase, "reverse_sectors":reverse_sectors, "fast_read_mode":True, "verify_write":verify_write, "fix_header":fix_header } + self.CONN.TransferData(signal=self.PROGRESS.SetProgress, args=args) + #self.CONN.TransferData(args={ 'mode':4, 'path':path, 'cart_type':cart_type, 'override_voltage':override_voltage, 'start_addr':0, 'buffer':buffer, 'prefer_chip_erase':prefer_chip_erase, 'reverse_sectors':reverse_sectors, 'fast_read_mode':True, 'verify_write':verify_write, 'fix_header':fix_header }, signal=self.PROGRESS.SetProgress) + buffer = None def BackupRestoreRAM(self, args, header): @@ -911,11 +954,11 @@ class FlashGBX_CLI(): if self.CONN.GetMode() == "DMG": if args.dmg_mbc == "auto": try: - mbc = header["features_raw"] - if mbc == 0: mbc = 5 + mbc = header["mapper_raw"] + if mbc == 0: mbc = 0x19 except: print("{:s}Couldn’t determine MBC type, will try to use MBC5. It can also be manually set with the “--dmg-mbc” command line switch.{:s}".format(ANSI.YELLOW, ANSI.RESET)) - mbc = 5 + mbc = 0x19 else: mbc = int(args.dmg_mbc) if mbc == 2: mbc = 0x06 @@ -926,15 +969,15 @@ class FlashGBX_CLI(): if args.dmg_savesize == "auto": try: - if header['features_raw'] == 0x06: # MBC2 + if header['mapper_raw'] == 0x06: # MBC2 save_type = Util.DMG_Header_RAM_Sizes_Flasher_Map[1] - elif header['features_raw'] == 0x22 and header["game_title"] in ("KORO2 KIRBYKKKJ", "KIRBY TNT_KTNE"): # MBC7 Kirby + elif header['mapper_raw'] == 0x22 and header["game_title"] in ("KORO2 KIRBYKKKJ", "KIRBY TNT_KTNE"): # MBC7 Kirby save_type = Util.DMG_Header_RAM_Sizes_Flasher_Map[Util.DMG_Header_RAM_Sizes_Map.index(0x101)] - elif header['features_raw'] == 0x22 and header["game_title"] in ("CMASTER_KCEJ"): # MBC7 Command Master + elif header['mapper_raw'] == 0x22 and header["game_title"] in ("CMASTER_KCEJ"): # MBC7 Command Master save_type = Util.DMG_Header_RAM_Sizes_Flasher_Map[Util.DMG_Header_RAM_Sizes_Map.index(0x102)] - elif header['features_raw'] == 0xFD: # TAMA5 + elif header['mapper_raw'] == 0xFD: # TAMA5 save_type = Util.DMG_Header_RAM_Sizes_Flasher_Map[Util.DMG_Header_RAM_Sizes_Map.index(0x103)] - elif header['features_raw'] == 0x20: # TAMA5 + elif header['mapper_raw'] == 0x20: # TAMA5 save_type = Util.DMG_Header_RAM_Sizes_Flasher_Map[Util.DMG_Header_RAM_Sizes_Map.index(0x104)] else: save_type = Util.DMG_Header_RAM_Sizes_Flasher_Map[Util.DMG_Header_RAM_Sizes_Map.index(header['ram_size_raw'])] @@ -1017,7 +1060,7 @@ class FlashGBX_CLI(): if self.CONN.GetMode() == "AGB": print("Using Save Type “{:s}”.".format(Util.AGB_Header_Save_Types[save_type])) elif self.CONN.GetMode() == "DMG": - if rtc and header["features_raw"] in (0x10, 0xFE): # RTC of MBC3, HuC-3 + if rtc and header["mapper_raw"] in (0x10, 0xFE): # RTC of MBC3, HuC-3 print("Real Time Clock register values will also be written if applicable/possible.") try: @@ -1102,9 +1145,9 @@ class FlashGBX_CLI(): found_length = len(test2) - found_offset if self.CONN.GetMode() == "DMG": - print("\nDone! The writable save data size is {:s} out of {:s} checked.".format(Util.formatFileSize(found_length), Util.formatFileSize(save_type))) + print("\n{:s}Done! The writable save data size is {:s} out of {:s} checked.{:s}".format(ANSI.GREEN, Util.formatFileSize(found_length), Util.formatFileSize(save_type), ANSI.RESET)) elif self.CONN.GetMode() == "AGB": - print("\nDone! The writable save data size using save type “{:s}” is {:s}.".format(Util.AGB_Header_Save_Types[save_type], Util.formatFileSize(found_length))) + print("\n{:s}Done! The writable save data size using save type “{:s}” is {:s}.{:s}".format(ANSI.GREEN, Util.AGB_Header_Save_Types[save_type], Util.formatFileSize(found_length), ANSI.RESET)) try: (_, _, cfi) = self.CONN.CheckFlashChip(limitVoltage=False) diff --git a/FlashGBX/FlashGBX_GUI.py b/FlashGBX/FlashGBX_GUI.py index dc34aa4..db59239 100644 --- a/FlashGBX/FlashGBX_GUI.py +++ b/FlashGBX/FlashGBX_GUI.py @@ -89,6 +89,7 @@ class FlashGBX_GUI(QtWidgets.QWidget): rowActionsGeneral2.addWidget(self.btnBackupRAM) self.cmbDMGCartridgeTypeResult.currentIndexChanged.connect(self.CartridgeTypeChanged) + self.cmbHeaderFeaturesResult.currentIndexChanged.connect(self.DMGMapperTypeChanged) rowActionsGeneral3 = QtWidgets.QHBoxLayout() self.btnFlashROM = QtWidgets.QPushButton("&Write ROM") @@ -665,9 +666,11 @@ class FlashGBX_GUI(QtWidgets.QWidget): if dontShowAgain: self.SETTINGS.setValue("SkipFirmwareUpdate", "enabled") if answer == QtWidgets.QMessageBox.Yes: self.ShowFirmwareUpdateWindow() - else: - #self.mnuTools.actions()[2].setEnabled(False) - pass + elif dev.FW_UPDATE_REQ: + text = "A firmware update for your {:s} device is required to use this software. Please visit the official website ({:s}) for updates.

Current firmware version: {:s}".format(dev.GetFullName(), dev.GetOfficialWebsite(), dev.GetOfficialWebsite(), dev.GetFirmwareVersion()) + if not Util.DEBUG: + self.DisconnectDevice() + QtWidgets.QMessageBox.warning(self, "{:s} {:s}".format(APPNAME, VERSION), text, QtWidgets.QMessageBox.Ok) return True return False @@ -755,7 +758,6 @@ class FlashGBX_GUI(QtWidgets.QWidget): dontShowAgain = str(self.SETTINGS.value("SkipFinishMessage", default="disabled")).lower() == "enabled" msgbox = QtWidgets.QMessageBox(parent=self, icon=QtWidgets.QMessageBox.Information, windowTitle="{:s} {:s}".format(APPNAME, VERSION), text="Operation complete!", standardButtons=QtWidgets.QMessageBox.Ok) cb = QtWidgets.QCheckBox("Don’t show this message again", checked=False) - msgbox.setCheckBox(cb) time_elapsed = None if "time_start" in self.STATUS and self.STATUS["time_start"] > 0: @@ -791,32 +793,41 @@ class FlashGBX_GUI(QtWidgets.QWidget): self.lblHeaderROMChecksumResult.setStyleSheet("QLabel { color: green; }") self.lblStatus4a.setText("Done!") msg = "The ROM backup is complete and the checksum was verified successfully!" - msg += "\n\nCRC32: {:04X}\nSHA-1: {:s}".format(self.CONN.INFO["file_crc32"], self.CONN.INFO["file_sha1"]) + msg += "\n\nCRC32: {:08x}\nSHA-1: {:s}".format(self.CONN.INFO["file_crc32"], self.CONN.INFO["file_sha1"]) if time_elapsed is not None: msg += "\n\nTotal time elapsed: {:s}".format(Util.formatProgressTime(time_elapsed)) msgbox.setText(msg) + msgbox.setCheckBox(cb) if not dontShowAgain: msgbox.exec() dontShowAgain = cb.isChecked() else: - self.lblHeaderROMChecksumResult.setText("Invalid (0x{:04X}≠0x{:04X})".format(self.CONN.INFO["rom_checksum_calc"], self.CONN.INFO["rom_checksum"])) - self.lblHeaderROMChecksumResult.setStyleSheet("QLabel { color: red; }") self.lblStatus4a.setText("Done.") - if ("cart_type" in self.STATUS and "dmg-mmsa-jpn" in self.STATUS["cart_type"]) or ("features_raw" in self.CONN.INFO and self.CONN.INFO["features_raw"] in (0x105, 0x202)): + if ("cart_type" in self.STATUS and "dmg-mmsa-jpn" in self.STATUS["cart_type"]) or ("mapper_raw" in self.CONN.INFO and self.CONN.INFO["mapper_raw"] in (0x105, 0x202, 0x203)): msg = "The ROM backup is complete." - msg += "\n\nCRC32: {:04X}\nSHA-1: {:s}".format(self.CONN.INFO["file_crc32"], self.CONN.INFO["file_sha1"]) - QtWidgets.QMessageBox.information(self, "{:s} {:s}".format(APPNAME, VERSION), msg, QtWidgets.QMessageBox.Ok) + msg += "\n\nCRC32: {:08x}\nSHA-1: {:s}".format(self.CONN.INFO["file_crc32"], self.CONN.INFO["file_sha1"]) + msgbox.setText(msg) + msgbox.exec() else: - msg = "The ROM was dumped, but the checksum is not correct. This may indicate a bad dump, however this can be normal for some reproduction cartridges, unlicensed games, prototypes, patched games and intentional overdumps." - msg += "\n\nCRC32: {:04X}\nSHA-1: {:s}".format(self.CONN.INFO["file_crc32"], self.CONN.INFO["file_sha1"]) - QtWidgets.QMessageBox.warning(self, "{:s} {:s}".format(APPNAME, VERSION), msg, QtWidgets.QMessageBox.Ok) + self.lblHeaderROMChecksumResult.setText("Invalid (0x{:04X}≠0x{:04X})".format(self.CONN.INFO["rom_checksum_calc"], self.CONN.INFO["rom_checksum"])) + self.lblHeaderROMChecksumResult.setStyleSheet("QLabel { color: red; }") + msg = "The ROM was dumped, but the checksum is not correct." + if self.CONN.INFO["loop_detected"] is not False: + msg += "\n\nA data loop was detected in the ROM backup at position 0x{:X} ({:s}). This may indicate a bad dump or overdump.".format(self.CONN.INFO["loop_detected"], Util.formatFileSize(self.CONN.INFO["loop_detected"], asInt=True)) + else: + msg += " This may indicate a bad dump, however this can be normal for some reproduction cartridges, unlicensed games, prototypes, patched games and intentional overdumps." + msg += "\n\nCRC32: {:08x}\nSHA-1: {:s}".format(self.CONN.INFO["file_crc32"], self.CONN.INFO["file_sha1"]) + msgbox.setText(msg) + msgbox.setIcon(QtWidgets.QMessageBox.Warning) + msgbox.exec() elif self.CONN.GetMode() == "AGB": if Util.AGB_Global_CRC32 == self.CONN.INFO["rom_checksum_calc"]: self.lblAGBHeaderROMChecksumResult.setText("Valid (0x{:06X})".format(Util.AGB_Global_CRC32)) self.lblAGBHeaderROMChecksumResult.setStyleSheet("QLabel { color: green; }") self.lblStatus4a.setText("Done!") msg = "The ROM backup is complete and the checksum was verified successfully!" - msg += "\n\nCRC32: {:04X}\nSHA-1: {:s}".format(self.CONN.INFO["file_crc32"], self.CONN.INFO["file_sha1"]) + msg += "\n\nCRC32: {:08x}\nSHA-1: {:s}".format(self.CONN.INFO["file_crc32"], self.CONN.INFO["file_sha1"]) msgbox.setText(msg) + msgbox.setCheckBox(cb) if not dontShowAgain: msgbox.exec() dontShowAgain = cb.isChecked() @@ -825,17 +836,27 @@ class FlashGBX_GUI(QtWidgets.QWidget): self.lblAGBHeaderROMChecksumResult.setStyleSheet(self.lblHeaderRevisionResult.styleSheet()) self.lblStatus4a.setText("Done!") msg = "The ROM backup is complete! As there is no known checksum for this ROM in the database, verification was skipped." - msg += "\n\nCRC32: {:04X}\nSHA-1: {:s}".format(self.CONN.INFO["rom_checksum_calc"], self.CONN.INFO["file_sha1"]) + if self.CONN.INFO["loop_detected"] is not False: + msg += "\n\nNOTE: A data loop was detected in the ROM backup at position 0x{:X} ({:s}). This may indicate a bad dump or overdump.".format(self.CONN.INFO["loop_detected"], Util.formatFileSize(self.CONN.INFO["loop_detected"], asInt=True)) + msgbox.setIcon(QtWidgets.QMessageBox.Warning) + msg += "\n\nCRC32: {:08x}\nSHA-1: {:s}".format(self.CONN.INFO["rom_checksum_calc"], self.CONN.INFO["file_sha1"]) if time_elapsed is not None: msg += "\n\nTotal time elapsed: {:s}".format(Util.formatProgressTime(time_elapsed)) - QtWidgets.QMessageBox.information(self, "{:s} {:s}".format(APPNAME, VERSION), msg, QtWidgets.QMessageBox.Ok) + msgbox.setText(msg) + msgbox.exec() else: self.lblAGBHeaderROMChecksumResult.setText("Invalid (0x{:06X}≠0x{:06X})".format(self.CONN.INFO["rom_checksum_calc"], Util.AGB_Global_CRC32)) self.lblAGBHeaderROMChecksumResult.setStyleSheet("QLabel { color: red; }") self.lblStatus4a.setText("Done.") - msg = "The ROM backup is complete, but the checksum doesn’t match the known database entry. This may indicate a bad dump, however this can be normal for some reproduction cartridges, unlicensed games, prototypes, patched games and intentional overdumps." - msg += "\n\nCRC32: {:04X}\nSHA-1: {:s}".format(self.CONN.INFO["rom_checksum_calc"], self.CONN.INFO["file_sha1"]) + msg = "The ROM backup is complete, but the checksum doesn’t match the known database entry." + if self.CONN.INFO["loop_detected"] is not False: + msg += "\n\nA data loop was detected in the ROM backup at position 0x{:X} ({:s}). This may indicate a bad dump or overdump.".format(self.CONN.INFO["loop_detected"], Util.formatFileSize(self.CONN.INFO["loop_detected"], asInt=True)) + else: + msg += " This may indicate a bad dump, however this can be normal for some reproduction cartridges, unlicensed games, prototypes, patched games and intentional overdumps." + msg += "\n\nCRC32: {:08x}\nSHA-1: {:s}".format(self.CONN.INFO["rom_checksum_calc"], self.CONN.INFO["file_sha1"]) if time_elapsed is not None: msg += "\n\nTotal time elapsed: {:s}".format(Util.formatProgressTime(time_elapsed)) - QtWidgets.QMessageBox.warning(self, "{:s} {:s}".format(APPNAME, VERSION), msg, QtWidgets.QMessageBox.Ok) + msgbox.setText(msg) + msgbox.setIcon(QtWidgets.QMessageBox.Warning) + msgbox.exec() elif self.CONN.INFO["last_action"] == 2: # Backup RAM self.lblStatus4a.setText("Done!") @@ -853,6 +874,7 @@ class FlashGBX_GUI(QtWidgets.QWidget): return msgbox.setText("The save data backup is complete!") + msgbox.setCheckBox(cb) if not dontShowAgain: msgbox.exec() dontShowAgain = cb.isChecked() @@ -868,6 +890,7 @@ class FlashGBX_GUI(QtWidgets.QWidget): else: msg_text = "Save data writing complete!" msgbox.setText(msg_text) + msgbox.setCheckBox(cb) if not dontShowAgain: msgbox.exec() dontShowAgain = cb.isChecked() @@ -895,6 +918,7 @@ class FlashGBX_GUI(QtWidgets.QWidget): if time_elapsed is not None: msg += "\n\nTotal time elapsed: {:s}".format(Util.formatProgressTime(time_elapsed)) msgbox.setText(msg) + msgbox.setCheckBox(cb) if not dontShowAgain: msgbox.exec() dontShowAgain = cb.isChecked() @@ -908,8 +932,14 @@ class FlashGBX_GUI(QtWidgets.QWidget): if dontShowAgain: self.SETTINGS.setValue("SkipFinishMessage", "enabled") self.SetProgressBars(min=0, max=1, value=1) + + def DMGMapperTypeChanged(self, index): + if index in (-1, 0): return + #if ((list(Util.DMG_Header_Mapper.items())[index])[0]) == 0x203: # Xploder GB + # self.cmbHeaderROMSizeResult.setCurrentIndex(Util.DMG_Header_ROM_Sizes_Flasher_Map.index(0x40000)) def CartridgeTypeChanged(self, index): + if index in (-1, 0): return if self.CONN.GetMode() == "DMG": cart_types = self.CONN.GetSupportedCartridgesDMG() if cart_types[1][index] == "RETAIL": # special keyword @@ -996,7 +1026,11 @@ class FlashGBX_GUI(QtWidgets.QWidget): just_erase = False path = "" if dpath != "": - text = "The following ROM file will now be written to the flash cartridge:\n" + dpath + ext = os.path.splitext(dpath)[1] + if ext.lower() == ".isx": + text = "The following ISX file will now be converted to a regular ROM file and then written to the flash cartridge:\n" + dpath + else: + text = "The following ROM file will now be written to the flash cartridge:\n" + dpath answer = QtWidgets.QMessageBox.question(self, "{:s} {:s}".format(APPNAME, VERSION), text, QtWidgets.QMessageBox.Ok | QtWidgets.QMessageBox.Cancel, QtWidgets.QMessageBox.Ok) if answer == QtWidgets.QMessageBox.Cancel: return path = dpath @@ -1029,9 +1063,11 @@ class FlashGBX_GUI(QtWidgets.QWidget): elif self.CONN.GetMode() == "AGB": self.cmbAGBCartridgeTypeResult.setCurrentIndex(cart_type) + mbc = (list(Util.DMG_Header_Mapper.items())[self.cmbHeaderFeaturesResult.currentIndex()])[0] + if (path == ""): if self.CONN.GetMode() == "DMG": - path = QtWidgets.QFileDialog.getOpenFileName(self, "Write ROM", last_dir, "Game Boy ROM File (*.gb *.gbc *.sgb *.bin);;All Files (*.*)")[0] + path = QtWidgets.QFileDialog.getOpenFileName(self, "Write ROM", last_dir, "Game Boy ROM File (*.gb *.gbc *.sgb *.bin *.isx);;All Files (*.*)")[0] elif self.CONN.GetMode() == "AGB": path = QtWidgets.QFileDialog.getOpenFileName(self, "Write ROM", last_dir, "Game Boy Advance ROM File (*.gba *.srl *.bin);;All Files (*.*)")[0] @@ -1049,7 +1085,13 @@ class FlashGBX_GUI(QtWidgets.QWidget): QtWidgets.QMessageBox.critical(self, "{:s} {:s}".format(APPNAME, VERSION), "ROM files bigger than 256 MB are not supported.", QtWidgets.QMessageBox.Ok) return - with open(path, "rb") as file: buffer = bytearray(file.read(0x1000)) + with open(path, "rb") as file: + ext = os.path.splitext(path)[1] + if ext.lower() == ".isx": + buffer = bytearray(file.read()) + buffer = Util.isx2bin(buffer) + else: + buffer = bytearray(file.read(0x1000)) rom_size = os.stat(path).st_size if "flash_size" in carts[cart_type]: if rom_size > carts[cart_type]['flash_size']: @@ -1057,6 +1099,8 @@ class FlashGBX_GUI(QtWidgets.QWidget): msg += " You can still give it a try, but it’s possible that it’s too large which may cause the ROM writing to fail." answer = QtWidgets.QMessageBox.warning(self, "{:s} {:s}".format(APPNAME, VERSION), msg, QtWidgets.QMessageBox.Ok | QtWidgets.QMessageBox.Cancel, QtWidgets.QMessageBox.Cancel) if answer == QtWidgets.QMessageBox.Cancel: return + if "mbc" in carts[cart_type]: + mbc = carts[cart_type]["mbc"] override_voltage = False if 'voltage_variants' in carts[cart_type] and carts[cart_type]['voltage'] == 3.3: @@ -1102,10 +1146,10 @@ class FlashGBX_GUI(QtWidgets.QWidget): hdr = RomFileDMG(buffer).GetHeader() elif self.CONN.GetMode() == "AGB": hdr = RomFileAGB(buffer).GetHeader() - if not hdr["logo_correct"]: + if not hdr["logo_correct"] and mbc != 0x203: answer = QtWidgets.QMessageBox.warning(self, "{:s} {:s}".format(APPNAME, VERSION), "Warning: The ROM file you selected will not boot on actual hardware due to invalid logo data.", QtWidgets.QMessageBox.Ok | QtWidgets.QMessageBox.Cancel, QtWidgets.QMessageBox.Cancel) if answer == QtWidgets.QMessageBox.Cancel: return - if not hdr["header_checksum_correct"]: + if not hdr["header_checksum_correct"] and mbc != 0x203: msg_text = "Warning: The ROM file you selected will not boot on actual hardware due to an invalid header checksum (expected 0x{:02X} instead of 0x{:02X}).".format(hdr["header_checksum_calc"], hdr["header_checksum"]) msgbox = QtWidgets.QMessageBox(parent=self, icon=QtWidgets.QMessageBox.Warning, windowTitle="{:s} {:s}".format(APPNAME, VERSION), text=msg_text) button_fix = msgbox.addButton(" &Fix and Continue ", QtWidgets.QMessageBox.ActionRole) @@ -1128,9 +1172,10 @@ class FlashGBX_GUI(QtWidgets.QWidget): self.btnConfig.setEnabled(False) self.lblStatus4a.setText("Preparing...") qt_app.processEvents() - if just_erase: - prefer_chip_erase = True - verify_write = False + if len(buffer) > 0x1000 or just_erase: + if just_erase: + prefer_chip_erase = True + verify_write = False args = { "path":"", "buffer":buffer, "cart_type":cart_type, "override_voltage":override_voltage, "prefer_chip_erase":prefer_chip_erase, "reverse_sectors":reverse_sectors, "fast_read_mode":True, "verify_write":verify_write, "fix_header":fix_header } else: args = { "path":path, "cart_type":cart_type, "override_voltage":override_voltage, "prefer_chip_erase":prefer_chip_erase, "reverse_sectors":reverse_sectors, "fast_read_mode":True, "verify_write":verify_write, "fix_header":fix_header } @@ -1254,7 +1299,7 @@ class FlashGBX_GUI(QtWidgets.QWidget): answer = QtWidgets.QMessageBox.warning(self, "{:s} {:s}".format(APPNAME, VERSION), "The save data on your cartridge will now be erased.", QtWidgets.QMessageBox.Ok | QtWidgets.QMessageBox.Cancel, QtWidgets.QMessageBox.Cancel) if answer == QtWidgets.QMessageBox.Cancel: return else: - path = path + ".sav" + path = re.sub(r"[<>:\"/\\|\?\*]", "_", path) + ".sav" path = QtWidgets.QFileDialog.getOpenFileName(self, "Restore Save Data", last_dir + "/" + path, "Save Data File (*.sav);;All Files (*.*)")[0] if not path == "": self.SETTINGS.setValue(setting_name, os.path.dirname(path)) if (path == ""): return @@ -1282,7 +1327,10 @@ class FlashGBX_GUI(QtWidgets.QWidget): cb = QtWidgets.QCheckBox("&Adjust RTC", checked=True) msgbox = QtWidgets.QMessageBox(parent=self, icon=QtWidgets.QMessageBox.Question, windowTitle="{:s} {:s}".format(APPNAME, VERSION), text=msg, standardButtons=QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No | QtWidgets.QMessageBox.Cancel) msgbox.setDefaultButton(QtWidgets.QMessageBox.Yes) - msgbox.setCheckBox(cb) + if erase: + cb.setChecked(True) + else: + msgbox.setCheckBox(cb) answer = msgbox.exec() if answer == QtWidgets.QMessageBox.Cancel: return rtc_advance = cb.isChecked() @@ -1442,7 +1490,7 @@ class FlashGBX_GUI(QtWidgets.QWidget): if data['has_rtc']: if 'rtc_buffer' in data: try: - if data['features_raw'] == 0x10: # MBC3 + if data['mapper_raw'] == 0x10: # MBC3 rtc_s = data["rtc_buffer"][0x00] rtc_m = data["rtc_buffer"][0x04] rtc_h = data["rtc_buffer"][0x08] @@ -1456,7 +1504,7 @@ class FlashGBX_GUI(QtWidgets.QWidget): self.lblHeaderRtcResult.setText("{:d} day, {:02d}:{:02d}:{:02d}".format(rtc_d, rtc_h, rtc_m, rtc_s)) else: self.lblHeaderRtcResult.setText("{:d} days, {:02d}:{:02d}:{:02d}".format(rtc_d, rtc_h, rtc_m, rtc_s)) - elif data['features_raw'] == 0xFE: # HuC-3 + elif data['mapper_raw'] == 0xFE: # HuC-3 rtc_buffer = struct.unpack("= 1: self.lblStatus3aResult.setText(Util.formatProgressTime(elapsed)) @@ -2065,7 +2143,7 @@ class FlashGBX_GUI(QtWidgets.QWidget): self.SetMode() if self.CONN.GetMode() == "DMG": header = self.CONN.ReadInfo(setPinsAsInputs=True) - if header["features_raw"] == 252: # GBD + if header["mapper_raw"] == 252: # GBD args = { "path":None, "mbc":252, "save_type":128*1024, "rtc":False } self.CONN.BackupRAM(fncSetProgress=False, args=args) data = self.CONN.INFO["data"] @@ -2100,7 +2178,7 @@ class FlashGBX_GUI(QtWidgets.QWidget): fn_split = os.path.splitext(os.path.abspath(fn)) if fn_split[1].lower() == ".sav": return True - elif self.CONN.GetMode() == "DMG" and fn_split[1].lower() in (".gb", ".sgb", ".gbc", ".bin"): + elif self.CONN.GetMode() == "DMG" and fn_split[1].lower() in (".gb", ".sgb", ".gbc", ".bin", ".isx"): return True elif self.CONN.GetMode() == "AGB" and fn_split[1].lower() in (".gba", ".srl"): return True @@ -2120,7 +2198,7 @@ class FlashGBX_GUI(QtWidgets.QWidget): fn = str(url.toLocalFile()) fn_split = os.path.splitext(os.path.abspath(fn)) - if fn_split[1].lower() in (".gb", ".sgb", ".gbc", ".bin", ".gba", ".srl"): + if fn_split[1].lower() in (".gb", ".sgb", ".gbc", ".bin", ".isx", ".gba", ".srl"): self.FlashROM(fn) elif fn_split[1].lower() == ".sav": self.WriteRAM(fn) diff --git a/FlashGBX/Flashcart.py b/FlashGBX/Flashcart.py index b519469..2274b10 100644 --- a/FlashGBX/Flashcart.py +++ b/FlashGBX/Flashcart.py @@ -9,6 +9,7 @@ class Flashcart: CONFIG = {} COMMAND_SET = None CART_WRITE_FNCPTR = None + CART_WRITE_FAST_FNCPTR = None CART_READ_FNCPTR = None CART_POWERCYCLE_FNCPTR = None PROGRESS_FNCPTR = None @@ -17,9 +18,10 @@ class Flashcart: SECTOR_MAP = None CFI = None - def __init__(self, config=None, cart_write_fncptr=None, cart_read_fncptr=None, cart_powercycle_fncptr=None, progress_fncptr=None): + def __init__(self, config=None, cart_write_fncptr=None, cart_write_fast_fncptr=None, cart_read_fncptr=None, cart_powercycle_fncptr=None, progress_fncptr=None): if config is None: config = {} self.CART_WRITE_FNCPTR = cart_write_fncptr + self.CART_WRITE_FAST_FNCPTR = cart_write_fast_fncptr self.CART_READ_FNCPTR = cart_read_fncptr self.CART_POWERCYCLE_FNCPTR = cart_powercycle_fncptr self.PROGRESS_FNCPTR = progress_fncptr @@ -40,12 +42,16 @@ class Flashcart: return self.CART_READ_FNCPTR(address, length) def CartWrite(self, commands, flashcart=True, sram=False): - if "command_set" in self.CONFIG and self.CONFIG["command_set"] == "DMG-MBC5-32M-FLASH": flashcart = False - for command in commands: - address = command[0] - value = command[1] - self.CART_WRITE_FNCPTR(address, value, flashcart=flashcart, sram=sram) - + if "command_set" in self.CONFIG and self.CONFIG["command_set"] in ("GBMEMORY", "DMG-MBC5-32M-FLASH"): flashcart = False + dprint(commands, flashcart, sram) + if flashcart and not sram: + self.CART_WRITE_FAST_FNCPTR(commands, flashcart=True) + else: + for command in commands: + address = command[0] + value = command[1] + self.CART_WRITE_FNCPTR(address, value, flashcart=flashcart, sram=sram) + def GetCommandSetType(self): return self.CONFIG["_command_set"].upper() @@ -144,6 +150,12 @@ class Flashcart: def Unlock(self): self.CartRead(0) # dummy read + if "unlock_read" in self.CONFIG["commands"]: + for command in self.CONFIG["commands"]["unlock_read"]: + for _ in range(0, command[2]): + temp = self.CartRead(command[0], command[1]) + dprint("Reading 0x{:X} bytes from cartridge at 0x{:X} = {:s}".format(command[1], command[0], str(temp))) + time.sleep(0.001) if "unlock" in self.CONFIG["commands"]: self.CartWrite(self.CONFIG["commands"]["unlock"]) time.sleep(0.001) @@ -167,13 +179,18 @@ class Flashcart: def VerifyFlashID(self): if "read_identifier" not in self.CONFIG["commands"]: return False if len(self.CONFIG["flash_ids"]) == 0: return False + if "power_cycle" in self.CONFIG and self.CONFIG["power_cycle"] is True: + self.CART_POWERCYCLE_FNCPTR() self.Reset() rom = list(self.CartRead(0, len(self.CONFIG["flash_ids"][0]))) self.Unlock() self.CartWrite(self.CONFIG["commands"]["read_identifier"]) time.sleep(0.001) - cart_flash_id = list(self.CartRead(0, len(self.CONFIG["flash_ids"][0]))) + read_identifier_at = 0 + if "read_identifier_at" in self.CONFIG: read_identifier_at = self.CONFIG["read_identifier_at"] + cart_flash_id = list(self.CartRead(read_identifier_at, len(self.CONFIG["flash_ids"][0]))) self.Reset() + dprint(self.CONFIG["names"], self.CONFIG["commands"]["read_identifier"]) dprint("Flash ID: {:s}".format(' '.join(format(x, '02X') for x in cart_flash_id))) verified = True if (rom == cart_flash_id): diff --git a/FlashGBX/GBMemory.py b/FlashGBX/GBMemory.py index 70d35b3..1f6f3c5 100644 --- a/FlashGBX/GBMemory.py +++ b/FlashGBX/GBMemory.py @@ -11,7 +11,9 @@ class GBMemoryMap: IS_MENU = False def __init__(self, rom=None): - if rom is not None: + if rom == bytearray([0xFF] * len(rom)): + self.MAP_DATA = bytearray([0x00] * 0x80) + elif rom is not None: self.ImportROM(rom) def ImportROM(self, data): @@ -26,7 +28,7 @@ class GBMemoryMap: data = bytearray(data) + bytearray([0xFF] * (0x20000 - len(data))) if not self.IS_MENU: - mbc_type = self.MapperToMBCType(info["rom_header"]["features_raw"]) + mbc_type = self.MapperToMBCType(info["rom_header"]["mapper_raw"]) if mbc_type is False: return if len(data) <= 0x20000: rom_size = 0b010 @@ -127,7 +129,7 @@ class GBMemoryMap: info["ram_start_block"] = int(ram_offset / 0x800) ram_offset += info["ram_data_size"] info["rom_header"] = RomFileDMG(data[info["rom_data_offset"]:info["rom_data_offset"]+0x180]).GetHeader() - mbc_type = self.MapperToMBCType(info["rom_header"]["features_raw"]) + mbc_type = self.MapperToMBCType(info["rom_header"]["mapper_raw"]) if mbc_type is False: return if info["rom_data_size"] <= 0x20000: @@ -192,7 +194,7 @@ class GBMemoryMap: mbc_type = 2 elif mbc in (0x10, 0x13): # MBC3 mbc_type = 3 - elif mbc in (0x19, 0x1A, 0x1B, 0x1C, 0x1E): # MBC5 + elif mbc in (0x19, 0x1A, 0x1B, 0x1C, 0x1E, 0x105): # MBC5 mbc_type = 5 else: mbc_type = False @@ -217,5 +219,6 @@ class GBMemoryMap: return self.IS_MENU def GetMapData(self): - if self.MAP_DATA == bytearray([0xFF] * 0x80): return False + if self.MAP_DATA == bytearray([0xFF] * 0x80): + return False return self.MAP_DATA diff --git a/FlashGBX/Mapper.py b/FlashGBX/Mapper.py index 0e2fb6f..0264494 100644 --- a/FlashGBX/Mapper.py +++ b/FlashGBX/Mapper.py @@ -18,6 +18,7 @@ class DMG_MBC: RAM_BANK_SIZE = 0x2000 ROM_BANK_NUM = 0 CURRENT_ROM_BANK = 0 + START_BANK = 0 def __init__(self, args=None, cart_write_fncptr=None, cart_read_fncptr=None, cart_powercycle_fncptr=None, clk_toggle_fncptr=None): if args is None: args = {} @@ -66,6 +67,10 @@ class DMG_MBC: return DMG_Unlicensed_256M(args=args, cart_write_fncptr=cart_write_fncptr, cart_read_fncptr=cart_read_fncptr, cart_powercycle_fncptr=cart_powercycle_fncptr, clk_toggle_fncptr=clk_toggle_fncptr) elif mbc_id == 0x202: # 0x202:'Wisdom Tree Mapper', return DMG_Unlicensed_WisdomTree(args=args, cart_write_fncptr=cart_write_fncptr, cart_read_fncptr=cart_read_fncptr, cart_powercycle_fncptr=cart_powercycle_fncptr, clk_toggle_fncptr=clk_toggle_fncptr) + elif mbc_id == 0x203: # 0x203:'Xploder GB', + return DMG_Unlicensed_XploderGB(args=args, cart_write_fncptr=cart_write_fncptr, cart_read_fncptr=cart_read_fncptr, cart_powercycle_fncptr=cart_powercycle_fncptr, clk_toggle_fncptr=clk_toggle_fncptr) + elif mbc_id == 0x204: # 0x204:'Sachen', + return DMG_Unlicensed_Sachen(args=args, cart_write_fncptr=cart_write_fncptr, cart_read_fncptr=cart_read_fncptr, cart_powercycle_fncptr=cart_powercycle_fncptr, clk_toggle_fncptr=clk_toggle_fncptr) else: self.__init__(args=args, cart_write_fncptr=cart_write_fncptr, cart_read_fncptr=cart_read_fncptr, cart_powercycle_fncptr=cart_powercycle_fncptr, clk_toggle_fncptr=clk_toggle_fncptr) return self @@ -83,6 +88,9 @@ class DMG_MBC: self.CART_WRITE_FNCPTR(address, value) if delay is not False: time.sleep(delay) + def GetID(self): + return self.MBC_ID + def GetName(self): return "Unknown MBC {:d}".format(self.MBC_ID) @@ -145,6 +153,9 @@ class DMG_MBC: start_address = 0 self.CartWrite(commands) return (start_address, self.RAM_BANK_SIZE) + + def SetStartBank(self, index): + self.START_BANK = index def HasHiddenSector(self): return False @@ -210,19 +221,19 @@ class DMG_MBC3(DMG_MBC): def EnableRAM(self, enable=True): dprint(self.GetName(), "|", enable) commands = [ - #[ 0x6000, 0x01 if enable else 0x00 ], [ 0x0000, 0x0A if enable else 0x00 ], ] self.CartWrite(commands) def HasRTC(self): - dprint(self.GetName()) - if self.MBC_ID != 16: return False + dprint("Checking for RTC") + if self.MBC_ID != 16: + dprint("No RTC because wrong MBC ID", self.MBC_ID) + return False self.EnableRAM(enable=False) self.EnableRAM(enable=True) - #self.CartWrite([ [0x4000, 0x08] ]) self.LatchRTC() - + skipped = True for i in range(0x08, 0x0D): self.CLK_TOGGLE_FNCPTR(60) @@ -230,26 +241,20 @@ class DMG_MBC3(DMG_MBC): data = self.CartRead(0xA000, 0x800) if data[0] in (0, 0xFF): continue skipped = False - if data != bytearray([data[0]] * 0x800): return False - return skipped is False + if data != bytearray([data[0]] * 0x800): + dprint("No RTC because whole bank is not the same value", data[0]) + skipped = True + break - #ram1 = self.CartRead(0xA000, 0x10) - #ram2 = ram1 - #t1 = time.time() - #t2 = 0 - #while t2 < (t1 + 1): - # self.LatchRTC() - # ram2 = self.CartRead(0xA000, 0x10) - # if ram1 != ram2: break - # t2 = time.time() - #dprint("RTC_S {:02X} != {:02X}?".format(ram1[0], ram2[0]), ram1 != ram2) - #time.sleep(0.1) - #return (ram1 != ram2) + self.EnableRAM(enable=False) + self.CartWrite([ [0x4000, 0] ]) + return skipped is False def GetRTCBufferSize(self): return 0x30 def LatchRTC(self): + dprint("Latching RTC") self.CLK_TOGGLE_FNCPTR(60) self.CartWrite([ [ 0x0000, 0x0A ] ]) time.sleep(0.01) @@ -260,7 +265,9 @@ class DMG_MBC3(DMG_MBC): time.sleep(0.01) def ReadRTC(self): - #self.EnableRAM(enable=True) + dprint("Reading RTC") + self.EnableRAM(enable=True) + buffer = bytearray() for i in range(0x08, 0x0D): self.CLK_TOGGLE_FNCPTR(60) @@ -272,15 +279,17 @@ class DMG_MBC3(DMG_MBC): ts = int(time.time()) buffer.extend(struct.pack("= 512: - index -= (512 * flash_bank) + index = index % 512 if index == 0: self.CART_POWERCYCLE_FNCPTR() @@ -1122,9 +1130,6 @@ class DMG_Unlicensed_WisdomTree(DMG_MBC): if args is None: args = {} self.ROM_BANK_SIZE = 0x8000 super().__init__(args=args, cart_write_fncptr=cart_write_fncptr, cart_read_fncptr=cart_read_fncptr, cart_powercycle_fncptr=cart_powercycle_fncptr, clk_toggle_fncptr=None) - - #def GetROMSize(self): - # return self.ROM_BANK_SIZE * math.floor(self.ROM_BANK_NUM / 2) def SelectBankROM(self, index): dprint(self.GetName(), "|", index) @@ -1134,6 +1139,44 @@ class DMG_Unlicensed_WisdomTree(DMG_MBC): self.CartWrite(commands) return (0, 0x8000) +class DMG_Unlicensed_XploderGB(DMG_MBC): + def GetName(self): + return "Xploder GB" + + def __init__(self, args=None, cart_write_fncptr=None, cart_read_fncptr=None, cart_powercycle_fncptr=None, clk_toggle_fncptr=None): + if args is None: args = {} + super().__init__(args=args, cart_write_fncptr=cart_write_fncptr, cart_read_fncptr=cart_read_fncptr, cart_powercycle_fncptr=cart_powercycle_fncptr, clk_toggle_fncptr=None) + self.RAM_BANK_SIZE = 0x4000 + + def SelectBankROM(self, index): + dprint(self.GetName(), "|", index) + if index == 0: + self.CART_POWERCYCLE_FNCPTR() + self.CartRead(0x0102, 1) + self.CartWrite([[ 0x0006, index & 0xFF ]]) + start_address = 0x4000 + return (start_address, self.ROM_BANK_SIZE) + + def SelectBankRAM(self, index): + dprint(self.GetName(), "|", index) + if index == 0: + self.CART_POWERCYCLE_FNCPTR() + self.CartRead(0x0102, 1) + return self.SelectBankROM(index + 8) + +class DMG_Unlicensed_Sachen(DMG_MBC): + def GetName(self): + return "Sachen" + + def SelectBankROM(self, index): + dprint(self.GetName(), "|", index) + commands = [ + [ 0x2000, index + self.START_BANK ] + ] + self.CartWrite(commands) + start_address = 0x4000 + return (start_address, self.ROM_BANK_SIZE) + class AGB_GPIO: CART_WRITE_FNCPTR = None @@ -1328,16 +1371,11 @@ class AGB_GPIO: seconds = Util.DecodeBCD(buffer[0x06]) timestamp_then = struct.unpack("= 0x280: + sachen_hash = hashlib.sha1(buffer[0x200:0x280]).digest() + if sachen_hash == bytearray([ 0x73, 0x9B, 0x46, 0x86, 0x97, 0x1C, 0x0B, 0xAE, 0xAF, 0x26, 0xF1, 0x73, 0xAC, 0xAE, 0x4B, 0x2B, 0xBF, 0x00, 0x70, 0x77 ]): + data["rom_size_raw"] = 0x03 + data["mapper_raw"] = 0x204 + data["rom_checksum"] = 0x8793 + data["game_title"] = "SACHEN 4B-001" + elif sachen_hash == bytearray([ 0x22, 0x8B, 0x40, 0x4F, 0x1C, 0xCF, 0x4D, 0xDC, 0x4D, 0xF2, 0x35, 0xF3, 0x7B, 0x6D, 0x61, 0x5E, 0xBE, 0xF1, 0xEF, 0x42 ]): + data["rom_size_raw"] = 0x01 + data["mapper_raw"] = 0x204 + data["rom_checksum"] = 0xB180 + data["game_title"] = "SACHEN 4B-002" + elif sachen_hash == bytearray([ 0xF9, 0xB8, 0x6A, 0x8F, 0x2E, 0x8B, 0x31, 0xD4, 0xC5, 0x02, 0xC8, 0x80, 0x75, 0x35, 0x9C, 0x02, 0xB3, 0xB5, 0x68, 0x01 ]): + data["rom_size_raw"] = 0x03 + data["mapper_raw"] = 0x204 + data["rom_checksum"] = 0x7CFD + data["game_title"] = "SACHEN 4B-004" + elif sachen_hash == bytearray([ 0xD6, 0xB5, 0x33, 0x81, 0x1A, 0x01, 0x0D, 0x4D, 0x1C, 0xCC, 0x5A, 0x2C, 0x34, 0x9D, 0x0F, 0x63, 0xD3, 0xF4, 0x9D, 0x34 ]): + data["rom_size_raw"] = 0x03 + data["mapper_raw"] = 0x204 + data["rom_checksum"] = 0x80DF + data["game_title"] = "SACHEN 4B-005" + elif sachen_hash == bytearray([ 0x3E, 0x56, 0xCC, 0x2D, 0xDF, 0xE0, 0x00, 0xED, 0x53, 0xA7, 0x9D, 0x62, 0xC8, 0xBF, 0x7F, 0x20, 0x27, 0x47, 0xCD, 0x8E ]): + data["rom_size_raw"] = 0x03 + data["mapper_raw"] = 0x204 + data["rom_checksum"] = 0x55E6 + data["game_title"] = "SACHEN 4B-006" + elif sachen_hash == bytearray([ 0xD8, 0x24, 0xD2, 0xB2, 0x71, 0x6B, 0x08, 0xFA, 0xEA, 0xA4, 0xFB, 0xD9, 0x7D, 0x81, 0x94, 0x57, 0x46, 0x77, 0x91, 0x60 ]): + data["rom_size_raw"] = 0x03 + data["mapper_raw"] = 0x204 + data["rom_checksum"] = 0x8E9F + data["game_title"] = "SACHEN 4B-007" + elif sachen_hash == bytearray([ 0x19, 0x3E, 0xF8, 0xE2, 0x12, 0x8A, 0x24, 0x10, 0xFE, 0xE9, 0xEA, 0x27, 0xC9, 0x1B, 0xC4, 0xDD, 0x04, 0x74, 0x1B, 0xA8 ]): + data["rom_size_raw"] = 0x03 + data["mapper_raw"] = 0x204 + data["rom_checksum"] = 0x99C7 + data["game_title"] = "SACHEN 4B-008" + elif sachen_hash == bytearray([ 0xA5, 0x07, 0xCB, 0xB0, 0x63, 0x7A, 0xE7, 0x1A, 0xF2, 0xC8, 0x32, 0x9B, 0xA6, 0x6D, 0xC4, 0x21, 0x68, 0x78, 0xE5, 0x39 ]): + data["rom_size_raw"] = 0x03 + data["mapper_raw"] = 0x204 + data["rom_checksum"] = 0xCDD3 + data["game_title"] = "SACHEN 4B-009" + elif sachen_hash == bytearray([ 0x18, 0xEC, 0x2B, 0x15, 0x97, 0xD7, 0x80, 0x51, 0x58, 0xB2, 0xB8, 0x53, 0xA7, 0x00, 0xD7, 0x0B, 0xCE, 0x0A, 0xB3, 0xFF ]): + data["rom_size_raw"] = 0x01 + data["mapper_raw"] = 0x204 + data["rom_checksum"] = 0x3889 + data["game_title"] = "SACHEN 4B-003" + elif sachen_hash == bytearray([ 0x96, 0x1C, 0xE3, 0x5D, 0x3A, 0x81, 0x44, 0x95, 0xCF, 0x42, 0x92, 0x42, 0x30, 0x83, 0x14, 0x17, 0xA9, 0xBF, 0xE0, 0x9F ]): + data["rom_size_raw"] = 0x06 + data["mapper_raw"] = 0x204 + data["rom_checksum"] = 0x3934 + data["cgb"] = 0x80 + data["game_title"] = "SACHEN 31B-001" + elif sachen_hash == bytearray([ 0xB8, 0x59, 0x61, 0x1C, 0x03, 0xAF, 0x5F, 0x7F, 0x50, 0x3E, 0x8C, 0xB0, 0x9C, 0x81, 0x4A, 0x0C, 0xE8, 0xBA, 0xB5, 0x99 ]): + data["rom_size_raw"] = 0x06 + data["mapper_raw"] = 0x204 + data["rom_checksum"] = 0x2E45 + data["cgb"] = 0x80 + data["game_title"] = "SACHEN 31B-001" + elif sachen_hash == bytearray([ 0xF5, 0xC6, 0xC2, 0xE6, 0xA6, 0xF2, 0xEE, 0x86, 0x29, 0x22, 0x3D, 0x7C, 0x72, 0xF9, 0xDD, 0x6F, 0x32, 0x0A, 0xA0, 0x9D ]): + data["rom_size_raw"] = 0x04 + data["mapper_raw"] = 0x204 + data["rom_checksum"] = 0x125B + data["cgb"] = 0x80 + data["game_title"] = "SACHEN 8B-001" + elif sachen_hash == bytearray([ 0xF2, 0x8A, 0xDF, 0x84, 0xBA, 0x56, 0x8C, 0x54, 0xF9, 0x4B, 0x25, 0xFA, 0x12, 0x92, 0x4E, 0xD6, 0x7D, 0xD1, 0x7E, 0x9D ]): + data["rom_size_raw"] = 0x04 + data["mapper_raw"] = 0x204 + data["rom_checksum"] = 0x598F + data["cgb"] = 0x80 + data["game_title"] = "SACHEN 8B-002" + elif sachen_hash == bytearray([ 0x1C, 0x08, 0x6F, 0x94, 0xD8, 0xFD, 0x40, 0x4D, 0xA3, 0x85, 0xCE, 0x57, 0x35, 0xF3, 0x43, 0x92, 0xEE, 0xB7, 0x26, 0xE1 ]): + data["rom_size_raw"] = 0x04 + data["mapper_raw"] = 0x204 + data["rom_checksum"] = 0x6485 + data["cgb"] = 0x80 + data["game_title"] = "SACHEN 8B-003" + elif sachen_hash == bytearray([ 0x2C, 0xFD, 0xE1, 0x8D, 0x2C, 0x57, 0xBA, 0xDB, 0xC0, 0xF8, 0xDF, 0x52, 0x79, 0x38, 0x44, 0x56, 0x3B, 0xB0, 0xA0, 0xDE ]): + data["rom_size_raw"] = 0x04 + data["mapper_raw"] = 0x204 + data["rom_checksum"] = 0x02A4 + data["cgb"] = 0x80 + data["game_title"] = "SACHEN 8B-004" + elif sachen_hash == bytearray([ 0x4E, 0xEA, 0x3C, 0x0A, 0x23, 0x5C, 0xF9, 0x2D, 0xC6, 0x22, 0xC2, 0x21, 0xD3, 0xBB, 0x73, 0x3B, 0xA7, 0x21, 0xFB, 0x78 ]): + data["rom_size_raw"] = 0x02 + data["mapper_raw"] = 0x204 + data["rom_checksum"] = 0xA709 + data["cgb"] = 0x80 + data["game_title"] = "SACHEN" + elif sachen_hash == bytearray([ 0x3F, 0xE9, 0xB7, 0xAB, 0xBC, 0x18, 0x95, 0x60, 0x80, 0xF7, 0xDF, 0x9B, 0x5E, 0x5A, 0x0C, 0x9F, 0x18, 0x63, 0x34, 0x7B ]): + data["rom_size_raw"] = 0x04 + data["mapper_raw"] = 0x204 + data["rom_checksum"] = 0x929D + data["cgb"] = 0x80 + data["game_title"] = "SACHEN 1B-003" + elif sachen_hash == bytearray([ 0xD8, 0x9B, 0x0D, 0x55, 0x48, 0x97, 0x7F, 0xD5, 0x0E, 0x46, 0x20, 0xD6, 0x9E, 0x0B, 0x8C, 0x6B, 0x05, 0xD4, 0x8F, 0x2C ]): + data["rom_size_raw"] = 0x04 + data["mapper_raw"] = 0x204 + data["rom_checksum"] = 0x2F50 + data["cgb"] = 0x80 + data["game_title"] = "SACHEN 4B-003" + elif sachen_hash == bytearray([ 0x8B, 0x98, 0xB1, 0xD3, 0x6B, 0x84, 0x66, 0x51, 0xC0, 0x23, 0x19, 0xF2, 0xDC, 0xD3, 0xF4, 0x97, 0xDB, 0x39, 0x47, 0xE7 ]): + data["rom_size_raw"] = 0x06 + data["mapper_raw"] = 0x204 + data["rom_checksum"] = 0x9769 + data["cgb"] = 0x80 + data["game_title"] = "SACHEN" + elif sachen_hash == bytearray([ 0xD0, 0xAE, 0xC9, 0xFB, 0xF0, 0x8D, 0x7A, 0x72, 0x34, 0x8E, 0x96, 0xB6, 0x75, 0x6B, 0x30, 0xC1, 0xCB, 0xF6, 0x2F, 0x00 ]): + data["rom_size_raw"] = 0x06 + data["mapper_raw"] = 0x204 + data["rom_checksum"] = 0x0346 + data["game_title"] = "SACHEN 6B-001" - if data["features_raw"] in Util.DMG_Header_Mapper: - data["features"] = Util.DMG_Header_Mapper[data["features_raw"]] + + if data["mapper_raw"] in Util.DMG_Header_Mapper: + data["mapper"] = Util.DMG_Header_Mapper[data["mapper_raw"]] elif data["logo_correct"]: - print("{:s}WARNING: Unknown memory bank controller type 0x{:02X}{:s}".format(Util.ANSI.YELLOW, data["features_raw"], Util.ANSI.RESET)) + print("{:s}WARNING: Unknown memory bank controller type 0x{:02X}{:s}".format(Util.ANSI.YELLOW, data["mapper_raw"], Util.ANSI.RESET)) return data diff --git a/FlashGBX/Util.py b/FlashGBX/Util.py index 37dfbcf..684a452 100644 --- a/FlashGBX/Util.py +++ b/FlashGBX/Util.py @@ -2,12 +2,12 @@ # FlashGBX # Author: Lesserkuma (github.com/lesserkuma) -import math, time, datetime, copy, configparser, threading, statistics, os, platform, traceback +import math, time, datetime, copy, configparser, threading, statistics, os, platform, traceback, io, struct from enum import Enum # Common constants APPNAME = "FlashGBX" -VERSION_PEP440 = "3.7" +VERSION_PEP440 = "3.8" VERSION = "v{:s}".format(VERSION_PEP440) DEBUG = False @@ -19,13 +19,13 @@ AGB_Global_CRC32 = 0 AGB_Flash_Save_Chips = { 0xBFD4:"SST 39VF512", 0x1F3D:"Atmel AT29LV512", 0xC21C:"Macronix MX29L512", 0x321B:"Panasonic MN63F805MNP", 0xC209:"Macronix MX29L010", 0x6213:"SANYO LE26FV10N1TS" } AGB_Flash_Save_Chips_Sizes = [ 0x10000, 0x10000, 0x10000, 0x10000, 0x20000, 0x20000 ] -DMG_Header_Mapper = { 0x00:'None', 0x01:'MBC1', 0x02:'MBC1+SRAM', 0x03:'MBC1+SRAM+BATTERY', 0x06:'MBC2+SRAM+BATTERY', 0x10:'MBC3+RTC+SRAM+BATTERY', 0x13:'MBC3+SRAM+BATTERY', 0x19:'MBC5', 0x1A:'MBC5+SRAM', 0x1B:'MBC5+SRAM+BATTERY', 0x1C:'MBC5+RUMBLE', 0x1E:'MBC5+RUMBLE+SRAM+BATTERY', 0x20:'MBC6+SRAM+FLASH+BATTERY', 0x22:'MBC7+ACCELEROMETER+EEPROM', 0x101:'MBC1M', 0x103:'MBC1M+SRAM+BATTERY', 0x0B:'MMM01', 0x0D:'MMM01+SRAM+BATTERY', 0xFC:'GBD+SRAM+BATTERY', 0x105:'G-MMC1+SRAM+BATTERY', 0x104:'M161', 0xFF:'HuC-1+IR+SRAM+BATTERY', 0xFE:'HuC-3+RTC+SRAM+BATTERY', 0xFD:'TAMA5+RTC+EEPROM', 0x201:'Unlicensed 256M Mapper', 0x202:'Unlicensed Wisdom Tree Mapper' } +DMG_Header_Mapper = { 0x00:'None', 0x01:'MBC1', 0x02:'MBC1+SRAM', 0x03:'MBC1+SRAM+BATTERY', 0x06:'MBC2+SRAM+BATTERY', 0x10:'MBC3+RTC+SRAM+BATTERY', 0x13:'MBC3+SRAM+BATTERY', 0x19:'MBC5', 0x1A:'MBC5+SRAM', 0x1B:'MBC5+SRAM+BATTERY', 0x1C:'MBC5+RUMBLE', 0x1E:'MBC5+RUMBLE+SRAM+BATTERY', 0x20:'MBC6+SRAM+FLASH+BATTERY', 0x22:'MBC7+ACCELEROMETER+EEPROM', 0x101:'MBC1M', 0x103:'MBC1M+SRAM+BATTERY', 0x0B:'MMM01', 0x0D:'MMM01+SRAM+BATTERY', 0xFC:'GBD+SRAM+BATTERY', 0x105:'G-MMC1+SRAM+BATTERY', 0x104:'M161', 0xFF:'HuC-1+IR+SRAM+BATTERY', 0xFE:'HuC-3+RTC+SRAM+BATTERY', 0xFD:'TAMA5+RTC+EEPROM', 0x201:'Unlicensed 256M Mapper', 0x202:'Unlicensed Wisdom Tree Mapper', 0x203:'Unlicensed Xploder GB Mapper', 0x204:'Unlicensed Sachen Mapper' } DMG_Header_ROM_Sizes = [ "32 KB", "64 KB", "128 KB", "256 KB", "512 KB", "1 MB", "2 MB", "4 MB", "8 MB", "16 MB", "32 MB" ] DMG_Header_ROM_Sizes_Map = [ 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A ] DMG_Header_ROM_Sizes_Flasher_Map = [ 0x8000, 0x10000, 0x20000, 0x40000, 0x80000, 0x100000, 0x200000, 0x400000, 0x800000, 0x1000000, 0x2000000 ] -DMG_Header_RAM_Sizes = [ "None", "4K SRAM (512 Bytes)", "16K SRAM (2 KB)", "64K SRAM (8 KB)", "256K SRAM (32 KB)", "512K SRAM (64 KB)", "1M SRAM (128 KB)", "MBC6 SRAM+FLASH (1.03 MB)", "MBC7 2K EEPROM (256 Bytes)", "MBC7 4K EEPROM (512 Bytes)", "TAMA5 EEPROM (32 Bytes)", "Unlicensed 4M SRAM (512 KB)" ] -DMG_Header_RAM_Sizes_Map = [ 0x00, 0x01, 0x01, 0x02, 0x03, 0x05, 0x04, 0x104, 0x101, 0x102, 0x103, 0x201 ] -DMG_Header_RAM_Sizes_Flasher_Map = [ 0, 0x200, 0x800, 0x2000, 0x8000, 0x10000, 0x20000, 0x108000, 0x100, 0x200, 0x20, 0x80000 ] # RAM size in bytes +DMG_Header_RAM_Sizes = [ "None", "4K SRAM (512 Bytes)", "16K SRAM (2 KB)", "64K SRAM (8 KB)", "256K SRAM (32 KB)", "512K SRAM (64 KB)", "1M SRAM (128 KB)", "MBC6 SRAM+FLASH (1.03 MB)", "MBC7 2K EEPROM (256 Bytes)", "MBC7 4K EEPROM (512 Bytes)", "TAMA5 EEPROM (32 Bytes)", "Unlicensed 4M SRAM (512 KB)", "Unlicensed 1M EEPROM (128 KB)" ] +DMG_Header_RAM_Sizes_Map = [ 0x00, 0x01, 0x01, 0x02, 0x03, 0x05, 0x04, 0x104, 0x101, 0x102, 0x103, 0x201, 0x203 ] +DMG_Header_RAM_Sizes_Flasher_Map = [ 0, 0x200, 0x800, 0x2000, 0x8000, 0x10000, 0x20000, 0x108000, 0x100, 0x200, 0x20, 0x80000, 0x20000 ] # RAM size in bytes DMG_Header_SGB = { 0x00:'No support', 0x03:'Supported' } DMG_Header_CGB = { 0x00:'No support', 0x80:'Supported', 0xC0:'Required' } @@ -147,7 +147,7 @@ class Progress(): self.UPDATER(args) self.PROGRESS = {} - elif args["action"] in ("ERASE", "SECTOR_ERASE", "UNLOCK"): + elif args["action"] in ("ERASE", "SECTOR_ERASE", "UNLOCK", "UPDATE_RTC"): if "time_start" in self.PROGRESS: args["time_elapsed"] = now - self.PROGRESS["time_start"] elif "time_start" in args: @@ -247,6 +247,32 @@ class TAMA5_REG(Enum): MEM_READ_L = 0xC MEM_READ_H = 0xD +def isx2bin(buffer): + data_input = io.BytesIO(buffer) + data_output = bytearray(8 * 1024 * 1024) + rom_size = 0 + temp = 32 * 1024 + while 1: + try: + type = struct.unpack('B', data_input.read(1))[0] + if type == 4: + break + elif type != 1: + print("WARNING: Unhandled ISX record type 0x{:02X} found. Converted ROM may not be working correctly.".format(type)) + continue + bank = struct.unpack('B', data_input.read(1))[0] + offset = struct.unpack('
  • Please check gbxcart.com for the latest official firmware version
  • ") - #self.lblExternal_Blerb.setWordWrap(True) - #self.lblExternal_Blerb.mousePressEvent = lambda x: [ self.optOFW.setChecked(True) ] self.rowUpdate = QtWidgets.QHBoxLayout() self.btnUpdate = QtWidgets.QPushButton("Install Firmware Update") @@ -97,20 +96,22 @@ class FirmwareUpdaterWindow(QtWidgets.QDialog): self.rowUpdate.addStretch() self.rowUpdate.addWidget(self.btnUpdate) self.rowUpdate.addStretch() - - self.grpAvailableFwUpdatesLayout.addWidget(self.optCFW) - self.grpAvailableFwUpdatesLayout.addWidget(self.lblCFW_Blerb) + + if self.PCB_VER == "v1.3": + self.grpAvailableFwUpdatesLayout.addWidget(self.optCFW) + self.grpAvailableFwUpdatesLayout.addWidget(self.lblCFW_Blerb) + self.optCFW.setChecked(True) + else: + self.optOFW.setChecked(True) self.grpAvailableFwUpdatesLayout.addWidget(self.optOFW) self.grpAvailableFwUpdatesLayout.addWidget(self.lblOFW_Blerb) self.grpAvailableFwUpdatesLayout.addWidget(self.optExternal) - #self.grpAvailableFwUpdatesLayout.addWidget(self.lblExternal_Blerb) self.grpAvailableFwUpdatesLayout.addSpacing(3) self.grpAvailableFwUpdatesLayout.addItem(self.rowUpdate) - #self.grpAvailableFwUpdatesLayout.addWidget(self.btnUpdate) self.grpAvailableFwUpdates.setLayout(self.grpAvailableFwUpdatesLayout) self.layout_device.addWidget(self.grpAvailableFwUpdates) # ↑↑↑ Available Firmware Updates - + self.grpStatus = QtWidgets.QGroupBox("") self.grpStatusLayout = QtWidgets.QGridLayout() self.prgStatus = QtWidgets.QProgressBar() @@ -119,7 +120,6 @@ class FirmwareUpdaterWindow(QtWidgets.QDialog): self.prgStatus.setValue(0) self.lblStatus = QtWidgets.QLabel("Status: Ready.") - #self.grpStatusLayout.addWidget(self.btnUpdate, 0, 0, QtCore.Qt.AlignCenter) self.grpStatusLayout.addWidget(self.prgStatus, 1, 0) self.grpStatusLayout.addWidget(self.lblStatus, 2, 0) @@ -200,18 +200,18 @@ class FirmwareUpdaterWindow(QtWidgets.QDialog): fn = "ofw.hex" else: path = self.APP.SETTINGS.value("LastDirFirmwareUpdate") - path = QtWidgets.QFileDialog.getOpenFileName(self, "Choose GBxCart RW v1.3 Firmware File", path, "Firmware Update (*.hex);;All Files (*.*)")[0] + path = QtWidgets.QFileDialog.getOpenFileName(self, "Choose GBxCart RW Firmware File", path, "Firmware Update (*.hex);;All Files (*.*)")[0] if path == "": return - temp = re.search(r"^(gbxcart_rw_v1\.3_pcb_r.+\.hex)$", os.path.basename(path)) + temp = re.search(r"^(gbx(?:cart|mas)_rw_.+_pcb_r.+\.hex)$", os.path.basename(path)) if temp is None: - msgbox = QtWidgets.QMessageBox(parent=self, icon=QtWidgets.QMessageBox.Critical, windowTitle="FlashGBX", text="The expected filename for a valid firmware file is gbxcart_rw_v1.3_pcb_r**.hex. Please visit gbxcart.com for the latest official firmware updates.", standardButtons=QtWidgets.QMessageBox.Ok) + msgbox = QtWidgets.QMessageBox(parent=self, icon=QtWidgets.QMessageBox.Critical, windowTitle="FlashGBX", text="The expected filename for a valid firmware file is gbx*_rw_*_pcb_r*.hex. Please visit https://www.gbxcart.com for the latest official firmware updates.", standardButtons=QtWidgets.QMessageBox.Ok) answer = msgbox.exec() return self.APP.SETTINGS.setValue("LastDirFirmwareUpdate", os.path.dirname(path)) - fw = "{:s}

    Please double check that this is a valid firmware file for the GBxCart RW v1.3. If it is invalid or an update for a different device, it may render your device unusable.".format(path) + fw = "{:s}

    Please double check that this is a valid firmware file for your GBxCart RW. If it is invalid or an update for a different device, it may render your device unusable.".format(path) fn = None - text = "The following firmware will now be written to your GBxCart v1.3 device:
    - {:s}".format(fw) + text = "The following firmware will now be written to your GBxCart RW device:
    - {:s}".format(fw) text += "

    Do you want to continue?" msgbox = QtWidgets.QMessageBox(parent=self, icon=QtWidgets.QMessageBox.Question, windowTitle="FlashGBX", text=text, standardButtons=QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No) msgbox.setDefaultButton(QtWidgets.QMessageBox.Yes) @@ -223,7 +223,7 @@ class FirmwareUpdaterWindow(QtWidgets.QDialog): self.grpAvailableFwUpdates.setEnabled(False) if path == "": - with zipfile.ZipFile(self.APP_PATH + "/res/fw_GBxCart_RW_v1_3.zip") as archive: + with zipfile.ZipFile(self.APP_PATH + "/res/{:s}".format(self.FW_FILES[self.PCB_VER])) as archive: with archive.open(fn) as f: ihex = f.read().decode("ascii") else: with open(path, "rb") as f: ihex = f.read().decode("ascii") @@ -298,7 +298,7 @@ class FirmwareUpdaterWindow(QtWidgets.QDialog): if self.ResetAVR(delay) is False: fncSetStatus(text="Status: Bootloader error.", enableUI=True) self.prgStatus.setValue(0) - msgbox = QtWidgets.QMessageBox(parent=self, icon=QtWidgets.QMessageBox.Critical, windowTitle="FlashGBX", text="The firmware update was not successful as the GBxCart RW v1.3 bootloader is not responding. If it doesn’t work even after multiple retries, please use the official firmware updater instead.", standardButtons=QtWidgets.QMessageBox.Ok) + msgbox = QtWidgets.QMessageBox(parent=self, icon=QtWidgets.QMessageBox.Critical, windowTitle="FlashGBX", text="The firmware update was not successful as the GBxCart RW bootloader is not responding. If it doesn’t work even after multiple retries, please use the official firmware updater instead.", standardButtons=QtWidgets.QMessageBox.Ok) answer = msgbox.exec() return 2 @@ -326,13 +326,13 @@ class FirmwareUpdaterWindow(QtWidgets.QDialog): fncSetStatus("Status: Waiting for bootloader... (+{:d}ms)".format(math.ceil(delay * 1000))) if self.ResetAVR(delay) is False: fncSetStatus(text="Status: Bootloader error.", enableUI=True) - msgbox = QtWidgets.QMessageBox(parent=self, icon=QtWidgets.QMessageBox.Critical, windowTitle="FlashGBX", text="The firmware update was not successful as the GBxCart RW v1.3 bootloader is not responding. If it doesn’t work even after multiple retries, please use the official firmware updater instead.", standardButtons=QtWidgets.QMessageBox.Ok) + msgbox = QtWidgets.QMessageBox(parent=self, icon=QtWidgets.QMessageBox.Critical, windowTitle="FlashGBX", text="The firmware update was not successful as the GBxCart RW bootloader is not responding. If it doesn’t work even after multiple retries, please use the official firmware updater instead.", standardButtons=QtWidgets.QMessageBox.Ok) answer = msgbox.exec() return 2 lives -= 1 if lives < 0: fncSetStatus(text="Status: Bootloader timeout.", enableUI=True) - msgbox = QtWidgets.QMessageBox(parent=self, icon=QtWidgets.QMessageBox.Critical, windowTitle="FlashGBX", text="The firmware update was not successful as the GBxCart RW v1.3 bootloader is not responding. If it doesn’t work even after multiple retries, please use the official firmware updater instead.", standardButtons=QtWidgets.QMessageBox.Ok) + msgbox = QtWidgets.QMessageBox(parent=self, icon=QtWidgets.QMessageBox.Critical, windowTitle="FlashGBX", text="The firmware update was not successful as the GBxCart RW bootloader is not responding. If it doesn’t work even after multiple retries, please use the official firmware updater instead.", standardButtons=QtWidgets.QMessageBox.Ok) answer = msgbox.exec() return 2 continue diff --git a/FlashGBX/hw_GBxCartRW.py b/FlashGBX/hw_GBxCartRW.py index 2f71390..b61d5e5 100644 --- a/FlashGBX/hw_GBxCartRW.py +++ b/FlashGBX/hw_GBxCartRW.py @@ -16,7 +16,7 @@ from . import Util class GbxDevice: DEVICE_NAME = "GBxCart RW" DEVICE_MIN_FW = 1 - DEVICE_MAX_FW = 5 + DEVICE_MAX_FW = 6 DEVICE_CMD = { "NULL":0x30, @@ -49,6 +49,8 @@ class GbxDevice: "DMG_MBC7_READ_EEPROM":0xB5, "DMG_MBC7_WRITE_EEPROM":0xB6, "DMG_MBC6_MMSA_WRITE_FLASH":0xB7, + "DMG_SET_BANK_CHANGE_CMD":0xB8, + "DMG_EEPROM_WRITE":0xB9, "AGB_CART_READ":0xC1, "AGB_CART_WRITE":0xC2, "AGB_CART_READ_SRAM":0xC3, @@ -59,7 +61,7 @@ class GbxDevice: "AGB_CART_READ_3D_MEMORY":0xC8, "AGB_BOOTUP_SEQUENCE":0xC9, "DMG_FLASH_WRITE_BYTE":0xD1, - "AGB_FLASH_WRITE_BYTE":0xD2, + "AGB_FLASH_WRITE_SHORT":0xD2, "FLASH_PROGRAM":0xD3, "CART_WRITE_FLASH_CMD":0xD4, } @@ -80,6 +82,8 @@ class GbxDevice: "DMG_READ_CS_PULSE":[8, 0x08], "DMG_WRITE_CS_PULSE":[8, 0x09], "FLASH_DOUBLE_DIE":[8, 0x0A], + "DMG_READ_METHOD":[8, 0x0B], + "AGB_READ_METHOD":[8, 0x0C], } PCB_VERSIONS = {4:'v1.3', 5:'v1.4', 6:'v1.4a', 101:'Mini v1.0d'} @@ -88,6 +92,7 @@ class GbxDevice: FW = [] FW_UPDATE_REQ = False + FW_VAR = {} MODE = None PORT = '' DEVICE = None @@ -187,7 +192,6 @@ class GbxDevice: except SerialException as e: if "Permission" in str(e): conn_msg.append([3, "The GBxCart RW device on port " + ports[i] + " couldn’t be accessed. Make sure your user account has permission to use it and it’s not already in use by another application."]) - print(str(e)) else: conn_msg.append([3, "A critical error occured while trying to access the GBxCart RW device on port " + ports[i] + ".\n\n" + str(e)]) continue @@ -247,7 +251,7 @@ class GbxDevice: def IsSupportedMbc(self, mbc): if self.CanPowerCycleCart(): - return mbc in ( 0x00, 0x01, 0x02, 0x03, 0x06, 0x0B, 0x0D, 0x10, 0x13, 0x19, 0x1A, 0x1B, 0x1C, 0x1E, 0x20, 0x22, 0xFC, 0xFD, 0xFE, 0xFF, 0x101, 0x103, 0x104, 0x105, 0x201, 0x202 ) + return mbc in ( 0x00, 0x01, 0x02, 0x03, 0x06, 0x0B, 0x0D, 0x10, 0x13, 0x19, 0x1A, 0x1B, 0x1C, 0x1E, 0x20, 0x22, 0xFC, 0xFD, 0xFE, 0xFF, 0x101, 0x103, 0x104, 0x105, 0x201, 0x202, 0x203, 0x204 ) else: return mbc in ( 0x00, 0x01, 0x02, 0x03, 0x06, 0x0B, 0x0D, 0x10, 0x13, 0x19, 0x1A, 0x1B, 0x1C, 0x1E, 0x20, 0x22, 0xFC, 0xFD, 0xFE, 0xFF, 0x101, 0x103, 0x104, 0x105, 0x202 ) @@ -277,6 +281,13 @@ class GbxDevice: self.DEVICE.reset_output_buffer() return self.LoadFirmwareVersion() except SerialException as e: + print("Connection lost!") + try: + if e.args[0].startswith("ClearCommError failed"): + self.DEVICE.close() + return False + except: + pass print(str(e)) return False @@ -321,6 +332,9 @@ class GbxDevice: else: return "{:s} – Firmware {:s} ({:s})".format(self.GetFullName(), self.GetFirmwareVersion(), self.PORT) + def GetOfficialWebsite(self): + return "https://www.gbxcart.com/" + def SupportsFirmwareUpdates(self): return self.FW["pcb_ver"] in (4, 5, 6) @@ -373,8 +387,8 @@ class GbxDevice: if not isinstance(data, bytearray): data = bytearray([data]) - dstr = ' '.join(format(x, '02X') for x in data) - dprint("[{:02X}] {:s}".format(int(len(dstr)/3) + 1, dstr[:96])) + #dstr = ' '.join(format(x, '02X') for x in data) + #dprint("[{:02X}] {:s}".format(int(len(dstr)/3) + 1, dstr[:96])) self.DEVICE.write(data) self.DEVICE.flush() @@ -405,6 +419,7 @@ class GbxDevice: def _set_fw_variable(self, key, value): dprint("Setting firmware variable {:s} to 0x{:X}".format(key, value)) + self.FW_VAR[key] = value size = 0 for (k, v) in self.DEVICE_VAR.items(): @@ -421,14 +436,20 @@ class GbxDevice: buffer.extend(struct.pack(">I", key)) buffer.extend(struct.pack(">I", value)) self._write(buffer) - + def _cart_read(self, address, length=0, agb_save_flash=False): if self.MODE == "DMG": if length == 0: length = 1 - return struct.unpack("B", self.ReadROM(address, 1))[0] + if address < 0xA000: + return struct.unpack("B", self.ReadROM(address, 1))[0] + else: + return struct.unpack("B", self.ReadRAM(address - 0xA000, 1))[0] else: - return self.ReadROM(address, length) + if address < 0xA000: + return self.ReadROM(address, length) + else: + return self.ReadRAM(address - 0xA000, length) elif self.MODE == "AGB": if length == 0: length = 2 @@ -440,7 +461,7 @@ class GbxDevice: return self.ReadROM(address, length) def _cart_write(self, address, value, flashcart=False, sram=False): - dprint("Writing to cartridge: 0x{:X} = 0x{:X}".format(address, value & 0xFF), flashcart, sram) + dprint("Writing to cartridge: 0x{:X} = 0x{:X} (flashcart={:s}, sram={:s})".format(address, value & 0xFF, str(flashcart), str(sram))) if self.MODE == "DMG": if flashcart: buffer = bytearray([self.DEVICE_CMD["DMG_FLASH_WRITE_BYTE"]]) @@ -457,7 +478,7 @@ class GbxDevice: self._read(1) return elif flashcart: - buffer = bytearray([self.DEVICE_CMD["AGB_FLASH_WRITE_BYTE"]]) + buffer = bytearray([self.DEVICE_CMD["AGB_FLASH_WRITE_SHORT"]]) else: buffer = bytearray([self.DEVICE_CMD["AGB_CART_WRITE"]]) @@ -465,19 +486,33 @@ class GbxDevice: buffer.extend(struct.pack(">H", value & 0xFFFF)) self._write(buffer) - def _cart_write_flash(self, commands): + def _cart_write_flash(self, commands, flashcart=False): + if self.FW["fw_ver"] < 6 and not (self.MODE == "AGB" and not flashcart): + for command in commands: + self._cart_write(command[0], command[1], flashcart=flashcart) + return + num = len(commands) buffer = bytearray([self.DEVICE_CMD["CART_WRITE_FLASH_CMD"]]) + if self.FW["fw_ver"] >= 6: + buffer.extend(struct.pack("B", 1 if flashcart else 0)) buffer.extend(struct.pack("B", num)) for i in range(0, num): - #dprint("Writing to cartridge: 0x{:X} = 0x{:X}".format(commands[i][0], commands[i][1] & 0xFF)) - buffer.extend(struct.pack(">I", commands[i][0])) - buffer.extend(struct.pack("B", commands[i][1])) + dprint("Writing to cartridge: 0x{:X} = 0x{:X} ({:d} of {:d})".format(commands[i][0], commands[i][1], i+1, num)) + if self.MODE == "AGB" and flashcart: + buffer.extend(struct.pack(">I", commands[i][0] >> 1)) + else: + buffer.extend(struct.pack(">I", commands[i][0])) + + if self.FW["fw_ver"] < 6: + buffer.extend(struct.pack("B", commands[i][1])) + else: + buffer.extend(struct.pack(">H", commands[i][1])) self._write(buffer) - - if self._read(1) != 0x01: - print("Error!") + ret = self._read(1) + if ret != 0x01: + print("Error in _cart_write_flash():", ret) def _clk_toggle(self, num): if self.FW["pcb_ver"] not in (5, 6, 101): return False @@ -495,8 +530,6 @@ class GbxDevice: self._write(self.DEVICE_CMD["SET_MODE_DMG"]) elif self.MODE == "AGB": self._write(self.DEVICE_CMD["SET_MODE_AGB"]) - else: - print("{:s}NOTE: Cartridge power cycling is not supported by this device.{:s}".format(ANSI.YELLOW, ANSI.RESET)) def CartPowerOff(self, delay=0.1): if self.FW["pcb_ver"] in (5, 6): @@ -519,10 +552,14 @@ class GbxDevice: if mode == "DMG": self._write(self.DEVICE_CMD["SET_MODE_DMG"]) self._write(self.DEVICE_CMD["SET_VOLTAGE_5V"]) + self._set_fw_variable("DMG_READ_METHOD", 1) + self._set_fw_variable("CART_MODE", 1) self.MODE = "DMG" elif mode == "AGB": self._write(self.DEVICE_CMD["SET_MODE_AGB"]) self._write(self.DEVICE_CMD["SET_VOLTAGE_3_3V"]) + self._set_fw_variable("AGB_READ_METHOD", 0) + self._set_fw_variable("CART_MODE", 2) self.MODE = "AGB" self._set_fw_variable(key="ADDRESS", value=0) self.CartPowerOn() @@ -534,7 +571,7 @@ class GbxDevice: return (list(self.SUPPORTED_CARTS['AGB'].keys()), list(self.SUPPORTED_CARTS['AGB'].values())) def SetProgress(self, args): - if self.CANCEL and args["action"] != "ABORT": return + if self.CANCEL and args["action"] not in ("ABORT", "FINISHED"): return if args["action"] == "UPDATE_POS": self.POS = args["pos"] self.INFO["transferred"] = args["pos"] @@ -543,7 +580,9 @@ class GbxDevice: except AttributeError: if self.SIGNAL is not None: self.SIGNAL(args) - if args["action"] == "FINISHED": self.SIGNAL = None + + if args["action"] == "FINISHED": + self.SIGNAL = None def ReadInfo(self, setPinsAsInputs=False, checkRtc=True): if not self.IsConnected(): raise Exception("Couldn’t access the the device.") @@ -555,7 +594,7 @@ class GbxDevice: self._write(self.DEVICE_CMD["OFW_CART_MODE"]) # Reset LEDs self._read(1) self.CartPowerOn() - + if self.MODE == "DMG": self._write(self.DEVICE_CMD["SET_VOLTAGE_5V"]) self._write(self.DEVICE_CMD["DMG_MBC_RESET"], wait=True) @@ -566,28 +605,30 @@ class GbxDevice: self._write(self.DEVICE_CMD["AGB_BOOTUP_SEQUENCE"], wait=True) header = self.ReadROM(0, 0x180) + if Util.DEBUG: + with open("debug_header.bin", "wb") as f: f.write(header) if header is False or len(header) != 0x180: print("{:s}\n{:s}Couldn’t read the cartridge information. Please try again.{:s}".format(str(header), ANSI.RED, ANSI.RESET)) return False - if Util.DEBUG: - with open("debug_header.bin", "wb") as f: f.write(header) - - # Unlock DACS carts on older firmware - if self.MODE == "AGB" and (self.FW["pcb_ver"] not in (5, 6, 101) or self.FW["fw_ver"] == 1): - if header[0x04:0x04+0x9C] == bytearray([0x00] * 0x9C): - self.ReadROM(0x1FFFFE0, 20) - header = self.ReadROM(0, 0x180) # Parse ROM header if self.MODE == "DMG": data = RomFileDMG(header).GetHeader() - if data["logo_correct"] is False: # try to fix weird bootlegs + if data["game_title"] == "TETRIS" and hashlib.sha1(header).digest() != bytearray([0x1D, 0x69, 0x2A, 0x4B, 0x31, 0x7A, 0xA5, 0xE9, 0x67, 0xEE, 0xC2, 0x2F, 0xCC, 0x32, 0x43, 0x8C, 0xCB, 0xC5, 0x78, 0x0B]): # Sachen + header = self.ReadROM(0, 0x280) + data = RomFileDMG(header).GetHeader() + if data["logo_correct"] is False and not b"Future Console Design" in header: # workaround for strange bootlegs self._cart_write(0, 0xFF) time.sleep(0.1) - header = self.ReadROM(0, 0x180) + header = self.ReadROM(0, 0x280) + data = RomFileDMG(header).GetHeader() + if data["mapper_raw"] == 0x203 or b"Future Console Design" in header: # Xploder GB version number + self._cart_write(0x0006, 0) + header[0:0x10] = self.ReadROM(0x4000, 0x10) + header[0xD0:0xE0] = self.ReadROM(0x40D0, 0x10) data = RomFileDMG(header).GetHeader() - _mbc = DMG_MBC().GetInstance(args={"mbc":data["features_raw"]}, cart_write_fncptr=self._cart_write, cart_read_fncptr=self._cart_read, cart_powercycle_fncptr=self.CartPowerCycle, clk_toggle_fncptr=self._clk_toggle) + _mbc = DMG_MBC().GetInstance(args={"mbc":data["mapper_raw"]}, cart_write_fncptr=self._cart_write, cart_read_fncptr=self._cart_read, cart_powercycle_fncptr=self.CartPowerCycle, clk_toggle_fncptr=self._clk_toggle) if checkRtc: data["has_rtc"] = _mbc.HasRTC() is True if data["has_rtc"] is True: @@ -597,26 +638,35 @@ class GbxDevice: data["has_rtc"] = False elif self.MODE == "AGB": + # Unlock DACS carts on older firmware + if self.FW["pcb_ver"] not in (5, 6, 101) or self.FW["fw_ver"] == 1: + if header[0x04:0x04+0x9C] == bytearray([0x00] * 0x9C): + self.ReadROM(0x1FFFFE0, 20) + header = self.ReadROM(0, 0x180) + data = RomFileAGB(header).GetHeader() - if data["logo_correct"] is False: # workaround for weird bootlegs + if data["logo_correct"] is False: # workaround for strange bootlegs self._cart_write(0, 0xFF) time.sleep(0.1) header = self.ReadROM(0, 0x180) data = RomFileAGB(header).GetHeader() - # Check where the ROM data repeats - size_check = header[0xA0:0xA0+16] - currAddr = 0x400000 - while currAddr < 0x2000000: - buffer = self.ReadROM(currAddr + 0xA0, 64)[:16] - if buffer == size_check: break - currAddr += 0x400000 - data["rom_size"] = currAddr - - if (data["3d_memory"] == True): + if data["empty"] or data["empty_nocart"]: + data["rom_size"] = 0x2000000 + elif (data["3d_memory"] == True): data["rom_size"] = 0x4000000 - elif (self.ReadROM(0x1FFE000, 0x0C) == b"AGBFLASHDACS"): - data["dacs_8m"] = True + else: + # Check where the ROM data repeats (for unlicensed carts) + size_check = header[0xA0:0xA0+16] + currAddr = 0x400000 + while currAddr < 0x2000000: + buffer = self.ReadROM(currAddr + 0xA0, 64)[:16] + if buffer == size_check: break + currAddr += 0x400000 + data["rom_size"] = currAddr + + if (self.ReadROM(0x1FFE000, 0x0C) == b"AGBFLASHDACS"): + data["dacs_8m"] = True if checkRtc and data["logo_correct"] is True and header[0xC5] == 0 and header[0xC7] == 0 and header[0xC9] == 0: _agb_gpio = AGB_GPIO(args={"rtc":True}, cart_write_fncptr=self._cart_write, cart_read_fncptr=self._cart_read, cart_powercycle_fncptr=self.CartPowerCycle, clk_toggle_fncptr=self._clk_toggle) @@ -653,7 +703,7 @@ class GbxDevice: self.INFO["has_rtc"] = has_rtc if self.MODE == "DMG" and mbc is None: - mbc = info["features_raw"] + mbc = info["mapper_raw"] if mbc > 0x200: checkSaveType = False (cart_types, cart_type_id, flash_id, cfi_s, cfi) = self.AutoDetectFlash(limitVoltage=limitVoltage) @@ -1124,7 +1174,7 @@ class GbxDevice: for i in range(0, num): self._set_fw_variable("TRANSFER_SIZE", length) self._set_fw_variable("ADDRESS", address) - #dprint("Now in iteration {:d}".format(i)) + dprint("Now in iteration {:d}".format(i)) if buffer[i*length:i*length+length] == bytearray([0xFF] * length): skip_write = True @@ -1248,14 +1298,22 @@ class GbxDevice: [ address, length - 1 ], [ address, 0x00 ], ]) - while True: + lives = 100 + while lives > 0: self._cart_write(0x4000, 0x70) sr = self._cart_read(address + length - 1) dprint("sr=0x{:X}".format(sr)) if sr & 0x80 == 0x80: break time.sleep(0.001) + lives -= 1 self._cart_write(0x4000, 0xFF) + if lives == 0: + self.CANCEL_ARGS = {"info_type":"msgbox_critical", "info_msg":"Flash write error (response = {:s}) in iteration {:d} while trying to write 0x{:X} bytes".format(str(sr), i, length)} + self.CANCEL = True + self.ERROR = True + return False + address += length if self.INFO["action"] == self.ACTIONS["ROM_WRITE"] and not self.NO_PROG_UPDATE: self.SetProgress({"action":"WRITE", "bytes_added":length}) @@ -1263,11 +1321,48 @@ class GbxDevice: self._cart_write(address - 1, 0xFF) self.SKIPPING = skip_write + def WriteROM_DMG_EEPROM(self, address, buffer, bank, eeprom_buffer_size=0x80): + length = len(buffer) + if self.FW["pcb_ver"] not in (5, 6, 101): + max_length = 256 + else: + max_length = 1024 + num = math.ceil(length / max_length) + if length > max_length: length = max_length + dprint("Writing 0x{:X} bytes to EEPROM in {:d} iteration(s)".format(length, num)) + + for i in range(0, num): + self._set_fw_variable("BUFFER_SIZE", eeprom_buffer_size) + self._set_fw_variable("TRANSFER_SIZE", length) + self._set_fw_variable("ADDRESS", address) + dprint("Now in iteration {:d}".format(i)) + + self._write(self.DEVICE_CMD["DMG_EEPROM_WRITE"]) + self._write(buffer[i*length:i*length+length]) + ret = self._read(1) + if ret not in (0x01, 0x03): + self.CANCEL_ARGS = {"info_type":"msgbox_critical", "info_msg":"EEPROM write error (response = {:s}) in iteration {:d} while trying to write 0x{:X} bytes".format(str(ret), i, length)} + self.CANCEL = True + self.ERROR = True + return False + + address += length + if self.INFO["action"] == self.ACTIONS["ROM_WRITE"] and not self.NO_PROG_UPDATE: + self.SetProgress({"action":"WRITE", "bytes_added":length}) + + self._cart_write_flash([ + [0x0006, 0x01], + [0x5555, 0xAA], + [0x2AAA, 0x55], + [0x5555, 0xF0], + [0x0006, bank] + ]) + def CheckROMStable(self): if not self.IsConnected(): raise Exception("Couldn’t access the the device.") - buffer1 = self.ReadROM(0x100, 0x40) + buffer1 = self.ReadROM(0x80, 0x40) time.sleep(0.05) - buffer2 = self.ReadROM(0x100, 0x40) + buffer2 = self.ReadROM(0x80, 0x40) return buffer1 == buffer2 def AutoDetectFlash(self, limitVoltage=False): @@ -1277,12 +1372,13 @@ class GbxDevice: flash_id_found = False supported_carts = list(self.SUPPORTED_CARTS[self.MODE].values()) + if self.MODE == "DMG": if limitVoltage: self._write(self.DEVICE_CMD["SET_VOLTAGE_3_3V"]) else: self._write(self.DEVICE_CMD["SET_VOLTAGE_5V"]) - time.sleep(0.25) + time.sleep(0.1) self._write(self.DEVICE_CMD["SET_MODE_DMG"]) elif self.MODE == "DMG": self._write(self.DEVICE_CMD["SET_MODE_AGB"]) @@ -1295,21 +1391,21 @@ class GbxDevice: dprint("*** Now checking: {:s}\n".format(flashcart_meta["names"][0])) if self.MODE == "DMG": - if flashcart_meta["write_pin"] == "WR": - we = 0x01 # FLASH_WE_PIN_WR - elif flashcart_meta["write_pin"] in ("AUDIO", "VIN"): - we = 0x02 # FLASH_WE_PIN_AUDIO - elif flashcart_meta["write_pin"] == "WR+RESET": - we = 0x03 # FLASH_WE_PIN_WR_RESET - self._set_fw_variable("FLASH_WE_PIN", we) + we = flashcart_meta["write_pin"] + if we == "WR": + self._set_fw_variable("FLASH_WE_PIN", 0x01) # FLASH_WE_PIN_WR + elif we in ("AUDIO", "VIN"): + self._set_fw_variable("FLASH_WE_PIN", 0x02) # FLASH_WE_PIN_AUDIO + elif we == "WR+RESET": + self._set_fw_variable("FLASH_WE_PIN", 0x03) # FLASH_WE_PIN_WR_RESET - flashcart = Flashcart(config=flashcart_meta, cart_write_fncptr=self._cart_write, cart_read_fncptr=self.ReadROM, cart_powercycle_fncptr=self.CartPowerCycle) + flashcart = Flashcart(config=flashcart_meta, cart_write_fncptr=self._cart_write, cart_write_fast_fncptr=self._cart_write_flash, cart_read_fncptr=self.ReadROM, cart_powercycle_fncptr=self.CartPowerCycle) flashcart.Reset(full_reset=False) flashcart.Unlock() if "flash_ids" in flashcart_meta and len(flashcart_meta["flash_ids"]) > 0: vfid = flashcart.VerifyFlashID() if vfid is not False: - (verified, cart_flash_id) = flashcart.VerifyFlashID() + (verified, cart_flash_id) = vfid if verified and cart_flash_id in flashcart_meta["flash_ids"]: flash_id = cart_flash_id flash_id_found = True @@ -1349,10 +1445,20 @@ class GbxDevice: if self.MODE == "DMG" and not flash_id_found: self._write(self.DEVICE_CMD["SET_VOLTAGE_5V"]) - time.sleep(0.25) + time.sleep(0.1) return (flash_types, flash_type_id, flash_id, cfi_s, cfi) def CheckFlashChip(self, limitVoltage=False, cart_type=None): # aka. the most horribly written function + if cart_type is not None: + if self.MODE == "DMG": + if cart_type["write_pin"] == "WR": + we = 0x01 # FLASH_WE_PIN_WR + elif cart_type["write_pin"] in ("AUDIO", "VIN"): + we = 0x02 # FLASH_WE_PIN_AUDIO + elif cart_type["write_pin"] == "WR+RESET": + we = 0x03 # FLASH_WE_PIN_WR_RESET + self._set_fw_variable("FLASH_WE_PIN", we) + if self.FW["pcb_ver"] in (5, 6, 101): self._write(self.DEVICE_CMD["OFW_CART_MODE"]) self._read(1) @@ -1375,6 +1481,13 @@ class GbxDevice: #{ 'read_cfi':[[0x4000, 0x98]], 'read_identifier':[[ 0x4000, 0x90 ]], 'reset':[[ 0x4000, 0xFF ]] }, ] + if self.MODE == "DMG": + if limitVoltage: + self._write(self.DEVICE_CMD["SET_VOLTAGE_3_3V"]) + else: + self._write(self.DEVICE_CMD["SET_VOLTAGE_5V"]) + time.sleep(0.1) + check_buffer = self.ReadROM(0, 0x400) d_swap = None cfi_info = "" @@ -1385,11 +1498,6 @@ class GbxDevice: cfi = {'raw':b''} if self.MODE == "DMG": - if limitVoltage: - self._write(self.DEVICE_CMD["SET_VOLTAGE_3_3V"]) - else: - self._write(self.DEVICE_CMD["SET_VOLTAGE_5V"]) - time.sleep(0.25) rom_string = "[ ROM ] " + rom_string we_pins = [ "WR", "AUDIO" ] else: @@ -1414,7 +1522,8 @@ class GbxDevice: buffer = self.ReadROM(0, 0x400) for i in range(0, len(method['reset'])): self._cart_write(method['reset'][i][0], method['reset'][i][1], flashcart=True) - if buffer == check_buffer: continue + #if buffer == check_buffer: continue + if buffer == bytearray([0x00] * len(buffer)): continue magic = "{:s}{:s}{:s}".format(chr(buffer[0x20]), chr(buffer[0x22]), chr(buffer[0x24])) if magic == "QRY": # nothing swapped @@ -1482,6 +1591,7 @@ class GbxDevice: self._cart_write(method['read_identifier'][i][0], method["read_identifier"][i][1], flashcart=True) flash_id = self.ReadROM(0, 8) if flash_id == check_buffer[:len(flash_id)]: continue + if flash_id == bytearray([0x00] * len(flash_id)): continue if self.MODE == "DMG": method_string = "[" + we.ljust(5) + "/{:4X}/{:2X}]".format(method['read_identifier'][0][0], method['read_identifier'][0][1]) @@ -1495,7 +1605,7 @@ class GbxDevice: self._cart_write(method['reset'][i][0], method['reset'][i][1], flashcart=True) if cart_type is not None: # reset cartridge if method is known - flashcart = Flashcart(config=cart_type, cart_write_fncptr=self._cart_write, cart_read_fncptr=self.ReadROM, cart_powercycle_fncptr=self.CartPowerCycle, progress_fncptr=None) + flashcart = Flashcart(config=cart_type, cart_write_fncptr=self._cart_write, cart_write_fast_fncptr=self._cart_write_flash, cart_read_fncptr=self.ReadROM, cart_powercycle_fncptr=self.CartPowerCycle, progress_fncptr=None) flashcart.Reset(full_reset=False) if "method" in cfi: @@ -1543,7 +1653,8 @@ class GbxDevice: flash_id = self.ReadROM(method["start_addr"], 8) else: flash_id = self.ReadROM(0, 8) - + if flash_id == bytearray([0x00] * len(flash_id)): continue + for i in range(0, len(method['reset'])): self._cart_write(method['reset'][i][0], method["reset"][i][1], flashcart=True) @@ -1561,10 +1672,10 @@ class GbxDevice: if self.MODE == "DMG": self._write(self.DEVICE_CMD["SET_VOLTAGE_5V"]) - time.sleep(0.25) + time.sleep(0.2) if cart_type is not None: # reset cartridge if method is known - flashcart = Flashcart(config=cart_type, cart_write_fncptr=self._cart_write, cart_read_fncptr=self.ReadROM, cart_powercycle_fncptr=self.CartPowerCycle, progress_fncptr=None) + flashcart = Flashcart(config=cart_type, cart_write_fncptr=self._cart_write, cart_write_fast_fncptr=self._cart_write_flash, cart_read_fncptr=self.ReadROM, cart_powercycle_fncptr=self.CartPowerCycle, progress_fncptr=None) flashcart.Reset(full_reset=True) flash_id = "" @@ -1626,13 +1737,18 @@ class GbxDevice: if i == args["cart_type"]: try: cart_type["_index"] = cart_type["names"].index(list(self.SUPPORTED_CARTS[self.MODE].keys())[i]) - flashcart = Flashcart(config=cart_type, cart_write_fncptr=self._cart_write, cart_read_fncptr=self.ReadROM, cart_powercycle_fncptr=self.CartPowerCycle, progress_fncptr=self.SetProgress) + flashcart = Flashcart(config=cart_type, cart_write_fncptr=self._cart_write, cart_write_fast_fncptr=self._cart_write_flash, cart_read_fncptr=self.ReadROM, cart_powercycle_fncptr=self.CartPowerCycle, progress_fncptr=self.SetProgress) except: pass buffer_len = 0x4000 if self.MODE == "DMG": - self.INFO["features_raw"] = args["mbc"] + self.INFO["mapper_raw"] = args["mbc"] + if not self.IsSupportedMbc(args["mbc"]): + msg = "This cartridge uses a mapper that is not supported by {:s} using your {:s} device. An updated hardware revision is required.".format(Util.APPNAME, self.GetFullName()) + self.SetProgress({"action":"ABORT", "info_type":"msgbox_critical", "info_msg":msg, "abortable":False}) + return False + _mbc = DMG_MBC().GetInstance(args=args, cart_write_fncptr=self._cart_write, cart_read_fncptr=self._cart_read, cart_powercycle_fncptr=self.CartPowerCycle, clk_toggle_fncptr=self._clk_toggle) self._write(self.DEVICE_CMD["SET_MODE_DMG"]) @@ -1643,6 +1759,9 @@ class GbxDevice: self._set_fw_variable("DMG_READ_CS_PULSE", 1) _mbc.EnableMapper() self._set_fw_variable("DMG_READ_CS_PULSE", 0) + elif _mbc.GetName() == "Sachen": + start_bank = int(args["rom_size"] / 0x4000) + _mbc.SetStartBank(start_bank) else: _mbc.EnableMapper() @@ -1701,6 +1820,7 @@ class GbxDevice: cancel_args = {"action":"ABORT", "abortable":False} cancel_args.update(self.CANCEL_ARGS) self.CANCEL_ARGS = {} + self.ERROR_ARGS = {} self.SetProgress(cancel_args) try: if file is not None: file.close() @@ -1770,14 +1890,26 @@ class GbxDevice: file.close() # Calculate Global Checksum + self.INFO["file_crc32"] = zlib.crc32(buffer) & 0xFFFFFFFF + self.INFO["file_sha1"] = hashlib.sha1(buffer).hexdigest() + #self.INFO["file_sha256"] = hashlib.sha256(buffer).hexdigest() + #self.INFO["file_md5"] = hashlib.md5(buffer).hexdigest() if self.MODE == "DMG": chk = _mbc.CalcChecksum(buffer) elif self.MODE == "AGB": - chk = zlib.crc32(buffer) & 0xFFFFFFFF - + chk = self.INFO["file_crc32"] self.INFO["rom_checksum_calc"] = chk - self.INFO["file_crc32"] = zlib.crc32(buffer) & 0xFFFFFFFF - self.INFO["file_sha1"] = hashlib.sha1(buffer).hexdigest() + + # Check for ROM loops + self.INFO["loop_detected"] = False + temp = min(0x2000000, len(buffer)) + while temp > 0x4000: + temp = temp >> 1 + if (buffer[0:0x4000] == buffer[temp:temp+0x4000]): + if buffer[0:temp] == buffer[temp:temp*2]: + self.INFO["loop_detected"] = temp + else: + break # ↓↓↓ Switch to first ROM bank if self.MODE == "DMG": @@ -1807,6 +1939,10 @@ class GbxDevice: extra_size = 0 audio_low = False if self.MODE == "DMG": + if not self.IsSupportedMbc(args["mbc"]): + msg = "This cartridge uses a mapper that is not supported by {:s} using your {:s} device. An updated hardware revision is required.".format(Util.APPNAME, self.GetFullName()) + self.SetProgress({"action":"ABORT", "info_type":"msgbox_critical", "info_msg":msg, "abortable":False}) + return False save_size = args["save_type"] ram_banks = _mbc.GetRAMBanks(save_size) buffer_len = min(0x200, _mbc.GetRAMBankSize()) @@ -1814,6 +1950,7 @@ class GbxDevice: self._set_fw_variable("DMG_WRITE_CS_PULSE", 0) self._set_fw_variable("DMG_READ_CS_PULSE", 0) + # Enable mappers if _mbc.GetName() == "TAMA5": self._set_fw_variable("DMG_WRITE_CS_PULSE", 1) self._set_fw_variable("DMG_READ_CS_PULSE", 1) @@ -1828,6 +1965,32 @@ class GbxDevice: self._set_fw_variable("FLASH_METHOD", 0x04) # FLASH_METHOD_DMG_MBC6 self._set_fw_variable("FLASH_WE_PIN", 0x01) # WR _mbc.EnableFlash(enable=True, enable_write=True if (args["mode"] == 3) else False) + elif _mbc.GetName() == "Xploder GB": + empty_data_byte = 0xFF + self._set_fw_variable("FLASH_PULSE_RESET", 0) + self._set_fw_variable("FLASH_DOUBLE_DIE", 0) + self._write(self.DEVICE_CMD["SET_FLASH_CMD"]) + self._write(0x00) # FLASH_COMMAND_SET_NONE + self._write(0x01) # FLASH_METHOD_UNBUFFERED + self._write(0x01) # FLASH_WE_PIN_WR + commands = [ + [ 0x5555, 0xAA ], + [ 0x2AAA, 0x55 ], + [ 0x5555, 0xA0 ], + ] + for i in range(0, 6): + if i >= len(commands): + self._write(bytearray(struct.pack(">I", 0)) + bytearray(struct.pack(">H", 0))) + else: + self._write(bytearray(struct.pack(">I", commands[i][0])) + bytearray(struct.pack(">H", commands[i][1]))) + self._set_fw_variable("FLASH_COMMANDS_BANK_1", 1) + self._write(self.DEVICE_CMD["DMG_SET_BANK_CHANGE_CMD"]) + self._write(1) # number of commands + self._write(bytearray(struct.pack(">I", 0x0006))) # address/value + self._write(0) # type = address + ret = self._read(1) + if ret != 0x01: + print("Error in DMG_SET_BANK_CHANGE_CMD:", ret) else: _mbc.EnableMapper() @@ -1918,7 +2081,7 @@ class GbxDevice: command = commands[args["save_type"]][args["mode"] - 2] if args["rtc"] is True: extra_size = 0x10 - + if args["mode"] == 2: # Backup action = "SAVE_READ" buffer = bytearray() @@ -1927,6 +2090,8 @@ class GbxDevice: self.INFO["save_erase"] = args["erase"] if args["erase"] == True: buffer = bytearray([ empty_data_byte ] * save_size) + if self.MODE == "DMG" and _mbc.GetName() == "Xploder GB": + buffer[0] = 0x00 else: if args["path"] is None: buffer = self.INFO["data"] @@ -1956,6 +2121,10 @@ class GbxDevice: if ((buffer_offset - 0x8000) % 0x20000) == 0: dprint("Erasing flash sector at position 0x{:X}".format(buffer_offset)) _mbc.EraseFlashSector() + elif _mbc.GetName() == "Xploder GB": + self._set_fw_variable("DMG_ROM_BANK", bank + 8) + (start_address, bank_size) = _mbc.SelectBankRAM(bank) + end_address = min(save_size, start_address + bank_size) else: self._set_fw_variable("DMG_WRITE_CS_PULSE", 1 if _mbc.WriteWithCSPulse() else 0) (start_address, bank_size) = _mbc.SelectBankRAM(bank) @@ -1994,6 +2163,7 @@ class GbxDevice: cancel_args = {"action":"ABORT", "abortable":False} cancel_args.update(self.CANCEL_ARGS) self.CANCEL_ARGS = {} + self.ERROR_ARGS = {} self.SetProgress(cancel_args) if self.CanPowerCycleCart(): self.CartPowerCycle() return @@ -2005,6 +2175,8 @@ class GbxDevice: temp = self.ReadROM(address=pos, length=buffer_len, skip_init=False, max_length=max_length) elif self.MODE == "DMG" and _mbc.GetName() == "TAMA5": temp = self.ReadRAM_TAMA5() + elif self.MODE == "DMG" and _mbc.GetName() == "Xploder GB": + temp = self.ReadROM(address=0x20000+pos, length=buffer_len, skip_init=False, max_length=max_length) elif self.MODE == "AGB" and args["save_type"] in (1, 2): # EEPROM temp = self.ReadRAM(address=int(pos/8), length=buffer_len, command=command, max_length=max_length) elif self.MODE == "AGB" and args["save_type"] == 6: # DACS @@ -2037,6 +2209,8 @@ class GbxDevice: self.WriteFlash_MBC6(address=pos, buffer=buffer[buffer_offset:buffer_offset+buffer_len], mapper=_mbc) elif self.MODE == "DMG" and _mbc.GetName() == "TAMA5": self.WriteRAM_TAMA5(buffer=buffer[buffer_offset:buffer_offset+buffer_len]) + elif self.MODE == "DMG" and _mbc.GetName() == "Xploder GB": + self.WriteROM_DMG_EEPROM(address=pos, buffer=buffer[buffer_offset:buffer_offset+buffer_len], bank=bank+8) elif self.MODE == "AGB" and args["save_type"] in (1, 2): # EEPROM self.WriteRAM(address=int(pos/8), buffer=buffer[buffer_offset:buffer_offset+buffer_len], command=command) elif self.MODE == "AGB" and args["save_type"] in (4, 5): # FLASH @@ -2121,17 +2295,18 @@ class GbxDevice: file.close() else: self.INFO["data"] = buffer + self.INFO["file_crc32"] = zlib.crc32(buffer) & 0xFFFFFFFF self.INFO["file_sha1"] = hashlib.sha1(buffer).hexdigest() if "verify_write" in args and args["verify_write"] not in (None, False): return True - + elif args["mode"] == 3: # Restore self.INFO["transferred"] = len(buffer) if args["rtc"] is True: advance = args["rtc_advance"] - dprint("rtc_advance:", advance) + self.SetProgress({"action":"UPDATE_RTC", "method":"write"}) if self.MODE == "DMG" and args["rtc"] is True: _mbc.WriteRTC(buffer[-_mbc.GetRTCBufferSize():], advance=advance) elif self.MODE == "AGB": @@ -2174,7 +2349,7 @@ class GbxDevice: _mbc.EnableRAM(enable=False) self._set_fw_variable("DMG_READ_CS_PULSE", 0) if audio_low: self._set_fw_variable("FLASH_WE_PIN", 0x02) - + # Clean up self.INFO["last_action"] = self.INFO["action"] self.INFO["action"] = None @@ -2239,7 +2414,7 @@ class GbxDevice: # Firmware check L2 # Firmware check L5 if (self.FW["pcb_ver"] not in (5, 6, 101) or self.FW["fw_ver"] < 5) and ("flash_commands_on_bank_1" in cart_type and cart_type["flash_commands_on_bank_1"] is True): - self.SetProgress({"action":"ABORT", "info_type":"msgbox_critical", "info_msg":"This cartridge type requires firmware version L5 or higher on the GBxCart RW v1.4 hardware revision. It may also work with older hardware revisions using the official firmware and insideGadgets flasher software available from https://www.gbxcart.com/.", "abortable":False}) + self.SetProgress({"action":"ABORT", "info_type":"msgbox_critical", "info_msg":"This cartridge type requires at least firmware version L5 on the GBxCart RW v1.4 hardware revision or newer. It may also work with older hardware revisions using the official firmware and insideGadgets flasher software available from https://www.gbxcart.com/.", "abortable":False}) return False if (self.FW["pcb_ver"] not in (5, 6, 101) or self.FW["fw_ver"] < 5) and ("double_die" in cart_type and cart_type["double_die"] is True): if self.FW["pcb_ver"] in (5, 6): @@ -2256,7 +2431,7 @@ class GbxDevice: pass if cart_type["command_set"] == "GBMEMORY": - flashcart = Flashcart_DMG_MMSA(config=cart_type, cart_write_fncptr=self._cart_write, cart_read_fncptr=self.ReadROM, progress_fncptr=self.SetProgress) + flashcart = Flashcart_DMG_MMSA(config=cart_type, cart_write_fncptr=self._cart_write, cart_write_fast_fncptr=self._cart_write_flash, cart_read_fncptr=self.ReadROM, progress_fncptr=self.SetProgress) if "buffer_map" not in args: if os.path.exists(os.path.splitext(args["path"])[0] + ".map"): with open(os.path.splitext(args["path"])[0] + ".map", "rb") as file: args["buffer_map"] = file.read() @@ -2280,7 +2455,7 @@ class GbxDevice: data_map_import = bytearray(data_map_import) dprint("Hidden sector data loaded") else: - flashcart = Flashcart(config=cart_type, cart_write_fncptr=self._cart_write, cart_read_fncptr=self.ReadROM, cart_powercycle_fncptr=self.CartPowerCycle, progress_fncptr=self.SetProgress) + flashcart = Flashcart(config=cart_type, cart_write_fncptr=self._cart_write, cart_write_fast_fncptr=self._cart_write_flash, cart_read_fncptr=self.ReadROM, cart_powercycle_fncptr=self.CartPowerCycle, progress_fncptr=self.SetProgress) rumble = "rumble" in flashcart.CONFIG and flashcart.CONFIG["rumble"] is True @@ -2317,6 +2492,12 @@ class GbxDevice: args["mbc"] = mbc else: args["mbc"] = 0x1B # MBC5+SRAM+BATTERY + + if not self.IsSupportedMbc(args["mbc"]): + msg = "This cartridge uses a mapper that is not supported by {:s} using your {:s} device. An updated hardware revision is required.".format(Util.APPNAME, self.GetFullName()) + self.SetProgress({"action":"ABORT", "info_type":"msgbox_critical", "info_msg":msg, "abortable":False}) + return False + _mbc = DMG_MBC().GetInstance(args=args, cart_write_fncptr=self._cart_write, cart_read_fncptr=self._cart_read, cart_powercycle_fncptr=self.CartPowerCycle, clk_toggle_fncptr=self._clk_toggle) self._set_fw_variable("FLASH_PULSE_RESET", 1 if flashcart.PulseResetAfterWrite() else 0) @@ -2356,6 +2537,8 @@ class GbxDevice: elif command_set_type in ("GBMEMORY", "DMG-MBC5-32M-FLASH"): temp = 0x00 dprint("Using GB Memory command set") + elif command_set_type == "BLAZE_XPLODER": + temp = 0x00 else: self.SetProgress({"action":"ABORT", "info_type":"msgbox_critical", "info_msg":"This cartridge type is currently not supported for ROM flashing.", "abortable":False}) return False @@ -2427,10 +2610,36 @@ class GbxDevice: dprint("Setting command #{:d} to 0x{:X}=0x{:X}".format(i, address, value)) self._write(bytearray(struct.pack(">I", address)) + bytearray(struct.pack(">H", value))) - if "flash_commands_on_bank_1" in cart_type and cart_type["flash_commands_on_bank_1"] is True: - self._set_fw_variable("FLASH_COMMANDS_BANK_1", 1) + if self.FW["fw_ver"] >= 6: + if "flash_commands_on_bank_1" in cart_type and cart_type["flash_commands_on_bank_1"] is True: + self._set_fw_variable("FLASH_COMMANDS_BANK_1", 1) + self._write(self.DEVICE_CMD["DMG_SET_BANK_CHANGE_CMD"]) + if "bank_switch" in cart_type["commands"]: + self._write(len(cart_type["commands"]["bank_switch"])) # number of commands + for command in cart_type["commands"]["bank_switch"]: + address = command[0] + value = command[1] + if value == "ID": + self._write(bytearray(struct.pack(">I", address))) # address + self._write(0) # type = address + else: + self._write(bytearray(struct.pack(">I", value))) # value + self._write(1) # type = value + ret = self._read(1) + if ret != 0x01: + print("Error in DMG_SET_BANK_CHANGE_CMD:", ret) + else: + self._write(0, wait=True) + else: + self._set_fw_variable("FLASH_COMMANDS_BANK_1", 0) + self._write(self.DEVICE_CMD["DMG_SET_BANK_CHANGE_CMD"]) + self._write(0, wait=True) else: - self._set_fw_variable("FLASH_COMMANDS_BANK_1", 0) + if "flash_commands_on_bank_1" in cart_type and cart_type["flash_commands_on_bank_1"] is True: + self._set_fw_variable("FLASH_COMMANDS_BANK_1", 1) + else: + self._set_fw_variable("FLASH_COMMANDS_BANK_1", 0) + # ↑↑↑ Load commands into firmware # ↓↓↓ Unlock cartridge @@ -2531,6 +2740,7 @@ class GbxDevice: cancel_args = {"action":"ABORT", "abortable":False} cancel_args.update(self.CANCEL_ARGS) self.CANCEL_ARGS = {} + self.ERROR_ARGS = {} self.SetProgress(cancel_args) if self.CanPowerCycleCart(): self.CartPowerCycle() return @@ -2560,6 +2770,8 @@ class GbxDevice: self._cart_write(pos + buffer_len - 1, 0xF0) elif command_set_type == "DMG-MBC5-32M-FLASH": status = self.WriteROM_DMG_MBC5_32M_FLASH(address=pos, buffer=data_import[buffer_pos:buffer_pos+buffer_len], bank=bank) + elif command_set_type == "BLAZE_XPLODER": + status = self.WriteROM_DMG_EEPROM(address=pos, buffer=data_import[buffer_pos:buffer_pos+buffer_len], bank=bank) else: status = self.WriteROM(address=pos, buffer=data_import[buffer_pos:buffer_pos+buffer_len], flash_buffer_size=flash_buffer_size, skip_init=(skip_init and not self.SKIPPING), rumble_stop=rumble) if status is False: @@ -2606,8 +2818,14 @@ class GbxDevice: verify_args.update({"verify_write":data_import, "rom_size":len(data_import), "path":"", "rtc_area":flashcart.HasRTC()}) self.ReadROM(0, 4) # dummy read verified_size = self._BackupROM(verify_args) - if self.CANCEL is True: - pass + if self.CANCEL: + cancel_args = {"action":"ABORT", "abortable":False} + cancel_args.update(self.CANCEL_ARGS) + self.CANCEL_ARGS = {} + self.ERROR_ARGS = {} + self.SetProgress(cancel_args) + if self.CanPowerCycleCart(): self.CartPowerCycle() + return elif (verified_size is not True) and (len(data_import) != verified_size): self.SetProgress({"action":"ABORT", "info_type":"msgbox_critical", "info_msg":"The ROM was written completely, but verification of written data failed at address 0x{:X}.\n\nPlease make sure that the correct cart type was selected, that your cartridge actually does support ROMs that are {:s} in size and that the cartridge contacts are clean, then re-connect the device and try again from the beginning.".format(verified_size, Util.formatFileSize(len(data_import))), "abortable":False}) return False diff --git a/FlashGBX/hw_GBxCartRW_ofw.py b/FlashGBX/hw_GBxCartRW_ofw.py index 7501670..cda2270 100644 --- a/FlashGBX/hw_GBxCartRW_ofw.py +++ b/FlashGBX/hw_GBxCartRW_ofw.py @@ -17,7 +17,7 @@ class GbxDevice: DEVICE_NAME = "GBxCart RW" DEVICE_MIN_FW = 26 DEVICE_MAX_FW = 30 - + DEVICE_CMD = { "CART_MODE":'C', "GB_MODE":1, @@ -119,7 +119,7 @@ class GbxDevice: "AUDIO_HIGH":'8', "AUDIO_LOW":'9', } - PCB_VERSIONS = {1:'v1.0', 2:'v1.1/v1.2', 4:'v1.3', 90:'XMAS', 100:'Mini v1.0', 101:'Mini v1.0d'} + PCB_VERSIONS = {2:'v1.1/v1.2', 4:'v1.3', 90:'XMAS v1.0', 100:'Mini v1.0', 101:'Mini v1.0d'} SUPPORTED_CARTS = {} FW = [] @@ -185,15 +185,14 @@ class GbxDevice: self.DEVICE = None conn_msg.append([0, "Couldn’t communicate with the GBxCart RW device on port " + ports[i] + ". Please disconnect and reconnect the device, then try again."]) continue + if self.FW[0] == 0: dev.close() self.DEVICE = None return False - elif (self.FW[1] == 4 and self.FW[0] < self.DEVICE_MAX_FW) or (self.FW[0] < self.DEVICE_MIN_FW): - #dev.close() - #self.DEVICE = None - #conn_msg.append([3, "The GBxCart RW device on port " + ports[i] + " requires a firmware update to work with this software. Please try again after updating it to version R" + str(self.DEVICE_MIN_FW) + " or higher.

    Firmware updates are available at https://www.gbxcart.com/."]) - #continue + elif (self.FW[1] == 100 and self.FW[0] < 26): + self.FW_UPDATE_REQ = True + elif (self.FW[1] in (2, 4, 90) and self.FW[0] < self.DEVICE_MAX_FW) or (self.FW[0] < self.DEVICE_MIN_FW): self.FW_UPDATE_REQ = True elif self.FW[0] < self.DEVICE_MAX_FW: #conn_msg.append([1, "The GBxCart RW device on port " + ports[i] + " is running an older firmware version. Please consider updating to version R" + str(self.DEVICE_MAX_FW) + " to make use of the latest features.

    Firmware updates are available at https://www.gbxcart.com/."]) @@ -214,7 +213,7 @@ class GbxDevice: conn_msg.append([0, "For help please visit the insideGadgets Discord: https://gbxcart.com/discord"]) if (self.FW[1] not in (4, 5)): - conn_msg.append([0, "\nNow running in Legacy Mode.\nThis version of FlashGBX was developed to be used with GBxCart RW v1.3 and v1.4.\nOther revisions are untested and may not be fully compatible.\n"]) + conn_msg.append([0, "\nNow running in Legacy Mode.\nThis version of FlashGBX has been tested with GBxCart RW v1.3 and v1.4.\nOther revisions may not be fully compatible.\n"]) elif self.FW[1] == 4: conn_msg.append([0, "{:s}Now running in Legacy Mode. You can install the optimized firmware version L1 in GUI mode from the “Tools” menu.{:s}".format(ANSI.YELLOW, ANSI.RESET)]) @@ -230,10 +229,9 @@ class GbxDevice: except SerialException as e: if "Permission" in str(e): conn_msg.append([3, "The GBxCart RW device on port " + ports[i] + " couldn’t be accessed. Make sure your user account has permission to use it and it’s not already in use by another application."]) - print(str(e)) else: conn_msg.append([3, "A critical error occured while trying to access the GBxCart RW device on port " + ports[i] + ".\n\n" + str(e)]) - continue + continue return conn_msg @@ -290,18 +288,24 @@ class GbxDevice: def GetFullNameExtended(self, more=False): return "{:s} – Firmware {:s} ({:s})".format(self.GetFullName(), self.GetFirmwareVersion(), str(self.PORT)) + def GetOfficialWebsite(self): + return "https://www.gbxcart.com/" + def SupportsFirmwareUpdates(self): - _, pcb = self.FW - if pcb != 4: return False - return True + fw, pcb = self.FW + #return pcb == 4 + return (pcb in (2, 4, 90, 100) and fw > 3) def FirmwareUpdateAvailable(self): fw, pcb = self.FW - if pcb != 4: return False + #if pcb != 4: return False + if pcb not in (2, 4, 90, 100) and fw <= 3: return False + if (self.FW[1] == 100 and self.FW[0] >= 26): return False return fw < self.DEVICE_MAX_FW def GetFirmwareUpdaterClass(self): - if self.FW[1] == 4: # v1.3 + _, pcb = self.FW + if pcb in (2, 4, 90, 100): try: from . import fw_GBxCartRW_v1_3 return (None, fw_GBxCartRW_v1_3.FirmwareUpdaterWindow) @@ -1449,6 +1453,20 @@ class GbxDevice: self.INFO["file_crc32"] = zlib.crc32(data_dump) & 0xFFFFFFFF self.INFO["file_sha1"] = hashlib.sha1(data_dump).hexdigest() + #self.INFO["file_sha256"] = hashlib.sha256(buffer).hexdigest() + #self.INFO["file_md5"] = hashlib.md5(buffer).hexdigest() + + # Check for ROM loops + self.INFO["loop_detected"] = False + temp = min(0x2000000, len(data_dump)) + while temp > 0x4000: + temp = temp >> 1 + if (data_dump[0:0x4000] == data_dump[temp:temp+0x4000]): + if data_dump[0:temp] == data_dump[temp:temp*2]: + self.INFO["loop_detected"] = temp + else: + break + self.SetProgress({"action":"FINISHED"}) ######################################### diff --git a/FlashGBX/res/config.zip b/FlashGBX/res/config.zip index fdaae439879829186f7f124a5d5550ead4bb66a3..51c959e4d3077f4caa9ba021bde9ce1c48e7ec59 100644 GIT binary patch delta 6097 zcmZvf2|Sg}_rUM-*teVI+Ow}&Z>a2B3faQVp0%=zkRq;R$*#hbt*oU|qEf`0vQ?Bd zS0YPMO4g!9{&VHN>i7PAK0e0xoH=La%$YOiDVd-z`Ai>u8#$E70GQGUDsBFT!r*Ws zB2IKPd5p(JeRf?H$&-pJ>M9I~%zcj4Gw7_b)c3Ne(^V8Ru4h^OVP5x0pX5wCnvpki zb|!E77%qBcNZ0`HqODJ=_q8uP>9uPKlWkXQvnAQY`{gHNpKJSUgzHB7m7^G%jKjNZ ztsU$$UcB|`-qC&M6@72_rOK<1%I?Gl4SUcMtp~l_dWDjlc4Qd^)12v(|6FRe?}<7G ztudA2;fpVBj-a@J!Gs>8OcC!&cg+JoC)29ML->Q**~6`$>xn5BDqXL5%IRaFHLh@+ zzE)`bVQjiu$lD*EWpCD~cGqdOXsn!B&VHKoAgHR(-#*@4;vZ#JoyQ@h^cs=ksiG2` zgT3^y$cgl(Kmjk8-^U+YeGeh!(cGac|GzuOh~>Bfy+xbmSsnK{Squy$g5nAf$O z6pcmDAw$@u{B9whRX|%R4zSrgO#y66w-J#Xv@O}=zj(o|mbM!d=SA2_i z4LPnf2XNmj>$Rxxb^N%8*duo8lFLNKqw7(prvxwH?Vi}^8y9|Ml6d*R6Gq5nr$ntT zXQ4$~#9e6*_I9n9;|0&o)8Y?m|LUr}B0a6fdYNHs{RjJb>*kTwbe$_4W$ksPu2&U! z(+0Ptnu^xc*-1p2{7U-9_tUFqb$DRkKnWQ~C)I2(a8y_R{*H=j;)1Wm zbmW4Q;f37pB^OGQrLnqpFxL1S& z81>Tr2+tjUIr)f*<8g*+y67{KLP=M&K%Lv~0?YuEZeZSL6pQK`33A~Qs)>0t{a z|597rr|qFPw=G?q&gyYe*|DfR=ja=%Ny9j~yNK}~Umwx0Je}^oQOiNY8=e>b;bBh) zY<205ufP#U?xbIZ+s>XH468`$-&AwxBH}=WWjU8jxUw8(F%OT7hJk!!oZuo4N(ZKj z5H4bdnK5^w&jlMZE)!Fa#B|0y3CBCD+Jh_=bjBrRQ*`>=s*{0gOHQAkn_FZoM?T9) zFnV8nBmDVz%D%R`>?J8iEuol5tc_yy%02Ni`ThnUy|eM` zyCbS~wg#lWA`_+?F&3^XH|vucOv{5u&&`qih$OCWB3-F9EtML<3c`y5^ZBY~hnFqI zbK{C1pV+%#wzK#6^01$Vfy$8;ts!m0s;o?v55reJ?4H*N`D)B(Yx0yk^jE(C3G?|X z24ybJBc9W1%X`G8fO9PaV{^xXo+uF6wU>V^%sCP;p2pA|Ni( z+K#T!Gcx<6w$bd9o5iPvf63VG?YWyC-M4)S12dewJz>J>-+jz4tzN1z z?jtyKDt$@Y8Fwo2my-m8?1$R7W2a4pY~vky=EV-UTBn-Pd9~IZxLCS$K{{#5{^)Lp zxu22;TX*}&xOqlRAFj8zN!t5vW}sYi6(N^3(8+`H)XbM4E=HlD5;s+;J0ok{C*#v1;F<~^Hh_LLB#V$199De0I_g{le1Tlwd!?C zbf0F1{~m{4;*2_%xK^Nd>Gq`EU)!(OS8IQ%6~NC#AX~+&HD;$9M(%gA-V7W%xl6w- zL!K^n*C`^udVbZru_(cm2gNEHiaZuV#ySqDLx1SBRlr zPI(~zwQ}J-uJ$&kOggF{gFM27nxa)1%@JKshTGol2LrEqUZUo663)Gnh7V$MivKDh z@4^{h_@(<+!d+$Fy;F|zwHc|w1{pt(A76E_{?u0LmTR?bh%-TM!SQtK%B`^p)u82y zAo*l}$@7sNZ#vGMjgM8M5!!mvH*BT#{B_sS>i*_crW(UaM1zy1`a~w7MV+!W2 zq&Fj?#9QR3<|jAl*cB+N(ulVaYAj`Fd$H4dMGjnOitcOD`)cWBxmxdeneh2xY_CJt zmg7sGhw78lSB5#`mMV&PJ3Nu+P3pqwFrW$tuS-rxfwdE>(0tWmN8$OFJ1-wC$v( z?~iKJpEr{fe(}BbuD+2q3ns=Y=Dn^hp6Q|XFn@99N4nX!*i9E#1hrji*jo`TF6y6G zQVBKIL&$boYU`GWFEW$R#5}#=4iCbUdM)XJo(zg3He2n7Z4fTfW>zOt zliN>Y%IS@0sZzY3V#Hats;^hw{2k7!hE}aMNO{u*!MXdK7S>uJ6rGe zMb5(1@>EMd4@+wE&%~Z{Z$1nLfA5{GuV8T*CzaP7oyOv1HyupPuymX)>mH+O^wLS4 zRdwRO!92dNlyxRc-}|xi{S#vLpWbEr_XOH|zuePWMUwr;`Vr=1tii(j;HXv7plyIb zt8NfW`)S%&6(R#>Zzk$C_qh5!7nRaZnGndIWZc`S@u59%&@dq4KuyEBS;iGw@1{M` z^G!c+=^dAF)0%}0r79tx7{1&NiwY&i`7&9E8Qi?e>cW>geGl)~7yj&-|NaE$rf_1* z@|&yg#|K6n+Co{{f2r8k?a{J*!XY8jlIP^JBxG*?DE>tk>qW(y>mQJ65hA4<-NBMZ z2%s)@J`T^~BP>AV9(BC{*#-I?K5+1ep+30+RakZTkdNl*2t1l4k)RmlcZ zNp^SsZ6ryF#A^Yc-h`cdu3+`XiN2=ezSQJ!M4Ro>jZ^LA2fU(pD{|moRbQg%@v4kn z#qtr&$CrXCrGqbQ0zrIyM9V#svpUG+HIDPgRUB2nkHXVzVik(lDpgwu*=CYT2wl+rQNvrrd!_gD znP$K5>U=NGSYZ0Ef4f^-tj?655NY@FzADzn?bO#XRqTGQPOcq|&)=En>(iZi7Gm+q zwLvK@P?1@bWJ}^UND1%3qDolp<^l~O>ZmOpbU(LD2z~4|8R!PKw{YCl5EF0qr7SxTxaoCM<1QvCt@bN#! zCYvB%@mTX!y$SWEj73>D#%Ea301BsJLp>?uBW$RX#P7Kz#hmz-%oeEtwrXD!F* zF&iI{3v8(7E}HuA9cdOMgfhi}P`!`N`tK?KemcQqJqweS7b6bSmHZIeE<Y)Z@(r5A{Qy|8*fe2iv3f1cz1pLZDxIqyY zilZn$fD%L(%yFSI6ePlpatZzYMS;O^{T1UPV8e|{V9Ul5-#;-XA)Bk#wwcHcZTW)+ zZd9Ij9nF!EE|BCwT(U=-$(FwH~hcu>Om?GbZ35yl>5-^Uw03ApzU$+ zg$GrjxZA=D(SG2}yRm{ZWYh*)c;N=u7xk5lf`KgG26E&>RZyyH;0zxs&qP`1!5f*s zBR$NA@=+lb2w$Q?u!dng$mr5Jv?mb00wo>eL68 z#bE&o3SIM#mw>GQB{(Fp!3rQl%z>UH$_J(;P+p2Ivn14Q9RT{08@l0Sra1^ICE@j3 zxA~ULw1)tn6e_J5n@9FTLKyct^Jy*qecj0oVWQTT{Ohpdl4$<-y5AWFQBtTlR&=LE zt2cZLep>>SQqX$V2$+yUrD)d~hcskNW58H?gB>Na_;GMu8kOyx`iswPuAM7QWU0MXp@-U~@^|a;5>?;V7hY#X9DGA$DEdS407}rE^k9A~tUT-5Ta+LhX8dCkO delta 5093 zcmZXW2|SeB8^`B;F$q~}F!p`Rz9vle?5-`69k|HLJxG7QAoG7i9TuX~BVv3Tb zEG0~~EG-ndLMoO28O&AxYd$`6=KK84^E~f!p7)%S|B*5OEn{3I;+wQt6FjmbG+wn8 z1))iZIO$2iLnU?{?KBcQF`%*1++grgO=Yp+)hq)d^KM4s4C8mkj3KAV`zCj`aER8@ zKN9WeO!1A<6=b+VieluD>rZ7Li0Y7aU=Myuh*R&AHZ~v6D3OmIx3fs+G%cT;iFZ#J z6VID0kx^Gn>6-TKL#7<3@#f`e{7%rn0rx5I3`&hYaZA_v)?0m|+|q=_ z_`263e!7p-Hw#(ZdQDtDce2ddqsP8}Ug&nHg4z>4DVO1FpGk*_DE*c$C85s~!KO)d z(pF6BGHNj&qW=Nf;U>f#)rIAw7l*^ux}tWyc=Gnn7MGtbViBx17IgGD3Q>;{ePlh!AO~!l5IFnr$0l1z89+-6SQgU=NKbiR*10F8+Tc3|lBm+S(b8<64Hb z+JD9#%#Ppm!RLfJecQ2^K4Nxi<{abq7FH3u+89*uv*Qkmes$p@qod!moQv{-!@(~r zB1dwr8AOT|)PpH`_P`z?VqoA++4pMqE-dPi)wNGpN8KRR``;QSwidFD-|N#jQ+?un z_ogYp@JOjxhhaW)5k7geG_ilVQ+ckL44)?34idHSxn19b+$os@L zpLctY)#JQ5SUPN zzj<3$S=9Kk0G}ycEF6A%B1FJvWtW6SW5VK{Cf@k{x`YpeCoOG9=*PTIrp3Dxj!%E? z?4uaey@fTJ?6#N8J{Mdw{_@+XjH2z3U)gLGnZY;sn|G)cXHZ+7Z&Za~YjvgSWN@;i z&6$o}dRnnIuc`{m%ScbGlCNeSbJk<2_;%ZAsr9eMc0c(n-1}P&UJr?m0@4~gvaT~b zuzx#s)Fb)MAMv$PN@iPM79lA)srM^8nNAZ~A4MCbgvO+QcUAtfG+}@EaaME<&hnYj z!|>(5Gi+Z+t~e&W%D8X;(~~UWbR5D_5pIAdR0p%=!cG_08{@+`e2)`MQGE z=$t{q*jdkbCpg&VUpGlcFqnEQsLMkHNp68nO1N0Ze{{2S07uW^3i2M;*c==7qZ~5N z3VCnLnA+)Te0~Ltg6|Y7S`QhR^+e1PmM$62KMT-^qcm`IGmKr?~k~Cb8piOx>L6L z7KFyuI#}TokE5UbP z{j6v(?{JP#ecBvarv8;t^vkem2Wps*%STjc$#bQjD;5JK(CE#WNMXSzX*U!IzPJbgT_N#~iv}X$C(~~}B2I2u;Vkx@`|7zX;Ooi6^HD1e(W>-> zrH6#!U3R_ZW@%pcbNqVbik&Rw29Ml~xy3Nl&v__T_c`aprHuRZXU2G@!VjehX1FcH zI_S1rO7`&7I(%iz8GH{6)X(uB4-cH?HM`?*;mrwL@#r~; zk*!|4CCHb^3~v3sS3am;DOK=pA-*19u@kbb&cLX2t#zcZ6zvh zVIh0jE6*pk^gsR#a4LV2e-j_VCHL=21T=rMq*TeT01DyR0z$>Yi_uuJi zCh`#?bIp_6h%)Ck*;s#vBae-MH(YrOdJKjKg8|**C@-nmPM`Xo?vni?WY9^-x7R1` ztA0MFU;Qu=FDg=9oLE&X9$we>bJ_j*PSx&ACBx{-SIZ+yBTKolHwPO(d&`H#_e{~9 z9jY0xQqs(6ByK9objpede&#s9nL9Gva;gM<_O4M=j4nQeBi2kR72g{1r<>zY>3KO; zCRL-za4u3U#bo}^0iwM#`7iRrEA6d+=(UCzjUE}=JK1r>*6)E!;fu$27@R^ilL&T- z2c3+*9k&~Kq5jl=?{yu}ta&>|Ovk6BfHO#*^KMAdQW!ZSP*QD2yt8w%ulH3$?U7V| z)uy^!|I>!}(vLsxRALN14Dv;ltB$)72QM`-Q_k8rlCte47?i zm@KpkT(;@5h&GSZVY+d3(Bc4|Lr>A}R9nt7O18z5c1cR+fWsb~QTK9<#(AckX3syM2zum8VfQ47WZZq#C;|h%diSdb3j+3K0VskkH2RrBY5=<+KKPw#mL(DZ$ z$M=!@8ynZUTusviuVKlt8FKkNecNoRY$-8Q;Ah?49g+5d9D*Ia_HWu8#*z%J_cj(x z{PecF%03#=T{O#FqwxW$5CK`Ns0CJBR=m{21iWEI-DzVpwvBNa8|qEtTVHQ><+utNcgQAraOeTrxCn>3O04;$AXVNG&*Uuo5gQ8x)y1q`^EPhC(wwM{e4ZR9*~_^fafzm; zK|%I8RP5Pg|NE4Un_)U9%1Kvo8UdZB5nhlYgR;oeeQD~M(A#iuKjBgIS&iQvY}}Nn z4v=NAl?#<-jq0)Pqbk=z15++k4bca&(1cw_B~;V^M!8TG8p3l!lnAW2Q59xd&NuZA ztrhw*H_A(wn1O&x83-Tf=SFd~X>-(R3_uc(%F>V%9-=Cc@(XH!h%Oo+ARQo_U^5Sj z`!|Nj^Vqc*1Rj(Zu?2g1pr^f8R@W|x2YMO+X-tb3X-66yr_ov z_saZ!Ny1>bFyinsPW=;x|3!HSxSxk`^D?de#9$B%9y5?LnqmxBYCSla7e?|0^Sm3u zCHWxQ30(M4Ic-|-p!tOV1poc$|Nr3CpotHa*j7hwauhS7g`*%EiivB5VB?3y?#oBA z3lKao;)k_f&&Zu0hPMHk{2LiPrlNabnIGMb4QyF0nykQT-W~!QC=DXI&wnF8xCFX< zB>>C6CZ-@hrW+9<&>@PlupY|N>x_q*=_p7mNAZ7pKC0zT$c7~q#*O@0wz+h)z(=Il`V7w5owegv`C?XG{Xm~A^bVjpiX11(vVH}WXqsD zphOzwq49Rekpm!I8s!B%(1@+_UQJI^2J#WGA`N%-e`HVwaPk*pAvB^+6zB{`H44%u z;kHBm^d&Igjw;aDF)HgE1eM!SY2h{J6okpfotLoi`vyV11%U6{VbdEA1H3FM&gzpH zqU#ym4>$MKl+0zJCy7@eP8OBIK0Lk3%BbwZD46(#vCBcmH4b*mq1%PmbQI(ygXH%_ zM#^so1og53(&S)7(gY}#Lxov+9OKgbB>Rzd?*mj%?_L9T`HhGKdB`rk1rhS7EP>`7 zU#4(%Y54c&`R)D!FFV_Q+wfE;5%6FN!GT$M*w3?}MJhPhUF&f-E5I0~Q$SY%#(jBv z^+|&uqjUOzfS=O{52#mw{jzT9qgt4z5pa73;R0fc8y12h^iwedA{619y{^uOjBej7 z0+2bl^Z)aUnn?+&dFFtg670Kmbr@uHT=NKMSb$whR)W4~DXaNVsA{f7z^V*Sv2~`S z4At*GBjD^8m`RfIhWa|Rpu{iWr80ag*44}^khy;Y`YIbNg34IGgW_M^rJgVbVC;K7LU13nG7P^6T3e+d-|vaWbYRX8)E*-WU&+RhaD&Nr93 xGqFmorlhU`-Og}h%6O_p&Z z(i=Gki@JHay$Bi;9u?6S)=Ami)X@`4gXoGhCe-T~3h6|D39l^fc%0zxV34c|Z;kbK zwy99fEkVYpvKTcH!qt79Bay1<8O#qU%vxY>sf94T3hPiqb#b_NxIsv`DBN3K%R5}f z&oDeABiBa5Jt0LAo^c~6gb@J$-^P41;|%L(Mpy690054H008i&G4GqsT%RBUEzC3b zw36_gO>Me+WB9?)Yo3$`1jgnk)wcO^LfQNeqlqY|uS3qRLRzetWaLs4%QpDOezk^c zoqxMLxK0vFqxR)q!{O~^qkP%eScfh4dFd%T?KXJlalyzyeaw1m#FllK%|WWrJvQH4 z!J`A8vr;?G>=HJu_9uREXe8YYqSuqD2o5~6j8pQ84=y<}BI=W!r#F^)EYli_uN8V) zG*i8}INpM$C|}BpTpCT-a#g@yo;YHIgTszx7*cJFS~nJDrdoZYc7uj%zlR^S1@V6^ z_z+p`(?3&yBq zduKOZc9xSllT~0IorlsZcE?v?vChHd3&u1<#zuD&!j@2_j(PrA**8G)1pYq1b6i1GL-Kp8xs z`G{PQg!UfNF{%a78q(x@wkzvQNTo~X8RTXbve9Nls&aOhcJ(~o>-|%jhsW(6dNlG!PNpga zW;P@T7vqL8e8@2qm0}R8WANFSOhJ7Z>+rjhJ}PwdT|8n)(XT!ZT$-kMm~ok<@|zq10qVAk zxNbfEq!SbL?!JtgHTCg@4NMce+=1&qwIq@vKKqr#(9M46cH!twjShHdt@O|SlTi+$ z?$0+GXi^>U+od5yAYC;wY4aMzrus;FrNFBA2{#?-kKwJPP0rZ!r>=rzCytw|lx;1* zdnXR8yvXamLv=FSAMYrd8=c>B{bAPg(R%&`2?n?(;m|V26-SfQe}9oRMLbmHbZ&R! z$iF64sX?E9TOO~y+)Ua&Ek_waa-c~rQ*+VBx~p$kjLdK~7>at$F}_I63IA8w$vz>f z{UaTW8Vs^tT_X*uoFOL%Vt@Jkj8e9Dl#W;^5wOvwUWr}uc@T;1yz*BJU&Zfc;5S-oXPipJ71nIx}_wh@0Lt+-?o9P7L)~w?epz@>nD|$q*79#68St) z@B6AcCfEdrmb3a`MC7dXqpY(veV2tJc$n@1ZeW?3LF}1mKhz>qB(~|&t%Zs=dWX{l+h@QV2m|n_x zR`Q~{ahcA%_U)r$wzcTbf>|3$54fuki}QTV{-x7=HM*dfoUw*Io<*s`5==e#Hrj7m z(`V9>teBlfJ|<8hjez|Hp+NTVmj}#9MyNblIEZ>wrpUiz$9+qGKq8$mKX$#}OUn{y z|7qn(kk*@wg^z4}pV5sVJK6a(3I&JX$6a0bh91gC)j2TU9$rJ|vqG1$61{MxhtcPs zaSpu(zMgziXiG;i8nhU*Fu$AKqK49(xw9NO$C}4V8myN)8sw9JkGm1w11&fQ~HgST>Gx2`$btAKGrE zJ7MmU(9*Y%AJAj>$^;Jfs)PNI)QJuDFB7ub8lf#}dOpk<;;jPUSb(ZG_4u;ATVHGG ztV*MwHH{o!X059bMbzoo)T8(H<8hch3L0Z@-^Y%OUskrypB8SZ*`Q*KQY;_syKB_y zSowb+RXXx>y!E2=T zXvJef;SJrREji;J67{P%qQ_l~2K^0!bAOzw9gy7VstTK|zYV-9C$t!`>sfMd=SG}H zS*nQsejvGGNB%*Ka)S^bLbZjZ^XJ96)CA}m$kV#=3+r0g9t3{sg-qK%JS

    cSWgnjyXj-}I3xIEDz5?VEg*fzl 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

    lIykR5fwlQ1eXo5 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 literal 0 HcmV?d00001 diff --git a/FlashGBX/res/fw_GBxCart_RW_v1_4.zip b/FlashGBX/res/fw_GBxCart_RW_v1_4.zip index 0ee49505a754cd433bb5e9ea5fd3209307ae5e83..9ab29d5abae341f3d250d7c400d72a028986d50e 100644 GIT binary patch literal 24489 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 literal 22952 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 L{8w3Y{