summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authordannye <corrnondacqb@yahoo.com>2015-09-27 10:22:58 -0500
committerdannye <corrnondacqb@yahoo.com>2015-10-10 10:01:23 -0500
commitd867415edd72d2956b53aea66bae93566f3b984a (patch)
tree4d1345aa9ffbad480279fd480a62a1ae12c8aff7
Initial commit
-rw-r--r--.gitignore21
-rw-r--r--Makefile37
-rw-r--r--README.md13
-rw-r--r--extras/configuration.py57
-rw-r--r--extras/gbz80disasm.py947
-rw-r--r--extras/gfx.py750
-rw-r--r--extras/labels.py213
-rw-r--r--extras/scan_includes.py65
-rw-r--r--extras/wram.py313
-rw-r--r--src/main.asm383
10 files changed, 2799 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..af98acd
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,21 @@
+# precompiled python
+*.pyc
+
+# compiled object file
+*.o
+
+# roms
+*.gbc
+
+# rgbds extras
+*.map
+*.sym
+
+# save game files
+*.sgm
+*.sav
+*.sys
+
+# converted image/palette data
+*.1bpp
+*.2bpp
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..d1651ab
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,37 @@
+.PHONY: all compare clean
+
+.SUFFIXES:
+.SUFFIXES: .asm .o .gbc .png .2bpp .1bpp .pal
+.SECONDEXPANSION:
+
+OBJS = src/main.o
+
+$(foreach obj, $(OBJS), \
+ $(eval $(obj:.o=)_dep = $(shell python extras/scan_includes.py $(obj:.o=.asm))) \
+)
+
+all: pokepuzzle.gbc compare
+
+compare: baserom.gbc pokepuzzle.gbc
+ cmp $^
+
+$(OBJS): $$*.asm $$($$*_dep)
+ @python extras/gfx.py 2bpp $(2bppq)
+ @python extras/gfx.py 1bpp $(1bppq)
+ rgbasm -i src/ -o $@ $<
+
+pokepuzzle.gbc: $(OBJS)
+ rgblink -n $*.sym -o $@ $^
+ rgbfix -v $@
+
+clean:
+ rm -f pokepuzzle.gbc $(OBJS) *.sym
+ find . \( -iname '*.1bpp' -o -iname '*.2bpp' \) -exec rm {} +
+
+%.2bpp: %.png
+ $(eval 2bppq += $<)
+ @rm -f $@
+
+%.1bpp: %.png
+ $(eval 1bppq += $<)
+ @rm -f $@
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..b0048a3
--- /dev/null
+++ b/README.md
@@ -0,0 +1,13 @@
+# Pokémon Puzzle Challenge
+
+This is a disassembly of Pokémon Puzzle Challenge.
+
+It uses the following ROM as a base:
+
+* Pokemon Puzzle Challenge (U) [C][!].gbc `md5: f9ec4cc3c9df3887dc731ccf53663ffb`
+
+To assemble, first install RGBDS and put it in your path.
+Then copy the above ROM to this directory as "baserom.gbc"
+Then run "make" in your shell.
+
+This will output a file named "pokepuzzle.gbc"
diff --git a/extras/configuration.py b/extras/configuration.py
new file mode 100644
index 0000000..1592fe6
--- /dev/null
+++ b/extras/configuration.py
@@ -0,0 +1,57 @@
+"""
+Configuration
+"""
+
+import os
+
+class ConfigException(Exception):
+ """
+ Configuration error. Maybe a missing config variable.
+ """
+
+class Config(object):
+ """
+ The Config class handles all configuration for pokemontools. Other classes
+ and functions use a Config object to determine where expected files can be
+ located.
+ """
+
+ def __init__(self, **kwargs):
+ """
+ Store all parameters.
+ """
+ self._config = {}
+
+ for (key, value) in kwargs.items():
+ if key not in self.__dict__:
+ self._config[key] = value
+ else:
+ raise ConfigException(
+ "Can't store \"{0}\" in configuration because the key conflicts with an existing property."
+ .format(key)
+ )
+
+ if "path" not in self._config:
+ self._config["path"] = os.getcwd()
+
+ # vba save states go into ./save-states/
+ if "save_state_path" not in self._config:
+ self._config["save_state_path"] = os.path.join(self._config["path"], "save-states/")
+
+ # assume rom is at ./baserom.gbc
+ if "rom" not in self._config:
+ self._config["rom_path"] = os.path.join(self._config["path"], "baserom.gbc")
+
+ def __getattr__(self, key):
+ """
+ Grab the value from the class properties, then check the configuration,
+ and raise an exception if nothing works.
+ """
+ if key in self.__dict__:
+ return self.__dict__[key]
+ elif key in self._config:
+ return self._config[key]
+ else:
+ raise ConfigException(
+ "no config found for \"{0}\"".format(key)
+ )
diff --git a/extras/gbz80disasm.py b/extras/gbz80disasm.py
new file mode 100644
index 0000000..bd55d21
--- /dev/null
+++ b/extras/gbz80disasm.py
@@ -0,0 +1,947 @@
+# -*- coding: utf-8 -*-
+"""
+GBC disassembler
+"""
+
+import os
+import sys
+from copy import copy, deepcopy
+from ctypes import c_int8
+import random
+import json
+
+import configuration
+import labels
+import wram
+
+# New versions of json don't have read anymore.
+if not hasattr(json, "read"):
+ json.read = json.loads
+
+spacing = "\t"
+
+temp_opt_table = [
+ [ "ADC A", 0x8f, 0 ],
+ [ "ADC B", 0x88, 0 ],
+ [ "ADC C", 0x89, 0 ],
+ [ "ADC D", 0x8a, 0 ],
+ [ "ADC E", 0x8b, 0 ],
+ [ "ADC H", 0x8c, 0 ],
+ [ "ADC [HL]", 0x8e, 0 ],
+ [ "ADC L", 0x8d, 0 ],
+ [ "ADC x", 0xce, 1 ],
+ [ "ADD A", 0x87, 0 ],
+ [ "ADD B", 0x80, 0 ],
+ [ "ADD C", 0x81, 0 ],
+ [ "ADD D", 0x82, 0 ],
+ [ "ADD E", 0x83, 0 ],
+ [ "ADD H", 0x84, 0 ],
+ [ "ADD [HL]", 0x86, 0 ],
+ [ "ADD HL, BC", 0x9, 0 ],
+ [ "ADD HL, DE", 0x19, 0 ],
+ [ "ADD HL, HL", 0x29, 0 ],
+ [ "ADD HL, SP", 0x39, 0 ],
+ [ "ADD L", 0x85, 0 ],
+ [ "ADD SP, x", 0xe8, 1 ],
+ [ "ADD x", 0xc6, 1 ],
+ [ "AND A", 0xa7, 0 ],
+ [ "AND B", 0xa0, 0 ],
+ [ "AND C", 0xa1, 0 ],
+ [ "AND D", 0xa2, 0 ],
+ [ "AND E", 0xa3, 0 ],
+ [ "AND H", 0xa4, 0 ],
+ [ "AND [HL]", 0xa6, 0 ],
+ [ "AND L", 0xa5, 0 ],
+ [ "AND x", 0xe6, 1 ],
+ [ "BIT 0, A", 0x47cb, 3 ],
+ [ "BIT 0, B", 0x40cb, 3 ],
+ [ "BIT 0, C", 0x41cb, 3 ],
+ [ "BIT 0, D", 0x42cb, 3 ],
+ [ "BIT 0, E", 0x43cb, 3 ],
+ [ "BIT 0, H", 0x44cb, 3 ],
+ [ "BIT 0, [HL]", 0x46cb, 3 ],
+ [ "BIT 0, L", 0x45cb, 3 ],
+ [ "BIT 1, A", 0x4fcb, 3 ],
+ [ "BIT 1, B", 0x48cb, 3 ],
+ [ "BIT 1, C", 0x49cb, 3 ],
+ [ "BIT 1, D", 0x4acb, 3 ],
+ [ "BIT 1, E", 0x4bcb, 3 ],
+ [ "BIT 1, H", 0x4ccb, 3 ],
+ [ "BIT 1, [HL]", 0x4ecb, 3 ],
+ [ "BIT 1, L", 0x4dcb, 3 ],
+ [ "BIT 2, A", 0x57cb, 3 ],
+ [ "BIT 2, B", 0x50cb, 3 ],
+ [ "BIT 2, C", 0x51cb, 3 ],
+ [ "BIT 2, D", 0x52cb, 3 ],
+ [ "BIT 2, E", 0x53cb, 3 ],
+ [ "BIT 2, H", 0x54cb, 3 ],
+ [ "BIT 2, [HL]", 0x56cb, 3 ],
+ [ "BIT 2, L", 0x55cb, 3 ],
+ [ "BIT 3, A", 0x5fcb, 3 ],
+ [ "BIT 3, B", 0x58cb, 3 ],
+ [ "BIT 3, C", 0x59cb, 3 ],
+ [ "BIT 3, D", 0x5acb, 3 ],
+ [ "BIT 3, E", 0x5bcb, 3 ],
+ [ "BIT 3, H", 0x5ccb, 3 ],
+ [ "BIT 3, [HL]", 0x5ecb, 3 ],
+ [ "BIT 3, L", 0x5dcb, 3 ],
+ [ "BIT 4, A", 0x67cb, 3 ],
+ [ "BIT 4, B", 0x60cb, 3 ],
+ [ "BIT 4, C", 0x61cb, 3 ],
+ [ "BIT 4, D", 0x62cb, 3 ],
+ [ "BIT 4, E", 0x63cb, 3 ],
+ [ "BIT 4, H", 0x64cb, 3 ],
+ [ "BIT 4, [HL]", 0x66cb, 3 ],
+ [ "BIT 4, L", 0x65cb, 3 ],
+ [ "BIT 5, A", 0x6fcb, 3 ],
+ [ "BIT 5, B", 0x68cb, 3 ],
+ [ "BIT 5, C", 0x69cb, 3 ],
+ [ "BIT 5, D", 0x6acb, 3 ],
+ [ "BIT 5, E", 0x6bcb, 3 ],
+ [ "BIT 5, H", 0x6ccb, 3 ],
+ [ "BIT 5, [HL]", 0x6ecb, 3 ],
+ [ "BIT 5, L", 0x6dcb, 3 ],
+ [ "BIT 6, A", 0x77cb, 3 ],
+ [ "BIT 6, B", 0x70cb, 3 ],
+ [ "BIT 6, C", 0x71cb, 3 ],
+ [ "BIT 6, D", 0x72cb, 3 ],
+ [ "BIT 6, E", 0x73cb, 3 ],
+ [ "BIT 6, H", 0x74cb, 3 ],
+ [ "BIT 6, [HL]", 0x76cb, 3 ],
+ [ "BIT 6, L", 0x75cb, 3 ],
+ [ "BIT 7, A", 0x7fcb, 3 ],
+ [ "BIT 7, B", 0x78cb, 3 ],
+ [ "BIT 7, C", 0x79cb, 3 ],
+ [ "BIT 7, D", 0x7acb, 3 ],
+ [ "BIT 7, E", 0x7bcb, 3 ],
+ [ "BIT 7, H", 0x7ccb, 3 ],
+ [ "BIT 7, [HL]", 0x7ecb, 3 ],
+ [ "BIT 7, L", 0x7dcb, 3 ],
+ [ "CALL C, ?", 0xdc, 2 ],
+ [ "CALL NC, ?", 0xd4, 2 ],
+ [ "CALL NZ, ?", 0xc4, 2 ],
+ [ "CALL Z, ?", 0xcc, 2 ],
+ [ "CALL ?", 0xcd, 2 ],
+ [ "CCF", 0x3f, 0 ],
+ [ "CP A", 0xbf, 0 ],
+ [ "CP B", 0xb8, 0 ],
+ [ "CP C", 0xb9, 0 ],
+ [ "CP D", 0xba, 0 ],
+ [ "CP E", 0xbb, 0 ],
+ [ "CP H", 0xbc, 0 ],
+ [ "CP [HL]", 0xbe, 0 ],
+ [ "CPL", 0x2f, 0 ],
+ [ "CP L", 0xbd, 0 ],
+ [ "CP x", 0xfe, 1 ],
+ [ "DAA", 0x27, 0 ],
+ [ "DEBUG", 0xed, 0 ],
+ [ "DEC A", 0x3d, 0 ],
+ [ "DEC B", 0x5, 0 ],
+ [ "DEC BC", 0xb, 0 ],
+ [ "DEC C", 0xd, 0 ],
+ [ "DEC D", 0x15, 0 ],
+ [ "DEC DE", 0x1b, 0 ],
+ [ "DEC E", 0x1d, 0 ],
+ [ "DEC H", 0x25, 0 ],
+ [ "DEC HL", 0x2b, 0 ],
+ [ "DEC [HL]", 0x35, 0 ],
+ [ "DEC L", 0x2d, 0 ],
+ [ "DEC SP", 0x3b, 0 ],
+ [ "DI", 0xf3, 0 ],
+ [ "EI", 0xfb, 0 ],
+ [ "HALT", 0x76, 0 ],
+ [ "INC A", 0x3c, 0 ],
+ [ "INC B", 0x4, 0 ],
+ [ "INC BC", 0x3, 0 ],
+ [ "INC C", 0xc, 0 ],
+ [ "INC D", 0x14, 0 ],
+ [ "INC DE", 0x13, 0 ],
+ [ "INC E", 0x1c, 0 ],
+ [ "INC H", 0x24, 0 ],
+ [ "INC HL", 0x23, 0 ],
+ [ "INC [HL]", 0x34, 0 ],
+ [ "INC L", 0x2c, 0 ],
+ [ "INC SP", 0x33, 0 ],
+ [ "JP C, ?", 0xda, 2 ],
+ [ "JP [HL]", 0xe9, 0 ],
+ [ "JP NC, ?", 0xd2, 2 ],
+ [ "JP NZ, ?", 0xc2, 2 ],
+ [ "JP Z, ?", 0xca, 2 ],
+ [ "JP ?", 0xc3, 2 ],
+ [ "JR C, x", 0x38, 1 ],
+ [ "JR NC, x", 0x30, 1 ],
+ [ "JR NZ, x", 0x20, 1 ],
+ [ "JR Z, x", 0x28, 1 ],
+ [ "JR x", 0x18, 1 ],
+ [ "LD A, A", 0x7f, 0 ],
+ [ "LD A, B", 0x78, 0 ],
+ [ "LD A, C", 0x79, 0 ],
+ [ "LD A, D", 0x7a, 0 ],
+ [ "LD A, E", 0x7b, 0 ],
+ [ "LD A, H", 0x7c, 0 ],
+ [ "LD A, L", 0x7d, 0 ],
+ [ "LD A, [$FF00+C]", 0xf2, 0 ],
+ [ "LD A, [$FF00+x]", 0xf0, 1 ],
+# [ "LDH A, [x]", 0xf0, 1 ], # rgbds has trouble with this one?
+ [ "LD A, [BC]", 0xa, 0 ],
+ [ "LD A, [DE]", 0x1a, 0 ],
+# [ "LD A, [HL+]", 0x2a, 0 ],
+# [ "LD A, [HL-]", 0x3a, 0 ],
+ [ "LD A, [HL]", 0x7e, 0 ],
+ [ "LD A, [HLD]", 0x3a, 0 ],
+ [ "LD A, [HLI]", 0x2a, 0 ],
+ [ "LD A, [?]", 0xfa, 2 ],
+ [ "LD A, x", 0x3e, 1 ],
+ [ "LD B, A", 0x47, 0 ],
+ [ "LD B, B", 0x40, 0 ],
+ [ "LD B, C", 0x41, 0 ],
+ [ "LD [BC], A", 0x2, 0 ],
+ [ "LD B, D", 0x42, 0 ],
+ [ "LD B, E", 0x43, 0 ],
+ [ "LD B, H", 0x44, 0 ],
+ [ "LD B, [HL]", 0x46, 0 ],
+ [ "LD B, L", 0x45, 0 ],
+ [ "LD B, x", 0x6, 1 ],
+ [ "LD C, A", 0x4f, 0 ],
+ [ "LD C, B", 0x48, 0 ],
+ [ "LD C, C", 0x49, 0 ],
+ [ "LD C, D", 0x4a, 0 ],
+ [ "LD C, E", 0x4b, 0 ],
+ [ "LD C, H", 0x4c, 0 ],
+ [ "LD C, [HL]", 0x4e, 0 ],
+ [ "LD C, L", 0x4d, 0 ],
+ [ "LD C, x", 0xe, 1 ],
+ [ "LD D, A", 0x57, 0 ],
+# [ "LDD A, [HL]", 0x3a, 0 ],
+ [ "LD D, B", 0x50, 0 ],
+ [ "LD D, C", 0x51, 0 ],
+ [ "LD D, D", 0x52, 0 ],
+ [ "LD D, E", 0x53, 0 ],
+ [ "LD [DE], A", 0x12, 0 ],
+ [ "LD D, H", 0x54, 0 ],
+ [ "LD D, [HL]", 0x56, 0 ],
+# [ "LDD [HL], A", 0x32, 0 ],
+ [ "LD D, L", 0x55, 0 ],
+ [ "LD D, x", 0x16, 1 ],
+ [ "LD E, A", 0x5f, 0 ],
+ [ "LD E, B", 0x58, 0 ],
+ [ "LD E, C", 0x59, 0 ],
+ [ "LD E, D", 0x5a, 0 ],
+ [ "LD E, E", 0x5b, 0 ],
+ [ "LD E, H", 0x5c, 0 ],
+ [ "LD E, [HL]", 0x5e, 0 ],
+ [ "LD E, L", 0x5d, 0 ],
+ [ "LD E, x", 0x1e, 1 ],
+ [ "LD [$FF00+C], A", 0xe2, 0 ],
+ [ "LD [$FF00+x], A", 0xe0, 1 ],
+# [ "LDH [x], A", 0xe0, 1 ],
+ [ "LD H, A", 0x67, 0 ],
+ [ "LD H, B", 0x60, 0 ],
+ [ "LD H, C", 0x61, 0 ],
+ [ "LD H, D", 0x62, 0 ],
+ [ "LD H, E", 0x63, 0 ],
+ [ "LD H, H", 0x64, 0 ],
+ [ "LD H, [HL]", 0x66, 0 ],
+ [ "LD H, L", 0x65, 0 ],
+# [ "LD [HL+], A", 0x22, 0 ],
+# [ "LD [HL-], A", 0x32, 0 ],
+ [ "LD [HL], A", 0x77, 0 ],
+ [ "LD [HL], B", 0x70, 0 ],
+ [ "LD [HL], C", 0x71, 0 ],
+ [ "LD [HL], D", 0x72, 0 ],
+ [ "LD [HLD], A", 0x32, 0 ],
+ [ "LD [HL], E", 0x73, 0 ],
+ [ "LD [HL], H", 0x74, 0 ],
+ [ "LD [HLI], A", 0x22, 0 ],
+ [ "LD [HL], L", 0x75, 0 ],
+# [ "LD HL, SP+x", 0xf8, 1 ], # rgbds uses [sp+x]
+ [ "LD HL, [SP+x]", 0xf8, 1 ],
+ [ "LD [HL], x", 0x36, 1 ],
+ [ "LD H, x", 0x26, 1 ],
+# [ "LDI A, [HL]", 0x2a, 0 ],
+# [ "LDI [HL], A", 0x22, 0 ],
+ [ "LD L, A", 0x6f, 0 ],
+ [ "LD L, B", 0x68, 0 ],
+ [ "LD L, C", 0x69, 0 ],
+ [ "LD L, D", 0x6a, 0 ],
+ [ "LD L, E", 0x6b, 0 ],
+ [ "LD L, H", 0x6c, 0 ],
+ [ "LD L, [HL]", 0x6e, 0 ],
+ [ "LD L, L", 0x6d, 0 ],
+ [ "LD L, x", 0x2e, 1 ],
+# [ "LD PC, HL", 0xe9, 0 ], #prefer jp [hl]
+ [ "LD SP, HL", 0xf9, 0 ],
+ [ "LD BC, ?", 0x1, 2 ],
+ [ "LD DE, ?", 0x11, 2 ],
+ [ "LD HL, ?", 0x21, 2 ],
+ [ "LD SP, ?", 0x31, 2 ],
+ [ "LD [?], SP", 0x8, 2 ],
+ [ "LD [?], A", 0xea, 2 ],
+ [ "NOP", 0x0, 0 ],
+ [ "OR A", 0xb7, 0 ],
+ [ "OR B", 0xb0, 0 ],
+ [ "OR C", 0xb1, 0 ],
+ [ "OR D", 0xb2, 0 ],
+ [ "OR E", 0xb3, 0 ],
+ [ "OR H", 0xb4, 0 ],
+ [ "OR [HL]", 0xb6, 0 ],
+ [ "OR L", 0xb5, 0 ],
+ [ "OR x", 0xf6, 1 ],
+ [ "POP AF", 0xf1, 0 ],
+ [ "POP BC", 0xc1, 0 ],
+ [ "POP DE", 0xd1, 0 ],
+ [ "POP HL", 0xe1, 0 ],
+ [ "PUSH AF", 0xf5, 0 ],
+ [ "PUSH BC", 0xc5, 0 ],
+ [ "PUSH DE", 0xd5, 0 ],
+ [ "PUSH HL", 0xe5, 0 ],
+ [ "RES 0, A", 0x87cb, 3 ],
+ [ "RES 0, B", 0x80cb, 3 ],
+ [ "RES 0, C", 0x81cb, 3 ],
+ [ "RES 0, D", 0x82cb, 3 ],
+ [ "RES 0, E", 0x83cb, 3 ],
+ [ "RES 0, H", 0x84cb, 3 ],
+ [ "RES 0, [HL]", 0x86cb, 3 ],
+ [ "RES 0, L", 0x85cb, 3 ],
+ [ "RES 1, A", 0x8fcb, 3 ],
+ [ "RES 1, B", 0x88cb, 3 ],
+ [ "RES 1, C", 0x89cb, 3 ],
+ [ "RES 1, D", 0x8acb, 3 ],
+ [ "RES 1, E", 0x8bcb, 3 ],
+ [ "RES 1, H", 0x8ccb, 3 ],
+ [ "RES 1, [HL]", 0x8ecb, 3 ],
+ [ "RES 1, L", 0x8dcb, 3 ],
+ [ "RES 2, A", 0x97cb, 3 ],
+ [ "RES 2, B", 0x90cb, 3 ],
+ [ "RES 2, C", 0x91cb, 3 ],
+ [ "RES 2, D", 0x92cb, 3 ],
+ [ "RES 2, E", 0x93cb, 3 ],
+ [ "RES 2, H", 0x94cb, 3 ],
+ [ "RES 2, [HL]", 0x96cb, 3 ],
+ [ "RES 2, L", 0x95cb, 3 ],
+ [ "RES 3, A", 0x9fcb, 3 ],
+ [ "RES 3, B", 0x98cb, 3 ],
+ [ "RES 3, C", 0x99cb, 3 ],
+ [ "RES 3, D", 0x9acb, 3 ],
+ [ "RES 3, E", 0x9bcb, 3 ],
+ [ "RES 3, H", 0x9ccb, 3 ],
+ [ "RES 3, [HL]", 0x9ecb, 3 ],
+ [ "RES 3, L", 0x9dcb, 3 ],
+ [ "RES 4, A", 0xa7cb, 3 ],
+ [ "RES 4, B", 0xa0cb, 3 ],
+ [ "RES 4, C", 0xa1cb, 3 ],
+ [ "RES 4, D", 0xa2cb, 3 ],
+ [ "RES 4, E", 0xa3cb, 3 ],
+ [ "RES 4, H", 0xa4cb, 3 ],
+ [ "RES 4, [HL]", 0xa6cb, 3 ],
+ [ "RES 4, L", 0xa5cb, 3 ],
+ [ "RES 5, A", 0xafcb, 3 ],
+ [ "RES 5, B", 0xa8cb, 3 ],
+ [ "RES 5, C", 0xa9cb, 3 ],
+ [ "RES 5, D", 0xaacb, 3 ],
+ [ "RES 5, E", 0xabcb, 3 ],
+ [ "RES 5, H", 0xaccb, 3 ],
+ [ "RES 5, [HL]", 0xaecb, 3 ],
+ [ "RES 5, L", 0xadcb, 3 ],
+ [ "RES 6, A", 0xb7cb, 3 ],
+ [ "RES 6, B", 0xb0cb, 3 ],
+ [ "RES 6, C", 0xb1cb, 3 ],
+ [ "RES 6, D", 0xb2cb, 3 ],
+ [ "RES 6, E", 0xb3cb, 3 ],
+ [ "RES 6, H", 0xb4cb, 3 ],
+ [ "RES 6, [HL]", 0xb6cb, 3 ],
+ [ "RES 6, L", 0xb5cb, 3 ],
+ [ "RES 7, A", 0xbfcb, 3 ],
+ [ "RES 7, B", 0xb8cb, 3 ],
+ [ "RES 7, C", 0xb9cb, 3 ],
+ [ "RES 7, D", 0xbacb, 3 ],
+ [ "RES 7, E", 0xbbcb, 3 ],
+ [ "RES 7, H", 0xbccb, 3 ],
+ [ "RES 7, [HL]", 0xbecb, 3 ],
+ [ "RES 7, L", 0xbdcb, 3 ],
+ [ "RETI", 0xd9, 0 ],
+ [ "RET C", 0xd8, 0 ],
+ [ "RET NC", 0xd0, 0 ],
+ [ "RET NZ", 0xc0, 0 ],
+ [ "RET Z", 0xc8, 0 ],
+ [ "RET", 0xc9, 0 ],
+ [ "RLA", 0x17, 0 ],
+ [ "RL A", 0x17cb, 3 ],
+ [ "RL B", 0x10cb, 3 ],
+ [ "RL C", 0x11cb, 3 ],
+ [ "RLCA", 0x7, 0 ],
+ [ "RLC A", 0x7cb, 3 ],
+ [ "RLC B", 0xcb, 3 ],
+ [ "RLC C", 0x1cb, 3 ],
+ [ "RLC D", 0x2cb, 3 ],
+ [ "RLC E", 0x3cb, 3 ],
+ [ "RLC H", 0x4cb, 3 ],
+ [ "RLC [HL]", 0x6cb, 3 ],
+ [ "RLC L", 0x5cb, 3 ],
+ [ "RL D", 0x12cb, 3 ],
+ [ "RL E", 0x13cb, 3 ],
+ [ "RL H", 0x14cb, 3 ],
+ [ "RL [HL]", 0x16cb, 3 ],
+ [ "RL L", 0x15cb, 3 ],
+ [ "RRA", 0x1f, 0 ],
+ [ "RR A", 0x1fcb, 3 ],
+ [ "RR B", 0x18cb, 3 ],
+ [ "RR C", 0x19cb, 3 ],
+ [ "RRCA", 0xf, 0 ],
+ [ "RRC A", 0xfcb, 3 ],
+ [ "RRC B", 0x8cb, 3 ],
+ [ "RRC C", 0x9cb, 3 ],
+ [ "RRC D", 0xacb, 3 ],
+ [ "RRC E", 0xbcb, 3 ],
+ [ "RRC H", 0xccb, 3 ],
+ [ "RRC [HL]", 0xecb, 3 ],
+ [ "RRC L", 0xdcb, 3 ],
+ [ "RR D", 0x1acb, 3 ],
+ [ "RR E", 0x1bcb, 3 ],
+ [ "RR H", 0x1ccb, 3 ],
+ [ "RR [HL]", 0x1ecb, 3 ],
+ [ "RR L", 0x1dcb, 3 ],
+ [ "RST $0", 0xc7, 0 ],
+ [ "RST $10", 0xd7, 0 ],
+ [ "RST $18", 0xdf, 0 ],
+ [ "RST $20", 0xe7, 0 ],
+ [ "RST $28", 0xef, 0 ],
+ [ "RST $30", 0xf7, 0 ],
+ [ "RST $38", 0xff, 0 ],
+ [ "RST $8", 0xcf, 0 ],
+ [ "SBC A", 0x9f, 0 ],
+ [ "SBC B", 0x98, 0 ],
+ [ "SBC C", 0x99, 0 ],
+ [ "SBC D", 0x9a, 0 ],
+ [ "SBC E", 0x9b, 0 ],
+ [ "SBC H", 0x9c, 0 ],
+ [ "SBC [HL]", 0x9e, 0 ],
+ [ "SBC L", 0x9d, 0 ],
+ [ "SBC x", 0xde, 1 ],
+ [ "SCF", 0x37, 0 ],
+ [ "SET 0, A", 0xc7cb, 3 ],
+ [ "SET 0, B", 0xc0cb, 3 ],
+ [ "SET 0, C", 0xc1cb, 3 ],
+ [ "SET 0, D", 0xc2cb, 3 ],
+ [ "SET 0, E", 0xc3cb, 3 ],
+ [ "SET 0, H", 0xc4cb, 3 ],
+ [ "SET 0, [HL]", 0xc6cb, 3 ],
+ [ "SET 0, L", 0xc5cb, 3 ],
+ [ "SET 1, A", 0xcfcb, 3 ],
+ [ "SET 1, B", 0xc8cb, 3 ],
+ [ "SET 1, C", 0xc9cb, 3 ],
+ [ "SET 1, D", 0xcacb, 3 ],
+ [ "SET 1, E", 0xcbcb, 3 ],
+ [ "SET 1, H", 0xcccb, 3 ],
+ [ "SET 1, [HL]", 0xcecb, 3 ],
+ [ "SET 1, L", 0xcdcb, 3 ],
+ [ "SET 2, A", 0xd7cb, 3 ],
+ [ "SET 2, B", 0xd0cb, 3 ],
+ [ "SET 2, C", 0xd1cb, 3 ],
+ [ "SET 2, D", 0xd2cb, 3 ],
+ [ "SET 2, E", 0xd3cb, 3 ],
+ [ "SET 2, H", 0xd4cb, 3 ],
+ [ "SET 2, [HL]", 0xd6cb, 3 ],
+ [ "SET 2, L", 0xd5cb, 3 ],
+ [ "SET 3, A", 0xdfcb, 3 ],
+ [ "SET 3, B", 0xd8cb, 3 ],
+ [ "SET 3, C", 0xd9cb, 3 ],
+ [ "SET 3, D", 0xdacb, 3 ],
+ [ "SET 3, E", 0xdbcb, 3 ],
+ [ "SET 3, H", 0xdccb, 3 ],
+ [ "SET 3, [HL]", 0xdecb, 3 ],
+ [ "SET 3, L", 0xddcb, 3 ],
+ [ "SET 4, A", 0xe7cb, 3 ],
+ [ "SET 4, B", 0xe0cb, 3 ],
+ [ "SET 4, C", 0xe1cb, 3 ],
+ [ "SET 4, D", 0xe2cb, 3 ],
+ [ "SET 4, E", 0xe3cb, 3 ],
+ [ "SET 4, H", 0xe4cb, 3 ],
+ [ "SET 4, [HL]", 0xe6cb, 3 ],
+ [ "SET 4, L", 0xe5cb, 3 ],
+ [ "SET 5, A", 0xefcb, 3 ],
+ [ "SET 5, B", 0xe8cb, 3 ],
+ [ "SET 5, C", 0xe9cb, 3 ],
+ [ "SET 5, D", 0xeacb, 3 ],
+ [ "SET 5, E", 0xebcb, 3 ],
+ [ "SET 5, H", 0xeccb, 3 ],
+ [ "SET 5, [HL]", 0xeecb, 3 ],
+ [ "SET 5, L", 0xedcb, 3 ],
+ [ "SET 6, A", 0xf7cb, 3 ],
+ [ "SET 6, B", 0xf0cb, 3 ],
+ [ "SET 6, C", 0xf1cb, 3 ],
+ [ "SET 6, D", 0xf2cb, 3 ],
+ [ "SET 6, E", 0xf3cb, 3 ],
+ [ "SET 6, H", 0xf4cb, 3 ],
+ [ "SET 6, [HL]", 0xf6cb, 3 ],
+ [ "SET 6, L", 0xf5cb, 3 ],
+ [ "SET 7, A", 0xffcb, 3 ],
+ [ "SET 7, B", 0xf8cb, 3 ],
+ [ "SET 7, C", 0xf9cb, 3 ],
+ [ "SET 7, D", 0xfacb, 3 ],
+ [ "SET 7, E", 0xfbcb, 3 ],
+ [ "SET 7, H", 0xfccb, 3 ],
+ [ "SET 7, [HL]", 0xfecb, 3 ],
+ [ "SET 7, L", 0xfdcb, 3 ],
+ [ "SLA A", 0x27cb, 3 ],
+ [ "SLA B", 0x20cb, 3 ],
+ [ "SLA C", 0x21cb, 3 ],
+ [ "SLA D", 0x22cb, 3 ],
+ [ "SLA E", 0x23cb, 3 ],
+ [ "SLA H", 0x24cb, 3 ],
+ [ "SLA [HL]", 0x26cb, 3 ],
+ [ "SLA L", 0x25cb, 3 ],
+ [ "SRA A", 0x2fcb, 3 ],
+ [ "SRA B", 0x28cb, 3 ],
+ [ "SRA C", 0x29cb, 3 ],
+ [ "SRA D", 0x2acb, 3 ],
+ [ "SRA E", 0x2bcb, 3 ],
+ [ "SRA H", 0x2ccb, 3 ],
+ [ "SRA [HL]", 0x2ecb, 3 ],
+ [ "SRA L", 0x2dcb, 3 ],
+ [ "SRL A", 0x3fcb, 3 ],
+ [ "SRL B", 0x38cb, 3 ],
+ [ "SRL C", 0x39cb, 3 ],
+ [ "SRL D", 0x3acb, 3 ],
+ [ "SRL E", 0x3bcb, 3 ],
+ [ "SRL H", 0x3ccb, 3 ],
+ [ "SRL [HL]", 0x3ecb, 3 ],
+ [ "SRL L", 0x3dcb, 3 ],
+ [ "STOP", 0x10, 0 ],
+ [ "SUB A", 0x97, 0 ],
+ [ "SUB B", 0x90, 0 ],
+ [ "SUB C", 0x91, 0 ],
+ [ "SUB D", 0x92, 0 ],
+ [ "SUB E", 0x93, 0 ],
+ [ "SUB H", 0x94, 0 ],
+ [ "SUB [HL]", 0x96, 0 ],
+ [ "SUB L", 0x95, 0 ],
+ [ "SUB x", 0xd6, 1 ],
+ [ "SWAP A", 0x37cb, 3 ],
+ [ "SWAP B", 0x30cb, 3 ],
+ [ "SWAP C", 0x31cb, 3 ],
+ [ "SWAP D", 0x32cb, 3 ],
+ [ "SWAP E", 0x33cb, 3 ],
+ [ "SWAP H", 0x34cb, 3 ],
+ [ "SWAP [HL]", 0x36cb, 3 ],
+ [ "SWAP L", 0x35cb, 3 ],
+ [ "XOR A", 0xaf, 0 ],
+ [ "XOR B", 0xa8, 0 ],
+ [ "XOR C", 0xa9, 0 ],
+ [ "XOR D", 0xaa, 0 ],
+ [ "XOR E", 0xab, 0 ],
+ [ "XOR H", 0xac, 0 ],
+ [ "XOR [HL]", 0xae, 0 ],
+ [ "XOR L", 0xad, 0 ],
+ [ "XOR x", 0xee, 1 ],
+ [ "E", 0x100, -1 ],
+]
+
+# construct a more useful version of opt_table
+opt_table = {}
+for line in temp_opt_table:
+ opt_table[line[1]] = [line[0], line[2]]
+del line
+
+end_08_scripts_with = [
+0xc9, #ret
+0xd9, #reti
+0xe9, #jp hl
+#0xc3, #jp
+##0x18, #jr
+###0xda, 0xe9, 0xd2, 0xc2, 0xca, 0xc3, 0x38, 0x30, 0x20, 0x28, 0x18, 0xd8, 0xd0, 0xc0, 0xc8, 0xc9
+]
+
+discrete_jumps = [0xda, 0xe9, 0xd2, 0xc2, 0xca, 0xc3]
+relative_jumps = [0x38, 0x30, 0x20, 0x28, 0x18, 0xc3, 0xda, 0xc2]
+relative_unconditional_jumps = [0xc3, 0x18]
+
+call_commands = [0xdc, 0xd4, 0xc4, 0xcc, 0xcd]
+
+def asm_label(address):
+ """
+ Return the ASM label using the address.
+ """
+ # why using a random value when you can use the address?
+ return '.asm_%x' % address
+
+def data_label(address):
+ return '.data_%x' % address
+
+def get_local_address(address):
+ bank = address / 0x4000
+ return (address & 0x3fff) + 0x4000 * bool(bank)
+
+def get_global_address(address, bank):
+ if address < 0x8000:
+ return (address & 0x3fff) + 0x4000 * bank
+ return None
+
+ return ".ASM_" + hex(address)[2:]
+
+def has_outstanding_labels(byte_labels):
+ """
+ Check whether a label is used once in the asm output.
+
+ If so, then that means it has to be called or specified later.
+ """
+ for label_line in byte_labels.keys():
+ real_line = byte_labels[label_line]
+ if real_line["definition"] == False: return True
+ return False
+
+def all_outstanding_labels_are_reverse(byte_labels, offset):
+ for label_id in byte_labels.keys():
+ line = byte_labels[label_id] # label_id is also the address
+ if line["definition"] == False:
+ if not label_id < offset: return False
+ return True
+
+class Disassembler(object):
+ """
+ GBC disassembler
+ """
+
+ def __init__(self, config):
+ """
+ Setup the class instance.
+ """
+ self.config = config
+
+ self.wram = wram.WRAMProcessor(self.config)
+ self.labels = labels.Labels(self.config)
+
+ def initialize(self):
+ """
+ Setup the disassembler.
+ """
+ self.wram.initialize()
+ self.labels.initialize()
+
+ # TODO: fix how ROM is handled throughout the project.
+ rom_path = os.path.join(self.config.path, "baserom.gbc")
+ self.rom = bytearray(open(rom_path, "rb").read())
+
+ def find_label(self, local_address, bank_id=0):
+ # keep an integer
+ if type(local_address) == str:
+ local_address = int(local_address.replace("$", "0x"), 16)
+
+ if local_address < 0x8000:
+ for label_entry in self.labels.labels:
+ if get_local_address(label_entry["address"]) == local_address:
+ if "bank" in label_entry and (label_entry["bank"] == bank_id or label_entry["bank"] == 0):
+ return label_entry["label"]
+ if local_address in self.wram.wram_labels.keys():
+ return self.wram.wram_labels[local_address][-1]
+ for constants in [self.wram.gbhw_constants, self.wram.hram_constants]:
+ if local_address in constants.keys() and local_address >= 0xff00:
+ return constants[local_address]
+ return None
+
+ def find_address_from_label(self, label):
+ for label_entry in self.labels.labels:
+ if label == label_entry["label"]:
+ return label_entry["address"]
+ return None
+
+ def output_bank_opcodes(self, original_offset, max_byte_count=0x4000, include_last_address=True, stop_at=[], debug=False):
+ """
+ Output bank opcodes.
+
+ fs = current_address
+ b = bank_byte
+ in = input_data -- rom
+ bank_size = byte_count
+ i = offset
+ ad = end_address
+ a, oa = current_byte_number
+
+ stop_at can be used to supply a list of addresses to not disassemble
+ over. This is useful if you know in advance that there are a lot of
+ fall-throughs.
+ """
+
+ bank_id = original_offset / 0x4000
+ if debug: print "bank id is: " + str(bank_id)
+
+ last_hl_address = None #for when we're scanning the main map script
+ last_a_address = None
+ used_3d97 = False
+
+ rom = self.rom
+
+ offset = original_offset
+ current_byte_number = 0 #start from the beginning
+
+ #we don't actually have an end address, but we'll just say $4000
+ end_address = original_offset + max_byte_count
+
+ byte_labels = {}
+ data_tables = {}
+
+ first_loop = True
+ output = ""
+ keep_reading = True
+ is_data = False
+ while offset <= end_address and keep_reading:
+ current_byte = rom[offset]
+ maybe_byte = current_byte
+
+ # stop at any address
+ if not first_loop and offset in stop_at:
+ keep_reading = False
+ break
+
+ #first check if this byte already has a label
+ #if it does, use the label
+ #if not, generate a new label
+ if offset in byte_labels.keys():
+ line_label = byte_labels[offset]["name"]
+ byte_labels[offset]["usage"] += 1
+ output += "\n"
+ else:
+ line_label = asm_label(offset)
+ byte_labels[offset] = {}
+ byte_labels[offset]["name"] = line_label
+ byte_labels[offset]["usage"] = 0
+ byte_labels[offset]["definition"] = True
+ output += line_label + "\n" #" ; " + hex(offset) + "\n"
+
+ #find out if there's a two byte key like this
+ temp_maybe = maybe_byte
+ temp_maybe += ( rom[offset+1] << 8)
+ if not is_data and temp_maybe in opt_table.keys() and rom[offset+1]!=0:
+ opstr = opt_table[temp_maybe][0].lower()
+
+ if "x" in opstr:
+ for x in range(0, opstr.count("x")):
+ insertion = rom[offset + 1]
+ insertion = "$" + hex(insertion)[2:]
+
+ opstr = opstr[:opstr.find("x")].lower() + insertion + opstr[opstr.find("x")+1:].lower()
+
+ current_byte += 1
+ offset += 1
+ if "?" in opstr:
+ for y in range(0, opstr.count("?")):
+ byte1 = rom[offset + 1]
+ byte2 = rom[offset + 2]
+
+ number = byte1
+ number += byte2 << 8;
+
+ insertion = "$%.4x" % (number)
+
+ opstr = opstr[:opstr.find("?")].lower() + insertion + opstr[opstr.find("?")+1:].lower()
+
+ current_byte_number += 2
+ offset += 2
+
+ output += spacing + opstr #+ " ; " + hex(offset)
+ output += "\n"
+
+ current_byte_number += 2
+ offset += 2
+ elif not is_data and maybe_byte in opt_table.keys():
+ op_code = opt_table[maybe_byte]
+ op_code_type = op_code[1]
+ op_code_byte = maybe_byte
+
+ #type = -1 when it's the E op
+ #if op_code_type != -1:
+ if op_code_type == 0 and rom[offset] == op_code_byte:
+ op_str = op_code[0].lower()
+
+ output += spacing + op_code[0].lower() #+ " ; " + hex(offset)
+ output += "\n"
+
+ offset += 1
+ current_byte_number += 1
+ elif op_code_type == 1 and rom[offset] == op_code_byte:
+ oplen = len(op_code[0])
+ opstr = copy(op_code[0])
+ xes = op_code[0].count("x")
+ include_comment = False
+ for x in range(0, xes):
+ insertion = rom[offset + 1]
+ insertion = "$" + hex(insertion)[2:]
+
+ if current_byte == 0x18 or current_byte==0x20 or current_byte in relative_jumps: #jr or jr nz
+ #generate a label for the byte we're jumping to
+ target_address = offset + 2 + c_int8(rom[offset + 1]).value
+ if target_address in byte_labels.keys():
+ byte_labels[target_address]["usage"] = 1 + byte_labels[target_address]["usage"]
+ line_label2 = byte_labels[target_address]["name"]
+ else:
+ line_label2 = asm_label(target_address)
+ byte_labels[target_address] = {}
+ byte_labels[target_address]["name"] = line_label2
+ byte_labels[target_address]["usage"] = 1
+ byte_labels[target_address]["definition"] = False
+
+ insertion = line_label2
+ if has_outstanding_labels(byte_labels) and all_outstanding_labels_are_reverse(byte_labels, offset):
+ include_comment = True
+ elif current_byte == 0x3e:
+ last_a_address = rom[offset + 1]
+
+ opstr = opstr[:opstr.find("x")].lower() + insertion + opstr[opstr.find("x")+1:].lower()
+
+ # because the $ff00+$ff syntax is silly
+ if opstr.count("$") > 1 and "+" in opstr:
+ first_orig = opstr[opstr.find("$"):opstr.find("+")]
+ first_val = eval(first_orig.replace("$","0x"))
+
+ second_orig = opstr[opstr.find("+$")+1:opstr.find("]")]
+ second_val = eval(second_orig.replace("$","0x"))
+
+ combined_val = "$%.4x" % (first_val + second_val)
+ result = self.find_label(combined_val, bank_id)
+ if result != None:
+ combined_val = result
+
+ replacetron = "[%s+%s]" % (first_orig, second_orig)
+ opstr = opstr.replace(replacetron, "[%s]" % combined_val)
+
+ output += spacing + opstr
+ if include_comment:
+ output += " ; " + hex(offset)
+ if current_byte in relative_jumps:
+ output += " $" + hex(rom[offset + 1])[2:]
+ output += "\n"
+
+ current_byte_number += 1
+ offset += 1
+ insertion = ""
+
+ current_byte_number += 1
+ offset += 1
+ include_comment = False
+ elif op_code_type == 2 and rom[offset] == op_code_byte:
+ oplen = len(op_code[0])
+ opstr = copy(op_code[0])
+ qes = op_code[0].count("?")
+ for x in range(0, qes):
+ byte1 = rom[offset + 1]
+ byte2 = rom[offset + 2]
+
+ number = byte1
+ number += byte2 << 8
+
+ if current_byte not in call_commands + discrete_jumps + relative_jumps:
+ pointer = get_global_address(number, bank_id)
+ if pointer not in data_tables.keys():
+ data_tables[pointer] = {}
+ data_tables[pointer]['usage'] = 0
+ else:
+ data_tables[pointer]['usage'] += 1
+
+ insertion = "$%.4x" % (number)
+ result = self.find_label(insertion, bank_id)
+ if result != None:
+ insertion = result
+
+ opstr = opstr[:opstr.find("?")].lower() + insertion + opstr[opstr.find("?")+1:].lower()
+ output += spacing + opstr #+ " ; " + hex(offset)
+ output += "\n"
+
+ current_byte_number += 2
+ offset += 2
+
+ current_byte_number += 1
+ offset += 1
+
+ if current_byte == 0x21:
+ last_hl_address = byte1 + (byte2 << 8)
+ if current_byte == 0xcd:
+ if number == 0x3d97: used_3d97 = True
+
+ #duck out if this is jp $24d7
+ if current_byte == 0xc3 or current_byte in relative_unconditional_jumps:
+ if current_byte == 0xc3:
+ if number == 0x3d97: used_3d97 = True
+ #if number == 0x24d7: #jp
+ if not has_outstanding_labels(byte_labels) or all_outstanding_labels_are_reverse(byte_labels, offset):
+ keep_reading = False
+ is_data = False
+ break
+ else:
+ is_data = True
+ else:
+ #if is_data and keep_reading:
+ output += spacing + "db $" + hex(rom[offset])[2:] #+ " ; " + hex(offset)
+ output += "\n"
+ offset += 1
+ current_byte_number += 1
+ if offset in byte_labels.keys():
+ is_data = False
+ keep_reading = True
+ #else the while loop would have spit out the opcode
+
+ #these two are done prior
+ #offset += 1
+ #current_byte_number += 1
+
+ if not is_data and current_byte in relative_unconditional_jumps + end_08_scripts_with:
+ #stop reading at a jump, relative jump or return
+ if not has_outstanding_labels(byte_labels) or all_outstanding_labels_are_reverse(byte_labels, offset):
+ keep_reading = False
+ is_data = False #cleanup
+ break
+ elif offset not in byte_labels.keys() and offset in data_tables.keys():
+ is_data = True
+ keep_reading = True
+ else:
+ is_data = False
+ keep_reading = True
+ output += "\n"
+ elif is_data and offset not in byte_labels.keys():
+ is_data = True
+ keep_reading = True
+ else:
+ is_data = False
+ keep_reading = True
+
+ if offset in data_tables.keys():
+ output = output.replace('$%x' % (get_local_address(offset)), data_label(offset))
+ output += data_label(offset) + '\n'
+ is_data = True
+ keep_reading = True
+
+ first_loop = False
+
+ #clean up unused labels
+ for label_line in byte_labels.keys():
+ address = label_line
+ label_line = byte_labels[label_line]
+ if label_line["usage"] == 0:
+ output = output.replace((label_line["name"] + "\n"), "")
+
+ #tone down excessive spacing
+ output = output.replace("\n\n\n","\n\n")
+
+ #add the offset of the final location
+ if include_last_address:
+ output += "; " + hex(offset)
+
+ return (output, offset, last_hl_address, last_a_address, used_3d97)
+
+if __name__ == "__main__":
+ conf = configuration.Config()
+ disasm = Disassembler(conf)
+ disasm.initialize()
+
+ addr = sys.argv[1]
+ if ":" in addr:
+ addr = addr.split(":")
+ addr = int(addr[0], 16)*0x4000+(int(addr[1], 16)%0x4000)
+ else:
+ label_addr = disasm.find_address_from_label(addr)
+ if label_addr:
+ addr = label_addr
+ else:
+ addr = int(addr, 16)
+
+ output = disasm.output_bank_opcodes(addr)[0]
+ print output
diff --git a/extras/gfx.py b/extras/gfx.py
new file mode 100644
index 0000000..f27c17d
--- /dev/null
+++ b/extras/gfx.py
@@ -0,0 +1,750 @@
+# -*- coding: utf-8 -*-
+
+import os
+import sys
+import png
+from math import sqrt, floor, ceil
+import argparse
+
+def split(list_, interval):
+ """
+ Split a list by length.
+ """
+ for i in xrange(0, len(list_), interval):
+ j = min(i + interval, len(list_))
+ yield list_[i:j]
+
+def get_tiles(image):
+ """
+ Split a 2bpp image into 8x8 tiles.
+ """
+ return list(split(image, 0x10))
+
+def connect(tiles):
+ """
+ Combine 8x8 tiles into a 2bpp image.
+ """
+ return [byte for tile in tiles for byte in tile]
+
+def transpose(tiles, width=None):
+ """
+ Transpose a tile arrangement along line y=-x.
+
+ 00 01 02 03 04 05 00 06 0c 12 18 1e
+ 06 07 08 09 0a 0b 01 07 0d 13 19 1f
+ 0c 0d 0e 0f 10 11 <-> 02 08 0e 14 1a 20
+ 12 13 14 15 16 17 03 09 0f 15 1b 21
+ 18 19 1a 1b 1c 1d 04 0a 10 16 1c 22
+ 1e 1f 20 21 22 23 05 0b 11 17 1d 23
+
+ 00 01 02 03 00 04 08
+ 04 05 06 07 <-> 01 05 09
+ 08 09 0a 0b 02 06 0a
+ 03 07 0b
+ """
+ if width == None:
+ width = int(sqrt(len(tiles))) # assume square image
+ tiles = sorted(enumerate(tiles), key= lambda (i, tile): i % width)
+ return [tile for i, tile in tiles]
+
+def transpose_tiles(image, width=None):
+ return connect(transpose(get_tiles(image), width))
+
+def interleave(tiles, width):
+ """
+ 00 01 02 03 04 05 00 02 04 06 08 0a
+ 06 07 08 09 0a 0b 01 03 05 07 09 0b
+ 0c 0d 0e 0f 10 11 --> 0c 0e 10 12 14 16
+ 12 13 14 15 16 17 0d 0f 11 13 15 17
+ 18 19 1a 1b 1c 1d 18 1a 1c 1e 20 22
+ 1e 1f 20 21 22 23 19 1b 1d 1f 21 23
+ """
+ interleaved = []
+ left, right = split(tiles[::2], width), split(tiles[1::2], width)
+ for l, r in zip(left, right):
+ interleaved += l + r
+ return interleaved
+
+def deinterleave(tiles, width):
+ """
+ 00 02 04 06 08 0a 00 01 02 03 04 05
+ 01 03 05 07 09 0b 06 07 08 09 0a 0b
+ 0c 0e 10 12 14 16 --> 0c 0d 0e 0f 10 11
+ 0d 0f 11 13 15 17 12 13 14 15 16 17
+ 18 1a 1c 1e 20 22 18 19 1a 1b 1c 1d
+ 19 1b 1d 1f 21 23 1e 1f 20 21 22 23
+ """
+ deinterleaved = []
+ rows = list(split(tiles, width))
+ for left, right in zip(rows[::2], rows[1::2]):
+ for l, r in zip(left, right):
+ deinterleaved += [l, r]
+ return deinterleaved
+
+def interleave_tiles(image, width):
+ return connect(interleave(get_tiles(image), width))
+
+def deinterleave_tiles(image, width):
+ return connect(deinterleave(get_tiles(image), width))
+
+
+def condense_tiles_to_map(image, pic=0):
+ tiles = get_tiles(image)
+
+ # Leave the first frame intact for pics.
+ new_tiles = tiles[:pic]
+ tilemap = range(pic)
+
+ for i, tile in enumerate(tiles[pic:]):
+ if tile not in new_tiles:
+ new_tiles += [tile]
+
+ # Match the first frame where possible.
+ this_i = i % pic if pic else i
+ if tile == new_tiles[this_i]:
+ tilemap += [this_i]
+ else:
+ tilemap += [new_tiles.index(tile)]
+
+ new_image = connect(new_tiles)
+ return new_image, tilemap
+
+
+def to_file(filename, data):
+ """
+ Apparently open(filename, 'wb').write(bytearray(data)) won't work.
+ """
+ file = open(filename, 'wb')
+ for byte in data:
+ file.write('%c' % byte)
+ file.close()
+
+def bin_to_rgb(word):
+ red = word & 0b11111
+ word >>= 5
+ green = word & 0b11111
+ word >>= 5
+ blue = word & 0b11111
+ return (red, green, blue)
+
+def convert_binary_pal_to_text_by_filename(filename):
+ pal = bytearray(open(filename).read())
+ return convert_binary_pal_to_text(pal)
+
+def convert_binary_pal_to_text(pal):
+ output = ''
+ words = [hi * 0x100 + lo for lo, hi in zip(pal[::2], pal[1::2])]
+ for word in words:
+ red, green, blue = ['%.2d' % c for c in bin_to_rgb(word)]
+ output += '\tRGB ' + ', '.join((red, green, blue))
+ output += '\n'
+ return output
+
+def read_rgb_macros(lines):
+ colors = []
+ for line in lines:
+ macro = line.split(" ")[0].strip()
+ if macro == 'RGB':
+ params = ' '.join(line.split(" ")[1:]).split(',')
+ red, green, blue = [int(v) for v in params]
+ colors += [[red, green, blue]]
+ return colors
+
+def flatten(planar):
+ """
+ Flatten planar 2bpp image data into a quaternary pixel map.
+ """
+ strips = []
+ for bottom, top in split(planar, 2):
+ bottom = bottom
+ top = top
+ strip = []
+ for i in xrange(7,-1,-1):
+ color = (
+ (bottom >> i & 1) +
+ (top *2 >> i & 2)
+ )
+ strip += [color]
+ strips += strip
+ return strips
+
+
+def to_lines(image, width):
+ """
+ Convert a tiled quaternary pixel map to lines of quaternary pixels.
+ """
+ tile_width = 8
+ tile_height = 8
+ num_columns = width / tile_width
+ height = len(image) / width
+
+ lines = []
+ for cur_line in xrange(height):
+ tile_row = cur_line / tile_height
+ line = []
+ for column in xrange(num_columns):
+ anchor = (
+ num_columns * tile_row * tile_width * tile_height +
+ column * tile_width * tile_height +
+ cur_line % tile_height * tile_width
+ )
+ line += image[anchor : anchor + tile_width]
+ lines += [line]
+ return lines
+
+def pal_to_png(filename):
+ """
+ Interpret a .pal file as a png palette.
+ """
+ with open(filename) as rgbs:
+ colors = read_rgb_macros(rgbs.readlines())
+ a = 255
+ palette = []
+ for color in colors:
+ # even distribution over 000-255
+ r, g, b = [int(hue * 8.25) for hue in color]
+ palette += [(r, g, b, a)]
+ white = (255,255,255,255)
+ black = (000,000,000,255)
+ if white not in palette and len(palette) < 4:
+ palette = [white] + palette
+ if black not in palette and len(palette) < 4:
+ palette = palette + [black]
+ return palette
+
+
+def png_to_rgb(palette):
+ """
+ Convert a png palette to rgb macros.
+ """
+ output = ''
+ for color in palette:
+ r, g, b = [color[c] / 8 for c in 'rgb']
+ output += '\tRGB ' + ', '.join(['%.2d' % hue for hue in (r, g, b)])
+ output += '\n'
+ return output
+
+
+def read_filename_arguments(filename):
+ """
+ Infer graphics conversion arguments given a filename.
+
+ Arguments are separated with '.'.
+ """
+ parsed_arguments = {}
+
+ int_arguments = {
+ 'w': 'width',
+ 'h': 'height',
+ 't': 'tile_padding',
+ }
+ arguments = os.path.splitext(filename)[0].lstrip('.').split('.')[1:]
+ for argument in arguments:
+
+ # Check for integer arguments first (i.e. "w128").
+ arg = argument[0]
+ param = argument[1:]
+ if param.isdigit():
+ arg = int_arguments.get(arg, False)
+ if arg:
+ parsed_arguments[arg] = int(param)
+
+ elif argument == 'arrange':
+ parsed_arguments['norepeat'] = True
+ parsed_arguments['tilemap'] = True
+
+ # Pic dimensions (i.e. "6x6").
+ elif 'x' in argument and any(map(str.isdigit, argument)):
+ w, h = argument.split('x')
+ if w.isdigit() and h.isdigit():
+ parsed_arguments['pic_dimensions'] = (int(w), int(h))
+
+ else:
+ parsed_arguments[argument] = True
+
+ return parsed_arguments
+
+
+def export_2bpp_to_png(filein, fileout=None, pal_file=None, height=0, width=0, tile_padding=0, pic_dimensions=None, **kwargs):
+
+ if fileout == None:
+ fileout = os.path.splitext(filein)[0] + '.png'
+
+ image = open(filein, 'rb').read()
+
+ arguments = {
+ 'width': width,
+ 'height': height,
+ 'pal_file': pal_file,
+ 'tile_padding': tile_padding,
+ 'pic_dimensions': pic_dimensions,
+ }
+ arguments.update(read_filename_arguments(filein))
+
+ if pal_file == None:
+ if os.path.exists(os.path.splitext(fileout)[0]+'.pal'):
+ arguments['pal_file'] = os.path.splitext(fileout)[0]+'.pal'
+
+ result = convert_2bpp_to_png(image, **arguments)
+ width, height, palette, greyscale, bitdepth, px_map = result
+
+ w = png.Writer(
+ width,
+ height,
+ palette=palette,
+ compression=9,
+ greyscale=greyscale,
+ bitdepth=bitdepth
+ )
+ with open(fileout, 'wb') as f:
+ w.write(f, px_map)
+
+
+def convert_2bpp_to_png(image, **kwargs):
+ """
+ Convert a planar 2bpp graphic to png.
+ """
+
+ image = bytearray(image)
+
+ pad_color = bytearray([0])
+
+ width = kwargs.get('width', 0)
+ height = kwargs.get('height', 0)
+ tile_padding = kwargs.get('tile_padding', 0)
+ pic_dimensions = kwargs.get('pic_dimensions', None)
+ pal_file = kwargs.get('pal_file', None)
+ interleave = kwargs.get('interleave', False)
+
+ # Width must be specified to interleave.
+ if interleave and width:
+ image = interleave_tiles(image, width / 8)
+
+ # Pad the image by a given number of tiles if asked.
+ image += pad_color * 0x10 * tile_padding
+
+ # Some images are transposed in blocks.
+ if pic_dimensions:
+ w, h = pic_dimensions
+ if not width: width = w * 8
+
+ pic_length = w * h * 0x10
+
+ trailing = len(image) % pic_length
+
+ pic = []
+ for i in xrange(0, len(image) - trailing, pic_length):
+ pic += transpose_tiles(image[i:i+pic_length], h)
+ image = bytearray(pic) + image[len(image) - trailing:]
+
+ # Pad out trailing lines.
+ image += pad_color * 0x10 * ((w - (len(image) / 0x10) % h) % w)
+
+ def px_length(img):
+ return len(img) * 4
+ def tile_length(img):
+ return len(img) * 4 / (8*8)
+
+ if width and height:
+ tile_width = width / 8
+ more_tile_padding = (tile_width - (tile_length(image) % tile_width or tile_width))
+ image += pad_color * 0x10 * more_tile_padding
+
+ elif width and not height:
+ tile_width = width / 8
+ more_tile_padding = (tile_width - (tile_length(image) % tile_width or tile_width))
+ image += pad_color * 0x10 * more_tile_padding
+ height = px_length(image) / width
+
+ elif height and not width:
+ tile_height = height / 8
+ more_tile_padding = (tile_height - (tile_length(image) % tile_height or tile_height))
+ image += pad_color * 0x10 * more_tile_padding
+ width = px_length(image) / height
+
+ # at least one dimension should be given
+ if width * height != px_length(image):
+ # look for possible combos of width/height that would form a rectangle
+ matches = []
+ # Height need not be divisible by 8, but width must.
+ # See pokered gfx/minimize_pic.1bpp.
+ for w in range(8, px_length(image) / 2 + 1, 8):
+ h = px_length(image) / w
+ if w * h == px_length(image):
+ matches += [(w, h)]
+ # go for the most square image
+ if len(matches):
+ width, height = sorted(matches, key= lambda (w, h): (h % 8 != 0, w + h))[0] # favor height
+ else:
+ raise Exception, 'Image can\'t be divided into tiles (%d px)!' % (px_length(image))
+
+ # convert tiles to lines
+ lines = to_lines(flatten(image), width)
+
+ if pal_file == None:
+ palette = None
+ greyscale = True
+ bitdepth = 2
+ px_map = [[3 - pixel for pixel in line] for line in lines]
+
+ else: # gbc color
+ palette = pal_to_png(pal_file)
+ greyscale = False
+ bitdepth = 8
+ px_map = [[pixel for pixel in line] for line in lines]
+
+ return width, height, palette, greyscale, bitdepth, px_map
+
+def export_png_to_2bpp(filein, fileout=None, palout=None, **kwargs):
+
+ arguments = {
+ 'tile_padding': 0,
+ 'pic_dimensions': None,
+ 'animate': False,
+ 'stupid_bitmask_hack': [],
+ }
+ arguments.update(kwargs)
+ arguments.update(read_filename_arguments(filein))
+
+ image, arguments = png_to_2bpp(filein, **arguments)
+
+ if fileout == None:
+ fileout = os.path.splitext(filein)[0] + '.2bpp'
+ to_file(fileout, image)
+
+ tmap = arguments.get('tmap')
+
+ if tmap != None and arguments['animate'] and arguments['pic_dimensions']:
+ # Generate pic animation data.
+ frame_text, bitmask_text = get_pic_animation(tmap, *arguments['pic_dimensions'])
+
+ frames_path = os.path.join(os.path.split(fileout)[0], 'frames.asm')
+ with open(frames_path, 'w') as out:
+ out.write(frame_text)
+
+ bitmask_path = os.path.join(os.path.split(fileout)[0], 'bitmask.asm')
+
+ # The following Pokemon have a bitmask dummied out.
+ for exception in arguments['stupid_bitmask_hack']:
+ if exception in bitmask_path:
+ bitmasks = bitmask_text.split(';')
+ bitmasks[-1] = bitmasks[-1].replace('1', '0')
+ bitmask_text = ';'.join(bitmasks)
+
+ with open(bitmask_path, 'w') as out:
+ out.write(bitmask_text)
+
+ elif tmap != None and arguments.get('tilemap', False):
+ tilemap_path = os.path.splitext(fileout)[0] + '.tilemap'
+ to_file(tilemap_path, tmap)
+
+ palette = arguments.get('palette')
+ if palout == None:
+ palout = os.path.splitext(fileout)[0] + '.pal'
+ export_palette(palette, palout)
+
+
+def get_image_padding(width, height, wstep=8, hstep=8):
+
+ padding = {
+ 'left': 0,
+ 'right': 0,
+ 'top': 0,
+ 'bottom': 0,
+ }
+
+ if width % wstep and width >= wstep:
+ pad = float(width % wstep) / 2
+ padding['left'] = int(ceil(pad))
+ padding['right'] = int(floor(pad))
+
+ if height % hstep and height >= hstep:
+ pad = float(height % hstep) / 2
+ padding['top'] = int(ceil(pad))
+ padding['bottom'] = int(floor(pad))
+
+ return padding
+
+
+def png_to_2bpp(filein, **kwargs):
+ """
+ Convert a png image to planar 2bpp.
+ """
+
+ arguments = {
+ 'tile_padding': 0,
+ 'pic_dimensions': False,
+ 'interleave': False,
+ 'norepeat': False,
+ 'tilemap': False,
+ }
+ arguments.update(kwargs)
+
+ if type(filein) is str:
+ filein = open(filein)
+
+ assert type(filein) is file
+
+ width, height, rgba, info = png.Reader(filein).asRGBA8()
+
+ # png.Reader returns flat pixel data. Nested is easier to work with
+ len_px = len('rgba')
+ image = []
+ palette = []
+ for line in rgba:
+ newline = []
+ for px in xrange(0, len(line), len_px):
+ color = dict(zip('rgba', line[px:px+len_px]))
+ if color not in palette:
+ if len(palette) < 4:
+ palette += [color]
+ else:
+ # TODO Find the nearest match
+ print 'WARNING: %s: Color %s truncated to' % (filein, color),
+ color = sorted(palette, key=lambda x: sum(x.values()))[0]
+ print color
+ newline += [color]
+ image += [newline]
+
+ assert len(palette) <= 4, '%s: palette should be 4 colors, is really %d (%s)' % (filein, len(palette), palette)
+
+ # Pad out smaller palettes with greyscale colors
+ greyscale = {
+ 'black': { 'r': 0x00, 'g': 0x00, 'b': 0x00, 'a': 0xff },
+ 'grey': { 'r': 0x55, 'g': 0x55, 'b': 0x55, 'a': 0xff },
+ 'gray': { 'r': 0xaa, 'g': 0xaa, 'b': 0xaa, 'a': 0xff },
+ 'white': { 'r': 0xff, 'g': 0xff, 'b': 0xff, 'a': 0xff },
+ }
+ preference = 'white', 'black', 'grey', 'gray'
+ for hue in map(greyscale.get, preference):
+ if len(palette) >= 4:
+ break
+ if hue not in palette:
+ palette += [hue]
+
+ palette.sort(key=lambda x: sum(x.values()))
+
+ # Game Boy palette order
+ palette.reverse()
+
+ # Map pixels to quaternary color ids
+ padding = get_image_padding(width, height)
+ width += padding['left'] + padding['right']
+ height += padding['top'] + padding['bottom']
+ pad = bytearray([0])
+
+ qmap = []
+ qmap += pad * width * padding['top']
+ for line in image:
+ qmap += pad * padding['left']
+ for color in line:
+ qmap += [palette.index(color)]
+ qmap += pad * padding['right']
+ qmap += pad * width * padding['bottom']
+
+ # Graphics are stored in tiles instead of lines
+ tile_width = 8
+ tile_height = 8
+ num_columns = max(width, tile_width) / tile_width
+ num_rows = max(height, tile_height) / tile_height
+ image = []
+
+ for row in xrange(num_rows):
+ for column in xrange(num_columns):
+
+ # Split it up into strips to convert to planar data
+ for strip in xrange(min(tile_height, height)):
+ anchor = (
+ row * num_columns * tile_width * tile_height +
+ column * tile_width +
+ strip * width
+ )
+ line = qmap[anchor : anchor + tile_width]
+ bottom, top = 0, 0
+ for bit, quad in enumerate(line):
+ bottom += (quad & 1) << (7 - bit)
+ top += (quad /2 & 1) << (7 - bit)
+ image += [bottom, top]
+
+ dim = arguments['pic_dimensions']
+ if dim:
+ if type(dim) in (tuple, list):
+ w, h = dim
+ else:
+ # infer dimensions based on width.
+ w = width / tile_width
+ h = height / tile_height
+ if h % w == 0:
+ h = w
+
+ tiles = get_tiles(image)
+ pic_length = w * h
+ tile_width = width / 8
+ trailing = len(tiles) % pic_length
+ new_image = []
+ for block in xrange(len(tiles) / pic_length):
+ offset = (h * tile_width) * ((block * w) / tile_width) + ((block * w) % tile_width)
+ pic = []
+ for row in xrange(h):
+ index = offset + (row * tile_width)
+ pic += tiles[index:index + w]
+ new_image += transpose(pic, w)
+ new_image += tiles[len(tiles) - trailing:]
+ image = connect(new_image)
+
+ # Remove any tile padding used to make the png rectangular.
+ image = image[:len(image) - arguments['tile_padding'] * 0x10]
+
+ tmap = None
+
+ if arguments['interleave']:
+ image = deinterleave_tiles(image, num_columns)
+
+ if arguments['pic_dimensions']:
+ image, tmap = condense_tiles_to_map(image, w * h)
+ elif arguments['norepeat']:
+ image, tmap = condense_tiles_to_map(image)
+ if not arguments['tilemap']:
+ tmap = None
+
+ arguments.update({ 'palette': palette, 'tmap': tmap, })
+
+ return image, arguments
+
+
+def export_palette(palette, filename):
+ """
+ Export a palette from png to rgb macros in a .pal file.
+ """
+
+ if os.path.exists(filename):
+
+ # Pic palettes are 2 colors (black/white are added later).
+ with open(filename) as rgbs:
+ colors = read_rgb_macros(rgbs.readlines())
+
+ if len(colors) == 2:
+ palette = palette[1:3]
+
+ text = png_to_rgb(palette)
+ with open(filename, 'w') as out:
+ out.write(text)
+
+def convert_2bpp_to_1bpp(data):
+ """
+ Convert planar 2bpp image data to 1bpp. Assume images are two colors.
+ """
+ return data[::2]
+
+def convert_1bpp_to_2bpp(data):
+ """
+ Convert 1bpp image data to planar 2bpp (black/white).
+ """
+ output = []
+ for i in data:
+ output += [i, i]
+ return output
+
+
+def export_2bpp_to_1bpp(filename):
+ name, extension = os.path.splitext(filename)
+ image = open(filename, 'rb').read()
+ image = convert_2bpp_to_1bpp(image)
+ to_file(name + '.1bpp', image)
+
+def export_1bpp_to_2bpp(filename):
+ name, extension = os.path.splitext(filename)
+ image = open(filename, 'rb').read()
+ image = convert_1bpp_to_2bpp(image)
+ to_file(name + '.2bpp', image)
+
+
+def export_1bpp_to_png(filename, fileout=None):
+
+ if fileout == None:
+ fileout = os.path.splitext(filename)[0] + '.png'
+
+ arguments = read_filename_arguments(filename)
+
+ image = open(filename, 'rb').read()
+ image = convert_1bpp_to_2bpp(image)
+
+ result = convert_2bpp_to_png(image, **arguments)
+ width, height, palette, greyscale, bitdepth, px_map = result
+
+ w = png.Writer(width, height, palette=palette, compression=9, greyscale=greyscale, bitdepth=bitdepth)
+ with open(fileout, 'wb') as f:
+ w.write(f, px_map)
+
+
+def export_png_to_1bpp(filename, fileout=None):
+
+ if fileout == None:
+ fileout = os.path.splitext(filename)[0] + '.1bpp'
+
+ arguments = read_filename_arguments(filename)
+ image = png_to_1bpp(filename, **arguments)
+
+ to_file(fileout, image)
+
+def png_to_1bpp(filename, **kwargs):
+ image, kwargs = png_to_2bpp(filename, **kwargs)
+ return convert_2bpp_to_1bpp(image)
+
+def convert_to_2bpp(filenames=[]):
+ for filename in filenames:
+ filename, name, extension = try_decompress(filename)
+ if extension == '.1bpp':
+ export_1bpp_to_2bpp(filename)
+ elif extension == '.2bpp':
+ pass
+ elif extension == '.png':
+ export_png_to_2bpp(filename)
+ else:
+ raise Exception, "Don't know how to convert {} to 2bpp!".format(filename)
+
+def convert_to_1bpp(filenames=[]):
+ for filename in filenames:
+ filename, name, extension = try_decompress(filename)
+ if extension == '.1bpp':
+ pass
+ elif extension == '.2bpp':
+ export_2bpp_to_1bpp(filename)
+ elif extension == '.png':
+ export_png_to_1bpp(filename)
+ else:
+ raise Exception, "Don't know how to convert {} to 1bpp!".format(filename)
+
+def convert_to_png(filenames=[]):
+ for filename in filenames:
+ filename, name, extension = try_decompress(filename)
+ if extension == '.1bpp':
+ export_1bpp_to_png(filename)
+ elif extension == '.2bpp':
+ export_2bpp_to_png(filename)
+ elif extension == '.png':
+ pass
+ else:
+ raise Exception, "Don't know how to convert {} to png!".format(filename)
+
+def main():
+ ap = argparse.ArgumentParser()
+ ap.add_argument('mode')
+ ap.add_argument('filenames', nargs='*')
+ args = ap.parse_args()
+
+ method = {
+ '2bpp': convert_to_2bpp,
+ '1bpp': convert_to_1bpp,
+ 'png': convert_to_png,
+ }.get(args.mode, None)
+
+ if method == None:
+ raise Exception, "Unknown conversion method!"
+
+ method(args.filenames)
+
+
+if __name__ == "__main__":
+ main()
+
diff --git a/extras/labels.py b/extras/labels.py
new file mode 100644
index 0000000..72700a5
--- /dev/null
+++ b/extras/labels.py
@@ -0,0 +1,213 @@
+# -*- coding: utf-8 -*-
+"""
+Various label/line-related functions.
+"""
+
+import os
+import json
+import logging
+
+import pointers
+import sym
+
+class Labels(object):
+ """
+ Store all labels.
+ """
+
+ def __init__(self, config, filename="pokecrystal.map"):
+ """
+ Setup the instance.
+ """
+ self.config = config
+ self.filename = filename
+ self.path = os.path.join(self.config.path, self.filename)
+
+ def initialize(self):
+ """
+ Handle anything requiring file-loading and such.
+ """
+ # Look for a mapfile if it's not given
+ if not os.path.exists(self.path):
+ self.filename = find_mapfile_in_dir(self.config.path)
+ if self.filename == None:
+ raise Exception, "Couldn't find any mapfiles. Run rgblink -m to create a mapfile."
+ self.path = os.path.join(self.config.path, self.filename)
+
+ self.labels = sym.read_mapfile(self.path)
+
+def find_mapfile_in_dir(path):
+ for filename in os.listdir(path):
+ if os.path.splitext(filename)[1] == '.map':
+ return filename
+ return None
+
+def remove_quoted_text(line):
+ """get rid of content inside quotes
+ and also removes the quotes from the input string"""
+ while line.count("\"") % 2 == 0 and line.count("\"") > 0:
+ first = line.find("\"")
+ second = line.find("\"", first+1)
+ line = line[0:first] + line[second+1:]
+ while line.count("\'") % 2 == 0 and line.count("'") > 0:
+ first = line.find("\'")
+ second = line.find("\'", first+1)
+ line = line[0:first] + line[second+1:]
+ return line
+
+def line_has_comment_address(line, returnable={}, bank=None):
+ """checks that a given line has a comment
+ with a valid address, and returns the address in the object.
+ Note: bank is required if you have a 4-letter-or-less address,
+ because otherwise there is no way to figure out which bank
+ is curretly being scanned."""
+ #first set the bank/offset to nada
+ returnable["bank"] = None
+ returnable["offset"] = None
+ returnable["address"] = None
+ #only valid characters are 0-9a-fA-F
+ valid = [str(x) for x in range(10)] + \
+ [chr(x) for x in range(ord('a'), ord('f')+1)] + \
+ [chr(x) for x in range(ord('A'), ord('F')+1)]
+ #check if there is a comment in this line
+ if ";" not in line:
+ return False
+ #first throw away anything in quotes
+ if (line.count("\"") % 2 == 0 and line.count("\"")!=0) \
+ or (line.count("\'") % 2 == 0 and line.count("\'")!=0):
+ line = remove_quoted_text(line)
+ #check if there is still a comment in this line after quotes removed
+ if ";" not in line:
+ return False
+ #but even if there's a semicolon there must be later text
+ if line[-1] == ";":
+ return False
+ #and just a space doesn't count
+ if line[-2:] == "; ":
+ return False
+ #and multiple whitespace doesn't count either
+ line = line.rstrip(" ").lstrip(" ")
+ if line[-1] == ";":
+ return False
+ #there must be more content after the semicolon
+ if len(line)-1 == line.find(";"):
+ return False
+ #split it up into the main comment part
+ comment = line[line.find(";")+1:]
+ #don't want no leading whitespace
+ comment = comment.lstrip(" ").rstrip(" ")
+ #split up multi-token comments into single tokens
+ token = comment
+ if " " in comment:
+ #use the first token in the comment
+ token = comment.split(" ")[0]
+ if token in ["0x", "$", "x", ":"]:
+ return False
+ offset = None
+ #process a token with a A:B format
+ if ":" in token: #3:3F0A, $3:$3F0A, 0x3:0x3F0A, 3:3F0A
+ #split up the token
+ bank_piece = token.split(":")[0].lower()
+ offset_piece = token.split(":")[1].lower()
+ #filter out blanks/duds
+ if bank_piece in ["$", "0x", "x"] \
+ or offset_piece in ["$", "0x", "x"]:
+ return False
+ #they can't have both "$" and "x"
+ if "$" in bank_piece and "x" in bank_piece:
+ return False
+ if "$" in offset_piece and "x" in offset_piece:
+ return False
+ #process the bank piece
+ if "$" in bank_piece:
+ bank_piece = bank_piece.replace("$", "0x")
+ #check characters for validity?
+ for c in bank_piece.replace("x", ""):
+ if c not in valid:
+ return False
+ bank = int(bank_piece, 16)
+ #process the offset piece
+ if "$" in offset_piece:
+ offset_piece = offset_piece.replace("$", "0x")
+ #check characters for validity?
+ for c in offset_piece.replace("x", ""):
+ if c not in valid:
+ return False
+ if len(offset_piece) == 0:
+ return None
+ offset = int(offset_piece, 16)
+ #filter out blanks/duds
+ elif token in ["$", "0x", "x"]:
+ return False
+ #can't have both "$" and "x" in the number
+ elif "$" in token and "x" in token:
+ return False
+ elif "x" in token and not "0x" in token: #it should be 0x
+ return False
+ elif "$" in token and not "x" in token:
+ token = token.replace("$", "0x")
+ offset = int(token, 16)
+ elif "0x" in token and not "$" in token:
+ offset = int(token, 16)
+ else: #might just be "1" at this point
+ token = token.lower()
+ #check if there are bad characters
+ for c in token:
+ if c not in valid:
+ return False
+ offset = int(token, 16)
+ if offset == None and bank == None:
+ return False
+ if bank == None:
+ bank = pointers.calculate_bank(offset)
+ returnable["bank"] = bank
+ returnable["offset"] = offset
+ returnable["address"] = pointers.calculate_pointer(offset, bank=bank)
+ return True
+
+def get_address_from_line_comment(line, bank=None):
+ """
+ wrapper for line_has_comment_address
+ """
+ returnable = {}
+ result = line_has_comment_address(line, returnable=returnable, bank=bank)
+ if not result:
+ return False
+ return returnable["address"]
+
+def line_has_label(line):
+ """returns True if the line has an asm label"""
+ if not isinstance(line, str):
+ raise Exception, "can't check this type of object"
+ line = line.rstrip(" ").lstrip(" ")
+ line = remove_quoted_text(line)
+ if ";" in line:
+ line = line.split(";")[0]
+ if 0 <= len(line) <= 1:
+ return False
+ if ":" not in line:
+ return False
+ if line[0] == ";":
+ return False
+ if line[0] == "\"":
+ return False
+ return True
+
+def get_label_from_line(line):
+ """returns the label from the line"""
+ #check if the line has a label
+ if not line_has_label(line):
+ return None
+ #split up the line
+ label = line.split(":")[0]
+ return label
+
+def find_labels_without_addresses(asm):
+ """scans the asm source and finds labels that are unmarked"""
+ without_addresses = []
+ for (line_number, line) in enumerate(asm):
+ if line_has_label(line):
+ label = get_label_from_line(line)
+ if not line_has_comment_address(line):
+ without_addresses.append({"line_number": line_number, "line": line, "label": label})
+ return without_addresses
diff --git a/extras/scan_includes.py b/extras/scan_includes.py
new file mode 100644
index 0000000..8ce9a37
--- /dev/null
+++ b/extras/scan_includes.py
@@ -0,0 +1,65 @@
+#!/bin/python
+# coding: utf-8
+
+"""
+Recursively scan an asm file for dependencies.
+"""
+
+import os
+import sys
+import argparse
+
+
+class IncludeReader:
+ """
+ Usage:
+ includer = IncludeReader()
+ includer.read(filename)
+ or
+ includer = IncludeReader(filename='filename.asm')
+ includer.read()
+ """
+ path = ''
+ includes = []
+ directives = ['INCLUDE', 'INCBIN']
+ extensions = ['.asm']
+
+ def __init__(self, **kwargs):
+ self.__dict__.update(kwargs)
+
+ def read(self, filename=None):
+ """
+ Recursively look for includes in <filename> and add them to self.includes.
+ """
+ if filename is None:
+ if hasattr(self, 'filename'):
+ filename = os.path.join(self.path, self.filename)
+ else:
+ raise Exception, 'no filename given!'
+ if os.path.splitext(filename)[1] in self.extensions and os.path.exists(filename):
+ for line in open(filename).readlines():
+ self.read_line(line)
+
+ def read_line(self, line):
+ """
+ Add any includes in <line> to self.includes, and look for includes in those.
+ """
+ parts = line[:line.find(';')].split()
+ for directive in self.directives:
+ if directive in map(str.upper, parts):
+ include = os.path.join(self.path, "src/" + parts[parts.index(directive) + 1].split('"')[1])
+ if include not in self.includes and include != "src/baserom.gbc":
+ self.includes.append(include)
+ self.read(include)
+
+if __name__ == '__main__':
+ ap = argparse.ArgumentParser()
+ ap.add_argument('-i', default='')
+ ap.add_argument('filenames', nargs='*')
+ args = ap.parse_args()
+
+ includes = IncludeReader(path=args.i)
+ for filename in args.filenames:
+ includes.read(filename)
+ sys.stdout.write(' '.join(includes.includes))
+
diff --git a/extras/wram.py b/extras/wram.py
new file mode 100644
index 0000000..b6d7fc6
--- /dev/null
+++ b/extras/wram.py
@@ -0,0 +1,313 @@
+# coding: utf-8
+"""
+RGBDS BSS section and constant parsing.
+"""
+
+import os
+
+
+def separate_comment(line):
+ if ';' in line:
+ i = line.find(';')
+ return line[:i], line[i:]
+ return line, None
+
+
+def rgbasm_to_py(text):
+ return text.replace('$', '0x').replace('%', '0b')
+
+
+def make_wram_labels(wram_sections):
+ wram_labels = {}
+ for section in wram_sections:
+ for label in section['labels']:
+ if label['address'] not in wram_labels.keys():
+ wram_labels[label['address']] = []
+ wram_labels[label['address']] += [label['label']]
+ return wram_labels
+
+def bracket_value(string, i=0):
+ return string.split('[')[1 + i*2].split(']')[0]
+
+class BSSReader:
+ """
+ Read rgbasm BSS/WRAM sections and associate labels with addresses.
+ Also reads constants/variables, even in macros.
+ """
+ sections = []
+ section = None
+ address = None
+ macros = {}
+ constants = {}
+
+ section_types = {
+ 'VRAM': 0x8000,
+ 'SRAM': 0xa000,
+ 'WRAM0': 0xc000,
+ 'WRAMX': 0xd000,
+ 'HRAM': 0xff80,
+ }
+
+ def __init__(self, *args, **kwargs):
+ self.__dict__.update(kwargs)
+
+ def read_bss_line(self, l):
+ parts = l.strip().split(' ')
+ token = parts[0].strip()
+ params = ' '.join(parts[1:]).split(',')
+
+ if token in ['ds', 'db', 'dw']:
+ if any(params):
+ length = eval(rgbasm_to_py(params[0]), self.constants.copy())
+ else:
+ length = {'ds': 1, 'db': 1, 'dw': 2}[token]
+ self.address += length
+ # assume adjacent labels to use the same space
+ for label in self.section['labels'][::-1]:
+ if label['length'] == 0:
+ label['length'] = length
+ else:
+ break
+
+ elif token in self.macros.keys():
+ macro_text = '\n'.join(self.macros[token]) + '\n'
+ for i, p in enumerate(params):
+ macro_text = macro_text.replace('\\'+str(i+1),p)
+ macro_text = macro_text.split('\n')
+ macro_reader = BSSReader(
+ sections = list(self.sections),
+ section = dict(self.section),
+ address = self.address,
+ constants = self.constants,
+ )
+ macro_sections = macro_reader.read_bss_sections(macro_text)
+ self.section = macro_sections[-1]
+ if self.section['labels']:
+ self.address = self.section['labels'][-1]['address'] + self.section['labels'][-1]['length']
+
+
+ def read_bss_sections(self, bss):
+
+ if self.section is None:
+ self.section = {
+ "labels": [],
+ }
+
+ if type(bss) is str:
+ bss = bss.split('\n')
+
+ macro = False
+ macro_name = None
+ for line in bss:
+ line = line.lstrip()
+ line, comment = separate_comment(line)
+ line = line.strip()
+ split_line = line.split()
+ split_line_upper = map(str.upper, split_line)
+
+ if not line:
+ pass
+
+ elif line[-4:].upper() == 'ENDM':
+ macro = False
+ macro_name = None
+
+ elif macro:
+ self.macros[macro_name] += [line]
+
+ elif line[-5:].upper() == 'MACRO':
+ macro_name = line.split(':')[0]
+ macro = True
+ self.macros[macro_name] = []
+
+ elif 'INCLUDE' == line[:7].upper():
+ filename = line.split('"')[1]
+ self.read_bss_sections(open(filename).readlines())
+
+ elif 'SECTION' == line[:7].upper():
+ if self.section: # previous
+ self.sections += [self.section]
+
+ section_def = line.split(',')
+ name = section_def[0].split('"')[1]
+ type_ = section_def[1].strip()
+ if len(section_def) > 2:
+ bank = bracket_value(section_def[2])
+ else:
+ bank = None
+
+ if '[' in type_:
+ self.address = int(rgbasm_to_py(bracket_value(type_)), 16)
+ else:
+ if self.address == None or bank != self.section['bank'] or self.section['type'] != type_:
+ self.address = self.section_types.get(type_, self.address)
+ # else: keep going from this address
+
+ self.section = {
+ 'name': name,
+ 'type': type_,
+ 'bank': bank,
+ 'start': self.address,
+ 'labels': [],
+ }
+
+ elif ':' in line:
+ # rgbasm allows labels without :, but prefer convention
+ label = line[:line.find(':')]
+ if '\\' in label:
+ raise Exception, line + ' ' + label
+ if ';' not in label:
+ section_label = {
+ 'label': label,
+ 'address': self.address,
+ 'length': 0,
+ }
+ self.section['labels'] += [section_label]
+ self.read_bss_line(line.split(':')[-1])
+
+ elif any(x in split_line_upper for x in ['EQU', '=', 'SET']): # TODO: refactor
+ for x in ['EQU', '=', 'SET']:
+ if x in split_line_upper:
+ index = split_line_upper.index(x)
+ real = split_line[index]
+ name, value = map(' '.join, [split_line[:index], split_line[index+1:]])
+ value = rgbasm_to_py(value)
+ self.constants[name] = eval(value, self.constants.copy())
+
+ else:
+ self.read_bss_line(line)
+
+ self.sections += [self.section]
+ return self.sections
+
+def read_bss_sections(bss):
+ reader = BSSReader()
+ return reader.read_bss_sections(bss)
+
+
+def constants_to_dict(constants):
+ """Deprecated. Use BSSReader."""
+ return dict((eval(rgbasm_to_py(constant[constant.find('EQU')+3:constant.find(';')])), constant[:constant.find('EQU')].strip()) for constant in constants)
+
+def scrape_constants(text):
+ if type(text) is not list:
+ text = text.split('\n')
+ bss = BSSReader()
+ bss.read_bss_sections(text)
+ constants = bss.constants
+ return {v: k for k, v in constants.items()}
+
+def read_constants(filepath):
+ """
+ Load lines from a file and grab any constants using BSSReader.
+ """
+ lines = []
+ if os.path.exists(filepath):
+ with open(filepath, "r") as file_handler:
+ lines = file_handler.readlines()
+ return scrape_constants(lines)
+
+class WRAMProcessor(object):
+ """
+ RGBDS BSS section and constant parsing.
+ """
+
+ def __init__(self, config):
+ """
+ Setup for WRAM parsing.
+ """
+ self.config = config
+
+ self.paths = {}
+
+ if hasattr(self.config, "wram"):
+ self.paths["wram"] = self.config.wram
+ else:
+ self.paths["wram"] = os.path.join(self.config.path, "wram.asm")
+
+ if hasattr(self.config, "hram"):
+ self.paths["hram"] = self.config.hram
+ else:
+ self.paths["hram"] = os.path.join(self.config.path, "hram.asm")
+
+ if hasattr(self.config, "gbhw"):
+ self.paths["gbhw"] = self.config.gbhw
+ else:
+ self.paths["gbhw"] = os.path.join(self.config.path, "gbhw.asm")
+
+ def initialize(self):
+ """
+ Read constants.
+ """
+ self.setup_wram_sections()
+ self.setup_wram_labels()
+ self.setup_hram_constants()
+ self.setup_gbhw_constants()
+
+ self.reformat_wram_labels()
+
+ def read_wram_sections(self):
+ """
+ Opens the wram file and calls read_bss_sections.
+ """
+ wram_content = None
+ wram_file_path = self.paths["wram"]
+
+ with open(wram_file_path, "r") as wram:
+ wram_content = wram.readlines()
+
+ wram_sections = read_bss_sections(wram_content)
+ return wram_sections
+
+ def setup_wram_sections(self):
+ """
+ Call read_wram_sections and set a variable.
+ """
+ self.wram_sections = self.read_wram_sections()
+ return self.wram_sections
+
+ def setup_wram_labels(self):
+ """
+ Make wram labels based on self.wram_sections as input.
+ """
+ self.wram_labels = make_wram_labels(self.wram_sections)
+ return self.wram_labels
+
+ def read_hram_constants(self):
+ """
+ Read constants from hram.asm using read_constants.
+ """
+ hram_constants = read_constants(self.paths["hram"])
+ return hram_constants
+
+ def setup_hram_constants(self):
+ """
+ Call read_hram_constants and set a variable.
+ """
+ self.hram_constants = self.read_hram_constants()
+ return self.hram_constants
+
+ def read_gbhw_constants(self):
+ """
+ Read constants from gbhw.asm using read_constants.
+ """
+ gbhw_constants = read_constants(self.paths["gbhw"])
+ return gbhw_constants
+
+ def setup_gbhw_constants(self):
+ """
+ Call read_gbhw_constants and set a variable.
+ """
+ self.gbhw_constants = self.read_gbhw_constants()
+ return self.gbhw_constants
+
+ def reformat_wram_labels(self):
+ """
+ Flips the wram_labels dictionary the other way around to access
+ addresses by label.
+ """
+ self.wram = {}
+
+ for (address, labels) in self.wram_labels.iteritems():
+ for label in labels:
+ self.wram[label] = address
diff --git a/src/main.asm b/src/main.asm
new file mode 100644
index 0000000..ca09a5e
--- /dev/null
+++ b/src/main.asm
@@ -0,0 +1,383 @@
+SECTION "bank0",ROM0[0]
+INCBIN "baserom.gbc",$0,$4000
+
+SECTION "bank1",ROMX,BANK[$1]
+INCBIN "baserom.gbc",$4000,$4000
+
+SECTION "bank2",ROMX,BANK[$2]
+INCBIN "baserom.gbc",$8000,$4000
+
+SECTION "bank3",ROMX,BANK[$3]
+INCBIN "baserom.gbc",$C000,$4000
+
+SECTION "bank4",ROMX,BANK[$4]
+INCBIN "baserom.gbc",$10000,$4000
+
+SECTION "bank5",ROMX,BANK[$5]
+INCBIN "baserom.gbc",$14000,$4000
+
+SECTION "bank6",ROMX,BANK[$6]
+INCBIN "baserom.gbc",$18000,$4000
+
+SECTION "bank7",ROMX,BANK[$7]
+INCBIN "baserom.gbc",$1C000,$4000
+
+SECTION "bank8",ROMX,BANK[$8]
+INCBIN "baserom.gbc",$20000,$4000
+
+SECTION "bank9",ROMX,BANK[$9]
+INCBIN "baserom.gbc",$24000,$4000
+
+SECTION "bankA",ROMX,BANK[$A]
+INCBIN "baserom.gbc",$28000,$4000
+
+SECTION "bankB",ROMX,BANK[$B]
+INCBIN "baserom.gbc",$2C000,$4000
+
+SECTION "bankC",ROMX,BANK[$C]
+INCBIN "baserom.gbc",$30000,$4000
+
+SECTION "bankD",ROMX,BANK[$D]
+INCBIN "baserom.gbc",$34000,$4000
+
+SECTION "bankE",ROMX,BANK[$E]
+INCBIN "baserom.gbc",$38000,$4000
+
+SECTION "bankF",ROMX,BANK[$F]
+INCBIN "baserom.gbc",$3C000,$4000
+
+SECTION "bank10",ROMX,BANK[$10]
+INCBIN "baserom.gbc",$40000,$4000
+
+SECTION "bank11",ROMX,BANK[$11]
+INCBIN "baserom.gbc",$44000,$4000
+
+SECTION "bank12",ROMX,BANK[$12]
+INCBIN "baserom.gbc",$48000,$4000
+
+SECTION "bank13",ROMX,BANK[$13]
+INCBIN "baserom.gbc",$4C000,$4000
+
+SECTION "bank14",ROMX,BANK[$14]
+INCBIN "baserom.gbc",$50000,$4000
+
+SECTION "bank15",ROMX,BANK[$15]
+INCBIN "baserom.gbc",$54000,$4000
+
+SECTION "bank16",ROMX,BANK[$16]
+INCBIN "baserom.gbc",$58000,$4000
+
+SECTION "bank17",ROMX,BANK[$17]
+INCBIN "baserom.gbc",$5C000,$4000
+
+SECTION "bank18",ROMX,BANK[$18]
+INCBIN "baserom.gbc",$60000,$4000
+
+SECTION "bank19",ROMX,BANK[$19]
+INCBIN "baserom.gbc",$64000,$4000
+
+SECTION "bank1A",ROMX,BANK[$1A]
+INCBIN "baserom.gbc",$68000,$4000
+
+SECTION "bank1B",ROMX,BANK[$1B]
+INCBIN "baserom.gbc",$6C000,$4000
+
+SECTION "bank1C",ROMX,BANK[$1C]
+INCBIN "baserom.gbc",$70000,$4000
+
+SECTION "bank1D",ROMX,BANK[$1D]
+INCBIN "baserom.gbc",$74000,$4000
+
+SECTION "bank1E",ROMX,BANK[$1E]
+INCBIN "baserom.gbc",$78000,$4000
+
+SECTION "bank1F",ROMX,BANK[$1F]
+INCBIN "baserom.gbc",$7C000,$4000
+
+SECTION "bank20",ROMX,BANK[$20]
+INCBIN "baserom.gbc",$80000,$4000
+
+SECTION "bank21",ROMX,BANK[$21]
+INCBIN "baserom.gbc",$84000,$4000
+
+SECTION "bank22",ROMX,BANK[$22]
+INCBIN "baserom.gbc",$88000,$4000
+
+SECTION "bank23",ROMX,BANK[$23]
+INCBIN "baserom.gbc",$8C000,$4000
+
+SECTION "bank24",ROMX,BANK[$24]
+INCBIN "baserom.gbc",$90000,$4000
+
+SECTION "bank25",ROMX,BANK[$25]
+INCBIN "baserom.gbc",$94000,$4000
+
+SECTION "bank26",ROMX,BANK[$26]
+INCBIN "baserom.gbc",$98000,$4000
+
+SECTION "bank27",ROMX,BANK[$27]
+INCBIN "baserom.gbc",$9C000,$4000
+
+SECTION "bank28",ROMX,BANK[$28]
+INCBIN "baserom.gbc",$A0000,$4000
+
+SECTION "bank29",ROMX,BANK[$29]
+INCBIN "baserom.gbc",$A4000,$4000
+
+SECTION "bank2A",ROMX,BANK[$2A]
+INCBIN "baserom.gbc",$A8000,$4000
+
+SECTION "bank2B",ROMX,BANK[$2B]
+INCBIN "baserom.gbc",$AC000,$4000
+
+SECTION "bank2C",ROMX,BANK[$2C]
+INCBIN "baserom.gbc",$B0000,$4000
+
+SECTION "bank2D",ROMX,BANK[$2D]
+INCBIN "baserom.gbc",$B4000,$4000
+
+SECTION "bank2E",ROMX,BANK[$2E]
+INCBIN "baserom.gbc",$B8000,$4000
+
+SECTION "bank2F",ROMX,BANK[$2F]
+INCBIN "baserom.gbc",$BC000,$4000
+
+SECTION "bank30",ROMX,BANK[$30]
+INCBIN "baserom.gbc",$C0000,$4000
+
+SECTION "bank31",ROMX,BANK[$31]
+INCBIN "baserom.gbc",$C4000,$4000
+
+SECTION "bank32",ROMX,BANK[$32]
+INCBIN "baserom.gbc",$C8000,$4000
+
+SECTION "bank33",ROMX,BANK[$33]
+INCBIN "baserom.gbc",$CC000,$4000
+
+SECTION "bank34",ROMX,BANK[$34]
+INCBIN "baserom.gbc",$D0000,$4000
+
+SECTION "bank35",ROMX,BANK[$35]
+INCBIN "baserom.gbc",$D4000,$4000
+
+SECTION "bank36",ROMX,BANK[$36]
+INCBIN "baserom.gbc",$D8000,$4000
+
+SECTION "bank37",ROMX,BANK[$37]
+INCBIN "baserom.gbc",$DC000,$4000
+
+SECTION "bank38",ROMX,BANK[$38]
+INCBIN "baserom.gbc",$E0000,$4000
+
+SECTION "bank39",ROMX,BANK[$39]
+INCBIN "baserom.gbc",$E4000,$4000
+
+SECTION "bank3A",ROMX,BANK[$3A]
+INCBIN "baserom.gbc",$E8000,$4000
+
+SECTION "bank3B",ROMX,BANK[$3B]
+INCBIN "baserom.gbc",$EC000,$4000
+
+SECTION "bank3C",ROMX,BANK[$3C]
+INCBIN "baserom.gbc",$F0000,$4000
+
+SECTION "bank3D",ROMX,BANK[$3D]
+INCBIN "baserom.gbc",$F4000,$4000
+
+SECTION "bank3E",ROMX,BANK[$3E]
+INCBIN "baserom.gbc",$F8000,$4000
+
+SECTION "bank3F",ROMX,BANK[$3F]
+INCBIN "baserom.gbc",$FC000,$4000
+
+SECTION "bank40",ROMX,BANK[$40]
+INCBIN "baserom.gbc",$100000,$4000
+
+SECTION "bank41",ROMX,BANK[$41]
+INCBIN "baserom.gbc",$104000,$4000
+
+SECTION "bank42",ROMX,BANK[$42]
+INCBIN "baserom.gbc",$108000,$4000
+
+SECTION "bank43",ROMX,BANK[$43]
+INCBIN "baserom.gbc",$10C000,$4000
+
+SECTION "bank44",ROMX,BANK[$44]
+INCBIN "baserom.gbc",$110000,$4000
+
+SECTION "bank45",ROMX,BANK[$45]
+INCBIN "baserom.gbc",$114000,$4000
+
+SECTION "bank46",ROMX,BANK[$46]
+INCBIN "baserom.gbc",$118000,$4000
+
+SECTION "bank47",ROMX,BANK[$47]
+INCBIN "baserom.gbc",$11C000,$4000
+
+SECTION "bank48",ROMX,BANK[$48]
+INCBIN "baserom.gbc",$120000,$4000
+
+SECTION "bank49",ROMX,BANK[$49]
+INCBIN "baserom.gbc",$124000,$4000
+
+SECTION "bank4A",ROMX,BANK[$4A]
+INCBIN "baserom.gbc",$128000,$4000
+
+SECTION "bank4B",ROMX,BANK[$4B]
+INCBIN "baserom.gbc",$12C000,$4000
+
+SECTION "bank4C",ROMX,BANK[$4C]
+INCBIN "baserom.gbc",$130000,$4000
+
+SECTION "bank4D",ROMX,BANK[$4D]
+INCBIN "baserom.gbc",$134000,$4000
+
+SECTION "bank4E",ROMX,BANK[$4E]
+INCBIN "baserom.gbc",$138000,$4000
+
+SECTION "bank4F",ROMX,BANK[$4F]
+INCBIN "baserom.gbc",$13C000,$4000
+
+SECTION "bank50",ROMX,BANK[$50]
+INCBIN "baserom.gbc",$140000,$4000
+
+SECTION "bank51",ROMX,BANK[$51]
+INCBIN "baserom.gbc",$144000,$4000
+
+SECTION "bank52",ROMX,BANK[$52]
+INCBIN "baserom.gbc",$148000,$4000
+
+SECTION "bank53",ROMX,BANK[$53]
+INCBIN "baserom.gbc",$14C000,$4000
+
+SECTION "bank54",ROMX,BANK[$54]
+INCBIN "baserom.gbc",$150000,$4000
+
+SECTION "bank55",ROMX,BANK[$55]
+INCBIN "baserom.gbc",$154000,$4000
+
+SECTION "bank56",ROMX,BANK[$56]
+INCBIN "baserom.gbc",$158000,$4000
+
+SECTION "bank57",ROMX,BANK[$57]
+INCBIN "baserom.gbc",$15C000,$4000
+
+SECTION "bank58",ROMX,BANK[$58]
+INCBIN "baserom.gbc",$160000,$4000
+
+SECTION "bank59",ROMX,BANK[$59]
+INCBIN "baserom.gbc",$164000,$4000
+
+SECTION "bank5A",ROMX,BANK[$5A]
+INCBIN "baserom.gbc",$168000,$4000
+
+SECTION "bank5B",ROMX,BANK[$5B]
+INCBIN "baserom.gbc",$16C000,$4000
+
+SECTION "bank5C",ROMX,BANK[$5C]
+INCBIN "baserom.gbc",$170000,$4000
+
+SECTION "bank5D",ROMX,BANK[$5D]
+INCBIN "baserom.gbc",$174000,$4000
+
+SECTION "bank5E",ROMX,BANK[$5E]
+INCBIN "baserom.gbc",$178000,$4000
+
+SECTION "bank5F",ROMX,BANK[$5F]
+INCBIN "baserom.gbc",$17C000,$4000
+
+SECTION "bank60",ROMX,BANK[$60]
+INCBIN "baserom.gbc",$180000,$4000
+
+SECTION "bank61",ROMX,BANK[$61]
+INCBIN "baserom.gbc",$184000,$4000
+
+SECTION "bank62",ROMX,BANK[$62]
+INCBIN "baserom.gbc",$188000,$4000
+
+SECTION "bank63",ROMX,BANK[$63]
+INCBIN "baserom.gbc",$18C000,$4000
+
+SECTION "bank64",ROMX,BANK[$64]
+INCBIN "baserom.gbc",$190000,$4000
+
+SECTION "bank65",ROMX,BANK[$65]
+INCBIN "baserom.gbc",$194000,$4000
+
+SECTION "bank66",ROMX,BANK[$66]
+INCBIN "baserom.gbc",$198000,$4000
+
+SECTION "bank67",ROMX,BANK[$67]
+INCBIN "baserom.gbc",$19C000,$4000
+
+SECTION "bank68",ROMX,BANK[$68]
+INCBIN "baserom.gbc",$1A0000,$4000
+
+SECTION "bank69",ROMX,BANK[$69]
+INCBIN "baserom.gbc",$1A4000,$4000
+
+SECTION "bank6A",ROMX,BANK[$6A]
+INCBIN "baserom.gbc",$1A8000,$4000
+
+SECTION "bank6B",ROMX,BANK[$6B]
+INCBIN "baserom.gbc",$1AC000,$4000
+
+SECTION "bank6C",ROMX,BANK[$6C]
+INCBIN "baserom.gbc",$1B0000,$4000
+
+SECTION "bank6D",ROMX,BANK[$6D]
+INCBIN "baserom.gbc",$1B4000,$4000
+
+SECTION "bank6E",ROMX,BANK[$6E]
+INCBIN "baserom.gbc",$1B8000,$4000
+
+SECTION "bank6F",ROMX,BANK[$6F]
+INCBIN "baserom.gbc",$1BC000,$4000
+
+SECTION "bank70",ROMX,BANK[$70]
+INCBIN "baserom.gbc",$1C0000,$4000
+
+SECTION "bank71",ROMX,BANK[$71]
+INCBIN "baserom.gbc",$1C4000,$4000
+
+SECTION "bank72",ROMX,BANK[$72]
+INCBIN "baserom.gbc",$1C8000,$4000
+
+SECTION "bank73",ROMX,BANK[$73]
+INCBIN "baserom.gbc",$1CC000,$4000
+
+SECTION "bank74",ROMX,BANK[$74]
+INCBIN "baserom.gbc",$1D0000,$4000
+
+SECTION "bank75",ROMX,BANK[$75]
+INCBIN "baserom.gbc",$1D4000,$4000
+
+SECTION "bank76",ROMX,BANK[$76]
+INCBIN "baserom.gbc",$1D8000,$4000
+
+SECTION "bank77",ROMX,BANK[$77]
+INCBIN "baserom.gbc",$1DC000,$4000
+
+SECTION "bank78",ROMX,BANK[$78]
+INCBIN "baserom.gbc",$1E0000,$4000
+
+SECTION "bank79",ROMX,BANK[$79]
+INCBIN "baserom.gbc",$1E4000,$4000
+
+SECTION "bank7A",ROMX,BANK[$7A]
+INCBIN "baserom.gbc",$1E8000,$4000
+
+SECTION "bank7B",ROMX,BANK[$7B]
+INCBIN "baserom.gbc",$1EC000,$4000
+
+SECTION "bank7C",ROMX,BANK[$7C]
+INCBIN "baserom.gbc",$1F0000,$4000
+
+SECTION "bank7D",ROMX,BANK[$7D]
+INCBIN "baserom.gbc",$1F4000,$4000
+
+SECTION "bank7E",ROMX,BANK[$7E]
+INCBIN "baserom.gbc",$1F8000,$4000
+
+SECTION "bank7F",ROMX,BANK[$7F]
+INCBIN "baserom.gbc",$1FC000,$4000