# -*- coding: utf-8 -*- # GBA Multi Game Menu – ROM Builder # Author: Lesserkuma (github.com/lesserkuma) import sys, os, glob, json, math, re, struct, hashlib, argparse, datetime # Configuration app_version = "1.1" default_file = "LK_MULTIMENU_.gba" ################################ def UpdateSectorMap(start, length, c): sector_map[start + 1:start + length] = c * (length - 1) sector_map[start] = c.upper() def formatFileSize(size): if size == 1: return "{:d} Byte".format(size) elif size < 1024: return "{:d} Bytes".format(size) elif size < 1024 * 1024: val = size/1024 return "{:.1f} KB".format(val) else: val = size/1024/1024 return "{:.2f} MB".format(val) def logp(*args, **kwargs): global log s = format(" ".join(map(str, args))) print("{:s}".format(s), **kwargs) if "end" in kwargs and kwargs["end"] == "": log += "{:s}".format(s) else: log += "{:s}\n".format(s) ################################ cartridge_types = [ { "name":"MSP55LV100S", "flash_size":0x4000000, "sector_size":0x20000, "block_size":0x80000, }, { "name":"6600M0U0BE", "flash_size":0x10000000, "sector_size":0x40000, "block_size":0x80000, }, { "name":"MSP54LV100", "flash_size":0x8000000, "sector_size":0x20000, "block_size":0x80000, }, { "name":"F0095H0", "flash_size":0x20000000, "sector_size":0x40000, "block_size":0x80000, }, ] now = datetime.datetime.now() log = "" logp("GBA Multi Game Menu ROM Builder v{:s}\nby Lesserkuma\n".format(app_version)) class ArgParseCustomFormatter(argparse.ArgumentDefaultsHelpFormatter, argparse.RawDescriptionHelpFormatter): pass parser = argparse.ArgumentParser() parser.add_argument("--split", help="splits output files into 32 MiB parts", action="store_true", default=False) parser.add_argument("--no-wait", help="don’t wait for user input when finished", action="store_true", default=False) parser.add_argument("--no-log", help="don’t write a log file", action="store_true", default=False) parser.add_argument("--config", type=str, default="config.json", help="sets the config file to use") parser.add_argument("--bg", type=str, help="sets the background image to use") parser.add_argument("--output", type=str, default=default_file, help="sets the file name of the compilation ROM") args = parser.parse_args() output_file = args.output if output_file == "lk_multimenu.gba": logp("Error: The file must not be named “lk_multimenu.gba”") if not args.no_wait: input("\nPress ENTER to exit.\n") sys.exit(1) if not os.path.exists("lk_multimenu.gba"): logp("Error: The Menu ROM is missing.\nPlease put it in the same directory that you are running this tool from.\nExpected file name: “lk_multimenu.gba”") if not args.no_wait: input("\nPress ENTER to exit.\n") sys.exit() # Read game list files = [] if not os.path.exists(args.config): files = glob.glob("roms/*.gba") files = sorted(files, key=str.casefold) save_slot = 1 games = [] cartridge_type = 1 battery_present = False min_rom_size = 0x400000 for file in files: d = { "enabled": True, "file": os.path.split(file)[1], "title": os.path.splitext(os.path.split(file)[1])[0], "title_font": 1, "save_slot": save_slot, } with open(file, "rb") as f: f.seek(0xAC) code = f.read(0x4) if code[:3] in (b"BPG", b"BPR"): d["map_256m"] = True games.append(d) save_slot += 1 obj = { "cartridge": { "type": cartridge_type + 1, "battery_present": battery_present, "min_rom_size": min_rom_size, }, "games": games, } if len(games) == 0: logp("Error: No usable ROM files were found in the “roms” folder.") else: with open(args.config, "w", encoding="UTF-8-SIG") as f: f.write(json.dumps(obj=obj, indent=4, ensure_ascii=False)) logp(f"A new configuration file ({args.config:s}) was created based on the files inside the “roms” folder.\nPlease edit the file to your liking in a text editor, then run this tool again.") if not args.no_wait: input("\nPress ENTER to exit.\n") sys.exit() else: with open(args.config, "r", encoding="UTF-8-SIG") as f: try: j = json.load(f) except json.decoder.JSONDecodeError as e: logp(f"Error: The configuration file ({args.config:s}) is malformed and could not be loaded.\n" + str(e)) if not args.no_wait: input("\nPress ENTER to exit.\n") sys.exit() games = j["games"] cartridge_type = j["cartridge"]["type"] - 1 battery_present = j["cartridge"]["battery_present"] if "min_rom_size" in j["cartridge"]: min_rom_size = j["cartridge"]["min_rom_size"] else: min_rom_size = 0x400000 # Prepare compilation flash_size = cartridge_types[cartridge_type]["flash_size"] sector_size = cartridge_types[cartridge_type]["sector_size"] sector_count = flash_size // sector_size block_size = cartridge_types[cartridge_type]["block_size"] block_count = flash_size // block_size sectors_per_block = 0x80000 // sector_size compilation = bytearray() roms_keys = [0] for i in range(flash_size // 0x2000000): chunk = bytearray([0xFF] * 0x2000000) compilation += chunk sector_map = list("." * sector_count) # Read menu ROM with open("lk_multimenu.gba", "rb") as f: menu_rom = bytearray(f.read()) menu_rom += bytearray([0xFF] * ((len(menu_rom) + 0x10 - (len(menu_rom) % 0x10)) - len(menu_rom))) menu_rom += bytearray([0xFF] * 0x20) build_timestamp_offset = len(menu_rom) - 0x20 build_timestamp = datetime.datetime.now().astimezone().replace(microsecond=0).isoformat().encode("ASCII") menu_rom[build_timestamp_offset:build_timestamp_offset+len(build_timestamp)] = build_timestamp # Change background image if args.bg or os.path.exists("bg.png"): try: from PIL import Image if args.bg: img = Image.open(args.bg) else: img = Image.open("bg.png") img = img.convert('P') palette = img.getpalette() palette_rgb555 = [((b >> 3) << 10) | ((g >> 3) << 5) | (r >> 3) for r, g, b in zip(palette[::3], palette[1::3], palette[2::3])] raw_bitmap = bytearray(list(img.tobytes())) raw_palette = bytearray(0x200) pos = 0 for color in palette_rgb555: raw_palette[pos:pos+2] = struct.pack("