diff options
author | dannye <corrnondacqb@yahoo.com> | 2015-09-27 10:22:58 -0500 |
---|---|---|
committer | dannye <corrnondacqb@yahoo.com> | 2015-10-10 10:01:23 -0500 |
commit | d867415edd72d2956b53aea66bae93566f3b984a (patch) | |
tree | 4d1345aa9ffbad480279fd480a62a1ae12c8aff7 |
Initial commit
-rw-r--r-- | .gitignore | 21 | ||||
-rw-r--r-- | Makefile | 37 | ||||
-rw-r--r-- | README.md | 13 | ||||
-rw-r--r-- | extras/configuration.py | 57 | ||||
-rw-r--r-- | extras/gbz80disasm.py | 947 | ||||
-rw-r--r-- | extras/gfx.py | 750 | ||||
-rw-r--r-- | extras/labels.py | 213 | ||||
-rw-r--r-- | extras/scan_includes.py | 65 | ||||
-rw-r--r-- | extras/wram.py | 313 | ||||
-rw-r--r-- | src/main.asm | 383 |
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 |