This commit is contained in:
Lesserkuma
2021-02-28 20:07:48 +01:00
parent e66ed91cf1
commit 53dcb072d7
28 changed files with 4708 additions and 2783 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 66 KiB

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 67 KiB

After

Width:  |  Height:  |  Size: 67 KiB

View File

@@ -1,16 +1,16 @@
# -*- coding: utf-8 -*-
#
import sys, traceback
from PySide2.QtCore import QThread, Signal
import PySide2
class DataTransfer(QThread):
class DataTransfer(PySide2.QtCore.QThread):
CONFIG = None
FINISHED = False
updateProgress = Signal(object)
updateProgress = PySide2.QtCore.Signal(object)
def __init__(self, config=None):
QThread.__init__(self)
PySide2.QtCore.QThread.__init__(self)
if config is not None:
self.CONFIG = config
self.FINISHED = False

File diff suppressed because it is too large Load Diff

955
FlashGBX/FlashGBX_CLI.py Normal file
View File

@@ -0,0 +1,955 @@
# -*- coding: utf-8 -*-
#
import datetime, shutil, platform, os, json, math, traceback, re, time
try:
# pylint: disable=import-error
import readline
readline.set_completer_delims('\t\n=')
readline.parse_and_bind("tab:complete")
except:
pass
from .RomFileDMG import RomFileDMG
from .RomFileAGB import RomFileAGB
from .PocketCamera import PocketCamera
from .Util import APPNAME, VERSION, ANSI
from . import Util
from . import hw_GBxCartRW
hw_devices = [hw_GBxCartRW]
class FlashGBX_CLI():
ARGS = {}
CONFIG_PATH = ""
FLASHCARTS = { "DMG":{}, "AGB":{} }
CONN = None
DEVICE = None
PROGRESS = None
def __init__(self, args):
self.ARGS = args
self.CONFIG_PATH = args['config_path']
self.FLASHCARTS = args["flashcarts"]
self.PROGRESS = Util.Progress(self.UpdateProgress)
global prog_bar_part_char
if platform.system() == "Windows":
prog_bar_part_char = [" ", " ", " ", " ", "", "", "", ""]
else:
prog_bar_part_char = [" ", "", "", "", "", "", "", ""]
def run(self):
config_ret = self.ARGS["config_ret"]
for i in range(0, len(config_ret)):
if config_ret[i][0] < 1:
print(config_ret[i][1])
elif config_ret[i][0] == 2:
print("{:s}{:s}{:s}".format(ANSI.YELLOW, config_ret[i][1], ANSI.RESET))
elif config_ret[i][0] == 2:
print("{:s}{:s}{:s}".format(ANSI.RED, config_ret[i][1], ANSI.RESET))
args = self.ARGS["argparsed"]
config_path = Util.formatPathOS(self.CONFIG_PATH)
print("Configuration directory: {:s}\n".format(config_path))
# Ask interactively if no args set
if args.action is None:
actions = ["info", "backup-rom", "flash-rom", "backup-save", "restore-save", "erase-save", "gbcamera-extract", "debug-probe-save"]
print("Select Operation:\n 1) Read Cartridge Information\n 2) Backup ROM\n 3) Flash ROM\n 4) Backup Save Data\n 5) Restore Save Data\n 6) Erase Save Data\n 7) Extract Game Boy Camera Pictures\n")
args.action = input("Enter number 1-7 [1]: ").lower().strip()
print("")
try:
args.action = actions[int(args.action) - 1]
except:
if args.action == "":
args.action = "info"
else:
print("Canceled.")
return
if args.action is None or args.action not in ("gbcamera-extract"):
if not self.FindDevices():
print("No devices found.")
return
else:
if not self.ConnectDevice():
print("Couldnt connect to the device.")
return
print("Connected to {:s}\n".format(self.DEVICE[0]))
if args.action == "gbcamera-extract":
if args.path == "auto":
args.path = input("Enter file path of Game Boy Camera save data file: ").strip().replace("\"", "")
print("")
if args.path == "":
print("Canceled.")
return
pc = PocketCamera()
if pc.LoadFile(args.path) != False:
palettes = [ "grayscale", "dmg", "sgb", "cgb1", "cgb2", "cgb3" ]
pc.SetPalette(palettes.index(args.gbcamera_palette))
file = os.path.splitext(args.path)[0] + "/IMG_PC00.png"
if os.path.isfile(os.path.dirname(file)):
print("\n{:s}Cant save pictures at location “{:s}”.{:s}\n".format(ANSI.RED, os.path.abspath(os.path.dirname(file)), ANSI.RESET))
return
if not os.path.isdir(os.path.dirname(file)):
os.makedirs(os.path.dirname(file))
for i in range(0, 31):
file = os.path.splitext(args.path)[0] + "/IMG_PC{:02d}".format(i) + "." + args.gbcamera_outfile_format
pc.ExportPicture(i, file)
print("The pictures from “{:s}” were extracted to “{:s}”.".format(os.path.abspath(args.path), Util.formatPathOS(os.path.abspath(os.path.dirname(file)), end_sep=True) + "IMG_PC**.{:s}".format(args.gbcamera_outfile_format)))
else:
print("\n{:s}Couldnt parse the save data file.{:s}\n".format(ANSI.RED, ANSI.RESET))
return
if args.mode is None:
print("Select Cartridge Mode:\n 1) Game Boy or Game Boy Color\n 2) Game Boy Advance\n")
answer = input("Enter number 1-2 [2]: ").lower().strip()
print("")
if answer == "1":
args.mode = "dmg"
elif answer == "2" or answer == "":
args.mode = "agb"
else:
print("Canceled.")
return
print("")
if args.mode == "dmg":
print("Cartridge Mode: Game Boy")
self.CONN.SetMode("DMG")
else:
print("Cartridge Mode: Game Boy Advance")
self.CONN.SetMode("AGB")
time.sleep(0.2)
header = self.CONN.ReadInfo()
(bad_read, s_header, header) = self.ReadCartridge(header)
if s_header == "":
print("\n{:s}Couldnt read cartridge header. Please try again.{:s}\n".format(ANSI.RED, ANSI.RESET))
return
elif bad_read and not args.ignore_bad_header:
print("\n{:s}Invalid data was detected which usually means that the cartridge couldnt 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)
return
print("\nCartridge Information:")
print(s_header)
if args.action == "backup-rom":
self.BackupROM(args, header)
elif args.action == "backup-save":
self.BackupRestoreRAM(args, header)
elif args.action == "restore-save":
if args.path == "auto":
args.path = input("Enter file path of save data file: ").strip().replace("\"", "")
print("")
if args.path == "":
print("Canceled.")
return
self.BackupRestoreRAM(args, header)
elif args.action == "erase-save":
self.BackupRestoreRAM(args, header)
elif args.action == "debug-probe-save":
self.BackupRestoreRAM(args, header)
elif args.action == "flash-rom":
if args.path == "auto":
args.path = input("Enter file path of ROM file: ").strip().replace("\"", "")
print("")
if args.path == "":
print("Canceled.")
return
self.FlashROM(args, header)
if args.action != "info":
print("")
self.DisconnectDevice()
return 0
def UpdateProgress(self, args):
if args is None: return
if "error" in args:
print("{:s}{:s}{:s}".format(ANSI.RED, args["error"], ANSI.RESET))
return
pos = 0
size = 0
speed = 0
elapsed = 0
left = 0
if "pos" in args: pos = args["pos"]
if "size" in args: size = args["size"]
if "speed" in args: speed = args["speed"]
if "time_elapsed" in args: elapsed = args["time_elapsed"]
if "time_left" in args: left = args["time_left"]
if "action" in args:
if args["action"] == "INITIALIZE":
if args["method"] == "ROM_WRITE_VERIFY":
print("\n\nThe newly written ROM data will now be checked for errors.\n")
elif args["action"] == "ERASE":
print("\033[KPlease wait while the flash chip is being erased... (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"] == "ABORTING":
print("\nStopping...")
pass
elif args["action"] == "FINISHED":
print("\n")
self.FinishOperation()
elif args["action"] == "ABORT":
print("\nOperation stopped.\n")
if "info_type" in args.keys() and "info_msg" in args.keys():
if args["info_type"] == "msgbox_critical":
print(ANSI.RED + args["info_msg"] + ANSI.RESET)
elif args["info_type"] == "msgbox_information":
print(args["info_msg"])
elif args["info_type"] == "label":
print(args["info_msg"])
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_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)
remainder_width = (progress * prog_width) % 1
part_width = math.floor(remainder_width * 8)
try:
part_char = prog_bar_part_char[part_width]
if (prog_width - whole_width - 1) < 0: part_char = ""
prog_bar = "" * whole_width + part_char + " " * (prog_width - whole_width - 1)
print(prog_str.replace("%PROG_BAR%", prog_bar), end="\r")
except UnicodeEncodeError:
prog_bar = "#" * whole_width + " " * (prog_width - whole_width)
print(prog_str.replace("%PROG_BAR%", prog_bar), end="\r", flush=True)
except:
pass
def FinishOperation(self):
if self.CONN.INFO["last_action"] == 4: # Flash ROM
self.CONN.INFO["last_action"] = 0
if "verified" in self.PROGRESS.PROGRESS and self.PROGRESS.PROGRESS["verified"] == True:
print("{:s}The ROM was flashed and verified successfully!{:s}".format(ANSI.GREEN, ANSI.RESET))
else:
print("ROM flashing complete!")
elif self.CONN.INFO["last_action"] == 1: # Backup ROM
self.CONN.INFO["last_action"] = 0
if self.CONN.GetMode() == "DMG":
print("Checksum: 0x{:04X}".format(self.CONN.INFO["rom_checksum_calc"]))
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_handler:
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, patched games and intentional overdumps.{:s}".format(ANSI.YELLOW, ANSI.RESET))
elif self.CONN.GetMode() == "AGB":
print("CRC32: 0x{:08X}".format(self.CONN.INFO["rom_checksum_calc"]))
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.")
else:
print("{:s}The ROM backup is complete, but the checksum doesnt match the known database entry. This may indicate a bad dump, however this can be normal for some reproduction cartridges, prototypes, patched games and intentional overdumps.{:s}".format(ANSI.YELLOW, ANSI.RESET))
elif self.CONN.INFO["last_action"] == 2: # Backup RAM
self.CONN.INFO["last_action"] = 0
if not "debug" in self.ARGS and self.CONN.INFO["transferred"] == 131072: # 128 KB
with open(self.CONN.INFO["last_path"], "rb") as file: temp = file.read()
if temp[0x1FFB1:0x1FFB6] == b'Magic':
answer = input("Game Boy Camera save data was detected.\nWould you like to extract all pictures to “{:s}” now? [Y/n]: ".format(Util.formatPathOS(os.path.abspath(os.path.splitext(self.CONN.INFO["last_path"])[0]), end_sep=True) + "IMG_PC**.{:s}".format(self.ARGS["argparsed"].gbcamera_outfile_format))).strip().lower()
if answer != "n":
pc = PocketCamera()
if pc.LoadFile(self.CONN.INFO["last_path"]) != False:
palettes = [ "grayscale", "dmg", "sgb", "cgb1", "cgb2", "cgb3" ]
pc.SetPalette(palettes.index(self.ARGS["argparsed"].gbcamera_palette))
file = os.path.splitext(self.CONN.INFO["last_path"])[0] + "/IMG_PC00.png"
if os.path.isfile(os.path.dirname(file)):
print("Cant save pictures at location “{:s}”.".format(os.path.abspath(os.path.dirname(file))))
return
if not os.path.isdir(os.path.dirname(file)):
os.makedirs(os.path.dirname(file))
for i in range(0, 31):
file = os.path.splitext(self.CONN.INFO["last_path"])[0] + "/IMG_PC{:02d}".format(i) + "." + self.ARGS["argparsed"].gbcamera_outfile_format
pc.ExportPicture(i, file)
print("The pictures were extracted.")
print("")
print("The save data backup is complete!")
elif self.CONN.INFO["last_action"] == 3: # Restore RAM
self.CONN.INFO["last_action"] = 0
if "save_erase" in self.CONN.INFO and self.CONN.INFO["save_erase"]:
print("The save data was erased.")
del(self.CONN.INFO["save_erase"])
else:
print("The save data was restored!")
else:
self.CONN.INFO["last_action"] = 0
def FindDevices(self, connectToFirst=False):
global hw_devices
for hw_device in hw_devices:
dev = hw_device.GbxDevice()
ret = dev.Initialize(self.FLASHCARTS)
if ret is False:
self.CONN = None
elif isinstance(ret, list):
if len(ret) > 0: print("\n")
for i in range(0, len(ret)):
status = ret[i][0]
msg = re.sub('<[^<]+?>', '', ret[i][1])
if status == 3:
print("{:s}{:s}{:s}".format(ANSI.RED, msg.replace("\n\n", "\n"), ANSI.RESET))
self.CONN = None
if dev.IsConnected():
self.DEVICE = (dev.GetFullName(), dev)
dev.Close()
break
if self.DEVICE is None: return False
return True
def ConnectDevice(self):
dev = self.DEVICE[1]
ret = dev.Initialize(self.FLASHCARTS)
if ret is False:
print("\n{:s}An error occured while trying to connect to the device.{:s}".format(ANSI.RED, ANSI.RESET))
traceback.print_stack()
self.CONN = None
return False
elif isinstance(ret, list):
for i in range(0, len(ret)):
status = ret[i][0]
msg = re.sub('<[^<]+?>', '', ret[i][1])
if status == 0:
print("\n" + msg)
elif status == 1:
print("{:s}".format(msg))
elif status == 2:
print("{:s}{:s}{:s}".format(ANSI.YELLOW, msg, ANSI.RESET))
elif status == 3:
print("{:s}{:s}{:s}".format(ANSI.RED, msg, ANSI.RESET))
self.CONN = None
return False
self.CONN = dev
return True
def DisconnectDevice(self):
try:
devname = self.CONN.GetFullName()
self.CONN.Close()
print("Disconnected from {:s}".format(devname))
except:
pass
self.CONN = None
def ReadCartridge(self, data):
bad_read = False
str = ""
if self.CONN.GetMode() == "DMG":
str += "Game Title/Code: {:s}\n".format(data["game_title"])
str += "Super Game Boy: "
if data['sgb'] in Util.DMG_Header_SGB:
str += "{:s}\n".format(Util.DMG_Header_SGB[data['sgb']])
else:
str += "Unknown (0x{:02X})\n".format(data['sgb'])
bad_read = True
str += "Game Boy Color: "
if data['cgb'] in Util.DMG_Header_CGB:
str += "{:s}\n".format(Util.DMG_Header_CGB[data['cgb']])
else:
str += "Unknown (0x{:02X})\n".format(data['cgb'])
bad_read = True
if data["logo_correct"]:
str += "Nintendo Logo: OK\n"
else:
str += "Nintendo Logo: {:s}Invalid{:s}\n".format(ANSI.RED, ANSI.RESET)
bad_read = True
if data['header_checksum_correct']:
str += "Header Checksum: Valid (0x{:02X})\n".format(data['header_checksum'])
else:
str += "Header Checksum: {:s}Invalid (0x{:02X}){:s}\n".format(ANSI.RED, data['header_checksum'], ANSI.RESET)
bad_read = True
str += "ROM Checksum: 0x{:04X}\n".format(data['rom_checksum'])
try:
str += "ROM Size: {:s}\n".format(Util.DMG_Header_ROM_Sizes[data['rom_size_raw']])
except:
str += "ROM Size: {:s}Not detected{:s}\n".format(ANSI.RED, ANSI.RESET)
bad_read = True
try:
if data['features_raw'] == 0x06: # MBC2
str += "Save Type: {:s}\n".format(Util.DMG_Header_RAM_Sizes[1])
elif data['features_raw'] == 0x22 and data["game_title"] in ("KORO2 KIRBYKKKJ", "KIRBY TNT__KTNE"): # MBC7 Kirby
str += "Save Type: {:s}\n".format(Util.DMG_Header_RAM_Sizes[Util.DMG_Header_RAM_Sizes_Map.index(0x101)])
elif data['features_raw'] == 0x22 and data["game_title"] in ("CMASTER____KCEJ"): # MBC7 Command Master
str += "Save Type: {:s}\n".format(Util.DMG_Header_RAM_Sizes[Util.DMG_Header_RAM_Sizes_Map.index(0x102)])
elif data['features_raw'] == 0xFD: # TAMA5
str += "Save Type: {:s}\n".format(Util.DMG_Header_RAM_Sizes[Util.DMG_Header_RAM_Sizes_Map.index(0x103)])
else:
str += "Save Type: {:s}\n".format(Util.DMG_Header_RAM_Sizes[Util.DMG_Header_RAM_Sizes_Map.index(data['ram_size_raw'])])
except:
str += "Save Type: Not detected\n"
try:
str += "Mapper Type: {:s}\n".format(Util.DMG_Header_Features[data['features_raw']])
except:
str += "Mapper Type: {:s}Not detected{:s}\n".format(ANSI.RED, ANSI.RESET)
bad_read = True
if data['logo_correct'] and not self.CONN.IsSupportedMbc(data["features_raw"]):
print("{:s}\nWARNING: This cartridge uses a Memory Bank Controller that may not be completely supported yet. A future version of {:s} may add support for it.{:s}".format(ANSI.YELLOW, APPNAME, ANSI.RESET))
if data['logo_correct'] and data['game_title'] == "NP M-MENU MENU" and self.ARGS["argparsed"].flashcart_handler == "autodetect":
cart_types = self.CONN.GetSupportedCartridgesDMG()
for i in range(0, len(cart_types[0])):
if "DMG-MMSA-JPN" in cart_types[0][i]:
self.ARGS["argparsed"].flashcart_handler = cart_types[0][i]
elif self.CONN.GetMode() == "AGB":
str += "Game Title: {:s}\n".format(data["game_title"])
str += "Game Code: {:s}\n".format(data["game_code"])
str += "Revision: {:d}\n".format(data["version"])
if data["logo_correct"]:
str += "Nintendo Logo: OK\n"
else:
str += "Nintendo Logo: {:s}Invalid{:s}\n".format(ANSI.RED, ANSI.RESET)
bad_read = True
if data["96h_correct"]:
str += "Cartridge Identifier: OK\n"
else:
str += "Cartridge Identifier: {:s}Invalid{:s}\n".format(ANSI.RED, ANSI.RESET)
bad_read = True
if data['header_checksum_correct']:
str += "Header Checksum: Valid (0x{:02X})\n".format(data['header_checksum'])
else:
str += "Header Checksum: {:s}Invalid (0x{:02X}){:s}\n".format(ANSI.RED, data['header_checksum'], ANSI.RESET)
bad_read = True
str += "ROM Checksum: "
Util.AGB_Global_CRC32 = 0
db_agb_entry = None
if os.path.exists("{0:s}/db_AGB.json".format(self.CONFIG_PATH)):
with open("{0:s}/db_AGB.json".format(self.CONFIG_PATH)) as f:
db_agb = f.read()
db_agb = json.loads(db_agb)
if data["header_sha1"] in db_agb.keys():
db_agb_entry = db_agb[data["header_sha1"]]
else:
str += "Not in database\n"
else:
str += "FAIL: Database for Game Boy Advance titles not found in {:s}/db_AGB.json\n".format(self.CONFIG_PATH)
if db_agb_entry != None:
if data["rom_size_calc"] < 0x400000:
str += "In database (0x{:06X})\n".format(db_agb_entry['rc'])
Util.AGB_Global_CRC32 = db_agb_entry['rc']
str += "ROM Size: {:d} MB\n".format(int(db_agb_entry['rs']/1024/1024))
data['rom_size'] = db_agb_entry['rs']
elif data["rom_size"] != 0:
if not data["rom_size"] in Util.AGB_Header_ROM_Sizes_Map:
data["rom_size"] = 0x2000000
str += "ROM Size: {:d} MB\n".format(int(data["rom_size"]/1024/1024))
else:
str += "ROM Size: Not detected\n"
bad_read = True
stok = False
if data["save_type"] == None:
if db_agb_entry != None:
if db_agb_entry['st'] < len(Util.AGB_Header_Save_Types):
stok = True
str += "Save Type: {:s}\n".format(Util.AGB_Header_Save_Types[db_agb_entry['st']])
data["save_type"] = db_agb_entry['st']
if stok is False:
str += "Save Type: Not detected\n"
if data['logo_correct'] and isinstance(db_agb_entry, dict) and "rs" in db_agb_entry and db_agb_entry['rs'] == 0x4000000 and not self.CONN.IsSupported3dMemory():
print("{:s}\nWARNING: This cartridge uses a Memory Bank Controller that may not be completely supported yet. A future version of the {:s} device firmware may add support for it.{:s}".format(ANSI.YELLOW, self.CONN.GetName(), ANSI.RESET))
return (bad_read, str, data)
def CartridgeTypeAutoDetect(self, limitVoltage=True, knownCartCFI=False):
cart_type = 0
cart_text = ""
print("Now attempting to auto-detect the flash cartridge type...")
if self.CONN.CheckROMStable() is False:
print("{:s}Unstable ROM reading detected. Please make sure you selected the correct mode and that the cartridge contacts are clean.{:s}".format(ANSI.RED, ANSI.RESET))
return -1
if self.CONN.GetMode() in self.FLASHCARTS and len(self.FLASHCARTS[self.CONN.GetMode()]) == 0:
print("{:s}No flash cartridge type configuration files found. Try to restart the application with the “--reset” command line switch to reset the configuration.{:s}".format(ANSI.RED, ANSI.RESET))
return -2
detected = self.CONN.AutoDetectFlash(limitVoltage)
if len(detected) == 0:
print("\n{:s}No pre-configured flash cartridge type was detected.{:s} You can still manually specify one using the “--flashcart-handler” command line switch -- look for similar PCB text and/or flash chip markings. However, chances are this cartridge is currently not supported for flashing with {:s}.\n".format(ANSI.YELLOW, ANSI.RESET, APPNAME))
(flash_id, cfi_s, cfi) = self.CONN.CheckFlashChip(limitVoltage)
if cfi_s == "":
print("Flash chip query result:\n" + flash_id + "\nThere was no Common Flash Interface (CFI) response from the cartridge. Please clean the cartridge contacts and make sure that the cartridge is seated correctly. If a flash chip exists on the cartridge PCB, it may be too old or require unique unlocking and handling.")
else:
print("Flash chip query result:\n" + flash_id + "\n" + str(cfi_s))
with open(self.CONFIG_PATH + "/cfi.bin", "wb") as f: f.write(cfi['raw'])
else:
cart_type = detected[0]
size_undetected = False
sectors_undetected = False
if self.CONN.GetMode() == "DMG": cart_types = self.CONN.GetSupportedCartridgesDMG()
elif self.CONN.GetMode() == "AGB": cart_types = self.CONN.GetSupportedCartridgesAGB()
size = cart_types[1][detected[0]]["flash_size"]
if "sector_size" in cart_types[1][detected[0]]:
sectors = cart_types[1][detected[0]]["sector_size"]
else:
sectors = []
for i in range(0, len(detected)):
if size != cart_types[1][detected[i]]["flash_size"]:
size_undetected = True
if "sector_size_from_cfi" not in cart_types[1][detected[i]] and "sector_size" in cart_types[1][detected[i]] and sectors != cart_types[1][detected[i]]["sector_size"]:
sectors_undetected = True
cart_text += "- " + cart_types[0][detected[i]] + "\n"
if size_undetected:
(_, cfi_s, cfi) = self.CONN.CheckFlashChip(limitVoltage=limitVoltage, cart_type=cart_types[1][cart_type])
if isinstance(cfi, dict) and 'device_size' in cfi:
for i in range(0, len(detected)):
if cfi['device_size'] == cart_types[1][detected[i]]["flash_size"]:
cart_type = detected[i]
size_undetected = False
break
if len(detected) == 1:
msg_text = "The following flash cartridge type was detected:\n" + cart_text + "\nThe supported ROM size is up to {:d} MB.".format(int(cart_types[1][cart_type]['flash_size'] / 1024 / 1024))
else:
if size_undetected is True:
msg_text = "Your cartridge responds to flash commands also used by:\n" + cart_text + "\nHowever, you may need to manually adjust the ROM size selection.\n\nIMPORTANT: While these cartridges share the same electronic signature, their supported ROM size can differ. As the size can not be detected automatically at this time, please select it manually."
else:
msg_text = "Your cartridge responds to flash commands also used by:\n" + cart_text + "\nThe supported ROM size is up to {:d} MB.".format(int(cart_types[1][cart_type]['flash_size'] / 1024 / 1024))
if sectors_undetected and "sector_size_from_cfi" not in cart_types[1][cart_type]:
msg_text = msg_text + "\n\n{:s}IMPORTANT:{:s} While these share most of their attributes, some of them can not be automatically detected. If you encounter any errors while writing a ROM, please manually select the correct type based on the flash chip markings of your cartridge. Unchecking the “Prefer sector erase mode” config option can also help.".format(ANSI.RED, ANSI.RESET)
print(msg_text)
if knownCartCFI:
(flash_id, cfi_s, cfi) = self.CONN.CheckFlashChip(limitVoltage=limitVoltage, cart_type=cart_types[1][cart_type])
if cfi_s == "":
print("\nFlash chip query result:\n" + flash_id + "\nThere was no Common Flash Interface (CFI) response from the cartridge. If a flash chip exists on the cartridge PCB, it may be too old or require unique unlocking and handling.")
else:
print("\nFlash chip query result:\n" + flash_id + "\n" + str(cfi_s))
with open(self.CONFIG_PATH + "/cfi.bin", "wb") as f: f.write(cfi['raw'])
return cart_type
def BackupROM(self, args, header):
mbc = 1
rom_banks = 1
fast_read_mode = args.fast_read_mode is True
if self.CONN.GetMode() == "DMG":
if args.dmg_mbc == "auto":
try:
mbc = header["features_raw"]
if mbc == 0: mbc = 5
except:
print("{:s}Couldnt 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
else:
mbc = int(args.dmg_mbc)
if mbc == 2: mbc = 0x06
elif mbc == 3: mbc = 0x13
elif mbc == 5: mbc = 0x19
elif mbc == 6: mbc = 0x20
elif mbc == 7: mbc = 0x22
if args.dmg_romsize == "auto":
try:
rom_banks = Util.DMG_Header_ROM_Sizes_Flasher_Map[header["rom_size_raw"]]
except:
print("{:s}Couldnt determine ROM size, will use 8 MB. It can also be manually set with the “--dmg-romsize” command line switch.{:s}".format(ANSI.YELLOW, ANSI.RESET))
rom_banks = 512
else:
sizes = [ "auto", "32kb", "64kb", "128kb", "256kb", "512kb", "1mb", "2mb", "4mb", "8mb" ]
rom_banks = Util.DMG_Header_ROM_Sizes_Flasher_Map[sizes.index(args.dmg_romsize) - 1]
rom_size = rom_banks * 0x4000
path = header["game_title"].strip().encode('ascii', 'ignore').decode('ascii')
if path == "": path = "ROM"
path = re.sub(r"[<>:\"/\\|\?\*]", "_", path)
if self.CONN.INFO["cgb"] == 0xC0 or self.CONN.INFO["cgb"] == 0x80:
path = path + ".gbc"
elif self.CONN.INFO["sgb"] == 0x03:
path = path + ".sgb"
else:
path = path + ".gb"
elif self.CONN.GetMode() == "AGB":
if args.agb_romsize == "auto":
rom_size = header["rom_size"]
else:
sizes = [ "auto", "4mb", "8mb", "16mb", "32mb", "64mb" ]
rom_size = Util.AGB_Header_ROM_Sizes_Map[sizes.index(args.agb_romsize) - 1]
path = header["game_title"].strip().encode('ascii', 'ignore').decode('ascii')
if path == "": path = header["game_code"].strip().encode('ascii', 'ignore').decode('ascii')
if path == "": path = "ROM"
path = re.sub(r"[<>:\"/\\|\?\*]", "_", path)
path = path + ".gba"
if args.path != "auto":
if os.path.isdir(args.path):
path = args.path + "/" + path
else:
path = args.path
if (path == ""): return
if not args.overwrite and os.path.exists(os.path.abspath(path)):
answer = input("The target file “{:s}” already exists.\nDo you want to overwrite it? [y/N]: ".format(os.path.abspath(path))).strip().lower()
print("")
if answer != "y":
print("Canceled.")
return
try:
f = open(path, "ab+")
f.close()
except (PermissionError, FileNotFoundError):
print("{:s}Couldnt access “{:s}”.{:s}".format(ANSI.RED, path, ANSI.RESET))
return
if fast_read_mode: print("Fast Read Mode enabled.")
s_mbc = ""
if self.CONN.GetMode() == "DMG": s_mbc = " using Cart Type 0x{:X}".format(mbc)
if self.CONN.GetMode() == "DMG":
print("The ROM will now be read{:s} and saved to “{:s}”.".format(s_mbc, os.path.abspath(path)))
else:
print("The ROM will now be read and saved to “{:s}”.".format(os.path.abspath(path)))
print("")
cart_type = 0
if args.flashcart_handler != "autodetect":
if self.CONN.GetMode() == "DMG":
carts = self.CONN.GetSupportedCartridgesDMG()[1]
elif self.CONN.GetMode() == "AGB":
carts = self.CONN.GetSupportedCartridgesAGB()[1]
cart_type = 0
for i in range(0, len(carts)):
if not "names" in carts[i]: continue
if carts[i]["type"] != self.CONN.GetMode(): continue
if args.flashcart_handler in carts[i]["names"]:
print("Selected flash cartridge type: {:s}".format(args.flashcart_handler))
rom_banks = int(carts[i]["flash_size"] / 0x4000)
rom_size = carts[i]["flash_size"]
cart_type = i
break
self.CONN._TransferData(args={ 'mode':1, 'path':path, 'mbc':mbc, 'rom_banks':rom_banks, 'agb_rom_size':rom_size, 'start_addr':0, 'fast_read_mode':fast_read_mode, 'cart_type':cart_type }, signal=self.PROGRESS.SetProgress)
def FlashROM(self, args, header):
path = ""
mode = self.CONN.GetMode()
if mode == "DMG":
carts = self.CONN.GetSupportedCartridgesDMG()[1]
elif mode == "AGB":
carts = self.CONN.GetSupportedCartridgesAGB()[1]
else:
return
cart_type = 0
for i in range(0, len(carts)):
if not "names" in carts[i]: continue
if carts[i]["type"] != mode: continue
if args.flashcart_handler in carts[i]["names"]:
print("Selected flash cartridge type: {:s}".format(args.flashcart_handler))
cart_type = i
break
if cart_type <= 0 and args.flashcart_handler == "autodetect":
if args.force_5v is True:
cart_type = self.CartridgeTypeAutoDetect(limitVoltage=False)
else:
cart_type = self.CartridgeTypeAutoDetect()
if (cart_type == 1): cart_type = 0
if cart_type == 0:
msg_5v = ""
if mode == "DMG": msg_5v = "If your flash cartridge requires 5V to work, you can use the “--force-5v” command line switch, however please note that 5V can be unsafe for some flash chips."
print("\n{:s}Auto-detection failed. Please use the “--flashcart-handler” command line switch to select the flash cartridge handler manually.\n{:s}{:s}{:s}".format(ANSI.RED, ANSI.YELLOW, msg_5v, ANSI.RESET))
return
elif cart_type < 0: return
elif cart_type == 0 and args.flashcart_handler != "autodetect":
print("{:s}Couldnt find the selected flash cartridge type “{:s}”. Please make sure the correct cartridge mode is selected and copy the exact name from the configuration files located in {:s}.{:s}".format(ANSI.RED, args.flashcart_handler, self.CONFIG_PATH, ANSI.RESET))
return
if args.path == "auto":
print("{:s}No ROM file for flashing was selected.{:s}".format(ANSI.RED, ANSI.RESET))
return
else:
path = args.path
try:
if os.path.getsize(path) > 0x2000000: # reject too large files to avoid exploding RAM
print("{:s}Files bigger than 32 MB are not supported at this time.{:s}".format(ANSI.RED, ANSI.RESET))
return
with open(path, "rb") as file: buffer = file.read()
except (PermissionError, FileNotFoundError):
print("{:s}Couldnt access file path “{:s}”.{:s}".format(ANSI.RED, args.path, ANSI.RESET))
return
rom_size = len(buffer)
if rom_size > carts[cart_type]['flash_size']:
msg = "The selected flash cartridge type seems to support ROMs that are up to {:.2f} MB in size, but the file you selected is {:.2f} MB.".format(int(carts[cart_type]['flash_size'] / 1024 / 1024), os.path.getsize(path)/1024/1024)
msg += " Its possible that its too large which may cause the flashing 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
elif 'voltage_variants' in carts[cart_type] and carts[cart_type]['voltage'] == 3.3:
print("The selected flash cartridge type usually flashes fine with 3.3V, however sometimes it may require 5V. You can use the “--force-5v” command line switch if necessary. Please note that 5V can be unsafe for some flash chips.")
reverse_sectors = False
if args.reversed_sectors is True:
reverse_sectors = True
print("Will be writing to the cartridge with reversed flash sectors.")
elif 'sector_reversal' in carts[cart_type]:
print("The selected flash cartridge type is reported to sometimes have reversed sectors. You can use the “--reversed-sectors” command line switch if the cartridge is not working after flashing.")
prefer_chip_erase = args.prefer_chip_erase is True
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.")
fast_read_mode = args.fast_read_mode is True
verify_flash = args.no_verify_flash is 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))
except:
print("{:s}The selected file could not be read.{:s}".format(ANSI.RED, ANSI.RESET))
return
print("")
if fast_read_mode: print("Fast Read Mode enabled for flash verification.")
v = carts[cart_type]["voltage"]
if override_voltage: v = override_voltage
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':fast_read_mode, 'verify_flash':verify_flash }, signal=self.PROGRESS.SetProgress)
buffer = None
def BackupRestoreRAM(self, args, header):
add_date_time = args.save_filename_add_datetime is True
rtc = args.store_rtc is True
if self.CONN.GetMode() == "DMG":
if args.dmg_mbc == "auto":
try:
mbc = header["features_raw"]
if mbc == 0: mbc = 5
except:
print("{:s}Couldnt 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
else:
mbc = int(args.dmg_mbc)
if mbc == 2: mbc = 0x06
elif mbc == 3: mbc = 0x13
elif mbc == 5: mbc = 0x19
elif mbc == 6: mbc = 0x20
elif mbc == 7: mbc = 0x22
if args.dmg_savesize == "auto":
try:
if header['features_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
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
save_type = Util.DMG_Header_RAM_Sizes_Flasher_Map[Util.DMG_Header_RAM_Sizes_Map.index(0x102)]
elif header['features_raw'] == 0xFD: # TAMA5
save_type = Util.DMG_Header_RAM_Sizes_Flasher_Map[Util.DMG_Header_RAM_Sizes_Map.index(0x103)]
else:
save_type = Util.DMG_Header_RAM_Sizes_Flasher_Map[Util.DMG_Header_RAM_Sizes_Map.index(header['ram_size_raw'])]
except:
save_type = 0x20000
else:
sizes = [ "auto", "4k", "16k", "64k", "256k", "512k", "1m", "eeprom2k", "eeprom4k", "tama5" ]
save_type = Util.DMG_Header_RAM_Sizes_Flasher_Map[sizes.index(args.dmg_savesize)]
path = header["game_title"].strip().encode('ascii', 'ignore').decode('ascii')
if path == "": path = "ROM"
if save_type == 0:
print("{:s}Unable to auto-detect the save size. Please use the “--dmg-savesize” command line switch to manually select it.{:s}".format(ANSI.RED, ANSI.RESET))
return
elif self.CONN.GetMode() == "AGB":
if args.agb_savetype == "auto":
save_type = header["save_type"]
else:
sizes = [ "auto", "eeprom4k", "eeprom64k", "sram256k", "sram512k", "sram1m", "flash512k", "flash1m" ]
save_type = sizes.index(args.agb_savetype)
path = header["game_title"].strip().encode('ascii', 'ignore').decode('ascii')
if path == "": path = header["game_code"].strip().encode('ascii', 'ignore').decode('ascii')
if path == "": path = "ROM"
mbc = 0
if save_type == 0 or save_type == None:
print("{:s}Unable to auto-detect the save type. Please use the “--agb-savetype” command line switch to manually select it.{:s}".format(ANSI.RED, ANSI.RESET))
return
else:
return
if add_date_time:
path = re.sub(r"[<>:\"/\\|\?\*]", "_", path) + "_" + datetime.datetime.now().strftime("%Y-%m-%d_%H-%M-%S") + ".sav"
else:
path = re.sub(r"[<>:\"/\\|\?\*]", "_", path) + ".sav"
if args.path != "auto":
if os.path.isdir(args.path):
path = args.path + "/" + path
else:
path = args.path
if (path == ""): return
s_mbc = ""
if self.CONN.GetMode() == "DMG": s_mbc = " using Cart Type 0x{:X}".format(mbc)
if args.action == "backup-save":
if not args.overwrite and os.path.exists(os.path.abspath(path)):
answer = input("The target file “{:s}” already exists.\nDo you want to overwrite it? [y/N]: ".format(os.path.abspath(path))).strip().lower()
print("")
if answer != "y":
print("Canceled.")
return
print("The cartridge save data will now be read{:s} and saved to the following file:\n{:s}".format(s_mbc, os.path.abspath(path)))
elif args.action == "restore-save":
if not args.overwrite:
answer = input("Restoring save data to the cartridge will erase the previous save.\nDo you want to overwrite it? [y/N]: ").strip().lower()
if answer != "y":
print("Canceled.")
return
print("The following save data file will now be written to the cartridge{:s}:\n{:s}".format(s_mbc, os.path.abspath(path)))
elif args.action == "erase-save":
if not args.overwrite:
answer = input("Do you really want to erase the save data from the cartridge? [y/N]: ").strip().lower()
if answer != "y":
print("Canceled.")
return
print("The cartridge save data will now be erased from the cartridge{:s}.".format(s_mbc))
elif args.action == "debug-probe-save":
if mbc == 0xFD: # TAMA5
print("Cant examine save data size for TAMA5 cartridges.")
return
print("The cartridge save data size will now be examined{:s}. Note: This is for debug use only.\n".format(s_mbc))
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, 0xFD, 0xFE): # RTC of MBC3, TAMA5, HuC-3
print("Real Time Clock register values will also be written if applicable/possible.")
try:
if args.action == "backup-save":
f = open(path, "ab+")
f.close()
elif args.action == "restore-save":
f = open(path, "rb+")
f.close()
except (PermissionError, FileNotFoundError):
print("{:s}Couldnt access “{:s}”.{:s}".format(ANSI.RED, path, ANSI.RESET))
return
print("")
if args.action == "backup-save":
self.CONN._TransferData(args={ 'mode':2, 'path':path, 'mbc':mbc, 'save_type':save_type, 'rtc':rtc }, signal=self.PROGRESS.SetProgress)
elif args.action == "restore-save":
self.CONN._TransferData(args={ 'mode':3, 'path':path, 'mbc':mbc, 'save_type':save_type, 'erase':False, 'rtc':rtc }, signal=self.PROGRESS.SetProgress)
elif args.action == "erase-save":
self.CONN._TransferData(args={ 'mode':3, 'path':path, 'mbc':mbc, 'save_type':save_type, 'erase':True, 'rtc':rtc }, signal=self.PROGRESS.SetProgress)
elif args.action == "debug-probe-save": # debug
self.ARGS["debug"] = True
print("Making a backup of the original save data.")
self.CONN._TransferData(args={ 'mode':2, 'path':self.CONFIG_PATH + "/probe1.bin", 'mbc':mbc, 'save_type':save_type }, signal=self.PROGRESS.SetProgress)
print("Writing random data.")
probe2 = bytearray(os.urandom(os.path.getsize(self.CONFIG_PATH + "/probe1.bin")))
with open(self.CONFIG_PATH + "/probe2.bin", "wb") as f: f.write(probe2)
self.CONN._TransferData(args={ 'mode':3, 'path':self.CONFIG_PATH + "/probe2.bin", 'mbc':mbc, 'save_type':save_type, 'erase':False }, signal=self.PROGRESS.SetProgress)
print("Reading back and comparing data.")
self.CONN._TransferData(args={ 'mode':2, 'path':self.CONFIG_PATH + "/probe3.bin", 'mbc':mbc, 'save_type':save_type }, signal=self.PROGRESS.SetProgress)
with open(self.CONFIG_PATH + "/probe3.bin", "rb") as f: probe3 = bytearray(f.read())
print("Restoring original save data.")
self.CONN._TransferData(args={ 'mode':3, 'path':self.CONFIG_PATH + "/probe1.bin", 'mbc':mbc, 'save_type':save_type, 'erase':False }, signal=self.PROGRESS.SetProgress)
if mbc == 2:
for i in range(0, len(probe2)):
probe2[i] &= 0x0F
probe3[i] &= 0x0F
found_offset = probe2.find(probe3[0:512])
if found_offset < 0:
if self.CONN.GetMode() == "AGB":
print("\n{:s}It was not possible to save any data to the cartridge using save type “{:s}”.{:s}".format(ANSI.RED, Util.AGB_Header_Save_Types[save_type], ANSI.RESET))
else:
print("\n{:s}It was not possible to save any data to the cartridge.{:s}".format(ANSI.RED, ANSI.RESET))
else:
if found_offset == 0 and probe2 != probe3: # Pokémon Crystal JPN
found_length = 0
for i in range(0, len(probe2)):
if probe2[i] != probe3[i]: break
found_length += 1
else:
found_length = len(probe2) - 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, asInt=True), Util.formatFileSize(save_type, asInt=True)))
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, asInt=True)))
try:
(_, _, cfi) = self.CONN.CheckFlashChip(limitVoltage=False)
if len(cfi["raw"]) > 0:
with open(self.CONFIG_PATH + "/cfi.bin", "wb") as f: f.write(cfi["raw"])
print("CFI data was extracted to “cfi.bin”.")
except:
pass
input("\nPress ENTER to erase the temporary files.")
os.unlink(self.CONFIG_PATH + "/probe1.bin")
os.unlink(self.CONFIG_PATH + "/probe2.bin")
os.unlink(self.CONFIG_PATH + "/probe3.bin")

