diff options
-rw-r--r-- | README | 13 | ||||
-rw-r--r-- | gbimg.py | 122 | ||||
-rw-r--r-- | gbromgen.py | 96 |
3 files changed, 223 insertions, 8 deletions
@@ -13,6 +13,19 @@ It also includes complementary post-processing tools designed to make it easy to organize your Game Boy binary file and formats efficiently on the ROM so you can spend more time making progress and less time fighting binary formats. +Dependencies +------------ + +| Program/Library | Purpose | +|-------------------------|-------------------------------------------------| +| Small Device C Compiler | C compiler used to generate binary instructions | +| CPython 3 | Used for post-processing tools | +| Wand for Python 3 | Used for converting images to Game Boy format | + +On Debian-based systems, these can be installed as follows: + + # apt-get install sdcc python3 python3-wand + Notice ------ diff --git a/gbimg.py b/gbimg.py new file mode 100644 index 0000000..6a3613f --- /dev/null +++ b/gbimg.py @@ -0,0 +1,122 @@ +#! /usr/bin/env python3 +## +## gbimg - turn PC image files into Game Boy tilesets +## 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 <http://www.gnu.org/licenses/>. +## + +from wand.image import Image + +_PALETTE = { + 0: 3, + 85: 2, + 170: 1, + 255: 0 +} + +def map_palette(color): + if color not in _PALETTE: + raise ValueError('{} is not a valid color level'.format(color)) + + return _PALETTE[color] + +class Tileset: + def __init__(self, img, data, bank=None): + self._img = img + self._data = None + self._data_var = data + + if type(bank) is str: + self._bank_var = bank + self._bank_num = None + elif type(bank) is int: + self._bank_var = None + self._bank_num = bank + elif bank is None: + self._bank_var = None + self._bank_num = None + else: + raise TypeError('bank must be str, int, or None') + + def set_bank_num(self, num): + self._bank_num = num + + def _convert(self): + with Image(filename=self._img) as img: + width, height = img.size + if height != 8: + raise ValueError('{} height is {}'.format(self, height)) + + num_tiles = width / 8 + if num_tiles % 1 != 0: + raise ValueError('Irregular tile width in {}'.format(self)) + + num_tiles = int(num_tiles) + img.depth = 8 + blob = img.make_blob(format='RGB') + + encoded = [] + for tile in range(num_tiles): + encoded_tile = [] + + for i in range(0, width * 8 * 3, width * 3): + byte1 = 0 + byte2 = 0 + bit = 0x80 + + for j in range(0, 8 * 3, 3): + color = map_palette(blob[(i + j) + ((8 * 3) * tile)]) + + if color & 0x02: + byte1 |= bit + if color & 0x01: + byte2 |= bit + + bit >>= 1 + + encoded_tile.append(byte1) + encoded_tile.append(byte2) + + encoded += encoded_tile + + return bytes(encoded) + + @property + def data(self): + if not self._data: + self._data = self._convert() + + return self._data + + @property + def image_file(self): + return self._img + + @property + def data_var(self): + return self._data_var + + @property + def bank_var(self): + return self._bank_var + + @property + def bank_num(self): + return self._bank_num + + def __len__(self): + return len(self.data) + + def __str__(self): + return 'Tileset ' + self.image_file diff --git a/gbromgen.py b/gbromgen.py index bd56a5b..509fba2 100644 --- a/gbromgen.py +++ b/gbromgen.py @@ -17,9 +17,10 @@ ## from argparse import Action, ArgumentParser +from gbimg import Tileset from json import load from os import getpid, makedirs, remove -from os.path import abspath, exists, join +from os.path import abspath, dirname, exists, join from shutil import copy, rmtree from subprocess import call from sys import argv, stderr, stdin @@ -76,6 +77,21 @@ 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): @@ -117,6 +133,11 @@ def get_symbol_pos(noi_path, s): 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() @@ -156,10 +177,12 @@ def flush_bank(outfile, written): return written -def write_bank(n, outfile, verbose=False): +def write_bank(n, outfile, data, verbose=False): size = 0 - # TODO: determine write schedule and actually write + for d in data: + outfile.write(d.data) + size += len(d.data) print_bank_info(n, size, verbose) flush_bank(outfile, size) @@ -184,7 +207,7 @@ def main(args=argv[1:]): if type(spec) is not dict: fail('Input specification syntax error') - RELATIVE_PATH = abspath(args.spec) + RELATIVE_PATH = dirname(abspath(args.spec)) if 'hex' not in spec: fail("'hex' file not specified in input specification") @@ -250,6 +273,65 @@ def main(args=argv[1:]): 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'], @@ -314,10 +396,8 @@ def main(args=argv[1:]): flush_bank(outfile, size) - banks_written = 1 - while banks_written < spec['rom-banks']: - write_bank(banks_written, outfile, args.verbose) - banks_written += 1 + 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) |