summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDavid McMackins II <contact@mcmackins.org>2016-05-29 22:37:57 -0500
committerDavid McMackins II <contact@mcmackins.org>2016-05-29 22:37:57 -0500
commit3bb9af80cee29e083762250df7e6e3e424abe375 (patch)
treefc92540aea09920c00a42c3a09c01f576f740aa4
parentc9818ebdd685dc6e7f9f30e577f31a63a8180cae (diff)
Add ability to relocate tilesets to other ROM banks
-rw-r--r--README13
-rw-r--r--gbimg.py122
-rw-r--r--gbromgen.py96
3 files changed, 223 insertions, 8 deletions
diff --git a/README b/README
index 84f127d..046f0a1 100644
--- a/README
+++ b/README
@@ -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)