1683
FlashGBX/FlashGBX_GUI.py Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,19 +1,10 @@
import sys, functools, os, json, platform, hashlib
from PIL import Image, ImageDraw
from PIL.ImageQt import ImageQt
from PIL.PngImagePlugin import PngInfo
from PySide2 import QtCore, QtWidgets, QtGui
import os, hashlib
import email.utils
class PocketCameraWindow(QtWidgets.QDialog):
CUR_PIC = None
CUR_THUMBS = None
CUR_INDEX = 1
CUR_BICUBIC = True
CUR_FILE = ""
CUR_EXPORT_PATH = ""
CUR_PC = None
APP = None
class PocketCamera:
DATA = None
PALETTES = [
[ 255, 255, 255, 176, 176, 176, 104, 104, 104, 0, 0, 0 ], # Grayscale
[ 208, 217, 60, 120, 164, 106, 84, 88, 84, 36, 70, 36 ], # Game Boy
@@ -23,358 +14,6 @@ class PocketCameraWindow(QtWidgets.QDialog):
[ 240, 240, 240, 220, 160, 160, 136, 78, 78, 30, 30, 30 ], # Game Boy Color (USA Gold)
[ 240, 240, 240, 134, 200, 100, 58, 96, 132, 30, 30, 30 ], # Game Boy Color (USA/EUR)
]
def __init__(self, app, file=None, icon=None):
QtWidgets.QDialog.__init__(self)
self.setAcceptDrops(True)
if icon is not None: self.setWindowIcon(QtGui.QIcon(icon))
self.CUR_FILE = file
self.setWindowTitle("FlashGBX GB Camera Album Viewer")
if hasattr(QtGui, "Qt"):
self.setWindowFlags((self.windowFlags() | QtGui.Qt.MSWindowsFixedSizeDialogHint) & ~QtGui.Qt.WindowContextHelpButtonHint);
self.layout = QtWidgets.QGridLayout()
self.layout.setContentsMargins(-1, 8, -1, 8)
self.layout.setSizeConstraint(QtWidgets.QLayout.SetFixedSize)
self.layout_options1 = QtWidgets.QVBoxLayout()
self.layout_options2 = QtWidgets.QVBoxLayout()
self.layout_options3 = QtWidgets.QVBoxLayout()
self.layout_photos = QtWidgets.QHBoxLayout()
# Options
self.grpColors = QtWidgets.QGroupBox("Color palette")
grpColorsLayout = QtWidgets.QVBoxLayout()
grpColorsLayout.setContentsMargins(-1, 3, -1, -1)
self.rowColors1 = QtWidgets.QHBoxLayout()
self.optColorBW = QtWidgets.QRadioButton("&Grayscale")
self.optColorBW.setToolTip("Generic grayscale palette")
self.connect(self.optColorBW, QtCore.SIGNAL("clicked()"), self.SetColors)
self.optColorDMG = QtWidgets.QRadioButton("&DMG")
self.optColorDMG.setToolTip("Original Game Boy screen palette")
self.connect(self.optColorDMG, QtCore.SIGNAL("clicked()"), self.SetColors)
#self.optColorMGB = QtWidgets.QRadioButton("&MGB")
#self.optColorMGB.setToolTip("Game Boy Pocket palette")
#self.connect(self.optColorMGB, QtCore.SIGNAL("clicked()"), self.SetColors)
self.optColorSGB = QtWidgets.QRadioButton("&SGB")
self.optColorSGB.setToolTip("Super Game Boy palette")
self.connect(self.optColorSGB, QtCore.SIGNAL("clicked()"), self.SetColors)
self.optColorCGB1 = QtWidgets.QRadioButton("CGB &1")
self.optColorCGB1.setToolTip("Japanese Pocket Camera palette")
self.connect(self.optColorCGB1, QtCore.SIGNAL("clicked()"), self.SetColors)
self.optColorCGB2 = QtWidgets.QRadioButton("CGB &2")
self.optColorCGB2.setToolTip("Golden Limited Edition Game Boy Camera palette")
self.connect(self.optColorCGB2, QtCore.SIGNAL("clicked()"), self.SetColors)
self.optColorCGB3 = QtWidgets.QRadioButton("CGB &3")
self.optColorCGB3.setToolTip("Non-Japanese Game Boy Camera palette")
self.connect(self.optColorCGB3, QtCore.SIGNAL("clicked()"), self.SetColors)
self.rowColors1.addWidget(self.optColorBW)
self.rowColors1.addWidget(self.optColorDMG)
#self.rowColors1.addWidget(self.optColorMGB)
self.rowColors1.addWidget(self.optColorSGB)
self.rowColors1.addWidget(self.optColorCGB1)
self.rowColors1.addWidget(self.optColorCGB2)
self.rowColors1.addWidget(self.optColorCGB3)
self.optColorCGB1.setChecked(True)
grpColorsLayout.addLayout(self.rowColors1)
self.grpColors.setLayout(grpColorsLayout)
self.layout_options1.addWidget(self.grpColors)
rowActionsGeneral1 = QtWidgets.QHBoxLayout()
self.btnOpenSRAM = QtWidgets.QPushButton("&Open Save Data File")
self.btnOpenSRAM.setStyleSheet("padding: 5px 10px;")
self.btnOpenSRAM.clicked.connect(self.btnOpenSRAM_Clicked)
self.btnClose = QtWidgets.QPushButton("&Close")
self.btnClose.setStyleSheet("padding: 5px 15px;")
self.btnClose.clicked.connect(self.btnClose_Clicked)
rowActionsGeneral1.addWidget(self.btnOpenSRAM)
rowActionsGeneral1.addStretch()
rowActionsGeneral1.addWidget(self.btnClose)
self.layout_options3.addLayout(rowActionsGeneral1)
# Photo Viewer
self.grpPhotoView = QtWidgets.QGroupBox("Preview")
self.grpPhotoViewLayout = QtWidgets.QVBoxLayout()
self.grpPhotoViewLayout.setContentsMargins(-1, 3, -1, -1)
self.lblPhotoViewer = QtWidgets.QLabel(self)
self.lblPhotoViewer.setMinimumSize(256, 223)
self.lblPhotoViewer.setMaximumSize(256, 223)
self.lblPhotoViewer.setStyleSheet("border-top: 1px solid #adadad; border-left: 1px solid #adadad; border-bottom: 1px solid #ffffff; border-right: 1px solid #ffffff;")
self.lblPhotoViewer.mousePressEvent = self.lblPhotoViewer_Clicked
self.grpPhotoViewLayout.addWidget(self.lblPhotoViewer)
# Actions below Viewer
rowActionsGeneral2 = QtWidgets.QHBoxLayout()
self.btnSavePhoto = QtWidgets.QPushButton("&Save This Picture")
self.btnSavePhoto.setStyleSheet("padding: 5px 10px;")
self.btnSavePhoto.clicked.connect(self.btnSavePhoto_Clicked)
rowActionsGeneral2.addWidget(self.btnSavePhoto)
self.btnSaveAll = QtWidgets.QPushButton("Save &All Pictures")
self.btnSaveAll.setStyleSheet("padding: 5px 10px;")
self.btnSaveAll.clicked.connect(self.btnSaveAll_Clicked)
rowActionsGeneral2.addWidget(self.btnSaveAll)
self.grpPhotoViewLayout.addLayout(rowActionsGeneral2)
self.grpPhotoView.setLayout(self.grpPhotoViewLayout)
# Photo List
self.grpPhotoThumbs = QtWidgets.QGroupBox("Photo Album")
self.grpPhotoThumbsLayout = QtWidgets.QVBoxLayout()
self.grpPhotoThumbsLayout.setSpacing(2)
self.grpPhotoThumbsLayout.setContentsMargins(-1, 3, -1, -1)
self.lblPhoto = []
rowsPhotos = []
for row in range(0, 5):
rowsPhotos.append(QtWidgets.QHBoxLayout())
rowsPhotos[row].setSpacing(2)
for col in range(0, 6):
self.lblPhoto.append(QtWidgets.QLabel(self))
self.lblPhoto[len(self.lblPhoto)-1].setMinimumSize(49, 43)
self.lblPhoto[len(self.lblPhoto)-1].setMaximumSize(49, 43)
self.lblPhoto[len(self.lblPhoto)-1].mousePressEvent = functools.partial(self.lblPhoto_Clicked, index=len(self.lblPhoto)-1)
self.lblPhoto[len(self.lblPhoto)-1].setCursor(QtGui.QCursor(QtGui.Qt.PointingHandCursor))
self.lblPhoto[len(self.lblPhoto)-1].setAlignment(QtGui.Qt.AlignCenter)
self.lblPhoto[len(self.lblPhoto)-1].setStyleSheet("border-top: 1px solid #adadad; border-left: 1px solid #adadad; border-bottom: 1px solid #fefefe; border-right: 1px solid #fefefe;")
rowsPhotos[row].addWidget(self.lblPhoto[len(self.lblPhoto)-1])
self.grpPhotoThumbsLayout.addLayout(rowsPhotos[row])
rowActionsGeneral3 = QtWidgets.QHBoxLayout()
self.btnShowGameFace = QtWidgets.QPushButton("Load &Game Face")
self.btnShowGameFace.setStyleSheet("padding: 5px 10px;")
self.btnShowGameFace.clicked.connect(self.btnShowGameFace_Clicked)
rowActionsGeneral3.addWidget(self.btnShowGameFace)
self.grpPhotoThumbsLayout.addStretch()
self.grpPhotoThumbsLayout.addLayout(rowActionsGeneral3)
self.grpPhotoThumbsLayout.setAlignment(QtGui.Qt.AlignTop)
self.grpPhotoThumbs.setLayout(self.grpPhotoThumbsLayout)
self.layout_photos.addWidget(self.grpPhotoThumbs)
self.layout_photos.addWidget(self.grpPhotoView)
self.layout.addLayout(self.layout_options1, 0, 0)
self.layout.addLayout(self.layout_options2, 1, 0)
self.layout.addLayout(self.layout_photos, 2, 0)
self.layout.addLayout(self.layout_options3, 3, 0)
self.setLayout(self.layout)
self.APP = app
palette = self.APP.SETTINGS.value("PocketCameraPalette")
try:
palette = json.loads(palette)
except:
palette = None
if palette is not None:
for i in range(0, len(self.PALETTES)):
if palette == self.PALETTES[i]:
self.rowColors1.itemAt(i).widget().setChecked(True)
if self.CUR_FILE is not None:
self.OpenFile(self.CUR_FILE)
export_path = self.APP.SETTINGS.value("LastDirPocketCamera")
if export_path is not None:
self.CUR_EXPORT_PATH = export_path
self.SetColors()
self.btnSaveAll.setDefault(True)
self.btnSaveAll.setAutoDefault(True)
self.btnSaveAll.setFocus()
def run(self):
self.layout.update()
self.layout.activate()
screenGeometry = QtWidgets.QDesktopWidget().screenGeometry()
x = (screenGeometry.width() - self.width()) / 2
y = (screenGeometry.height() - self.height()) / 2
self.move(x, y)
self.show()
def SetColors(self):
if self.CUR_PC is None: return
for i in range(0, self.rowColors1.count()):
if self.rowColors1.itemAt(i).widget().isChecked():
self.CUR_PC.SetPalette(self.PALETTES[i])
self.BuildPhotoList()
self.UpdateViewer(self.CUR_INDEX)
def OpenFile(self, file):
self.CUR_PC = PocketCamera()
if self.CUR_PC.LoadFile(file) == False:
self.CUR_PC = None
QtWidgets.QMessageBox.warning(self, "FlashGBX", "The save data file couldnt be loaded.", QtWidgets.QMessageBox.Ok)
return False
self.CUR_FILE = file
if self.CUR_EXPORT_PATH == "":
self.CUR_EXPORT_PATH = os.path.dirname(self.CUR_FILE)
self.UpdateViewer(1)
self.SetColors()
return True
def lblPhoto_Clicked(self, event, index):
if event.button() == QtGui.Qt.LeftButton:
self.CUR_INDEX = index + 1
self.UpdateViewer(self.CUR_INDEX)
def lblPhotoViewer_Clicked(self, event):
if event.button() == QtGui.Qt.LeftButton:
self.CUR_BICUBIC = not self.CUR_BICUBIC
self.UpdateViewer(self.CUR_INDEX)
def btnOpenSRAM_Clicked(self):
last_dir = self.APP.SETTINGS.value("LastDirSaveDataDMG")
path = QtWidgets.QFileDialog.getOpenFileName(self, "Open GB Camera Save Data File", last_dir, "Save Data File (*.sav);;All Files (*.*)")[0]
if (path == ""): return
if self.OpenFile(path) is True:
self.APP.SETTINGS.setValue("LastDirSaveDataDMG", os.path.dirname(path))
def btnShowGameFace_Clicked(self, event):
self.UpdateViewer(0)
self.CUR_INDEX = 0
def btnSaveAll_Clicked(self, event):
if self.CUR_PC is None: return
path = self.CUR_EXPORT_PATH + "/IMG_PC.png"
path = QtWidgets.QFileDialog.getSaveFileName(self, "Export all pictures", path, "PNG Files (*.png);;All Files (*.*)")[0]
if path == "": return
self.CUR_EXPORT_PATH = os.path.dirname(path)
for i in range(0, 31):
file = os.path.splitext(path)[0] + "{:02d}".format(i) + os.path.splitext(path)[1]
if os.path.exists(file):
answer = QtWidgets.QMessageBox.warning(self, "FlashGBX", "There are already pictures that use the same file names. If you continue, these files will be overwritten.", QtWidgets.QMessageBox.Ok | QtWidgets.QMessageBox.Cancel)
if answer == QtWidgets.QMessageBox.Ok:
break
elif answer == QtWidgets.QMessageBox.Cancel:
return
for i in range(0, 31):
file = os.path.splitext(path)[0] + "{:02d}".format(i) + os.path.splitext(path)[1]
self.SavePicture(i, path=file)
def btnSavePhoto_Clicked(self, event):
if self.CUR_PC is None: return
self.SavePicture(self.CUR_INDEX)
def btnClose_Clicked(self, event):
self.reject()
def hideEvent(self, event):
for i in range(0, self.rowColors1.count()):
if self.rowColors1.itemAt(i).widget().isChecked():
self.APP.SETTINGS.setValue("PocketCameraPalette", json.dumps(self.PALETTES[i]))
self.APP.SETTINGS.setValue("LastDirPocketCamera", self.CUR_EXPORT_PATH)
self.APP.activateWindow()
def BuildPhotoList(self):
cam = self.CUR_PC
self.CUR_THUMBS = [None] * 30
for i in range(0, 30):
pic = cam.GetPicture(i+1).convert("RGBA")
self.lblPhoto[i].setToolTip("")
if cam.IsEmpty(i+1):
pass
#draw = ImageDraw.Draw(pic, "RGBA")
#draw.line([0, 0, 128, 112], fill=(255, 0, 0), width=8)
#draw.line([0, 112, 128, 0], fill=(255, 0, 0), width=8)
elif cam.IsDeleted(i+1):
draw_bg = Image.new("RGBA", pic.size)
draw = ImageDraw.Draw(draw_bg)
draw.line([0, 0, 128, 112], fill=(255, 0, 0, 192), width=8)
draw.line([0, 112, 128, 0], fill=(255, 0, 0, 192), width=8)
pic.paste(draw_bg, mask=draw_bg)
self.lblPhoto[i].setToolTip("This picture was marked as “deleted” and may be overwritten when you take new pictures.")
self.CUR_THUMBS[i] = ImageQt(pic.resize((47, 41), Image.HAMMING))
qpixmap = QtGui.QPixmap.fromImage(self.CUR_THUMBS[i])
self.lblPhoto[i].setPixmap(qpixmap)
def UpdateViewer(self, index):
resampler = Image.NEAREST
if self.CUR_BICUBIC: resampler = Image.BICUBIC
cam = self.CUR_PC
if cam is None: return
for i in range(0, 30):
self.lblPhoto[i].setStyleSheet("border-top: 1px solid #adadad; border-left: 1px solid #adadad; border-bottom: 1px solid #ffffff; border-right: 1px solid #ffffff;")
if index == 0:
self.CUR_PIC = ImageQt(cam.GetPicture(0).convert("RGBA").resize((256, 224), resampler))
else:
self.CUR_PIC = ImageQt(cam.GetPicture(index).convert("RGBA").resize((256, 224), resampler))
self.lblPhoto[index - 1].setStyleSheet("border: 3px solid green; padding: 1px;")
qpixmap = QtGui.QPixmap.fromImage(self.CUR_PIC)
self.lblPhotoViewer.setPixmap(qpixmap)
def SavePicture(self, index, path=""):
if path == "":
path = self.CUR_EXPORT_PATH + "/IMG_PC{:02d}.png".format(index)
path = QtWidgets.QFileDialog.getSaveFileName(self, "Save Photo", path, "PNG Files (*.png);;All Files (*.*)")[0]
if path != "": self.CUR_EXPORT_PATH = os.path.dirname(path)
if path == "": return
pnginfo = PngInfo()
pnginfo.add_text("Software", "FlashGBX")
pnginfo.add_text("Source", "Pocket Camera")
pnginfo.add_text("Creation Time", email.utils.formatdate())
cam = self.CUR_PC
if index == 0:
pic = cam.GetPicture(0)
pnginfo.add_text("Title", "Game Face")
else:
pic = cam.GetPicture(index)
pnginfo.add_text("Title", "Photo {:02d}".format(index))
pic.save(path, pnginfo=pnginfo)
def dragEnterEvent(self, e):
if self._dragEventHover(e):
e.accept()
else:
e.ignore()
def dragMoveEvent(self, e):
if self._dragEventHover(e):
e.accept()
else:
e.ignore()
def _dragEventHover(self, e):
if e.mimeData().hasUrls:
for url in e.mimeData().urls():
if platform.system() == 'Darwin':
fn = str(NSURL.URLWithString_(str(url.toString())).filePathURL().path())
else:
fn = str(url.toLocalFile())
fn_split = os.path.splitext(os.path.abspath(fn))
if fn_split[1] == ".sav":
return True
return False
def dropEvent(self, e):
if e.mimeData().hasUrls:
e.setDropAction(QtCore.Qt.CopyAction)
e.accept()
for url in e.mimeData().urls():
if platform.system() == 'Darwin':
fn = str(NSURL.URLWithString_(str(url.toString())).filePathURL().path())
else:
fn = str(url.toLocalFile())
fn_split = os.path.splitext(os.path.abspath(fn))
if fn_split[1] == ".sav":
self.OpenFile(fn)
else:
e.ignore()
class PocketCamera:
DATA = None
PALETTE = [ 240, 240, 240, 218, 196, 106, 112, 88, 52, 30, 30, 30 ] # default
IMAGES = [None] * 31
IMAGES_DELETED = []
@@ -410,6 +49,8 @@ class PocketCamera:
return True
def SetPalette(self, palette):
if isinstance(palette, int):
palette = self.PALETTES[palette]
for p in range (0, len(self.IMAGES)):
self.IMAGES[p].putpalette(palette)
self.PALETTE = palette
@@ -453,3 +94,30 @@ class PocketCamera:
offset = 0x2000 + (index * 0x1000)
imgbuffer = self.DATA[offset:offset+0x1000]
return self.ConvertPicture(imgbuffer)
def ExportPicture(self, index, path):
pnginfo = PngInfo()
pnginfo.add_text("Software", "FlashGBX")
pnginfo.add_text("Source", "Pocket Camera")
pnginfo.add_text("Creation Time", email.utils.formatdate())
if index == 0:
pic = self.GetPicture(0)
pnginfo.add_text("Title", "Game Face")
else:
pic = self.GetPicture(index)
pnginfo.add_text("Title", "Photo {:02d}".format(index))
ext = os.path.splitext(path)[1]
if ext.lower() == ".png":
outpic = pic
outpic.save(path, pnginfo=pnginfo)
elif ext.lower() == ".gif":
outpic = pic
outpic.save(path)
elif ext.lower() in (".jpg", ".jpeg"):
outpic = pic.convert("RGB")
outpic.save(path, quality=100, subsampling=0)
else:
outpic = pic.convert("RGB")
outpic.save(path)

