summaryrefslogtreecommitdiff
path: root/tools
diff options
context:
space:
mode:
Diffstat (limited to 'tools')
-rw-r--r--tools/disasm_coverage.py78
-rw-r--r--tools/dump_names.py52
-rw-r--r--tools/dump_text.py298
-rw-r--r--tools/mapreader.py171
-rw-r--r--tools/read_charmap.py42
-rw-r--r--tools/tests/README.txt1
-rw-r--r--tools/tests/charmap.asm293
-rw-r--r--tools/tests/dump_test.cc.txt26
-rw-r--r--tools/tests/dump_test.cc_endless.txt33
-rw-r--r--tools/tests/dump_test.cc_tc.txt27
-rw-r--r--tools/tests/dump_test.cc_tc_endless.txt35
-rw-r--r--tools/tests/dump_test.endless.txt9
-rw-r--r--tools/tests/dump_test.tc.txt4
-rw-r--r--tools/tests/dump_test.tc_endless.txt11
-rw-r--r--tools/tests/dump_test.txt3
-rw-r--r--tools/tests/dump_text_test.binbin0 -> 69 bytes
16 files changed, 997 insertions, 86 deletions
diff --git a/tools/disasm_coverage.py b/tools/disasm_coverage.py
new file mode 100644
index 0000000..0b19ab0
--- /dev/null
+++ b/tools/disasm_coverage.py
@@ -0,0 +1,78 @@
+# #!/usr/bin/env python3
+# coding: utf-8
+
+from __future__ import division
+
+import os
+import sys
+import argparse
+import png
+from mapreader import MapReader
+from colorsys import hls_to_rgb
+
+if __name__ == "__main__":
+ # argument parser
+ ap = argparse.ArgumentParser()
+ ap.add_argument("-o", dest="filename", default="coverage.png")
+ ap.add_argument("-s", dest="statsname", default="coverage.log")
+ ap.add_argument("-m", dest="mapfile", required=True)
+ ap.add_argument("-b", dest="num_banks", required=True, type=lambda x: int(x, 0))
+ args = ap.parse_args()
+
+ bank_mask = 0x3FFF
+ bank_size = 0x4000 # bytes
+ width = 256 # pixels per row
+ bpp = 8 # bytes per pixel
+
+ rom_size = args.num_banks * bank_size # bytes
+ height = (args.num_banks * bank_size + (width * bpp - 1)) // (width * bpp) # pixels
+ rows_per_bank = bank_size // (width * bpp)
+
+ r = MapReader()
+ with open(args.mapfile, 'r') as f:
+ l = f.readlines()
+ r.read_map_data(l)
+
+ hit_data = [[0] * width for _ in range(height)]
+ default_bank_data = {'sections': [], 'used': 0, 'slack': bank_size}
+ for bank in range(args.num_banks):
+ data = r.bank_data['ROM Bank'].get(bank, default_bank_data)
+ for s in data['sections']:
+ beg = (s['beg'] & bank_mask) + bank * bank_size
+ end = (s['end'] & bank_mask) + bank * bank_size
+ y_beg = beg // (width * bpp)
+ x_beg = (beg % (width * bpp)) // bpp
+ y_end = end // (width * bpp)
+ x_end = (end % (width * bpp)) // bpp
+ #print('beg {0} end {1}: {2}/{3} -- {4}/{5}'.format(beg, end, y_beg, x_beg, y_end, x_end))
+ # special case y_beg/x_beg and y_end/x_end
+ if (y_beg == y_end and x_beg == x_end):
+ hit_data[y_beg][x_beg] += end - beg + 1
+ else:
+ hit_data[y_beg][x_beg] += bpp - ((beg % (width * bpp)) - x_beg * bpp)
+ hit_data[y_end][x_end] += ((end % (width * bpp)) - x_end * bpp + 1)
+ # regular case
+ for y in range(y_beg, y_end + 1):
+ x_line_beg = 0 if y_beg != y else x_beg + 1
+ x_line_end = width - 1 if y_end != y else x_end - 1
+ for x in range(x_line_beg, x_line_end + 1):
+ hit_data[y][x] += bpp
+
+ with open(args.statsname, 'w') as stats:
+ # TODO: write stats
+ pass
+
+ png_data = []
+ for i, row in enumerate(hit_data):
+ bank = i // rows_per_bank
+ hue = 0 if bank % 2 else 120
+ row_png_data = ()
+ for col in row:
+ hls = (hue/360.0, 1.0 - (col/bpp * (100 - 15))/100.0, 1.0)
+ rgb = tuple(255 * x for x in hls_to_rgb(*hls))
+ row_png_data += rgb
+ png_data.append(row_png_data)
+
+ with open(args.filename, 'wb') as f:
+ w = png.Writer(width, height)
+ w.write(f, png_data)
diff --git a/tools/dump_names.py b/tools/dump_names.py
index bd2078e..81f357a 100644
--- a/tools/dump_names.py
+++ b/tools/dump_names.py
@@ -1,52 +1,30 @@
#!/usr/bin/env python
import sys, os, io
+from read_charmap import read_charmap
-def parse_int(s):
- s = s.strip()
- if s.startswith('$'):
- return int(s[1:], 16)
- if s.startswith('%'):
- return int(s[1:], 2)
- return int(s)
+def calc_bank(p):
+ return p // 0x4000
-def parse_string(s):
- # assumes strings are literal, no STRCAT() etc
- return s.strip('" ')
+def calc_address(p):
+ b = calc_bank(p)
+ o = b * 0x4000
+ return 0x4000 + p - o
-def strip_comment(s):
- # assumes ";" is not in the charmap
- return s.split(';')[0].rstrip()
+def get_sym_loc(p):
+ b, a = calc_bank(p), calc_address(p)
+ return '%02x:%04x' % (b, a)
def get_project_dir():
script_path = os.path.realpath(__file__)
script_dir = os.path.dirname(script_path)
project_dir = os.path.join(script_dir, '..')
return os.path.normpath(project_dir)
-
-def get_charmap_path():
- project_dir = get_project_dir()
- return os.path.join(project_dir, 'charmap.asm')
-
+
def get_baserom_path():
project_dir = get_project_dir()
return os.path.join(project_dir, 'baserom.gb')
-def read_charmap():
- charmap_path = get_charmap_path()
- charmap = {}
- with io.open(charmap_path, 'r', encoding='utf-8') as f:
- lines = f.readlines()
- for line in lines:
- line = strip_comment(line).lstrip()
- if not line.startswith('charmap '):
- continue
- char, value = line[len('charmap '):].rsplit(',', 1)
- char = parse_string(char)
- value = parse_int(value)
- charmap[value] = char
- return charmap
-
def dump_strings(data):
charmap = read_charmap()
ss = []
@@ -84,9 +62,7 @@ def read_data(bank, address, n):
data.append(v)
return data
-#data = read_data(0x0E, 0x4D90, 64) # TrainerClassNames
-#data = read_data(0x01, 0x6FEC, 255) # ItemNames
-#data = read_data(0x10, 0x52A1, 251) # MoveNames
-data = read_data(0x14, 0x6D75, 251) # PokemonNames
-
+p = 0xfcaaf # Landmarks
+print get_sym_loc(p)
+data = read_data(calc_bank(p), calc_address(p), 45)
dump_strings(data)
diff --git a/tools/dump_text.py b/tools/dump_text.py
index 721d953..057de04 100644
--- a/tools/dump_text.py
+++ b/tools/dump_text.py
@@ -1,50 +1,252 @@
#!/usr/bin/python3
-from sys import argv, stdout
-
-
-char_table = [
- "?", "イ゛", "ヴ", "エ゛", "オ゛", "ガ", "ギ", "グ", "ゲ", "ゴ", "ザ", "ジ", "ズ", "ゼ", "ゾ", "ダ",
- "ヂ", "ヅ", "デ", "ド", "<PLAY_G>", "<0x15>", "<0x16>", "ネ゛", "<JP_18>", "バ", "ビ", "ブ", "ボ", "<NI>", "<TTE>", "<WO>",
- "ィ゛", "あ゛", "<TA!>", "<KOUGEKI>", "<WA>", "<NO>", "が", "ぎ", "ぐ", "げ", "ご", "ざ", "じ", "ず", "ぜ", "ぞ",
- "だ", "ぢ", "づ", "で", "ど", "<ROUTE>", "<WATASHI>", "<KOKO_WA>", "<RED>", "<GREEN>", "ば", "び", "ぶ", "べ", "ぼ", "<ENEMY>",
- "パ", "ピ", "プ", "ポ", "ぱ", "ぴ", "ぷ", "ぺ", "ぽ", "<MOM>", "<GA>", "<_CONT>", "<SCROLL>", "も゜", "<NEXT>", "<LINE>",
- "@", "<PARA>", "<PLAYER>", "<RIVAL>", "#", "<CONT>", "<……>", "<DONE>", "<PROMPT>", "<TARGET>", "<USER>", "<PC>", "<TM>", "<TRAINER>", "<ROCKET>", "<DEXEND>",
- "■", "▲", "☎", "D", "E", "F", "G", "H", "I", "V", "S", "L", "M", ":", "ぃ", "ぅ",
- "「", "」", "『", "』", "・", "<・・・>", "ぁ", "ぇ", "ぉ", "┌", "─", "┐", "│", "└", "┘", " ",
- "ア", "イ", "ウ", "エ", "オ", "カ", "キ", "ク", "ケ", "コ", "サ", "シ", "ス", "セ", "ソ", "タ",
- "チ", "ツ", "テ", "ト", "ナ", "ニ", "ヌ", "ネ", "ノ", "ハ", "ヒ", "フ", "ホ", "マ", "ミ", "ム",
- "メ", "モ", "ヤ", "ユ", "ヨ", "ラ", "ル", "レ", "ロ", "ワ", "ヲ", "ン", "ッ", "ャ", "ュ", "ョ",
- "ィ", "あ", "い", "う", "え", "お", "か", "き", "く", "け", "こ", "さ", "し", "す", "せ", "そ",
- "た", "ち", "つ", "て", "と", "な", "に", "ぬ", "ね", "の", "は", "ひ", "ふ", "へ", "ほ", "ま",
- "み", "む", "め", "も", "や", "ゆ", "よ", "ら", "り", "る", "れ", "ろ", "わ", "を", "ん", "っ",
- "ゃ", "ゅ", "ょ", "ー", "゜", "゛", "?", "!", "。", "ァ", "ゥ", "ェ", "▷", "▶", "▼", "♂",
- "円", "×", ".", "/", "ォ", "♀", "0", "1", "2", "3", "4", "5", "6", "7", "8", "9"
-]
-
-if len(argv) != 4:
- print(f"Usage: {argv[0]} path/to/ROM.gb start_offset end_offset\noffsets are in the form bank:address (both hex), and end_offset is *not* included.")
- exit(1)
-
-
-try:
- start_bank,start_addr = [ int(s, 16) for s in argv[2].split(':') ]
- end_bank, end_addr = [ int(s, 16) for s in argv[3].split(':') ]
- if start_bank != 0:
- start_addr += (start_bank - 1) * 0x4000
- if end_bank != 0:
- end_addr += (end_bank - 1) * 0x4000
-except Error:
- print("Please specify valid offsets (bank:address, both hex)")
- exit(1)
-
-
-with open(argv[1], "rb") as f:
- f.seek(start_addr)
-
- string = ""
- while start_addr < end_addr:
- string += char_table[ int.from_bytes(f.read(1), "little") ]
- start_addr += 1
-
- stdout.buffer.write( f"db \"{string}\"\n".encode('utf-8') )
+import argparse
+import sys
+from read_charmap import read_charmap
+
+charmap = {}
+
+bank_size = 0x4000
+
+textcodes = {
+ '<NEXT>' : {'label': 'next', 'fin': False},
+ '<LINE>' : {'label': 'line', 'fin': False},
+ '<PARA>' : {'label': 'para', 'fin': False},
+ '<CONT>' : {'label': 'cont', 'fin': False},
+ '<DONE>' : {'label': 'done', 'fin': True},
+ '<PROMPT>': {'label': 'prompt', 'fin': True},
+}
+
+# codes that end text control code 0x00
+# True if this code exits control code parsing as well
+end_codes = {
+ '@' : False,
+ '<DONE>' : True,
+ '<PROMPT>': True,
+}
+
+def conv_address(x):
+ if ':' in x:
+ bank, addr = [ int(s, 16) for s in x.split(':') ]
+ if (addr < bank_size and bank != 0):
+ raise argparse.ArgumentTypeError('Illegal ROM bank 0x00 address {0:02X}:{1:04X}. '
+ 'Bank 0x{0:02X} must be 0x00.'.format(bank, addr))
+ elif (addr >= bank_size and bank == 0):
+ raise argparse.ArgumentTypeError('Illegal ROM bank 0x00 address {0:02X}:{1:04X}. '
+ 'Address 0x{1:04X} > 0x{2:04X}.'.format(bank, addr, bank_size - 1))
+ elif (addr >= 2*bank_size):
+ raise argparse.ArgumentTypeError('Illegal ROM bank address {0:02X}:{1:04X}. '
+ 'Address 0x{1:04X} > 0x{2:04X}.'.format(bank, addr, 2*bank_size - 1))
+ return bank * 0x4000 + (addr & 0x3fff)
+ return int(x, 0)
+
+def addr2gb(addr):
+ bank = addr // bank_size
+ offset = addr % bank_size
+ if (bank > 0):
+ offset += bank_size
+ return bank, offset
+
+def transform_char(char, do_transform, is_begin):
+ result = ''
+ if (do_transform and char in textcodes):
+ replace = textcodes[char]
+ if (not is_begin):
+ result += '"\n'
+ result += replace['label']
+ if (not replace['fin']):
+ result += ' "'
+ return replace['fin'], result
+ else:
+ if (is_begin):
+ result += '"'
+ return False, (result + char)
+
+def dump_asm(data):
+
+ result = 'start_asm ; text dumper cannot dump asm\n'
+ result += ' ; Try dumping asm from the following offset and\n'
+ result += ' ; then continue dumping text in control mode.\n'
+ return True, result
+
+def dump_control(data, label, signature):
+
+ lengths = {'b': 1, 'n': 1, 'w': 2}
+
+ required_bytes = sum([lengths.get(c, 0) for c in signature])
+
+ if (data['len'] - data['offset'] < required_bytes):
+ #silently drop split control code
+ return True, ''
+
+ result = ''
+
+ for c in signature:
+ if result != '':
+ result += ', '
+ if c == 'b':
+ byte = data['bytes'][data['offset']]
+ data['offset'] += 1
+ result += '${0:02x}'.format(byte)
+ elif c == 'n':
+ byte = data['bytes'][data['offset']]
+ data['offset'] += 1
+ result += '${0:01x}, ${1:01x}'.format(byte >> 4, byte & 0x0f)
+ elif c == 'w':
+ word = data['bytes'][data['offset']]
+ data['offset'] += 1
+ word |= data['bytes'][data['offset']] << 8
+ data['offset'] += 1
+ result += '${0:02x}'.format(word)
+ else:
+ raise ValueError('Unknown signature char in {0:s}\'s signature "{1:s}".'.format(label, signature))
+
+ if (result != ''):
+ result = ' ' + result
+ return False, label + result
+
+def dump_text(data):
+
+ string = ''
+ exit_control = False
+ done = False
+ while (not done):
+
+ byte = data['bytes'][data['offset']]
+ data['offset'] += 1
+
+ char = charmap[byte]
+ fin, tchar = transform_char(char, data['textmode'], string == '')
+ string += tchar
+ if (char in end_codes):
+ done = True
+ exit_control = end_codes[char]
+ # end string if textmode didn't do it
+ if not data['textmode'] or not fin:
+ string += '"'
+ if (data['offset'] >= data['len']):
+ done = True
+ string += '"'
+
+ return exit_control, string
+
+def dump_text_control(data):
+
+ res, text = dump_text(data)
+ return res, 'text ' + text
+
+control_codes = {
+ 0x00: dump_text_control,
+ 0x01: lambda data: dump_control(data, 'text_from_ram' , 'w' ),
+ 0x02: lambda data: dump_control(data, 'text_bcd' , 'wb' ),
+ 0x03: lambda data: dump_control(data, 'text_move' , 'w' ),
+ 0x04: lambda data: dump_control(data, 'text_box' , 'wbb' ),
+ 0x05: lambda data: dump_control(data, 'text_low' , '' ),
+ 0x06: lambda data: dump_control(data, 'text_waitbutton' , '' ),
+ 0x07: lambda data: dump_control(data, 'text_scroll' , '' ),
+ 0x08: dump_asm,
+ 0x09: lambda data: dump_control(data, 'deciram' , 'wn' ),
+ 0x0A: lambda data: dump_control(data, 'text_exit' , '' ),
+ 0x0B: lambda data: dump_control(data, 'sound_dex_fanfare_50_79' , '' ),
+ 0x0C: lambda data: dump_control(data, 'text_dots' , 'b' ),
+ 0x0D: lambda data: dump_control(data, 'link_wait_button' , '' ),
+ 0x0E: lambda data: dump_control(data, 'sound_dex_fanfare_20_49' , '' ),
+ 0x0F: lambda data: dump_control(data, 'sound_item' , '' ),
+ 0x10: lambda data: dump_control(data, 'sound_caught_mon' , '' ),
+ 0x11: lambda data: dump_control(data, 'sound_dex_fanfare_80_109', '' ),
+ 0x12: lambda data: dump_control(data, 'sound_fanfare' , '' ),
+ 0x13: lambda data: dump_control(data, 'sound_slot_machine_start', '' ),
+ 0x14: lambda data: dump_control(data, 'cry_nidorina' , '' ),
+ 0x15: lambda data: dump_control(data, 'cry_pigeot' , '' ),
+ 0x16: lambda data: dump_control(data, 'cry_jugon' , '' ),
+ 0x50: lambda data: (True, 'text_end\n'),
+}
+
+def print_location(data):
+ return '.loc_{0:04X}:\n'.format(data['offset'])
+
+if __name__ == '__main__':
+ # argument parser
+ ap = argparse.ArgumentParser()
+ ap.add_argument('--cc', dest='ccmode', action='store_true',
+ help='dump in control code mode, implies text code macro mode'
+ )
+ ap.add_argument('--endless', dest='endless', action='store_true',
+ help='continue dumping even if string end text code was reached'
+ )
+ ap.add_argument('--tc', dest='textmode', action='store_true',
+ help='dump text codes (line breaks, prompt etc) as macros instead of inline text'
+ )
+ ap.add_argument('-o', dest='outfile', default=sys.stdout, help='output file name')
+ ap.add_argument('-m', dest='charmap', default='../charmap.asm', help='charmap file name')
+ ap.add_argument('rom', help='path to ROM')
+ ap.add_argument('start', help='start offset', type=conv_address)
+ ap.add_argument('end', help='end offset', type=conv_address, nargs='?')
+
+ args = ap.parse_args()
+ romname = args.rom
+ start_addr = args.start
+ end_addr = start_addr + bank_size if args.end is None else args.end
+ ccmode = args.ccmode
+ endless = args.endless
+ outfile = args.outfile
+ charmap = read_charmap(args.charmap)
+
+ if (end_addr < start_addr):
+ print('End address 0x{0:06X} ({1:02X}:{2:04X}) is before '
+ 'start address 0x{3:06X} ({4:02X}:{5:04X}).'.format(
+ end_addr,
+ *addr2gb(end_addr),
+ start_addr,
+ *addr2gb(start_addr)
+ ),
+ file=sys.stderr
+ )
+ sys.exit(-1)
+
+ bank_addr = start_addr & (~(bank_size - 1))
+ offset = start_addr - bank_addr
+ end_offset = end_addr - bank_addr
+
+ with open(romname, 'rb') as f:
+ f.seek(bank_addr)
+ bank_data = f.read(bank_size)
+
+ data = {'offset': offset, 'bytes': bank_data, 'len': min(end_offset, len(bank_data)), 'textmode': args.textmode}
+
+ with open(outfile, 'wb') if outfile != sys.stdout else outfile.buffer as f:
+ string = print_location(data)
+ while(data['offset'] < data['len']):
+ if (not ccmode):
+ # dumb mode
+ _, text = dump_text(data)
+ # start with db unless starting with a control code
+ # in textmode
+ if text[0] == '"' and data['textmode']:
+ string += '\tdb '
+ elif text[0] == '"':
+ string += '\tdb '
+ string += text.replace('\n', '\n\t')
+ string += '\n{0:s}'.format(print_location(data))
+ if (not endless):
+ break
+ else:
+ # control code mode
+ control_byte = data['bytes'][data['offset']]
+ data['offset'] += 1
+
+ if (control_byte in control_codes):
+ res, text = control_codes[control_byte](data)
+ string += '\t' + text.replace('\n', '\n\t')
+ string += '\n'
+ if (res):
+ string += print_location(data)
+ # exit out of control code parsing
+ if (res and not endless):
+ break
+ else:
+ print('Encountered unknown control code 0x{0:02X}. Abort...'.format(control_byte), file=sys.stderr)
+ break
+
+ f.write(string.encode('utf-8'))
+ f.close()
diff --git a/tools/mapreader.py b/tools/mapreader.py
new file mode 100644
index 0000000..41ad935
--- /dev/null
+++ b/tools/mapreader.py
@@ -0,0 +1,171 @@
+# #!/usr/bin/env python3
+# coding: utf-8
+
+import re
+
+class MapReader:
+
+ # {'ROM Bank': { 0: { 'sections': [ { 'beg': 1234,
+ # 'end': 5678,
+ # 'name': 'Section001',
+ # 'symbols': [ { 'symbol': 'Function1234',
+ # 'address: 1234,
+ # },
+ # ]
+ # },
+ # ],
+ # 'used': 1234,
+ # 'slack': 4567,
+ # },
+ # },
+ # 'OAM': { 'sections': [ { 'beg': 1234,
+ # 'end': 5678,
+ # 'name': 'Section002',
+ # 'symbols': [ { 'symbol': 'Data1234',
+ # 'address: 1234,
+ # },
+ # ]
+ # },
+ # ],
+ # 'used': 1234,
+ # 'slack': 4567,
+ # },
+ # }
+ #
+ bank_data = {}
+
+ bank_types = {
+ 'HRAM' : { 'size': 0x80, 'banked': False, },
+ 'OAM' : { 'size': 0xA0, 'banked': False, },
+ 'ROM Bank' : { 'size': 0x4000, 'banked': True, },
+ 'SRAM Bank': { 'size': 0x2000, 'banked': True, },
+ 'VRAM Bank': { 'size': 0x1000, 'banked': True, },
+ 'WRAM Bank': { 'size': 0x2000, 'banked': True, },
+ }
+
+ # FSM states
+ INIT, BANK, SECTION = range(3)
+
+ # $506D-$519A ($012E bytes) ["Type Matchups"]
+ section_header_regex = re.compile('\$([0-9A-Fa-f]{4})-\$([0-9A-Fa-f]{4}) \(.*\) \["(.*)"\]')
+ # $506D = TypeMatchups
+ section_data_regex = re.compile('\$([0-9A-Fa-f]{4}) = (.*)')
+ # $3ED2 bytes
+ slack_regex = re.compile('\$([0-9A-Fa-f]{4}) bytes')
+
+ def __init__(self, *args, **kwargs):
+ self.__dict__.update(kwargs)
+
+ def _parse_init(self, line):
+
+ line = line.split(':', 1)[0]
+ parts = line.split(' #', 1)
+
+ if (parts[0] in self.bank_types):
+ self._cur_bank_name = parts[0]
+ self._cur_bank_type = self.bank_types[self._cur_bank_name]
+ if (self._cur_bank_type['banked'] and len(parts) > 1):
+ parts[1] = parts[1].split(':', 1)[0]
+ parts[1] = parts[1].split(' ', 1)[0]
+ self._cur_bank = int(parts[1], 10)
+ if self._cur_bank_name not in self.bank_data:
+ self.bank_data[self._cur_bank_name] = {}
+ if self._cur_bank_type['banked']:
+ if self._cur_bank not in self.bank_data[self._cur_bank_name]:
+ self.bank_data[self._cur_bank_name][self._cur_bank] = {}
+ self._cur_data = self.bank_data[self._cur_bank_name][self._cur_bank]
+ else:
+ self._cur_data = self.bank_data[self._cur_bank_name]
+
+ if ({} == self._cur_data):
+ self._cur_data['sections'] = []
+ self._cur_data['used'] = 0
+ self._cur_data['slack'] = self._cur_bank_type['size']
+ return True
+
+ return False
+
+ def _parse_section_header(self, header):
+
+ section_data = self.section_header_regex.match(header)
+ if section_data is not None:
+ beg = int(section_data.group(1), 16)
+ end = int(section_data.group(2), 16)
+ name = section_data.group(3)
+ self._cur_section = {'beg': beg, 'end': end, 'name': name, 'symbols': []}
+ self._cur_data['sections'].append(self._cur_section)
+ return True
+ return False
+
+ def _parse_slack(self, data):
+
+ slack_data = self.slack_regex.match(data)
+ slack_bytes = int(slack_data.group(1), 16)
+ self._cur_data['slack'] = slack_bytes
+
+ used_bytes = 0
+
+ for s in self._cur_data['sections']:
+ used_bytes += s['end'] - s['beg'] + 1
+
+ self._cur_data['used'] = used_bytes
+
+ def read_map_data(self, map):
+
+ if type(map) is str:
+ map = map.split('\n')
+
+ self._state = MapReader.INIT
+ self._cur_bank_name = ''
+ self._cur_bank_type = {}
+ self._cur_bank = 0
+ self._cur_data = {}
+
+ for line in map:
+
+ line = line.rstrip()
+ if (MapReader.INIT == self._state):
+
+ if (self._parse_init(line)):
+ self._state = MapReader.BANK
+
+ elif (MapReader.BANK == self._state or MapReader.SECTION == self._state):
+
+ if ('' == line):
+ self._state = MapReader.INIT
+ else:
+
+ line = line.lstrip()
+ parts = line.split(': ', 1)
+
+ if (MapReader.SECTION == self._state):
+ section_data = self.section_data_regex.match(parts[0])
+ if section_data is not None:
+ address = int(section_data.group(1), 16)
+ name = section_data.group(2)
+ self._cur_section['symbols'].append({'name': name, 'address': address})
+ continue
+
+ if ('SECTION' == parts[0]):
+ if (self._parse_section_header(parts[1])):
+ self._state = MapReader.SECTION
+ elif ('SLACK' == parts[0]):
+ self._parse_slack(parts[1])
+ self._state = MapReader.INIT
+ elif ('EMPTY' == parts[0]):
+ self._cur_data = {'sections': [], 'used': 0, 'slack': self._cur_bank_type['size']}
+ self._state = MapReader.INIT
+
+ else:
+ pass
+
+ for k, v in self.bank_data.items():
+ if (self.bank_types[k]['banked']):
+ for _, vv in v.items():
+ vv['sections'].sort(key=lambda x: x['beg'])
+ for vvv in vv['sections']:
+ vvv['symbols'].sort(key=lambda x: x['address'])
+ else:
+ v['sections'].sort(key=lambda x: x['beg'])
+ for vv in v['sections']:
+ vv['symbols'].sort(key=lambda x: x['address']) \ No newline at end of file
diff --git a/tools/read_charmap.py b/tools/read_charmap.py
new file mode 100644
index 0000000..af290c3
--- /dev/null
+++ b/tools/read_charmap.py
@@ -0,0 +1,42 @@
+import os, io
+from re import compile
+from sys import stderr
+
+charmap_regex = compile('[ \t]*charmap[ \t]+"(.*?)",[ \t]*(\$[0-9A-Fa-f]{2}|%[01]{8}|[0-9]{3})')
+# A charmap line is
+# [ \t]* - zero or more space chars
+# charmap - literal charmap
+# [ \t]+ - one or more space chars
+# "(.*?)" - a lazily-matched text identifier in quotes
+# , - literal comma
+# [ \t]* - zero or more space chars
+# ( - either of
+# \$[0-9A-Fa-f]{2} - two hexadecimal digits preceeded by literal $
+# %[01]{8} - eight dual digits preceeded by literal %
+# [0-9]{3} - three decimal digits
+# )
+
+def parse_int(s):
+ # assumes integers are literal; no +-*/, etc
+ s = s.strip()
+ if s.startswith('$'):
+ return int(s[1:], 16)
+ if s.startswith('%'):
+ return int(s[1:], 2)
+ return int(s)
+
+def read_charmap(charmap_path):
+ charmap = {}
+ with io.open(charmap_path, 'r', encoding='utf-8') as f:
+ lines = f.readlines()
+ for line in lines:
+ m = charmap_regex.match(line)
+ if m is None:
+ continue
+ char = m.group(1)
+ value = parse_int(m.group(2))
+ if value in charmap:
+ print('Value {0:s} already in charmap, dropping it in favor of first charmap entry'.format(m.group(2)))
+ continue
+ charmap[value] = char
+ return charmap
diff --git a/tools/tests/README.txt b/tools/tests/README.txt
new file mode 100644
index 0000000..750482e
--- /dev/null
+++ b/tools/tests/README.txt
@@ -0,0 +1 @@
+python dump_text.py -o dump_test.txt -m charmap.asm dump_text_test.bin 00:0000
diff --git a/tools/tests/charmap.asm b/tools/tests/charmap.asm
new file mode 100644
index 0000000..228bb6b
--- /dev/null
+++ b/tools/tests/charmap.asm
@@ -0,0 +1,293 @@
+ charmap "<NULL>", $00
+
+ charmap "イ゛", $01
+ charmap "ヴ", $02
+ charmap "エ゛", $03
+ charmap "オ゛", $04
+
+ charmap "ガ", $05
+ charmap "ギ", $06
+ charmap "グ", $07
+ charmap "ゲ", $08
+ charmap "ゴ", $09
+ charmap "ザ", $0a
+ charmap "ジ", $0b
+ charmap "ズ", $0c
+ charmap "ゼ", $0d
+ charmap "ゾ", $0e
+ charmap "ダ", $0f
+ charmap "ヂ", $10
+ charmap "ヅ", $11
+ charmap "デ", $12
+ charmap "ド", $13
+
+ charmap "<PLAY_G>", $14 ; "<PLAYER>くん" or "<PLAYER>ちゃん"
+
+ charmap "<15>", $15 ; nothing
+ charmap "<16>", $16 ; nothing
+
+ charmap "ネ゛", $17
+ charmap "ノ゛", $18
+
+ charmap "バ", $19
+ charmap "ビ", $1a
+ charmap "ブ", $1b
+ charmap "ボ", $1c
+
+ charmap "<NI>", $1d ; "に "
+ charmap "<TTE>", $1e ; "って"
+ charmap "<WO>", $1f ; "を "
+
+ charmap "ィ゛", $20
+ charmap "あ゛", $21
+
+ charmap "<TA!>", $22 ; "た!"
+ charmap "<KOUGEKI>", $23 ; "こうげき"
+ charmap "<WA>", $24 ; "は "
+ charmap "<NO>", $25 ; "の "
+
+ charmap "が", $26
+ charmap "ぎ", $27
+ charmap "ぐ", $28
+ charmap "げ", $29
+ charmap "ご", $2a
+ charmap "ざ", $2b
+ charmap "じ", $2c
+ charmap "ず", $2d
+ charmap "ぜ", $2e
+ charmap "ぞ", $2f
+ charmap "だ", $30
+ charmap "ぢ", $31
+ charmap "づ", $32
+ charmap "で", $33
+ charmap "ど", $34
+
+ charmap "<ROUTE>", $35 ; "ばん どうろ"
+ charmap "<WATASHI>", $36 ; "わたし"
+ charmap "<KOKO_WA>", $37 ; "ここは"
+ charmap "<RED>", $38 ; wRedsName
+ charmap "<GREEN>", $39 ; wGreensName
+
+ charmap "ば", $3a
+ charmap "び", $3b
+ charmap "ぶ", $3c
+ charmap "べ", $3d
+ charmap "ぼ", $3e
+
+ charmap "<ENEMY>", $3f
+
+ charmap "パ", $40
+ charmap "ピ", $41
+ charmap "プ", $42
+ charmap "ポ", $43
+ charmap "ぱ", $44
+ charmap "ぴ", $45
+ charmap "ぷ", $46
+ charmap "ぺ", $47
+ charmap "ぽ", $48
+
+ charmap "<MOM>", $49 ; wMomsName
+ charmap "<GA>", $4a ; "が "
+ charmap "<_CONT>", $4b ; implements "<CONT>"
+ charmap "<SCROLL>", $4c
+
+ charmap "も゜", $4d
+
+ charmap "<NEXT>", $4e
+ charmap "<LINE>", $4f
+ charmap "@", $50 ; string terminator
+ charmap "<PARA>", $51
+ charmap "<PLAYER>", $52 ; wPlayerName
+ charmap "<RIVAL>", $53 ; wRivalName
+ charmap "#", $54 ; "POKé"
+ charmap "<CONT>", $55
+ charmap "<……>", $56 ; "……"
+ charmap "<DONE>", $57
+ charmap "<PROMPT>", $58
+ charmap "<TARGET>", $59
+ charmap "<USER>", $5a
+ charmap "<PC>", $5b ; "PC"
+ charmap "<TM>", $5c ; "TM"
+ charmap "<TRAINER>", $5d ; "TRAINER"
+ charmap "<ROCKET>", $5e ; "ROCKET"
+ charmap "<DEXEND>", $5f
+
+ charmap "■", $60
+ charmap "▲", $61
+ charmap "☎", $62
+
+ charmap "D", $63
+ charmap "E", $64
+ charmap "F", $65
+ charmap "G", $66
+ charmap "H", $67
+ charmap "I", $68
+ charmap "V", $69
+ charmap "S", $6a
+ charmap "L", $6b
+ charmap "M", $6c
+
+ charmap ":", $6d
+
+ charmap "ぃ", $6e
+ charmap "ぅ", $6f
+
+ charmap "「", $70
+ charmap "」", $71
+ charmap "『", $72
+ charmap "』", $73
+ charmap "・", $74
+ charmap "…", $75
+
+ charmap "ぁ", $76
+ charmap "ぇ", $77
+ charmap "ぉ", $78
+
+ charmap "┌", $79
+ charmap "─", $7a
+ charmap "┐", $7b
+ charmap "│", $7c
+ charmap "└", $7d
+ charmap "┘", $7e
+
+ charmap " ", $7f
+
+ charmap "ア", $80
+ charmap "イ", $81
+ charmap "ウ", $82
+ charmap "エ", $83
+ charmap "オ", $84
+ charmap "カ", $85
+ charmap "キ", $86
+ charmap "ク", $87
+ charmap "ケ", $88
+ charmap "コ", $89
+ charmap "サ", $8a
+ charmap "シ", $8b
+ charmap "ス", $8c
+ charmap "セ", $8d
+ charmap "ソ", $8e
+ charmap "タ", $8f
+ charmap "チ", $90
+ charmap "ツ", $91
+ charmap "テ", $92
+ charmap "ト", $93
+ charmap "ナ", $94
+ charmap "ニ", $95
+ charmap "ヌ", $96
+ charmap "ネ", $97
+ charmap "ノ", $98
+ charmap "ハ", $99
+ charmap "ヒ", $9a
+ charmap "フ", $9b
+ charmap "ホ", $9c
+ charmap "マ", $9d
+ charmap "ミ", $9e
+ charmap "ム", $9f
+ charmap "メ", $a0
+ charmap "モ", $a1
+ charmap "ヤ", $a2
+ charmap "ユ", $a3
+ charmap "ヨ", $a4
+ charmap "ラ", $a5
+ charmap "ル", $a6
+ charmap "レ", $a7
+ charmap "ロ", $a8
+ charmap "ワ", $a9
+ charmap "ヲ", $aa
+ charmap "ン", $ab
+
+ charmap "ッ", $ac
+ charmap "ャ", $ad
+ charmap "ュ", $ae
+ charmap "ョ", $af
+ charmap "ィ", $b0
+
+ charmap "あ", $b1
+ charmap "い", $b2
+ charmap "う", $b3
+ charmap "え", $b4
+ charmap "お", $b5
+ charmap "か", $b6
+ charmap "き", $b7
+ charmap "く", $b8
+ charmap "け", $b9
+ charmap "こ", $ba
+ charmap "さ", $bb
+ charmap "し", $bc
+ charmap "す", $bd
+ charmap "せ", $be
+ charmap "そ", $bf
+ charmap "た", $c0
+ charmap "ち", $c1
+ charmap "つ", $c2
+ charmap "て", $c3
+ charmap "と", $c4
+ charmap "な", $c5
+ charmap "に", $c6
+ charmap "ぬ", $c7
+ charmap "ね", $c8
+ charmap "の", $c9
+ charmap "は", $ca
+ charmap "ひ", $cb
+ charmap "ふ", $cc
+ charmap "へ", $cd
+ charmap "ほ", $ce
+ charmap "ま", $cf
+ charmap "み", $d0
+ charmap "む", $d1
+ charmap "め", $d2
+ charmap "も", $d3
+ charmap "や", $d4
+ charmap "ゆ", $d5
+ charmap "よ", $d6
+ charmap "ら", $d7
+ charmap "り", $d8
+ charmap "る", $d9
+ charmap "れ", $da
+ charmap "ろ", $db
+ charmap "わ", $dc
+ charmap "を", $dd
+ charmap "ん", $de
+
+ charmap "っ", $df
+ charmap "ゃ", $e0
+ charmap "ゅ", $e1
+ charmap "ょ", $e2
+
+ charmap "ー", $e3
+
+ charmap "゚", $e4
+ charmap "゙", $e5
+
+ charmap "?", $e6
+ charmap "!", $e7
+ charmap "。", $e8
+
+ charmap "ァ", $e9
+ charmap "ゥ", $ea
+ charmap "ェ", $eb
+
+ charmap "▷", $ec
+ charmap "▶", $ed
+ charmap "▲", $ed
+ charmap "▼", $ee
+ charmap "♂", $ef
+ charmap "円", $f0
+ charmap "×", $f1
+ charmap ".", $f2
+ charmap "/", $f3
+
+ charmap "ォ", $f4
+
+ charmap "♀", $f5
+ charmap "0", $f6
+ charmap "1", $f7
+ charmap "2", $f8
+ charmap "3", $f9
+ charmap "4", $fa
+ charmap "5", $fb
+ charmap "6", $fc
+ charmap "7", $fd
+ charmap "8", $fe
+ charmap "9", $ff
diff --git a/tools/tests/dump_test.cc.txt b/tools/tests/dump_test.cc.txt
new file mode 100644
index 0000000..4dfd14a
--- /dev/null
+++ b/tools/tests/dump_test.cc.txt
@@ -0,0 +1,26 @@
+.loc_0000:
+ text "ほんとにりセットしますか?<LINE>@"
+ text_from_ram $ce33
+ text_bcd $ce34, $78
+ text_move $ce35
+ text_box $cc36, $08, $12
+ text_low
+ text_waitbutton
+ text_scroll
+ deciram $cc37, $6, $5
+ text_exit
+ sound_dex_fanfare_50_79
+ text_dots $14
+ link_wait_button
+ sound_dex_fanfare_20_49
+ sound_item
+ sound_caught_mon
+ sound_dex_fanfare_80_109
+ sound_fanfare
+ sound_slot_machine_start
+ cry_nidorina
+ cry_pigeot
+ cry_jugon
+ text_end
+
+.loc_0035:
diff --git a/tools/tests/dump_test.cc_endless.txt b/tools/tests/dump_test.cc_endless.txt
new file mode 100644
index 0000000..5c59830
--- /dev/null
+++ b/tools/tests/dump_test.cc_endless.txt
@@ -0,0 +1,33 @@
+.loc_0000:
+ text "ほんとにりセットしますか?<LINE>@"
+ text_from_ram $ce33
+ text_bcd $ce34, $78
+ text_move $ce35
+ text_box $cc36, $08, $12
+ text_low
+ text_waitbutton
+ text_scroll
+ deciram $cc37, $6, $5
+ text_exit
+ sound_dex_fanfare_50_79
+ text_dots $14
+ link_wait_button
+ sound_dex_fanfare_20_49
+ sound_item
+ sound_caught_mon
+ sound_dex_fanfare_80_109
+ sound_fanfare
+ sound_slot_machine_start
+ cry_nidorina
+ cry_pigeot
+ cry_jugon
+ text_end
+
+.loc_0035:
+ text "ほんとにりセットしますか?<DONE>"
+.loc_0044:
+ start_asm ; text dumper cannot dump asm
+ ; Try dumping asm from the following offset and
+ ; then continue dumping text in control mode.
+
+.loc_0045:
diff --git a/tools/tests/dump_test.cc_tc.txt b/tools/tests/dump_test.cc_tc.txt
new file mode 100644
index 0000000..e49a010
--- /dev/null
+++ b/tools/tests/dump_test.cc_tc.txt
@@ -0,0 +1,27 @@
+.loc_0000:
+ text "ほんとにりセットしますか?"
+ line "@"
+ text_from_ram $ce33
+ text_bcd $ce34, $78
+ text_move $ce35
+ text_box $cc36, $08, $12
+ text_low
+ text_waitbutton
+ text_scroll
+ deciram $cc37, $6, $5
+ text_exit
+ sound_dex_fanfare_50_79
+ text_dots $14
+ link_wait_button
+ sound_dex_fanfare_20_49
+ sound_item
+ sound_caught_mon
+ sound_dex_fanfare_80_109
+ sound_fanfare
+ sound_slot_machine_start
+ cry_nidorina
+ cry_pigeot
+ cry_jugon
+ text_end
+
+.loc_0035:
diff --git a/tools/tests/dump_test.cc_tc_endless.txt b/tools/tests/dump_test.cc_tc_endless.txt
new file mode 100644
index 0000000..dbf8e8b
--- /dev/null
+++ b/tools/tests/dump_test.cc_tc_endless.txt
@@ -0,0 +1,35 @@
+.loc_0000:
+ text "ほんとにりセットしますか?"
+ line "@"
+ text_from_ram $ce33
+ text_bcd $ce34, $78
+ text_move $ce35
+ text_box $cc36, $08, $12
+ text_low
+ text_waitbutton
+ text_scroll
+ deciram $cc37, $6, $5
+ text_exit
+ sound_dex_fanfare_50_79
+ text_dots $14
+ link_wait_button
+ sound_dex_fanfare_20_49
+ sound_item
+ sound_caught_mon
+ sound_dex_fanfare_80_109
+ sound_fanfare
+ sound_slot_machine_start
+ cry_nidorina
+ cry_pigeot
+ cry_jugon
+ text_end
+
+.loc_0035:
+ text "ほんとにりセットしますか?"
+ done
+.loc_0044:
+ start_asm ; text dumper cannot dump asm
+ ; Try dumping asm from the following offset and
+ ; then continue dumping text in control mode.
+
+.loc_0045:
diff --git a/tools/tests/dump_test.endless.txt b/tools/tests/dump_test.endless.txt
new file mode 100644
index 0000000..fbbdeef
--- /dev/null
+++ b/tools/tests/dump_test.endless.txt
@@ -0,0 +1,9 @@
+.loc_0000:
+ db "<NULL>ほんとにりセットしますか?<LINE>@"
+.loc_0010:
+ db "イ゛でほヴどほぉエ゛<ROUTE>ほオ゛<WATASHI>ふゲデガギグゴ<KOKO_WA>ふFザジズ<PLAY_G>ゼゾダヂヅデド<PLAY_G><15><16>@"
+.loc_0035:
+ db "<NULL>ほんとにりセットしますか?<DONE>"
+.loc_0044:
+ db "ゲ"
+.loc_0045:
diff --git a/tools/tests/dump_test.tc.txt b/tools/tests/dump_test.tc.txt
new file mode 100644
index 0000000..82c276d
--- /dev/null
+++ b/tools/tests/dump_test.tc.txt
@@ -0,0 +1,4 @@
+.loc_0000:
+ db "<NULL>ほんとにりセットしますか?"
+ line "@"
+.loc_0010:
diff --git a/tools/tests/dump_test.tc_endless.txt b/tools/tests/dump_test.tc_endless.txt
new file mode 100644
index 0000000..37bdda0
--- /dev/null
+++ b/tools/tests/dump_test.tc_endless.txt
@@ -0,0 +1,11 @@
+.loc_0000:
+ db "<NULL>ほんとにりセットしますか?"
+ line "@"
+.loc_0010:
+ db "イ゛でほヴどほぉエ゛<ROUTE>ほオ゛<WATASHI>ふゲデガギグゴ<KOKO_WA>ふFザジズ<PLAY_G>ゼゾダヂヅデド<PLAY_G><15><16>@"
+.loc_0035:
+ db "<NULL>ほんとにりセットしますか?"
+ done
+.loc_0044:
+ db "ゲ"
+.loc_0045:
diff --git a/tools/tests/dump_test.txt b/tools/tests/dump_test.txt
new file mode 100644
index 0000000..a181744
--- /dev/null
+++ b/tools/tests/dump_test.txt
@@ -0,0 +1,3 @@
+.loc_0000:
+ db "<NULL>ほんとにりセットしますか?<LINE>@"
+.loc_0010:
diff --git a/tools/tests/dump_text_test.bin b/tools/tests/dump_text_test.bin
new file mode 100644
index 0000000..778a446
--- /dev/null
+++ b/tools/tests/dump_text_test.bin
Binary files differ