#! /usr/bin/env python3 ## ## gbromgen - generate full ROM images for the Game Boy ## Copyright (C) 2016 Delwink, LLC ## ## This program is free software: you can redistribute it and/or modify ## it under the terms of the GNU Affero General Public License as published by ## the Free Software Foundation, version 3 only. ## ## This program is distributed in the hope that it will be useful, ## but WITHOUT ANY WARRANTY; without even the implied warranty of ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ## GNU Affero General Public License for more details. ## ## You should have received a copy of the GNU Affero General Public License ## along with this program. If not, see . ## from argparse import Action, ArgumentParser from gbimg import Tileset from json import load from os import getpid, makedirs, remove from os.path import abspath, dirname, exists, join from shutil import copy, rmtree from subprocess import call from sys import argv, stderr, stdin from tempfile import gettempdir __title__ = 'gbromgen' __version__ = '0.0.0' __author__ = 'David McMackins II' __version_info__ = '''{} {} Copyright (C) 2016 Delwink, LLC License AGPLv3: GNU AGPL version 3 only . This is free software: you are free to change and redistribute it. There is NO WARRANTY, to the extent permitted by law. Written by {}'''.format(__title__, __version__, __author__) VALID_CART_TYPES = { 'rom': 0x00, 'mbc1': 0x01, 'mbc1+ram': 0x02, 'batt+mbc1+ram': 0x03, 'mbc2': 0x05, 'batt+mbc2': 0x06, 'ram': 0x08, 'batt+ram': 0x09, 'mmm01': 0x0B, 'mmm01+sram': 0x0C, 'batt+mmm01+sram': 0x0D, 'batt+mbc3+timer': 0x0F, 'batt+mbc3+ram+timer': 0x10, 'mbc3': 0x11, 'mbc3+ram': 0x12, 'batt+mbc3+ram': 0x13, 'mbc5': 0x19, 'mbc5+ram': 0x1A, 'batt+mbc5+ram': 0x1B, 'mbc5+rumble': 0x1C, 'mbc5+rumble+sram': 0x1D, 'batt+mbc5+rumble+sram': 0x1E } ROM_BANKS = (2, 4, 8, 16, 32, 64, 128) RAM_BANKS = (0, -1, 1, 4, 16) ROMBANK_SIZE = 1024 * 16 CART_TYPE_ADDR = 0x147 COMP_ADDR = 0x14D ROMBANKS_ADDR = 0x148 RAMBANKS_ADDR = 0x149 VBLANK_ADDR = 0x40 SWITCHABLE_ROM_ADDR = 0x4000 class UnsignedIntegerField: def __init__(self, value, size): self._value = int(value) self._size = int(size) def __bytes__(self): b = [] for i in range(self._size): shift = i * 8 mask = 0xFF << shift b.append((self._value & mask) >> shift) return bytes(b) class VersionAction(Action): def __call__(self, parser, values, namespace, option_string): print(__version_info__) exit(0) CLI = ArgumentParser(__title__) # script meta CLI.add_argument('-v', '--verbose', action='store_true', help='Show more details') CLI.add_argument('--version', action=VersionAction, nargs=0, help='Show version information and exit') # files CLI.add_argument('spec', type=str, help='A specification for the ROM image') _num_runs = 0 def jp(addr, f): addr = (addr & 0xFF, (addr & 0xFF00) >> 8) f.write(b'\xC3') # jp instruction f.write(bytes(addr)) def reti(f): f.write(b'\xD9') # reti instruction def get_complement(total): total += 25 # because reasons return (0x100 - (total & 0xFF)) % 0x100 def get_symbol_pos(noi_path, s): with open(noi_path) as noi: for line in noi: line = line.split() if line[0] == 'DEF' and line[1] == '_' + s: return int(line[2], 16) return None def add_field(fields, field, pos): b = bytes(field) for i in range(len(b)): fields[pos + i] = b[i] def get_mbc_type(mbc_str): specs = [s.lower() for s in mbc_str.split('+')] specs.sort() return VALID_CART_TYPES['+'.join(specs)] def warn(*args): print(__title__ + ': warning: ', *args, file=stderr) def fail(*args, rm=None): if rm and exists(rm): remove(rm) exit(__title__ + ': error: ' + ' '.join(args)) def print_bank_info(n, written, verbose=False): if verbose: s = 'rom bank {} uses {} bytes ({}%)' s = s.format(n, written, format((written / ROMBANK_SIZE) * 100, '.1f')) print(__title__ + ':', s) def flush_bank(outfile, written): if (ROMBANK_SIZE - written) % 2 != 0: outfile.write(b'\x00') written += 1 while written < ROMBANK_SIZE: # This block fills the unused parts of the ROM bank with failure code. # It fills all spaces of 2 bytes with a STOP command. outfile.write(b'\x10\x00') written += 2 return written def write_bank(n, outfile, data, verbose=False): size = 0 for d in data: outfile.write(d.data) size += len(d.data) print_bank_info(n, size, verbose) flush_bank(outfile, size) def main(args=argv[1:]): global _num_runs PID = str(getpid()) STAMP = PID + '_' + str(_num_runs) _num_runs += 1 TEMPDIR = join(gettempdir(), 'gbromgen-' + STAMP) args = CLI.parse_args(args) if args.spec != '-': with open(args.spec) as f: spec = load(f) else: spec = load(stdin) if type(spec) is not dict: fail('Input specification syntax error') RELATIVE_PATH = dirname(abspath(args.spec)) if 'hex' not in spec: fail("'hex' file not specified in input specification") hex_path = join(RELATIVE_PATH, spec['hex']) if not hex_path.endswith('.ihx'): warn('Input file does not have Intel hex standard extension') base_file_name = hex_path else: base_file_name = hex_path[:-4] noi_path = base_file_name + '.noi' gb_path = spec['output'] if spec['output'] else base_file_name + '.gb' vblank = None if 'vblank' in spec: vblank = get_symbol_pos(noi_path, spec['vblank']) if vblank is None: warn('Could not locate vblank trigger function') fields = {} if 'const-fields' in spec: encoded = {} fields = spec['const-fields'] for field in fields: name, value = (field, fields[field]) pos = get_symbol_pos(noi_path, name) if pos is None: warn('Could not locate field {}'.format(name)) continue value = int(value) if value < 0: warn('Negative const field not supported: {}'.format(value)) continue byte = (value.bit_length() + 7) // 8 byte_list = [] for i in range(byte): shift = i * 8 mask = 0xFF << shift byte_list.append((value & mask) >> shift) for i in range(len(byte_list)): encoded[pos + i] = byte_list[i] fields = encoded if spec['mbc']: mbc = get_mbc_type(spec['mbc']) else: mbc = get_mbc_type('rom') makedirs(TEMPDIR) if 'name' not in spec: spec['name'] = '' if 'rom-banks' not in spec: spec['rom-banks'] = 2 if 'ram-banks' not in spec: spec['ram-banks'] = 0 bank_data = [] for _ in range(spec['rom-banks'] - 1): bank_data.append([]) unalloc_data = [] if 'tilesets' in spec: for tileset in spec['tilesets']: if 'bank' not in tileset: tileset['bank'] = None tileset = Tileset(tileset['img'], tileset['data'], tileset['bank']) if tileset.bank_num: bank_data[tileset.bank_num - 1].append(tileset) else: unalloc_data.append(tileset) for ud in unalloc_data: ulen = len(ud) allocated = False for i in range(len(bank_data)): b = bank_data[i] blen = 0 for d in b: blen += len(d) if ROMBANK_SIZE - blen >= ulen: ud.set_bank_num(i + 1) b.append(ud) allocated = True break if not allocated: fail('No ROM space for ' + str(ud)) del ud for b in bank_data: pointer = 0 for d in b: pos = get_symbol_pos(noi_path, d.data_var) if pos: field = UnsignedIntegerField(SWITCHABLE_ROM_ADDR + pointer, 2) add_field(fields, field, pos) else: warn('Could not locate field {}'.format(d.data_var)) if d.bank_var: pos = get_symbol_pos(noi_path, d.bank_var) if pos: field = UnsignedIntegerField(d.bank_num, 1) add_field(fields, field, pos) else: warn('Could not locate field {}'.format(d.data_var)) pointer += len(d) try: temp_gb = join(TEMPDIR, 'temp.gb') rc = call(['makebin', '-Z', '-p', '-yn', spec['name'], spec['hex'], temp_gb]) if rc: raise RuntimeError('makebin failed with rc={}'.format(rc)) size = 0 with open(temp_gb, 'rb') as infile, open(gb_path, 'wb') as outfile: comp_sum = 0 c = infile.read(1) while c: if size == VBLANK_ADDR: if vblank: jp(vblank, outfile) infile.read(2) size += 3 else: reti(outfile) size += 1 c = infile.read(1) continue if size == CART_TYPE_ADDR: byte = mbc elif size == ROMBANKS_ADDR: byte = ROM_BANKS.index(spec['rom-banks']) elif size == RAMBANKS_ADDR: byte = RAM_BANKS.index(spec['ram-banks']) elif size == COMP_ADDR: byte = get_complement(comp_sum) elif size in fields: byte = fields[size] else: byte = c[0] outfile.write(bytes([byte])) if 0x134 <= size < COMP_ADDR: comp_sum += byte size += 1 c = infile.read(1) print_bank_info(0, size, args.verbose) if mbc == 0x00: if size > ROMBANK_SIZE * 2: fail('ROM size exceeds 32k', rm=gb_path) if size < ROMBANK_SIZE: flush_bank(outfile, size) flush_bank(outfile, 0) else: flush_bank(outfile, size - ROMBANK_SIZE) exit(0) if size > ROMBANK_SIZE: fail('Game code exceeds space in ROM bank 0', rm=gb_path) flush_bank(outfile, size) for i in range(spec['rom-banks'] - 1): write_bank(i + 1, outfile, bank_data[i], args.verbose) except Exception as e: if exists(gb_path): remove(gb_path) raise finally: rmtree(TEMPDIR) if __name__ == '__main__': main()