View File

@@ -0,0 +1,362 @@
import functools, os, json, platform
from PIL.ImageQt import ImageQt
from PIL import Image, ImageDraw
from PySide2 import QtCore, QtWidgets, QtGui
from .PocketCamera import PocketCamera
class PocketCameraWindow(QtWidgets.QDialog):
CUR_PIC = None
CUR_THUMBS = None
CUR_INDEX = 1
CUR_BICUBIC = True
CUR_FILE = ""
CUR_EXPORT_PATH = ""
CUR_PC = None
APP = None
PALETTES = [
[ 255, 255, 255, 176, 176, 176, 104, 104, 104, 0, 0, 0 ], # Grayscale
[ 208, 217, 60, 120, 164, 106, 84, 88, 84, 36, 70, 36 ], # Game Boy
#[ 196, 207, 161, 139, 149, 109, 77, 83, 60, 31, 31, 31 ], # Game Boy Pocket
[ 255, 255, 255, 181, 179, 189, 84, 83, 103, 9, 7, 19 ], # Super Game Boy
[ 240, 240, 240, 218, 196, 106, 112, 88, 52, 30, 30, 30 ], # Game Boy Color (JPN)
[ 240, 240, 240, 220, 160, 160, 136, 78, 78, 30, 30, 30 ], # Game Boy Color (USA Gold)
[ 240, 240, 240, 134, 200, 100, 58, 96, 132, 30, 30, 30 ], # Game Boy Color (USA/EUR)
]
def __init__(self, app, file=None, icon=None):
QtWidgets.QDialog.__init__(self)
self.setAcceptDrops(True)
if icon is not None: self.setWindowIcon(QtGui.QIcon(icon))
self.CUR_FILE = file
self.setWindowTitle("FlashGBX GB Camera Album Viewer")
self.setWindowFlags((self.windowFlags() | QtCore.Qt.MSWindowsFixedSizeDialogHint) & ~QtCore.Qt.WindowContextHelpButtonHint)
self.layout = QtWidgets.QGridLayout()
self.layout.setContentsMargins(-1, 8, -1, 8)
self.layout.setSizeConstraint(QtWidgets.QLayout.SetFixedSize)
self.layout_options1 = QtWidgets.QVBoxLayout()
self.layout_options2 = QtWidgets.QVBoxLayout()
self.layout_options3 = QtWidgets.QVBoxLayout()
self.layout_photos = QtWidgets.QHBoxLayout()
# Options
self.grpColors = QtWidgets.QGroupBox("Color palette")
grpColorsLayout = QtWidgets.QVBoxLayout()
grpColorsLayout.setContentsMargins(-1, 3, -1, -1)
self.rowColors1 = QtWidgets.QHBoxLayout()
self.optColorBW = QtWidgets.QRadioButton("&Grayscale")
self.optColorBW.setToolTip("Generic grayscale palette")
self.connect(self.optColorBW, QtCore.SIGNAL("clicked()"), self.SetColors)
self.optColorDMG = QtWidgets.QRadioButton("&DMG")
self.optColorDMG.setToolTip("Original Game Boy screen palette")
self.connect(self.optColorDMG, QtCore.SIGNAL("clicked()"), self.SetColors)
#self.optColorMGB = QtWidgets.QRadioButton("&MGB")
#self.optColorMGB.setToolTip("Game Boy Pocket palette")
#self.connect(self.optColorMGB, QtCore.SIGNAL("clicked()"), self.SetColors)
self.optColorSGB = QtWidgets.QRadioButton("&SGB")
self.optColorSGB.setToolTip("Super Game Boy palette")
self.connect(self.optColorSGB, QtCore.SIGNAL("clicked()"), self.SetColors)
self.optColorCGB1 = QtWidgets.QRadioButton("CGB &1")
self.optColorCGB1.setToolTip("Japanese Pocket Camera palette")
self.connect(self.optColorCGB1, QtCore.SIGNAL("clicked()"), self.SetColors)
self.optColorCGB2 = QtWidgets.QRadioButton("CGB &2")
self.optColorCGB2.setToolTip("Golden Limited Edition Game Boy Camera palette")
self.connect(self.optColorCGB2, QtCore.SIGNAL("clicked()"), self.SetColors)
self.optColorCGB3 = QtWidgets.QRadioButton("CGB &3")
self.optColorCGB3.setToolTip("Non-Japanese Game Boy Camera palette")
self.connect(self.optColorCGB3, QtCore.SIGNAL("clicked()"), self.SetColors)
self.rowColors1.addWidget(self.optColorBW)
self.rowColors1.addWidget(self.optColorDMG)
#self.rowColors1.addWidget(self.optColorMGB)
self.rowColors1.addWidget(self.optColorSGB)
self.rowColors1.addWidget(self.optColorCGB1)
self.rowColors1.addWidget(self.optColorCGB2)
self.rowColors1.addWidget(self.optColorCGB3)
self.optColorCGB1.setChecked(True)
grpColorsLayout.addLayout(self.rowColors1)
self.grpColors.setLayout(grpColorsLayout)
self.layout_options1.addWidget(self.grpColors)
rowActionsGeneral1 = QtWidgets.QHBoxLayout()
self.btnOpenSRAM = QtWidgets.QPushButton("&Open Save Data File")
self.btnOpenSRAM.setStyleSheet("padding: 5px 10px;")
self.btnOpenSRAM.clicked.connect(self.btnOpenSRAM_Clicked)
self.btnClose = QtWidgets.QPushButton("&Close")
self.btnClose.setStyleSheet("padding: 5px 15px;")
self.btnClose.clicked.connect(self.btnClose_Clicked)
rowActionsGeneral1.addWidget(self.btnOpenSRAM)
rowActionsGeneral1.addStretch()
rowActionsGeneral1.addWidget(self.btnClose)
self.layout_options3.addLayout(rowActionsGeneral1)
# Photo Viewer
self.grpPhotoView = QtWidgets.QGroupBox("Preview")
self.grpPhotoViewLayout = QtWidgets.QVBoxLayout()
self.grpPhotoViewLayout.setContentsMargins(-1, 3, -1, -1)
self.lblPhotoViewer = QtWidgets.QLabel(self)
self.lblPhotoViewer.setMinimumSize(256, 223)
self.lblPhotoViewer.setMaximumSize(256, 223)
self.lblPhotoViewer.setStyleSheet("border-top: 1px solid #adadad; border-left: 1px solid #adadad; border-bottom: 1px solid #ffffff; border-right: 1px solid #ffffff;")
self.lblPhotoViewer.mousePressEvent = self.lblPhotoViewer_Clicked
self.grpPhotoViewLayout.addWidget(self.lblPhotoViewer)
# Actions below Viewer
rowActionsGeneral2 = QtWidgets.QHBoxLayout()
self.btnSavePhoto = QtWidgets.QPushButton("&Save This Picture")
self.btnSavePhoto.setStyleSheet("padding: 5px 10px;")
self.btnSavePhoto.clicked.connect(self.btnSavePhoto_Clicked)
rowActionsGeneral2.addWidget(self.btnSavePhoto)
self.btnSaveAll = QtWidgets.QPushButton("Save &All Pictures")
self.btnSaveAll.setStyleSheet("padding: 5px 10px;")
self.btnSaveAll.clicked.connect(self.btnSaveAll_Clicked)
rowActionsGeneral2.addWidget(self.btnSaveAll)
self.grpPhotoViewLayout.addLayout(rowActionsGeneral2)
self.grpPhotoView.setLayout(self.grpPhotoViewLayout)
# Photo List
self.grpPhotoThumbs = QtWidgets.QGroupBox("Photo Album")
self.grpPhotoThumbsLayout = QtWidgets.QVBoxLayout()
self.grpPhotoThumbsLayout.setSpacing(2)
self.grpPhotoThumbsLayout.setContentsMargins(-1, 3, -1, -1)
self.lblPhoto = []
rowsPhotos = []
for row in range(0, 5):
rowsPhotos.append(QtWidgets.QHBoxLayout())
rowsPhotos[row].setSpacing(2)
for _ in range(0, 6):
self.lblPhoto.append(QtWidgets.QLabel(self))
self.lblPhoto[len(self.lblPhoto)-1].setMinimumSize(49, 43)
self.lblPhoto[len(self.lblPhoto)-1].setMaximumSize(49, 43)
self.lblPhoto[len(self.lblPhoto)-1].mousePressEvent = functools.partial(self.lblPhoto_Clicked, index=len(self.lblPhoto)-1)
self.lblPhoto[len(self.lblPhoto)-1].setCursor(QtGui.QCursor(QtCore.Qt.PointingHandCursor))
self.lblPhoto[len(self.lblPhoto)-1].setAlignment(QtCore.Qt.AlignCenter)
self.lblPhoto[len(self.lblPhoto)-1].setStyleSheet("border-top: 1px solid #adadad; border-left: 1px solid #adadad; border-bottom: 1px solid #fefefe; border-right: 1px solid #fefefe;")
rowsPhotos[row].addWidget(self.lblPhoto[len(self.lblPhoto)-1])
self.grpPhotoThumbsLayout.addLayout(rowsPhotos[row])
rowActionsGeneral3 = QtWidgets.QHBoxLayout()
self.btnShowGameFace = QtWidgets.QPushButton("Load &Game Face")
self.btnShowGameFace.setStyleSheet("padding: 5px 10px;")
self.btnShowGameFace.clicked.connect(self.btnShowGameFace_Clicked)
rowActionsGeneral3.addWidget(self.btnShowGameFace)
self.grpPhotoThumbsLayout.addStretch()
self.grpPhotoThumbsLayout.addLayout(rowActionsGeneral3)
self.grpPhotoThumbsLayout.setAlignment(QtCore.Qt.AlignTop)
self.grpPhotoThumbs.setLayout(self.grpPhotoThumbsLayout)
self.layout_photos.addWidget(self.grpPhotoThumbs)
self.layout_photos.addWidget(self.grpPhotoView)
self.layout.addLayout(self.layout_options1, 0, 0)
self.layout.addLayout(self.layout_options2, 1, 0)
self.layout.addLayout(self.layout_photos, 2, 0)
self.layout.addLayout(self.layout_options3, 3, 0)
self.setLayout(self.layout)
self.APP = app
palette = self.APP.SETTINGS.value("PocketCameraPalette")
try:
palette = json.loads(palette)
except:
palette = None
if palette is not None:
for i in range(0, len(self.PALETTES)):
if palette == self.PALETTES[i]:
self.rowColors1.itemAt(i).widget().setChecked(True)
if self.CUR_FILE is not None:
self.OpenFile(self.CUR_FILE)
export_path = self.APP.SETTINGS.value("LastDirPocketCamera")
if export_path is not None:
self.CUR_EXPORT_PATH = export_path
self.SetColors()
self.btnSaveAll.setDefault(True)
self.btnSaveAll.setAutoDefault(True)
self.btnSaveAll.setFocus()
def run(self):
self.layout.update()
self.layout.activate()
screenGeometry = QtWidgets.QDesktopWidget().screenGeometry()
x = (screenGeometry.width() - self.width()) / 2
y = (screenGeometry.height() - self.height()) / 2
self.move(x, y)
self.show()
def SetColors(self):
if self.CUR_PC is None: return
for i in range(0, self.rowColors1.count()):
if self.rowColors1.itemAt(i).widget().isChecked():
self.CUR_PC.SetPalette(self.PALETTES[i])
self.BuildPhotoList()
self.UpdateViewer(self.CUR_INDEX)
def OpenFile(self, file):
self.CUR_PC = PocketCamera()
if self.CUR_PC.LoadFile(file) == False:
self.CUR_PC = None
QtWidgets.QMessageBox.warning(self, "FlashGBX", "The save data file couldnt be loaded.", QtWidgets.QMessageBox.Ok)
return False
self.CUR_FILE = file
if self.CUR_EXPORT_PATH == "":
self.CUR_EXPORT_PATH = os.path.dirname(self.CUR_FILE)
self.UpdateViewer(1)
self.SetColors()
return True
def lblPhoto_Clicked(self, event, index):
if event.button() == QtCore.Qt.LeftButton:
self.CUR_INDEX = index + 1
self.UpdateViewer(self.CUR_INDEX)
def lblPhotoViewer_Clicked(self, event):
if event.button() == QtCore.Qt.LeftButton:
self.CUR_BICUBIC = not self.CUR_BICUBIC
self.UpdateViewer(self.CUR_INDEX)
def btnOpenSRAM_Clicked(self):
last_dir = self.APP.SETTINGS.value("LastDirSaveDataDMG")
path = QtWidgets.QFileDialog.getOpenFileName(self, "Open GB Camera Save Data File", last_dir, "Save Data File (*.sav);;All Files (*.*)")[0]
if (path == ""): return
if self.OpenFile(path) is True:
self.APP.SETTINGS.setValue("LastDirSaveDataDMG", os.path.dirname(path))
def btnShowGameFace_Clicked(self, event):
self.UpdateViewer(0)
self.CUR_INDEX = 0
def btnSaveAll_Clicked(self, event):
if self.CUR_PC is None: return
path = self.CUR_EXPORT_PATH + "/IMG_PC.png"
path = QtWidgets.QFileDialog.getSaveFileName(self, "Export all pictures", path, "PNG files (*.png);;BMP files (*.bmp);;GIF files (*.gif);;JPEG files (*.jpg);;All files (*.*)")[0]
if path == "": return
self.CUR_EXPORT_PATH = os.path.dirname(path)
for i in range(0, 31):
file = os.path.splitext(path)[0] + "{:02d}".format(i) + os.path.splitext(path)[1]
if os.path.exists(file):
answer = QtWidgets.QMessageBox.warning(self, "FlashGBX", "There are already pictures that use the same file names. If you continue, these files will be overwritten.", QtWidgets.QMessageBox.Ok | QtWidgets.QMessageBox.Cancel)
if answer == QtWidgets.QMessageBox.Ok:
break
elif answer == QtWidgets.QMessageBox.Cancel:
return
for i in range(0, 31):
file = os.path.splitext(path)[0] + "{:02d}".format(i) + os.path.splitext(path)[1]
self.SavePicture(i, path=file)
def btnSavePhoto_Clicked(self, event):
if self.CUR_PC is None: return
self.SavePicture(self.CUR_INDEX)
def btnClose_Clicked(self, event):
self.reject()
def hideEvent(self, event):
for i in range(0, self.rowColors1.count()):
if self.rowColors1.itemAt(i).widget().isChecked():
self.APP.SETTINGS.setValue("PocketCameraPalette", json.dumps(self.PALETTES[i]))
self.APP.SETTINGS.setValue("LastDirPocketCamera", self.CUR_EXPORT_PATH)
self.APP.activateWindow()
def BuildPhotoList(self):
cam = self.CUR_PC
self.CUR_THUMBS = [None] * 30
for i in range(0, 30):
pic = cam.GetPicture(i+1).convert("RGBA")
self.lblPhoto[i].setToolTip("")
if cam.IsEmpty(i+1):
pass
#draw = ImageDraw.Draw(pic, "RGBA")
#draw.line([0, 0, 128, 112], fill=(255, 0, 0), width=8)
#draw.line([0, 112, 128, 0], fill=(255, 0, 0), width=8)
elif cam.IsDeleted(i+1):
draw_bg = Image.new("RGBA", pic.size)
draw = ImageDraw.Draw(draw_bg)
draw.line([0, 0, 128, 112], fill=(255, 0, 0, 192), width=8)
draw.line([0, 112, 128, 0], fill=(255, 0, 0, 192), width=8)
pic.paste(draw_bg, mask=draw_bg)
self.lblPhoto[i].setToolTip("This picture was marked as “deleted” and may be overwritten when you take new pictures.")
self.CUR_THUMBS[i] = ImageQt(pic.resize((47, 41), Image.HAMMING))
qpixmap = QtGui.QPixmap.fromImage(self.CUR_THUMBS[i])
self.lblPhoto[i].setPixmap(qpixmap)
def UpdateViewer(self, index):
resampler = Image.NEAREST
if self.CUR_BICUBIC: resampler = Image.BICUBIC
cam = self.CUR_PC
if cam is None: return
for i in range(0, 30):
self.lblPhoto[i].setStyleSheet("border-top: 1px solid #adadad; border-left: 1px solid #adadad; border-bottom: 1px solid #ffffff; border-right: 1px solid #ffffff;")
if index == 0:
self.CUR_PIC = ImageQt(cam.GetPicture(0).convert("RGBA").resize((256, 224), resampler))
else:
self.CUR_PIC = ImageQt(cam.GetPicture(index).convert("RGBA").resize((256, 224), resampler))
self.lblPhoto[index - 1].setStyleSheet("border: 3px solid green; padding: 1px;")
qpixmap = QtGui.QPixmap.fromImage(self.CUR_PIC)
self.lblPhotoViewer.setPixmap(qpixmap)
def SavePicture(self, index, path=""):
if path == "":
path = self.CUR_EXPORT_PATH + "/IMG_PC{:02d}.png".format(index)
path = QtWidgets.QFileDialog.getSaveFileName(self, "Save Photo", path, "PNG files (*.png);;BMP files (*.bmp);;GIF files (*.gif);;JPEG files (*.jpg);;All files (*.*)")[0]
if path != "": self.CUR_EXPORT_PATH = os.path.dirname(path)
if path == "": return
cam = self.CUR_PC
cam.ExportPicture(index, path)
def dragEnterEvent(self, e):
if self._dragEventHover(e):
e.accept()
else:
e.ignore()
def dragMoveEvent(self, e):
if self._dragEventHover(e):
e.accept()
else:
e.ignore()
def _dragEventHover(self, e):
if e.mimeData().hasUrls:
for url in e.mimeData().urls():
if platform.system() == 'Darwin':
# pylint: disable=undefined-variable
fn = str(NSURL.URLWithString_(str(url.toString())).filePathURL().path())
else:
fn = str(url.toLocalFile())
fn_split = os.path.splitext(os.path.abspath(fn))
if fn_split[1] == ".sav":
return True
return False
def dropEvent(self, e):
if e.mimeData().hasUrls:
e.setDropAction(QtCore.Qt.CopyAction)
e.accept()
for url in e.mimeData().urls():
if platform.system() == 'Darwin':
# pylint: disable=undefined-variable
fn = str(NSURL.URLWithString_(str(url.toString())).filePathURL().path())
else:
fn = str(url.toLocalFile())
fn_split = os.path.splitext(os.path.abspath(fn))
if fn_split[1] == ".sav":
self.OpenFile(fn)
else:
e.ignore()

View File

@@ -44,15 +44,18 @@ class RomFileAGB:
data["empty"] = (self.ROMFILE == bytearray([buffer[0]] * len(buffer)))
data["logo_correct"] = hashlib.sha1(buffer[0x04:0xA0]).digest() == bytearray([ 0x17, 0xDA, 0xA0, 0xFE, 0xC0, 0x2F, 0xC3, 0x3C, 0x0F, 0x6A, 0xBB, 0x54, 0x9A, 0x8B, 0x80, 0xB6, 0x61, 0x3B, 0x48, 0xEE ])
game_title = bytearray(buffer[0xA0:0xAC]).decode("ascii", "replace")
game_title = re.sub(r"(\x00+)$", "", game_title).replace("\x00", "_")
game_title = re.sub(r"(\x00+)$", "", game_title)
game_title = re.sub(r"((_)_+|(\x00)\x00+|(\s)\s+)", "\\2\\3\\4", game_title).replace("\x00", "_")
game_title = ''.join(filter(lambda x: x in set(string.printable), game_title))
data["game_title"] = game_title
game_code = bytearray(buffer[0xAC:0xB0]).decode("ascii", "replace")
game_code = re.sub(r"(\x00+)$", "", game_code).replace("\x00", "_")
game_code = re.sub(r"(\x00+)$", "", game_code)
game_title = re.sub(r"((_)_+|(\x00)\x00+|(\s)\s+)", "\\2\\3\\4", game_title).replace("\x00", "_")
game_code = ''.join(filter(lambda x: x in set(string.printable), game_code))
data["game_code"] = game_code
maker_code = bytearray(buffer[0xB0:0xB2]).decode("ascii", "replace")
maker_code = re.sub(r"(\x00+)$", "", maker_code).replace("\x00", "_")
maker_code = re.sub(r"(\x00+)$", "", maker_code)
game_title = re.sub(r"((_)_+|(\x00)\x00+|(\s)\s+)", "\\2\\3\\4", game_title).replace("\x00", "_")
maker_code = ''.join(filter(lambda x: x in set(string.printable), maker_code))
data["maker_code"] = maker_code

View File

@@ -1,14 +1,9 @@
# -*- coding: utf-8 -*-
#
import hashlib, re, sys, string
from . import Util
class RomFileDMG:
DMG_Header_Features = { 0x00:'ROM ONLY', 0x01:'MBC1', 0x02:'MBC1+RAM', 0x03:'MBC1+RAM+BATTERY', 0x05:'MBC2', 0x06:'MBC2+BATTERY', 0x08:'ROM+RAM', 0x09:'ROM+RAM+BATTERY', 0x0B:'MMM01', 0x0C:'MMM01+RAM', 0x0D:'MMM01+RAM+BATTERY', 0x0F:'MBC3+TIMER+BATTERY', 0x10:'MBC3+TIMER+RAM+BATTERY', 0x11:'MBC3', 0x12:'MBC3+RAM', 0x13:'MBC3+RAM+BATTERY', 0x15:'MBC4', 0x16:'MBC4+RAM', 0x17:'MBC4+RAM+BATTERY', 0x19:'MBC5', 0x1A:'MBC5+RAM', 0x1B:'MBC5+RAM+BATTERY', 0x1C:'MBC5+RUMBLE', 0x1D:'MBC5+RUMBLE+RAM', 0x1E:'MBC5+RUMBLE+RAM+BATTERY', 0x20:'MBC6', 0x22:'MBC7+SENSOR+RUMBLE+RAM+BATTERY', 0x55:'Game Genie', 0x56:'Game Genie v3.0', 0xFC:'POCKET CAMERA', 0xFD:'BANDAI TAMA5', 0xFE:'HuC3', 0xFF:'HuC1+RAM+BATTERY' }
DMG_Header_ROM_Sizes = { 0x00:0x8000, 0x01:0x10000, 0x02:0x20000, 0x03:0x40000, 0x04:0x80000, 0x05:0x100000, 0x52:0x120000, 0x53:0x140000, 0x54:0x180000, 0x06:0x200000, 0x07:0x400000, 0x08:0x800000 }
DMG_Header_RAM_Sizes = { 0x00:0, 0x01:0x800, 0x02:0x2000, 0x03:0x8000, 0x05:0x10000, 0x04:0x20000 }
DMG_Header_SGB = { 0x00:'No support', 0x03:'Supported' }
DMG_Header_CGB = { 0x00:'No support', 0x80:'Supported', 0xC0:'Required' }
ROMFILE_PATH = None
ROMFILE = bytearray()
@@ -59,35 +54,71 @@ class RomFileDMG:
data = {}
data["empty"] = (self.ROMFILE == bytearray([buffer[0]] * len(buffer)))
data["logo_correct"] = hashlib.sha1(buffer[0x104:0x134]).digest() == bytearray([ 0x07, 0x45, 0xFD, 0xEF, 0x34, 0x13, 0x2D, 0x1B, 0x3D, 0x48, 0x8C, 0xFB, 0xDF, 0x03, 0x79, 0xA3, 0x9F, 0xD5, 0x4B, 0x4C ])
game_title = bytearray(buffer[0x134:0x143]).decode("ascii", "replace")
game_title = re.sub(r"(\x00+)$", "", game_title).replace("\x00", "_")
data["cgb"] = int(buffer[0x143])
data["sgb"] = int(buffer[0x146])
if data["cgb"] in (0x00, 0x80, 0xC0):
game_title = bytearray(buffer[0x134:0x143]).decode("ascii", "replace")
else:
game_title = bytearray(buffer[0x134:0x144]).decode("ascii", "replace")
game_title = re.sub(r"(\x00+)$", "", game_title)
game_title = re.sub(r"((_)_+|(\x00)\x00+|(\s)\s+)", "\\2\\3\\4", game_title).replace("\x00", "_")
game_title = ''.join(filter(lambda x: x in set(string.printable), game_title))
data["game_title"] = game_title
data["maker_code"] = format(int(buffer[0x14B]), "02X")
if data["maker_code"] == '33':
maker_code = bytearray(buffer[0x144:0x146]).decode("ascii", "replace")
maker_code = ''.join(filter(lambda x: x in set(string.printable), maker_code))
data["maker_code_new"] = maker_code
data["cgb"] = int(buffer[0x143])
data["sgb"] = int(buffer[0x146])
data["features_raw"] = int(buffer[0x147])
data["features"] = "?"
if buffer[0x147] in self.DMG_Header_Features: data["features"] = self.DMG_Header_Features[buffer[0x147]]
data["rom_size_raw"] = int(buffer[0x148])
data["rom_size"] = "?"
if buffer[0x148] in self.DMG_Header_ROM_Sizes: data["rom_size"] = self.DMG_Header_ROM_Sizes[buffer[0x148]]
if buffer[0x148] in Util.DMG_Header_ROM_Sizes: data["rom_size"] = Util.DMG_Header_ROM_Sizes[buffer[0x148]]
data["ram_size_raw"] = int(buffer[0x149])
if data["features"] == 0x05 or data["features"] == 0x06:
if data["features_raw"] == 0x05 or data["features_raw"] == 0x06:
data["ram_size"] = 0x200
else:
data["ram_size"] = "?"
if buffer[0x149] in self.DMG_Header_RAM_Sizes: data["ram_size"] = self.DMG_Header_RAM_Sizes[buffer[0x149]]
if buffer[0x149] in Util.DMG_Header_RAM_Sizes:
data["ram_size"] = Util.DMG_Header_RAM_Sizes[buffer[0x149]]
data["header_checksum"] = int(buffer[0x14D])
data["header_checksum_calc"] = self.CalcChecksumHeader()
data["header_checksum_correct"] = data["header_checksum"] == data["header_checksum_calc"]
data["rom_checksum"] = int(256 * buffer[0x14E] + buffer[0x14F])
data["rom_checksum_calc"] = self.CalcChecksumGlobal()
data["rom_checksum_correct"] = data["rom_checksum"] == data["rom_checksum_calc"]
# MBC1M
if data["features_raw"] == 0x03 and data["game_title"] == "MOMOCOL" and data["header_checksum"] == 0x28 or \
data["features_raw"] == 0x01 and data["game_title"] == "BOMCOL" and data["header_checksum"] == 0x86 or \
data["features_raw"] == 0x01 and data["game_title"] == "GENCOL" and data["header_checksum"] == 0x8A or \
data["features_raw"] == 0x01 and data["game_title"] == "SUPERCHINESE 123" and data["header_checksum"] == 0xE4 or \
data["features_raw"] == 0x01 and data["game_title"] == "MORTALKOMBATI&II" and data["header_checksum"] == 0xB9 or \
data["features_raw"] == 0x01 and data["game_title"] == "MORTALKOMBAT DUO" and data["header_checksum"] == 0xA7:
data["features_raw"] += 0x100
# GB Memory
if data["features_raw"] == 0x19 and data["game_title"] == "NP M-MENU MENU" and data["header_checksum"] == 0xD3:
data["features_raw"] = 0x105
# M161 (Mani 4 in 1)
elif data["features_raw"] == 0x10 and data["game_title"] == "TETRIS SET" and data["header_checksum"] == 0x3F:
data["features_raw"] = 0x104
# MMM01 (Mani 4 in 1)
elif data["features_raw"] == 0x11 and data["game_title"] == "BOUKENJIMA2 SET" and data["header_checksum"] == 0 or \
data["features_raw"] == 0x11 and data["game_title"] == "BUBBLEBOBBLE SET" and data["header_checksum"] == 0xC6 or \
data["features_raw"] == 0x11 and data["game_title"] == "GANBARUGA SET" and data["header_checksum"] == 0x90 or \
data["features_raw"] == 0x11 and data["game_title"] == "RTYPE 2 SET" and data["header_checksum"] == 0x32:
data["features_raw"] = 0x0B
if data["features_raw"] in Util.DMG_Header_Features:
data["features"] = Util.DMG_Header_Features[data["features_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))
return data
def GetData(self):

View File

@@ -1,8 +1,282 @@
# -*- coding: utf-8 -*-
#
import math, time, datetime, copy
import math, time, datetime, copy, configparser, threading, statistics, os, platform
from enum import Enum
# Common constants
APPNAME = "FlashGBX"
VERSION_PEP440 = "1.4"
VERSION = "v{:s}".format(VERSION_PEP440)
DEBUG = False
AGB_Header_ROM_Sizes = [ "4 MB", "8 MB", "16 MB", "32 MB", "64 MB (GBA Video)" ]
AGB_Header_ROM_Sizes_Map = [ 0x400000, 0x800000, 0x1000000, 0x2000000, 0x4000000 ]
AGB_Header_Save_Types = [ "None", "4K EEPROM (512 Bytes)", "64K EEPROM (8 KB)", "256K SRAM (32 KB)", "512K SRAM (64 KB)", "1M SRAM (128 KB)", "512K FLASH (64 KB)", "1M FLASH (128 KB)" ]
AGB_Global_CRC32 = 0
DMG_Header_Features = { 0x00:'None', 0x01:'MBC1', 0x02:'MBC1+SRAM', 0x03:'MBC1+SRAM+BATTERY', 0x06:'MBC2+BATTERY', 0x10:'MBC3+RTC+SRAM+BATTERY', 0x13:'MBC3+SRAM+BATTERY', 0x19:'MBC5', 0x1B:'MBC5+SRAM+BATTERY', 0x1C:'MBC5+RUMBLE', 0x1E:'MBC5+RUMBLE+SRAM+BATTERY', 0x20:'MBC6+FLASH+SRAM+BATTERY', 0x22:'MBC7+ACCELEROMETER+EEPROM', 0x101:'MBC1M', 0x103:'MBC1M+SRAM+BATTERY', 0x0B:'MMM01', 0x0D:'MMM01+SRAM+BATTERY', 0xFC:'CAMERA+SRAM+BATTERY', 0x105:'G-MMC1', 0x104:'M161', 0xFF:'HuC-1+IR+SRAM+BATTERY', 0xFE:'HuC-3+RTC+SRAM+BATTERY', 0xFD:'TAMA5+RTC+EEPROM' }
DMG_Header_ROM_Sizes = [ "32 KB", "64 KB", "128 KB", "256 KB", "512 KB", "1 MB", "2 MB", "4 MB", "8 MB" ]
DMG_Header_ROM_Sizes_Map = [ 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08 ]
DMG_Header_ROM_Sizes_Flasher_Map = [ 2, 4, 8, 16, 32, 64, 128, 256, 512 ] # Number of ROM banks
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)", "2K MBC7 EEPROM (256 Bytes)", "4K MBC7 EEPROM (512 Bytes)", "TAMA5 EEPROM (32 Bytes)" ]
DMG_Header_RAM_Sizes_Map = [ 0x00, 0x01, 0x01, 0x02, 0x03, 0x05, 0x04, 0x101, 0x102, 0x103 ]
DMG_Header_RAM_Sizes_Flasher_Map = [ 0, 0x200, 0x800, 0x2000, 0x8000, 0x10000, 0x20000, 0x100, 0x200, 0x20 ] # RAM size in bytes
DMG_Header_SGB = { 0x00:'No support', 0x03:'Supported' }
DMG_Header_CGB = { 0x00:'No support', 0x80:'Supported', 0xC0:'Required' }
class ANSI:
BOLD = '\033[1m'
RED = '\033[91m'
GREEN = '\033[92m'
YELLOW = '\033[33m'
RESET = '\033[0m'
CLEAR_LINE = '\033[2K'
class IniSettings():
FILENAME = ""
SETTINGS = None
def __init__(self, ini_file):
try:
if not os.path.isdir(os.path.dirname(ini_file)):
os.makedirs(os.path.dirname(ini_file))
if os.path.exists(ini_file):
with open(ini_file, "a+") as f: f.close()
else:
with open(ini_file, "w+") as f: f.close()
except:
print("Error accessing the configuration directory or settings file.")
return
self.FILENAME = ini_file
self.SETTINGS = configparser.ConfigParser()
self.SETTINGS.optionxform = str
self.Reload()
def Reload(self):
if self.SETTINGS is None: return
with open(self.FILENAME, "r", encoding="utf-8") as f:
self.SETTINGS.read_file(f)
if len(self.SETTINGS.sections()) == 0:
self.SETTINGS.add_section("General")
def value(self, key, default=None): return self.GetValue(key, default)
def GetValue(self, key, default=None):
if self.SETTINGS is None: return None
self.Reload()
if key not in self.SETTINGS["General"]:
if default is not None: self.SetValue(key, default)
return default
return (self.SETTINGS["General"][key])
def setValue(self, key, value): self.SetValue(key, value)
def SetValue(self, key, value):
if self.SETTINGS is None: return None
self.Reload()
self.SETTINGS["General"][key] = value
dprint("Updating settings:", key, "=", value)
with open(self.FILENAME, "w", encoding="utf-8") as f:
self.SETTINGS.write(f)
def clear(self): self.Clear()
def Clear(self):
if self.SETTINGS is None: return None
self.SETTINGS.clear()
with open(self.FILENAME, "w", encoding="utf-8") as f:
self.SETTINGS.write(f)
class Progress():
MUTEX = threading.Lock()
PROGRESS = {}
UPDATER = None
def __init__(self, updater):
self.UPDATER = updater
pass
def SetProgress(self, args):
self.MUTEX.acquire(1)
try:
if not "method" in self.PROGRESS: self.PROGRESS = {}
now = time.time()
if args["action"] == "INITIALIZE":
self.PROGRESS["action"] = args["action"]
self.PROGRESS["method"] = args["method"]
self.PROGRESS["size"] = args["size"]
if "pos" in args:
self.PROGRESS["pos"] = args["pos"]
else:
self.PROGRESS["pos"] = 0
if "time_start" in args:
self.PROGRESS["time_start"] = args["time_start"]
else:
self.PROGRESS["time_start"] = now
self.PROGRESS["time_last_emit"] = now
self.PROGRESS["time_last_update_speed"] = now
self.PROGRESS["time_left"] = 0
self.PROGRESS["speed"] = 0
self.PROGRESS["speeds"] = []
self.PROGRESS["bytes_last_update_speed"] = 0
self.UPDATER(self.PROGRESS)
if args["action"] == "ABORT":
self.UPDATER(args)
self.PROGRESS = {}
elif args["action"] in ("ERASE", "SECTOR_ERASE"):
if "time_start" in self.PROGRESS:
args["time_elapsed"] = now - self.PROGRESS["time_start"]
elif "time_start" in args:
args["time_elapsed"] = now - args["time_start"]
args["pos"] = 1
args["size"] = 0
self.UPDATER(args)
elif self.PROGRESS == {}:
return
elif args["action"] == "UPDATE_POS":
self.PROGRESS["pos"] = args["pos"]
self.PROGRESS["action"] = "PROGRESS"
if "time_start" in self.PROGRESS:
self.PROGRESS["time_elapsed"] = now - self.PROGRESS["time_start"]
try:
total_speed = statistics.mean(self.PROGRESS["speeds"])
self.PROGRESS["time_left"] = (self.PROGRESS["size"] - self.PROGRESS["pos"]) / 1024 / total_speed
except:
pass
if "abortable" in args: self.PROGRESS["abortable"] = args["abortable"]
self.UPDATER(self.PROGRESS)
elif args["action"] in ("READ", "WRITE"):
if "method" not in self.PROGRESS: return
elif args["action"] in ("READ") and self.PROGRESS["method"] in ("SAVE_WRITE", "ROM_WRITE"): return
elif args["action"] in ("WRITE") and self.PROGRESS["method"] in ("SAVE_READ", "ROM_READ", "ROM_WRITE_VERIFY"): return
if self.PROGRESS["pos"] >= self.PROGRESS["size"]: return
self.PROGRESS["action"] = "PROGRESS"
self.PROGRESS["pos"] += args["bytes_added"]
if (now - self.PROGRESS["time_last_emit"]) > 0.05:
self.PROGRESS["time_elapsed"] = now - self.PROGRESS["time_start"]
if (now - self.PROGRESS["time_last_update_speed"]) > 0.25:
time_delta = now - self.PROGRESS["time_last_update_speed"]
pos_delta = self.PROGRESS["pos"] - self.PROGRESS["bytes_last_update_speed"]
if time_delta > 0:
speed = (pos_delta / time_delta) / 1024
self.PROGRESS["speeds"].append(speed)
if len(self.PROGRESS["speeds"]) > 256: self.PROGRESS["speeds"].pop(0)
self.PROGRESS["speed"] = statistics.median(self.PROGRESS["speeds"])
self.PROGRESS["time_last_update_speed"] = now
self.PROGRESS["bytes_last_update_speed"] = self.PROGRESS["pos"]
if "skipping" in args and args["skipping"] is True:
self.PROGRESS["speed"] = 0
self.PROGRESS["skipping"] = True
else:
self.PROGRESS["skipping"] = False
if self.PROGRESS["speed"] > 0:
total_speed = statistics.mean(self.PROGRESS["speeds"])
self.PROGRESS["time_left"] = (self.PROGRESS["size"] - self.PROGRESS["pos"]) / 1024 / total_speed
self.UPDATER(self.PROGRESS)
self.PROGRESS["time_last_emit"] = now
elif args["action"] == "FINISHED":
self.PROGRESS["pos"] = self.PROGRESS["size"]
self.UPDATER(self.PROGRESS)
self.PROGRESS["action"] = args["action"]
self.PROGRESS["bytes_last_update_speed"] = self.PROGRESS["size"]
self.PROGRESS["time_elapsed"] = now - self.PROGRESS["time_start"]
self.PROGRESS["time_last_emit"] = now
self.PROGRESS["time_last_update_speed"] = now
self.PROGRESS["time_left"] = 0
self.PROGRESS["speed"] = (self.PROGRESS["size"] / self.PROGRESS["time_elapsed"]) / 1024
self.PROGRESS["bytes_last_emit"] = self.PROGRESS["size"]
if "verified" in args: self.PROGRESS["verified"] = (args["verified"] == True)
if self.PROGRESS["speed"] > self.PROGRESS["size"] / 1024:
self.PROGRESS["speed"] = self.PROGRESS["size"] / 1024
self.UPDATER(self.PROGRESS)
del(self.PROGRESS["method"])
finally:
self.MUTEX.release()
class TAMA5_CMD(Enum):
RAM_WRITE = 0x0
RAM_READ = 0x1
RTC = 0x4
class TAMA5_REG(Enum):
ROM_BANK_L = 0x0
ROM_BANK_H = 0x1
MEM_WRITE_L = 0x4
MEM_WRITE_H = 0x5
ADDR_H_SET_MODE = 0x6
ADDR_L = 0x7
ENABLE = 0xA
MEM_READ_L = 0xC
MEM_READ_H = 0xD
def formatFileSize(size, asInt=False):
#size = size / 1024
if size == 1:
return "{:d} Byte".format(size)
elif size < 1024:
return "{:d} Bytes".format(size)
elif size < 1024 * 1024:
if asInt:
return "{:d} KB".format(int(size/1024))
else:
return "{:.1f} KB".format(size/1024)
else:
if asInt:
return "{:d} MB".format(int(size/1024/1024))
else:
return "{:.2f} MB".format(size/1024/1024)
def formatProgressTimeShort(sec):
sec = sec % (24 * 3600)
hr = sec // 3600
sec %= 3600
min = sec // 60
sec %= 60
return "{:02d}:{:02d}:{:02d}".format(int(hr), int(min), int(sec))
def formatProgressTime(sec):
if int(sec) == 1:
return "{:d} second".format(int(sec))
elif sec < 60:
return "{:d} seconds".format(int(sec))
elif int(sec) == 60:
return "1 minute"
else:
min = int(sec / 60)
sec = int(sec % 60)
s = str(min) + " "
if min == 1:
s = s + "minute"
else:
s = s + "minutes"
s = s + ", " + str(sec) + " "
if sec == 1:
s = s + "second"
else:
s = s + "seconds"
return s
def formatPathOS(path, end_sep=False):
if platform.system() == "Windows":
path = path.replace("/", "\\")
if end_sep:
path += "\\"
else:
if end_sep:
path += "/"
return path
# Utility functions
def bitswap(n, s):
p, q = s
if (((n & (1 << p)) >> p) ^ ((n & (1 << q)) >> q)) == 1:
@@ -29,6 +303,9 @@ def ParseCFI(buffer):
pass
return False
pri_address = (buffer[0x2A] | (buffer[0x2C] << 8)) * 2
if (pri_address + 0x3C) >= 0x400: pri_address = 0x80
info["vdd_min"] = (buffer[0x36] >> 4) + ((buffer[0x36] & 0x0F) / 10)
info["vdd_max"] = (buffer[0x38] >> 4) + ((buffer[0x38] & 0x0F) / 10)
@@ -61,9 +338,11 @@ def ParseCFI(buffer):
info["chip_erase"] = False
info["tb_boot_sector"] = False
if "{:s}{:s}{:s}".format(chr(buffer[0x80]), chr(buffer[0x82]), chr(buffer[0x84])) == "PRI":
if buffer[0x9E] != 0 and buffer[0x9E] != 0xFF:
temp = { 0x02: 'Bottom Boot Device', 0x03: 'Top Boot Device' }
info["tb_boot_sector_raw"] = 0
if "{:s}{:s}{:s}".format(chr(buffer[pri_address]), chr(buffer[pri_address+2]), chr(buffer[pri_address+4])) == "PRI":
if buffer[pri_address + 0x1E] not in (0, 0xFF):
temp = { 0x02: 'As shown', 0x03: 'Reversed' }
info["tb_boot_sector_raw"] = buffer[0x9E]
try:
info["tb_boot_sector"] = "{:s} (0x{:02X})".format(temp[buffer[0x9E]], buffer[0x9E])
except:
@@ -102,6 +381,5 @@ def ParseCFI(buffer):
return info
def dprint(*args, **kwargs):
# uncomment for some debug prints
#print(datetime.datetime.now().strftime("[%Y-%m-%d %H:%M:%S]"), " ".join(map(str, args)), **kwargs)
pass
if DEBUG:
print("{:s}{:s} {:s}".format(ANSI.CLEAR_LINE, datetime.datetime.now().strftime("[%Y-%m-%d %H:%M:%S]"), " ".join(map(str, args)), **kwargs))

View File

@@ -2400,27 +2400,6 @@
"st": 1,
"gc": "BOMJ"
},
"c2f45b5bafe2382326ae33938f1bc5058a1a27cc": {
"rs": 8388608,
"rc": 1701638702,
"ss": 8192,
"st": 2,
"gc": "AMHJ"
},
"4b068b46f218f23ac7fce7c64e748759ae2a66e5": {
"rs": 8388608,
"rc": 688752656,
"ss": 8192,
"st": 2,
"gc": "AMYJ"
},
"23ec7be5f5bdde5d7238e14599aa44cb0190e223": {
"rs": 4194304,
"rc": 4159963529,
"ss": 32768,
"st": 3,
"gc": "ABSJ"
},
"5d67242b52ea9e154cbc799b8f7eb369ab572472": {
"rs": 16777216,
"rc": 167529811,
@@ -2435,6 +2414,20 @@
"st": 2,
"gc": "AMHE"
},
"c2f45b5bafe2382326ae33938f1bc5058a1a27cc": {
"rs": 8388608,
"rc": 1701638702,
"ss": 8192,
"st": 2,
"gc": "AMHJ"
},
"4b068b46f218f23ac7fce7c64e748759ae2a66e5": {
"rs": 8388608,
"rc": 688752656,
"ss": 8192,
"st": 2,
"gc": "AMYJ"
},
"9317af9f4c86808a9d2fcfb3bcfaadeb8a4acb79": {
"rs": 16777216,
"rc": 1741874616,
@@ -2449,6 +2442,13 @@
"st": 2,
"gc": "AMYE"
},
"23ec7be5f5bdde5d7238e14599aa44cb0190e223": {
"rs": 4194304,
"rc": 4159963529,
"ss": 32768,
"st": 3,
"gc": "ABSJ"
},
"96a75e80641212f9ee3aea2c500bc3b1ebff945e": {
"rs": 4194304,
"rc": 604144358,
@@ -3415,90 +3415,6 @@
"st": 1,
"gc": "BPSJ"
},
"4917828a050424930b20c0f760ac716494b9dbdd": {
"rs": 4194304,
"rc": 3446141626,
"ss": 512,
"st": 1,
"gc": "FBME"
},
"58086ff435641c40d3d72060916b8884e105cf91": {
"rs": 4194304,
"rc": 3485594968,
"ss": 512,
"st": 1,
"gc": "FADE"
},
"e34dfd5715d058484ab10da49bf0bf31c13af68a": {
"rs": 4194304,
"rc": 3129136150,
"ss": 512,
"st": 1,
"gc": "FDKE"
},
"8c97edbfb3bc8637d131cc1bd156cd7247a92698": {
"rs": 4194304,
"rc": 3399441711,
"ss": 512,
"st": 1,
"gc": "FDME"
},
"3364725609fc5ac9e6fab3c3b931630f3393467c": {
"rs": 4194304,
"rc": 2489067823,
"ss": 8192,
"st": 2,
"gc": "FEBE"
},
"4b637f396eba0daebc25bb3de25610309bf27de3": {
"rs": 4194304,
"rc": 824628754,
"ss": 512,
"st": 1,
"gc": "FICE"
},
"e42088bc18d29dafb8c874a7d87fb2c1b983f292": {
"rs": 4194304,
"rc": 2996917187,
"ss": 512,
"st": 1,
"gc": "FMRE"
},
"e3a53b82b787f03ae7d3d75c9f9aa012d30b2c7e": {
"rs": 4194304,
"rc": 589267158,
"ss": 512,
"st": 1,
"gc": "FP7E"
},
"d42612677ba881015f36f88f56ba6ac415d38a32": {
"rs": 4194304,
"rc": 2128478125,
"ss": 512,
"st": 1,
"gc": "FSME"
},
"3bb590068613ef71bc99c3192169b8e62043f97b": {
"rs": 4194304,
"rc": 1756477352,
"ss": 8192,
"st": 2,
"gc": "FZLE"
},
"4cf49737115eabf5d64c64c86f757f010dbec429": {
"rs": 4194304,
"rc": 2660382486,
"ss": 512,
"st": 1,
"gc": "FXVE"
},
"9721d13f880f9e90b4077ebb81555832be66e360": {
"rs": 4194304,
"rc": 4074519305,
"ss": 8192,
"st": 2,
"gc": "FLBE"
},
"bb959dd1d8a89bbdba7de95729555220e0749a5a": {
"rs": 4194304,
"rc": 3669476354,
@@ -5872,34 +5788,6 @@
"st": 0,
"gc": "BF2E"
},
"b429a05c22bfd5d17e979e74a301671245d9ef61": {
"rs": 4194304,
"rc": 1111199685,
"ss": 8192,
"st": 2,
"gc": "FSRJ"
},
"baedf5b3d763721ba8f2b368bc8bc4ed91283d9b": {
"rs": 4194304,
"rc": 4209311212,
"ss": 512,
"st": 1,
"gc": "FGZJ"
},
"ad2592a0770975efb800afe7b98e7a3a77db1042": {
"rs": 4194304,
"rc": 1481148492,
"ss": 512,
"st": 1,
"gc": "FSMJ"
},
"ae557926846dd36c28bebe5bcaa358342ee59bf8": {
"rs": 4194304,
"rc": 2261993135,
"ss": 512,
"st": 1,
"gc": "FDKJ"
},
"4668c771a5a7a2a63906e5b66a60a4bf0c6b67bf": {
"rs": 4194304,
"rc": 1447492049,
@@ -9437,7 +9325,7 @@
},
"14fd9d51892d9f8a2f16a675ddc495d35f490d64": {
"rs": 8388608,
"rc": 1085398272,
"rc": 3357251329,
"ss": 32768,
"st": 3,
"gc": "AK5J"
@@ -12613,13 +12501,6 @@
"st": 2,
"gc": "ARNJ"
},
"5a7b3e826d91514fb9dbc119876db1ad48cf0e87": {
"rs": 4194304,
"rc": 726728774,
"ss": 512,
"st": 1,
"gc": "FADP"
},
"2f1ec7d3d3eedfea092d277bebc57a0659d0b3ca": {
"rs": 4194304,
"rc": 1385654586,
@@ -17471,13 +17352,6 @@
"st": 1,
"gc": "BMVJ"
},
"034c3eaa513db1efc98ad1b29e92d28e30c883fa": {
"rs": 4194304,
"rc": 140191515,
"ss": 0,
"st": 0,
"gc": "FSMJ"
},
"5942690b24639606bc87a8397cf673f7d6aa99be": {
"rs": 8388608,
"rc": 3613237405,

View File

@@ -1,7 +1,7 @@
{
"type":"AGB",
"names":[
"GE28F128W30 with 128W30B0"
"GE28F128W30 with 128W30B"
],
"flash_ids":[
[ 0x8A, 0x00, 0x57, 0x88 ]

View File

@@ -1,17 +1,17 @@
{
"type":"AGB",
"names":[
"AGB-E08-09 with 29LV128DTMC-90Q"
"AGB-E08-09 with 29LV128DTMC-90Q",
"AGB-E05-01 with MX29GL128FHT2I-90G"
],
"flash_ids":[
[ 0xC1, 0x00, 0x7D, 0x22 ],
[ 0xC1, 0x00, 0x7D, 0x22 ]
],
"voltage":3.3,
"flash_size":0x1000000,
"sector_size":[
[0x10000, 255],
[0x02000, 8]
],
"sector_size_from_cfi":true,
"chip_erase_timeout":120,
"commands":{
"reset":[
[ 0, 0xF0 ]
@@ -37,6 +37,22 @@
[ null, null, null ],
[ "SA", 0xFFFF, 0xFFFF ]
],
"chip_erase":[
[ 0xAAA, 0xA9 ],
[ 0x555, 0x56 ],
[ 0xAAA, 0x80 ],
[ 0xAAA, 0xA9 ],
[ 0x555, 0x56 ],
[ 0xAAA, 0x10 ]
],
"chip_erase_wait_for":[
[ null, null, null ],
[ null, null, null ],
[ null, null, null ],
[ null, null, null ],
[ null, null, null ],
[ 0, 0xFFFF, 0xFFFF ]
],
"single_write":[
[ 0xAAA, 0xA9 ],
[ 0x555, 0x56 ],

View File

@@ -2,11 +2,13 @@
"type":"AGB",
"names":[
"4050_4400_4000_4350_36L0R_V5 with M36L0R8060T",
"36L0R8-39VF512 with M36L0R8060T"
"36L0R8-39VF512 with M36L0R8060T",
"4050_4400_4000_4350_36L0R_V5 with 4050L0YTQ2"
],
"flash_ids":[
[ 0x20, 0x00, 0x0E, 0x88 ],
[ 0x20, 0x00, 0x0E, 0x88 ]
[ 0x20, 0x00, 0x0E, 0x88 ],
[ 0x8A, 0x00, 0x0E, 0x88 ]
],
"voltage":3.3,
"flash_size":0x2000000,

View File

@@ -6,7 +6,8 @@
"BX2006_0106_NEW with S29GL128N10TFI01",
"BX2006_TSOP_64BALL with GL128S",
"BX2006_TSOPBGA_0106 with M29W640GB6AZA6",
"AGB-E05-02 with M29W128GH"
"AGB-E05-02 with M29W128GH",
"AGB-E05-02 with M29W128FH"
],
"flash_ids":[
[ 0x02, 0x00, 0x7D, 0x22 ],
@@ -14,11 +15,13 @@
[ 0x02, 0x00, 0x7D, 0x22 ],
[ 0x02, 0x00, 0x7D, 0x22 ],
[ 0x20, 0x00, 0x7D, 0x22 ],
[ 0x20, 0x00, 0x7D, 0x22 ],
[ 0x20, 0x00, 0x7D, 0x22 ]
],
"voltage":3.3,
"flash_size":0x1000000,
"sector_size":0x20000,
"sector_size_from_cfi":true,
"chip_erase_timeout":120,
"commands":{
"reset":[
[ 0, 0xF0 ]
@@ -28,6 +31,9 @@
[ 0x555, 0x56 ],
[ 0xAAA, 0x90 ]
],
"read_cfi":[
[ 0xAA, 0x98 ]
],
"sector_erase":[
[ 0xAAA, 0xA9 ],
[ 0x555, 0x56 ],
@@ -44,6 +50,22 @@
[ null, null, null ],
[ "SA", 0xFFFF, 0xFFFF ]
],
"chip_erase":[
[ 0xAAA, 0xA9 ],
[ 0x555, 0x56 ],
[ 0xAAA, 0x80 ],
[ 0xAAA, 0xA9 ],
[ 0x555, 0x56 ],
[ 0xAAA, 0x10 ]
],
"chip_erase_wait_for":[
[ null, null, null ],
[ null, null, null ],
[ null, null, null ],
[ null, null, null ],
[ null, null, null ],
[ 0, 0xFFFF, 0xFFFF ]
],
"buffer_write":[
[ 0xAAA, 0xA9 ],
[ 0x555, 0x56 ],

View File

@@ -11,10 +11,7 @@
"start_addr":0,
"first_bank":1,
"write_pin":"WR",
"sector_size":[
[0x10000, 31],
[0x2000, 8]
],
"sector_size_from_cfi":true,
"chip_erase_timeout":120,
"commands":{
"reset":[

View File

@@ -11,11 +11,7 @@
"start_addr":0,
"first_bank":1,
"write_pin":"WR",
"sector_size":[
[0x2000, 8],
[0x10000, 31]
],
"sector_reversal":true,
"sector_size_from_cfi":true,
"chip_erase_timeout":50,
"commands":{
"reset":[

View File

@@ -0,0 +1,97 @@
{
"type":"DMG",
"names":[
"NP GB Memory Cartridge (DMG-MMSA-JPN)"
],
"flash_ids":[
[ 0xC2, 0x89, 0xC2, 0xFF ]
],
"voltage":5,
"flash_size":0x100000,
"start_addr":0,
"first_bank":1,
"write_pin":"WR",
"unlock_before_rom_dump":true,
"hidden_sector_size":128,
"commands":{
"reset":[
[ 0x120, 0x02 ],
[ 0x120, 0x0F ],
[ 0x125, 0x40 ],
[ 0x126, 0x80 ],
[ 0x127, 0xF0 ],
[ 0x13F, 0xA5 ],
[ 0x120, 0x04 ],
[ 0x13F, 0xA5 ],
[ 0x120, 0x08 ],
[ 0x13F, 0xA5 ]
],
"unlock":[
[ 0x120, 0x09, 1 ],
[ 0x121, 0xAA, 1 ],
[ 0x122, 0x55, 1 ],
[ 0x13F, 0xA5, 1 ],
[ 0x120, 0x11, 1 ],
[ 0x13F, 0xA5, 1 ]
],
"read_identifier":[
[ 0x120, 0x0F ],
[ 0x125, 0x55 ],
[ 0x126, 0x55 ],
[ 0x127, 0xAA ],
[ 0x13F, 0xA5 ],
[ 0x120, 0x0F ],
[ 0x125, 0x2A ],
[ 0x126, 0xAA ],
[ 0x127, 0x55 ],
[ 0x13F, 0xA5 ],
[ 0x120, 0x0F ],
[ 0x125, 0x55 ],
[ 0x126, 0x55 ],
[ 0x127, 0x90 ],
[ 0x13F, 0xA5 ]
],
"read_hidden_sector":[
[ 0x120, 0x0F ],
[ 0x125, 0x55 ],
[ 0x126, 0x55 ],
[ 0x127, 0xAA ],
[ 0x13F, 0xA5 ],
[ 0x120, 0x0F ],
[ 0x125, 0x2A ],
[ 0x126, 0xAA ],
[ 0x127, 0x55 ],
[ 0x13F, 0xA5 ],
[ 0x120, 0x0F ],
[ 0x125, 0x55 ],
[ 0x126, 0x55 ],
[ 0x127, 0x77 ],
[ 0x13F, 0xA5 ],
[ 0x120, 0x0F ],
[ 0x125, 0x55 ],
[ 0x126, 0x55 ],
[ 0x127, 0xAA ],
[ 0x13F, 0xA5 ],
[ 0x120, 0x0F ],
[ 0x125, 0x2A ],
[ 0x126, 0xAA ],
[ 0x127, 0x55 ],
[ 0x13F, 0xA5 ],
[ 0x120, 0x0F ],
[ 0x125, 0x55 ],
[ 0x126, 0x55 ],
[ 0x127, 0x77 ],
[ 0x13F, 0xA5 ]
]
}
}

View File

@@ -0,0 +1,73 @@
{
"type":"DMG",
"names":[
"SD007_TSOP_48BALL with K8D3216UTC"
],
"flash_ids":[
[ 0xEC, 0xEC, 0xA0, 0xA0 ]
],
"voltage":3.3,
"flash_size":0x400000,
"start_addr":0,
"first_bank":1,
"write_pin":"WR",
"sector_size":[
[0x10000, 63],
[0x02000, 8]
],
"chip_erase_timeout":70,
"commands":{
"reset":[
[ 0, 0xF0 ]
],
"read_identifier":[
[ 0xAAA, 0xA9 ],
[ 0x555, 0x56 ],
[ 0xAAA, 0x90 ]
],
"chip_erase":[
[ 0xAAA, 0xA9 ],
[ 0x555, 0x56 ],
[ 0xAAA, 0x80 ],
[ 0xAAA, 0xA9 ],
[ 0x555, 0x56 ],
[ 0xAAA, 0x10 ]
],
"chip_erase_wait_for":[
[ null, null, null ],
[ null, null, null ],
[ null, null, null ],
[ null, null, null ],
[ null, null, null ],
[ 0, 0xFF, 0xFF ]
],
"sector_erase":[
[ 0xAAA, 0xA9 ],
[ 0x555, 0x56 ],
[ 0xAAA, 0x80 ],
[ 0xAAA, 0xA9 ],
[ 0x555, 0x56 ],
[ "SA", 0x30 ]
],
"sector_erase_wait_for":[
[ null, null, null ],
[ null, null, null ],
[ null, null, null ],
[ null, null, null ],
[ null, null, null ],
[ "SA", 0xFF, 0xFF ]
],
"single_write":[
[ 0xAAA, 0xA9 ],
[ 0x555, 0x56 ],
[ 0xAAA, 0xA0 ],
[ "PA", "PD" ]
],
"single_write_wait_for":[
[ null, null, null ],
[ null, null, null ],
[ null, null, null ],
[ null, null, null ]
]
}
}

View File

@@ -0,0 +1,75 @@
{
"type":"DMG",
"names":[
"SD007_K8D3216_32M with MX29LV160CT"
],
"flash_ids":[
[ 0xC1, 0xC1, 0xC4, 0xC4 ]
],
"voltage":3.3,
"flash_size":0x200000,
"start_addr":0,
"first_bank":1,
"write_pin":"WR",
"sector_size":[
[0x10000, 31],
[0x8000, 1],
[0x2000, 2],
[0x4000, 1]
],
"chip_erase_timeout":60,
"commands":{
"reset":[
[ 0, 0xF0 ]
],
"read_identifier":[
[ 0xAAA, 0xA9 ],
[ 0x555, 0x56 ],
[ 0xAAA, 0x90 ]
],
"chip_erase":[
[ 0xAAA, 0xA9 ],
[ 0x555, 0x56 ],
[ 0xAAA, 0x80 ],
[ 0xAAA, 0xA9 ],
[ 0x555, 0x56 ],
[ 0xAAA, 0x10 ]
],
"chip_erase_wait_for":[
[ null, null, null ],
[ null, null, null ],
[ null, null, null ],
[ null, null, null ],
[ null, null, null ],
[ 0, 0xFF, 0xFF ]
],
"sector_erase":[
[ 0xAAA, 0xA9 ],
[ 0x555, 0x56 ],
[ 0xAAA, 0x80 ],
[ 0xAAA, 0xA9 ],
[ 0x555, 0x56 ],
[ "SA", 0x30 ]
],
"sector_erase_wait_for":[
[ null, null, null ],
[ null, null, null ],
[ null, null, null ],
[ null, null, null ],
[ null, null, null ],
[ "SA", 0xFF, 0xFF ]
],
"single_write":[
[ 0xAAA, 0xA9 ],
[ 0x555, 0x56 ],
[ 0xAAA, 0xA0 ],
[ "PA", "PD" ]
],
"single_write_wait_for":[
[ null, null, null ],
[ null, null, null ],
[ null, null, null ],
[ null, null, null ]
]
}
}

View File

@@ -1,55 +1,73 @@
{
"type":"DMG",
"names":[
"GB-M968 with MX29LV320ABTC",
"SD007_BV5_DRV with M29W320DT"
],
"flash_ids":[
[ 0xC2, 0xC2, 0xA8, 0xA8 ],
[ 0x20, 0x20, 0xCA, 0xCA ]
],
"voltage":3.3,
"flash_size":0x400000,
"start_addr":0,
"first_bank":1,
"write_pin":"WR",
"chip_erase_timeout":60,
"commands":{
"reset":[
[ 0, 0xF0 ]
],
"read_identifier":[
[ 0xAAA, 0xAA ],
[ 0x555, 0x55 ],
[ 0xAAA, 0x90 ]
],
"chip_erase":[
[ 0xAAA, 0xAA ],
[ 0x555, 0x55 ],
[ 0xAAA, 0x80 ],
[ 0xAAA, 0xAA ],
[ 0x555, 0x55 ],
[ 0xAAA, 0x10 ]
],
"chip_erase_wait_for":[
[ null, null, null ],
[ null, null, null ],
[ null, null, null ],
[ null, null, null ],
[ null, null, null ],
[ 0, 0xFF, 0xFF ]
],
"single_write":[
[ 0xAAA, 0xAA ],
[ 0x555, 0x55 ],
[ 0xAAA, 0xA0 ],
[ "PA", "PD" ]
],
"single_write_wait_for":[
[ null, null, null ],
[ null, null, null ],
[ null, null, null ],
[ null, null, null ]
]
}
}
{
"type":"DMG",
"names":[
"DMG-DHCN-20 with MX29LV320ET"
],
"flash_ids":[
[ 0xC2, 0xC2, 0xA7, 0xA7 ]
],
"voltage":3.3,
"flash_size":0x200000,
"start_addr":0,
"first_bank":1,
"write_pin":"WR",
"sector_size":[
[0x10000, 31],
[0x2000, 8]
],
"chip_erase_timeout":60,
"commands":{
"reset":[
[ 0, 0xF0 ]
],
"read_identifier":[
[ 0xAAA, 0xAA ],
[ 0x555, 0x55 ],
[ 0xAAA, 0x90 ]
],
"chip_erase":[
[ 0xAAA, 0xAA ],
[ 0x555, 0x55 ],
[ 0xAAA, 0x80 ],
[ 0xAAA, 0xAA ],
[ 0x555, 0x55 ],
[ 0xAAA, 0x10 ]
],
"chip_erase_wait_for":[
[ null, null, null ],
[ null, null, null ],
[ null, null, null ],
[ null, null, null ],
[ null, null, null ],
[ 0, 0xFF, 0xFF ]
],
"sector_erase":[
[ 0xAAA, 0xAA ],
[ 0x555, 0x55 ],
[ 0xAAA, 0x80 ],
[ 0xAAA, 0xAA ],
[ 0x555, 0x55 ],
[ "SA", 0x30 ]
],
"sector_erase_wait_for":[
[ null, null, null ],
[ null, null, null ],
[ null, null, null ],
[ null, null, null ],
[ null, null, null ],
[ "SA", 0xFF, 0xFF ]
],
"single_write":[
[ 0xAAA, 0xAA ],
[ 0x555, 0x55 ],
[ 0xAAA, 0xA0 ],
[ "PA", "PD" ]
],
"single_write_wait_for":[
[ null, null, null ],
[ null, null, null ],
[ null, null, null ],
[ null, null, null ]
]
}
}

View File

@@ -1,16 +1,23 @@
{
"type":"DMG",
"names":[
"insideGadgets 4 MB, 128 KB SRAM/FRAM"
"insideGadgets 4 MB (S29GL032M)",
"SD007_BV5_DRV with S29GL032M90TFIR4",
"GB-M968 with MX29LV320ABTC",
"SD007_BV5_DRV with M29W320DT"
],
"flash_ids":[
[ 0x01, 0x01, 0x7E, 0x7E ]
[ 0x01, 0x01, 0x7E, 0x7E ],
[ 0x01, 0x01, 0x7E, 0x7E ],
[ 0xC2, 0xC2, 0xA8, 0xA8 ],
[ 0x20, 0x20, 0xCA, 0xCA ]
],
"voltage":5,
"voltage":3.3,
"flash_size":0x400000,
"start_addr":0x4000,
"first_bank":0,
"write_pin":"WR",
"sector_size_from_cfi":true,
"chip_erase_timeout":60,
"commands":{
"reset":[
@@ -37,6 +44,22 @@
[ null, null, null ],
[ 0, 0xFF, 0xFF ]
],
"sector_erase":[
[ 0xAAA, 0xAA ],
[ 0x555, 0x55 ],
[ 0xAAA, 0x80 ],
[ 0xAAA, 0xAA ],
[ 0x555, 0x55 ],
[ "SA", 0x30 ]
],
"sector_erase_wait_for":[
[ null, null, null ],
[ null, null, null ],
[ null, null, null ],
[ null, null, null ],
[ null, null, null ],
[ 0, 0xFF, 0xFF ]
],
"single_write":[
[ 0xAAA, 0xAA ],
[ 0x555, 0x55 ],

File diff suppressed because it is too large Load Diff

Binary file not shown.

211
README.md
View File

@@ -17,104 +17,134 @@ by Lesserkuma
- Write new ROMs to a wide variety of Game Boy and Game Boy Advance flash cartridges
- Many reproduction cartridges and flash cartridges can be auto-detected
- A flash chip query can be performed for unsupported flash cartridges
- Decode and extract Game Boy Camera (Pocket Camera) photos from save data
- Decode and extract Game Boy Camera photos from save data
### Confirmed working reader/writer hardware
- [insideGadgets GBxCart RW v1.3 and v1.3 Pro](https://www.gbxcart.com/) with firmware versions from R19 up to R25 (other hardware revisions and firmware versions may also work, but are untested)
- [insideGadgets GBxCart RW v1.3 and v1.3 Pro](https://www.gbxcart.com/) with firmware versions from R19 up to R26 (other hardware revisions and firmware versions may also work, but are untested)
### Currently supported official cartridge memory mappers
- Game Boy
- All cartridges without memory mapping
- MBC1
- MBC2
- MBC3/MBC30
- MBC5
- MBC7 (ROM backup only)
- MBC1M
- MMM01
- Game Boy Camera
- G-MMC1 (ROM and map backup only)
- HuC-1
- HuC-3
- TAMA5
- Game Boy Advance
- All cartridges without memory mapping
- 3D Memory (GBA Video)¹
¹ Preliminary support; will not work until the GBxCart RW device is updated to a future firmware version
### Currently supported flash cartridges
- Game Boy
- BUNG Doctor GB Card 64M
- DIY cart with AM29F016/AM29F016B
- DIY cart with AM29F032/AM29F032B
- DIY cart with AT49F040
- GB Smart 32M
- insideGadgets 32 KB
- insideGadgets 512 KB
- insideGadgets 1 MB, 128 KB SRAM
- insideGadgets 2 MB, 128 KB SRAM/32 KB FRAM
- insideGadgets 4 MB, 128 KB SRAM/FRAM
- insideGadgets 4 MB, 32 KB FRAM, MBC3+RTC
- Mr Flash 64M
- BUNG Doctor GB Card 64M
- DIY cart with AM29F016/AM29F016B
- DIY cart with AM29F032/AM29F032B
- DIY cart with AT49F040
- GB Smart 32M
- insideGadgets 32 KB
- insideGadgets 512 KB
- insideGadgets 1 MB, 128 KB SRAM
- insideGadgets 2 MB, 128 KB SRAM/32 KB FRAM
- insideGadgets 4 MB, 128 KB SRAM/FRAM
- insideGadgets 4 MB, 32 KB FRAM, MBC3+RTC
- Mr Flash 64M
- Game Boy Advance
- Development AGB Cartridge 128M Flash S, E201850
- Development AGB Cartridge 256M Flash S, E201868
- Flash2Advance 256M (non-ultra variant, with 2× 28F128J3A150)
- insideGadgets 16 MB, 64K EEPROM with Solar and RTC options
- insideGadgets 32 MB, 1M FLASH with RTC option
- insideGadgets 32 MB, 512K FLASH
- insideGadgets 32 MB, 4K/64K EEPROM
- insideGadgets 32 MB, 256K FRAM with Rumble option
- Development AGB Cartridge 128M Flash S, E201850
- Development AGB Cartridge 256M Flash S, E201868
- Flash2Advance 256M (non-ultra variant, with 2× 28F128J3A150)
- insideGadgets 16 MB, 64K EEPROM with Solar Sensor and RTC options
- insideGadgets 32 MB, 1M FLASH with RTC option
- insideGadgets 32 MB, 512K FLASH
- insideGadgets 32 MB, 4K/64K EEPROM
- insideGadgets 32 MB, 256K FRAM with Rumble option
### Currently supported and tested reproduction cartridges
- Game Boy
- ES29LV160_DRV with 29DL32TF-70
- GB-M968 with M29W160EB
- GB-M968 with MX29LV320ABTC
- ALTERA CPLD and S29GL032N90T (no PCB text)
- SD007_48BALL_64M with GL032M11BAIR4
- SD007_48BALL_64M with M29W640
- SD007_48BALL_64M_V2 with GL032M11BAIR4
- SD007_48BALL_64M_V2 with M29W160ET
- SD007_48BALL_64M_V3 with 29DL161TD-90
- SD007_48BALL_64M_V5 with 36VF3203
- SD007_48BALL_64M_V5 with 36VF3204
- SD007_48BALL_64M_V6 with 36VF3204
- SD007_48BALL_64M_V6 with 29DL163BD-90
- SD007_BV5_DRV with M29W320DT
- SD007_BV5_V2 with HY29LV160TT
- SD007_BV5_V2 with MX29LV320BTC
- SD007_BV5_V3 with 29LV160BE-90PFTN
- SD007_BV5_V3 with HY29LV160BT-70
- SD007_BV5_V3 with AM29LV160MB
- SD007_TSOP_48BALL with 36VF3204
- SD007_TSOP_48BALL with AM29LV160DB
- SD007_TSOP_48BALL with AM29LV160DT
- SD007_TSOP_48BALL with M29W160ET
- SD007_TSOP_48BALL with L160DB12VI
- DMG-DHCN-20 with MX29LV320ET
- ES29LV160_DRV with 29DL32TF-70
- GB-M968 with M29W160EB
- GB-M968 with MX29LV320ABTC
- ALTERA CPLD and S29GL032N90T (no PCB text)
- SD007_48BALL_64M with GL032M11BAIR4
- SD007_48BALL_64M with M29W640
- SD007_48BALL_64M_V2 with GL032M11BAIR4
- SD007_48BALL_64M_V2 with M29W160ET
- SD007_48BALL_64M_V3 with 29DL161TD-90
- SD007_48BALL_64M_V5 with 36VF3203
- SD007_48BALL_64M_V5 with 36VF3204
- SD007_48BALL_64M_V6 with 36VF3204
- SD007_48BALL_64M_V6 with 29DL163BD-90
- SD007_BV5_DRV with M29W320DT
- SD007_BV5_DRV with S29GL032M90TFIR4
- SD007_BV5_V2 with HY29LV160TT
- SD007_BV5_V2 with MX29LV320BTC
- SD007_BV5_V3 with 29LV160BE-90PFTN
- SD007_BV5_V3 with HY29LV160BT-70
- SD007_BV5_V3 with AM29LV160MB
- SD007_K8D3216_32M with MX29LV160CT
- SD007_TSOP_48BALL with 36VF3204
- SD007_TSOP_48BALL with AM29LV160DB
- SD007_TSOP_48BALL with AM29LV160DT
- SD007_TSOP_48BALL with K8D3216UTC
- SD007_TSOP_48BALL with M29W160ET
- SD007_TSOP_48BALL with L160DB12VI
- Game Boy Advance
- 28F256L03B-DRV with 256L30B
- 36L0R8-39VF512 with M36L0R8060B
- 36L0R8-39VF512 with M36L0R8060T
- 4050_4400_4000_4350_36L0R_V5 with M36L0R7050T
- 4050_4400_4000_4350_36L0R_V5 with M36L0T8060T
- 4050_4400_4000_4350_36L0R_V5 with M36L0R8060T
- 4400 with 4400L0ZDQ0
- 4455_4400_4000_4350_36L0R_V3 with M36L0R7050T
- AGB-E05-01 with GL128S
- AGB-E05-01 with MSP55LV128M
- AGB-E05-02 with M29W128GH
- AGB-E08-09 with 29LV128DTMC-90Q
- AGB-SD-E05 with MSP55LV128
- BX2006_0106_NEW with S29GL128N10TFI01
- BX2006_TSOP_64BALL with GL128S
- BX2006_TSOP_64BALL with GL256S
- BX2006_TSOPBGA_0106 with M29W640GB6AZA6
- BX2006_TSOPBGA_0106 with K8D6316UTM-PI07
- GE28F128W30 with 128W30B0
- M6MGJ927 (no PCB text)
- 28F256L03B-DRV with 256L30B
- 36L0R8-39VF512 with M36L0R8060B
- 36L0R8-39VF512 with M36L0R8060T
- 4050_4400_4000_4350_36L0R_V5 with 4050L0YTQ2
- 4050_4400_4000_4350_36L0R_V5 with M36L0R7050T
- 4050_4400_4000_4350_36L0R_V5 with M36L0T8060T
- 4050_4400_4000_4350_36L0R_V5 with M36L0R8060T
- 4400 with 4400L0ZDQ0
- 4455_4400_4000_4350_36L0R_V3 with M36L0R7050T
- AGB-E05-01 with GL128S
- AGB-E05-01 with MSP55LV128M
- AGB-E05-01 with MX29GL128FHT2I-90G
- AGB-E05-02 with M29W128FH
- AGB-E05-02 with M29W128GH
- AGB-E08-09 with 29LV128DTMC-90Q
- AGB-SD-E05 with MSP55LV128
- BX2006_0106_NEW with S29GL128N10TFI01
- BX2006_TSOP_64BALL with GL128S
- BX2006_TSOP_64BALL with GL256S
- BX2006_TSOPBGA_0106 with M29W640GB6AZA6
- BX2006_TSOPBGA_0106 with K8D6316UTM-PI07
- GE28F128W30 with 128W30B0
- M6MGJ927 (no PCB text)
Many different reproduction cartridges share their flash chip command set, so even if yours is not on this list, it may still work fine or even be auto-detected as another one. Support for more cartridges can also be added by creating external config files that include the necessary flash chip commands.
## Installing and running
The application should work on pretty much every operating system that supports Qt-GUI applications built using [Python 3](https://www.python.org/downloads/) with [PySide2](https://pypi.org/project/PySide2/), [pyserial](https://pypi.org/project/pyserial/), [Pillow](https://pypi.org/project/Pillow/), [requests](https://pypi.org/project/requests/) and [setuptools](https://pypi.org/project/setuptools/) packages.
If you have Python and pip installed, you can use `pip install FlashGBX` to download and install the application, or use `pip install --upgrade FlashGBX` to upgrade from an older version. Then use `python -m FlashGBX` to run it.
If you have Python and pip installed, you can use `pip install FlashGBX` to download and install the application, or use `pip install --upgrade FlashGBX` to upgrade from an older version. Then use `python -m FlashGBX` or `python -m FlashGBX --cli` to run it.
To run FlashGBX in portable mode, you can also download the source code archive and call `python run.py` after installing the prerequisites yourself.
*On some platforms you may have to use `pip3`/`python3` instead of `pip`/`python`.*
The application should work on pretty much every operating system that supports Qt-GUI applications built using [Python 3](https://www.python.org/downloads/) with [PySide2](https://pypi.org/project/PySide2/), [pyserial](https://pypi.org/project/pyserial/), [Pillow](https://pypi.org/project/Pillow/), [requests](https://pypi.org/project/requests/) and [setuptools](https://pypi.org/project/setuptools/) packages.
### Windows binaries
Available in the GitHub [Releases](https://github.com/lesserkuma/FlashGBX/releases) section:
@@ -132,9 +162,9 @@ These executables have been created using *PyInstaller* and *Inno Setup*.
* On some Linux systems, you may need the *XCB Xinerama package* if you see an error regarding failed Qt platform plugin initialization. You can install it with `sudo apt install libxcb-xinerama0` etc.
* For save data backup/restore on Game Boy Advance reproduction cartridges, depending on how it was built, you may have to manually select the save type for it to work properly.
* On older systems such as MacOS X El Capitan 10.11, you may run into an error that says `TypeError: 'Shiboken.ObjectType' object is not iterable`. Installing [Python 3.7.9](https://www.python.org/downloads/release/python-379/) instead of the latest available version may resolve this issue. If that still doesnt work, you can try to uninstall PySide2 (`pip uninstall PySide2`) and then run FlashGBX again in command line interface mode.
* The save data backup/restore feature may not work on certain reproduction cartridges with batteryless-patched ROMs. As those cartridges use the same flash chip for both ROM and save data storage, a full ROM backup will usually include the save data. Also, when flashing a new unpatched ROM to a cartridge like this, the game may not be able to save progress without soldering in a battery.
* For save data backup/restore on Game Boy Advance reproduction cartridges, depending on how it was built, you may have to manually select the save type for it to work properly. However, the save data backup/restore feature may not work on certain reproduction cartridges with batteryless-patched ROMs. As those cartridges use the same flash chip for both ROM and save data storage, a full ROM backup will usually include the save data. Also, when flashing a new unpatched ROM to a cartridge like this, the game may not be able to save progress without soldering in a battery. See the [Flash Cart DB website](https://flashcartdb.com/index.php/Clone_and_Repo_Cart_Problems) for more information.
## DISCLAIMER
@@ -146,8 +176,9 @@ The author would like to thank the following very kind people for their help and
- AlexiG (GBxCart RW hardware, bug reports, flash chip info)
- AndehX (app icon, flash chip info)
- antPL (flash chip info)
- bbsan (flash chip info)
- ClassicOldSong (fix for Raspberry Pi)
- ClassicOldSong (bug reports)
- djedditt (testing)
- easthighNerd (feature suggestions)
- Frost Clock (flash chip info)
@@ -159,9 +190,11 @@ The author would like to thank the following very kind people for their help and
- LovelyA72 (flash chip info)
- LucentW (flash chip info, testing, bug reports)
- marv17 (flash chip info, testing, bug reports, feature suggestions)
- paarongiroux (bug reports)
- Paradoxical (flash chip info)
- RevZ (Linux help, testing, bug reports, flash chip info)
- Super Maker (flash chip info, testing)
- Veund (flash chip info)
- Zeii (flash chip info)
## Changes
@@ -202,7 +235,7 @@ The author would like to thank the following very kind people for their help and
- Fixed support for Windows 7 when using pre-compiled exe file packages
- Reduced size and decompression time of the pre-compiled exe file packages by excluding unnecessary DLL files
- Fixed a timing issue that could sometimes cause a loss of save data when hot-swapping Game Boy cartridges
- Added some warnings that help with troubleshooting, for example that manually setting the feature box to a MBC5 option may be necessary for a clean dump when dumping ROMs from flash cartridges
- Added some warnings that may help with troubleshooting
- Added taskbar progress visualization on Windows systems
### v0.9β (released 2020-12-17)
@@ -252,7 +285,7 @@ The author would like to thank the following very kind people for their help and
- Added the option to check for updates at application start *(thanks Icesythe7 and JFox for the suggestion and help)*
- Added support for BX2006_TSOPBGA_0106 with K8D6316UTM-PI07 *(thanks LucentW)*
- Added support for the currently available insideGadgets Game Boy Advance flash cartridges *(thanks AlexiG)*
- Added a Game Boy Camera (Pocket Camera) album viewer and picture extractor
- Added a Game Boy Camera album viewer and picture extractor
### v1.0 (released 2021-01-01)
- Added a firmware check when writing to insideGadgets Game Boy Advance flash cartridges (requires GBxCart RW firmware R20 or higher)
@@ -275,5 +308,33 @@ The author would like to thank the following very kind people for their help and
- Minor bug fixes
### v1.3 (released 2021-01-21)
- Fixed a bug introduced in v1.1 that broke AGB-E08-09 with 29LV128DTMC-90Q support *(thanks LucentW for reporting)*
- Fixed a bug introduced in v1.1 that broke support for AGB-E08-09 with 29LV128DTMC-90Q *(thanks LucentW for reporting)*
- Will now show the applications version number in message boxes
### v1.4 (released 2021-02-28)
- Added a command line interface (CLI) as an alternative to the GUI interface; see `--help` command line switch for details or run interactive mode with `--cli`
- Fixed some minor compatibility issues for older systems that only have access to slightly outdated versions of the PySide2 package
- Added support for DMG-DHCN-20 with MX29LV320ET *(thanks Veund)*
- Added the option to export Game Boy Camera pictures in more file formats
- Added support for SD007_BV5_DRV with S29GL032M90TFIR4
- Confirmed support for SD007_BV5_DRV with MX29LV320BTC
- Added support for SD007_K8D3216_32M with MX29LV160CT *(thanks marv17)*
- Added support for AGB-E05-02 with M29W128FH
- Reading the sector map from CFI is experimental and can be enabled by adding `"sector_size_from_cfi":true,` to a flash cartridge config file
- Several flash cartridge types can now be written via full chip erase mode
- For specifying a specific MBC for writing to a DIY flash cartridge, it is now possible to add `"mbc":3,` or the like to its config file
- Removed officially unused Game Boy MBC types and ROM sizes from the drop down lists
- Added support for AGB-E05-01 with MX29GL128FHT2I-90G *(thanks antPL)*
- Added support for official cartridges with the HuC-1 memory bank controller; tested with “Pokémon Card GB” (DMG-ACXJ-JPN)
- Added support for official cartridges with the HuC-3 memory bank controller; tested with “Robot Poncots Sun Version” (DMG-HREJ-JPN)
- Added support for official cartridges with the TAMA5 memory bank controller; tested with “Game de Hakken!! Tamagotchi Osutchi to Mesutchi” (DMG-AOMJ-JPN) (requires GBxCart RW firmware R26 or higher)
- Added preliminary support for official GBA Video cartridges with 3D Memory; tested with “Shrek 2” (AGB-M2SE-USA) *(thanks to endrifts article [“Dumping the Undumped”](https://mgba.io/2015/10/20/dumping-the-undumped/))* requires a future firmware update of GBxCart RW
- Added support for optionally saving and restoring RTC registers of official TAMA5 cartridges inside the save file
- Experimental support for optionally saving RTC registers of official MBC3+RTC+SRAM+BATTERY cartridges inside the save file using the 48 bytes save format explained on the [BGB website](https://bgb.bircd.org/rtcsave.html) was added. Latching the RTC register and restoring RTC register values to the cartridge is not supported at this time as it requires a new GBxCart RW hardware device revision.
- Added support for 4050_4400_4000_4350_36L0R_V5 with 4050L0YTQ2 *(thanks Shinichi999)*
- Fixed GUI support on macOS Big Sur *(thanks paarongiroux)*
- Added support for official cartridges with the MBC1M memory bank controller; tested with “Bomberman Collection” (DMG-ABCJ-JPN); save data backup is untested but should work
- Added support for official cartridges with the MMM01 memory bank controller; tested with “Momotarou Collection 2” (DMG-AM3J-JPN) (requires GBxCart RW firmware R26 or higher)
- Support for optionally saving and restoring RTC registers of official HuC-3+RTC+SRAM+BATTERY cartridges inside the save file using the 12 bytes save format used by the [hhugboy emulator](https://github.com/tzlion/hhugboy) was added.
- Added support for SD007_TSOP_48BALL with K8D3216UTC *(thanks marv17)*
- Added ROM and map backup support for official Nintendo Power GB Memory cartridges (DMG-MMSA-JPN); save data handling and ROM writing is not supported yet

View File

@@ -4,9 +4,9 @@ with open("README.md", "r", encoding="utf-8") as fh: long_description = fh.read(
setuptools.setup(
name="FlashGBX",
version="1.3",
version="1.4",
author="Lesserkuma",
description="A GUI application that can read and write Game Boy and Game Boy Advance cartridge data. Currently supports the GBxCart RW hardware device by insideGadgets.",
description="Reads and writes Game Boy and Game Boy Advance cartridge data. Currently supports the GBxCart RW hardware device by insideGadgets.",
url="https://github.com/lesserkuma/FlashGBX",
packages=setuptools.find_packages(),
install_requires=['PySide2', 'pyserial', 'setuptools', 'requests', 'Pillow'],