shithub: pokecrystal

Download patch

ref: f5a6c18b89d71ea3cad792f2f2f2af49a1505172
parent: 89a76e887d9f40ecb8008fe503fa972567f38cb1
author: Bryan Bishop <kanzure@gmail.com>
date: Mon Mar 5 19:15:35 EST 2012

python tooling

--- /dev/null
+++ b/extras/crystal.py
@@ -1,0 +1,1301 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+#author: Bryan Bishop <kanzure@gmail.com>
+#date: 2012-03-04
+#utilities to help disassemble pokémon crystal
+import sys
+from copy import copy
+
+#table of pointers to map groups
+#each map group contains some number of map headers
+map_group_pointer_table = 0x94000
+map_group_count = 26
+map_group_offsets = []
+map_header_byte_size = 9
+second_map_header_byte_size = 12
+
+#event segment sizes
+warp_byte_size = 5
+trigger_byte_size = 8
+signpost_byte_size = 5
+people_event_byte_size = 13
+
+#a message to show with NotImplementedErrors
+bryan_message = "bryan hasn't got to this yet"
+
+#this is straight out of ../textpre.py because i'm lazy
+#see jap_chars for overrides if you are in japanese mode?
+chars = {
+    0x50: "@",
+    0x54: "#",
+    0x75: "…",
+    
+    0x79: "┌",
+    0x7A: "─",
+    0x7B: "┐",
+    0x7C: "│",
+    0x7D: "└",
+    0x7E: "┘",
+    
+    0x74: "№",
+    
+    0x7F: " ",
+    0x80: "A",
+    0x81: "B",
+    0x82: "C",
+    0x83: "D",
+    0x84: "E",
+    0x85: "F",
+    0x86: "G",
+    0x87: "H",
+    0x88: "I",
+    0x89: "J",
+    0x8A: "K",
+    0x8B: "L",
+    0x8C: "M",
+    0x8D: "N",
+    0x8E: "O",
+    0x8F: "P",
+    0x90: "Q",
+    0x91: "R",
+    0x92: "S",
+    0x93: "T",
+    0x94: "U",
+    0x95: "V",
+    0x96: "W",
+    0x97: "X",
+    0x98: "Y",
+    0x99: "Z",
+    0x9A: "(",
+    0x9B: ")",
+    0x9C: ":",
+    0x9D: ";",
+    0x9E: "[",
+    0x9F: "]",
+    0xA0: "a",
+    0xA1: "b",
+    0xA2: "c",
+    0xA3: "d",
+    0xA4: "e",
+    0xA5: "f",
+    0xA6: "g",
+    0xA7: "h",
+    0xA8: "i",
+    0xA9: "j",
+    0xAA: "k",
+    0xAB: "l",
+    0xAC: "m",
+    0xAD: "n",
+    0xAE: "o",
+    0xAF: "p",
+    0xB0: "q",
+    0xB1: "r",
+    0xB2: "s",
+    0xB3: "t",
+    0xB4: "u",
+    0xB5: "v",
+    0xB6: "w",
+    0xB7: "x",
+    0xB8: "y",
+    0xB9: "z",
+    0xC0: "Ä",
+    0xC1: "Ö",
+    0xC2: "Ü",
+    0xC3: "ä",
+    0xC4: "ö",
+    0xC5: "ü",
+    0xD0: "'d",
+    0xD1: "'l",
+    0xD2: "'m",
+    0xD3: "'r",
+    0xD4: "'s",
+    0xD5: "'t",
+    0xD6: "'v",
+    0xE0: "'",
+    0xE3: "-",
+    0xE6: "?",
+    0xE7: "!",
+    0xE8: ".",
+    0xE9: "&",
+    0xEA: "é",
+    0xEB: "→",
+    0xEF: "♂",
+    0xF0: "¥",
+    0xF1: "×",
+    0xF3: "/",
+    0xF4: ",",
+    0xF5: "♀",
+    0xF6: "0",
+    0xF7: "1",
+    0xF8: "2",
+    0xF9: "3",
+    0xFA: "4",
+    0xFB: "5",
+    0xFC: "6",
+    0xFD: "7",
+    0xFE: "8",
+    0xFF: "9",
+}
+
+#override whatever defaults for japanese symbols
+jap_chars = copy(chars)
+jap_chars.update({
+    0x05: "ガ",
+    0x06: "ギ",
+    0x07: "グ",
+    0x08: "ゲ",
+    0x09: "ゴ",
+    0x0A: "ザ",
+    0x0B: "ジ",
+    0x0C: "ズ",
+    0x0D: "ゼ",
+    0x0E: "ゾ",
+    0x0F: "ダ",
+    0x10: "ヂ",
+    0x11: "ヅ",
+    0x12: "デ",
+    0x13: "ド",
+    0x19: "バ",
+    0x1A: "ビ",
+    0x1B: "ブ",
+    0x1C: "ボ",
+    0x26: "が",
+    0x27: "ぎ",
+    0x28: "ぐ",
+    0x29: "げ",
+    0x2A: "ご",
+    0x2B: "ざ",
+    0x2C: "じ",
+    0x2D: "ず",
+    0x2E: "ぜ",
+    0x2F: "ぞ",
+    0x30: "だ",
+    0x31: "ぢ",
+    0x32: "づ",
+    0x33: "で",
+    0x34: "ど",
+    0x3A: "ば",
+    0x3B: "び",
+    0x3C: "ぶ",
+    0x3D: "べ",
+    0x3E: "ぼ",
+    0x40: "パ",
+    0x41: "ピ",
+    0x42: "プ",
+    0x43: "ポ",
+    0x44: "ぱ",
+    0x45: "ぴ",
+    0x46: "ぷ",
+    0x47: "ぺ",
+    0x48: "ぽ",
+    0x80: "ア",
+    0x81: "イ",
+    0x82: "ウ",
+    0x83: "エ",
+    0x84: "ォ",
+    0x85: "カ",
+    0x86: "キ",
+    0x87: "ク",
+    0x88: "ケ",
+    0x89: "コ",
+    0x8A: "サ",
+    0x8B: "シ",
+    0x8C: "ス",
+    0x8D: "セ",
+    0x8E: "ソ",
+    0x8F: "タ",
+    0x90: "チ",
+    0x91: "ツ",
+    0x92: "テ",
+    0x93: "ト",
+    0x94: "ナ",
+    0x95: "ニ",
+    0x96: "ヌ",
+    0x97: "ネ",
+    0x98: "ノ",
+    0x99: "ハ",
+    0x9A: "ヒ",
+    0x9B: "フ",
+    0x9C: "ホ",
+    0x9D: "マ",
+    0x9E: "ミ",
+    0x9F: "ム",
+    0xA0: "メ",
+    0xA1: "モ",
+    0xA2: "ヤ",
+    0xA3: "ユ",
+    0xA4: "ヨ",
+    0xA5: "ラ",
+    0xA6: "ル",
+    0xA7: "レ",
+    0xA8: "ロ",
+    0xA9: "ワ",
+    0xAA: "ヲ",
+    0xAB: "ン",
+    0xAC: "ッ",
+    0xAD: "ャ",
+    0xAE: "ュ",
+    0xAF: "ョ",
+    0xB0: "ィ",
+    0xB1: "あ",
+    0xB2: "い",
+    0xB3: "う",
+    0xB4: "え",
+    0xB5: "お",
+    0xB6: "か",
+    0xB7: "き",
+    0xB8: "く",
+    0xB9: "け",
+    0xBA: "こ",
+    0xBB: "さ",
+    0xBC: "し",
+    0xBD: "す",
+    0xBE: "せ",
+    0xBF: "そ",
+    0xC0: "た",
+    0xC1: "ち",
+    0xC2: "つ",
+    0xC3: "て",
+    0xC4: "と",
+    0xC5: "な",
+    0xC6: "に",
+    0xC7: "ぬ",
+    0xC8: "ね",
+    0xC9: "の",
+    0xCA: "は",
+    0xCB: "ひ",
+    0xCC: "ふ",
+    0xCD: "へ",
+    0xCE: "ほ",
+    0xCF: "ま",
+    0xD0: "み",
+    0xD1: "む",
+    0xD2: "め",
+    0xD3: "も",
+    0xD4: "や",
+    0xD5: "ゆ",
+    0xD6: "よ",
+    0xD7: "ら",
+    0xD8: "り",
+    0xD9: "る",
+    0xDA: "れ",
+    0xDB: "ろ",
+    0xDC: "わ",
+    0xDD: "を",
+    0xDE: "ん",
+    0xDF: "っ",
+    0xE0: "ゃ",
+    0xE1: "ゅ",
+    0xE2: "ょ",
+    0xE3: "ー",
+})
+
+#some of the japanese characters can probably fit into the english table
+#without overriding any of the other mappings.
+for key, value in jap_chars.items():
+    if key not in chars.keys():
+        chars[key] = value
+
+def map_name_cleaner(input):
+    """generate a valid asm label for a given map name"""
+    return input.replace(":", "").\
+                 replace("(", "").\
+                 replace(")", "").\
+                 replace("'", "").\
+                 replace("/", "").\
+                 replace(".", "").\
+                 replace("Pokémon Center", "PokeCenter").\
+                 replace(" ", "")
+
+class RomStr(str):
+    """simple wrapper to prevent a giant rom from being shown on screen"""
+    def __repr__(self):
+        return "RomStr(too long)"
+
+def grouper(some_list, count=2):
+    """splits a list into sublists
+    given: [1, 2, 3, 4]
+    returns: [[1, 2], [3, 4]]"""
+    return [some_list[i:i+count] for i in range(0, len(some_list), count)]
+
+def load_rom(filename="../baserom.gbc"):
+    """loads bytes into memory"""
+    global rom
+    file_handler = open(filename, "r")
+    rom = RomStr(file_handler.read())
+    file_handler.close()
+    return rom
+
+def rom_interval(offset, length, strings=True):
+    """returns hex values for the rom starting at offset until offset+length"""
+    global rom
+    returnable = []
+    for byte in rom[offset:offset+length]:
+        if strings:
+            returnable.append(hex(ord(byte)))
+        else:
+            returnable.append(ord(byte))
+    return returnable
+
+def rom_until(offset, byte, strings=True):
+    """returns hex values from rom starting at offset until the given byte"""
+    global rom
+    return rom_interval(offset, rom.find(chr(byte), offset) - offset, strings=strings)
+
+def load_map_group_offsets():
+    """reads the map group table for the list of pointers"""
+    global map_group_pointer_table, map_group_count, map_group_offsets
+    global rom
+    data = rom_interval(map_group_pointer_table, map_group_count*2, strings=False)
+    data = grouper(data)
+    for pointer_parts in data:
+        pointer = pointer_parts[0] + (pointer_parts[1] << 8)
+        offset = pointer - 0x4000 + map_group_pointer_table
+        map_group_offsets.append(offset)
+    return map_group_offsets
+
+def calculate_bank(address):
+    """you are too lazy to divide on your own?"""
+    if type(address) == str:
+        address = int(address, 16)
+    return int(address) / 0x4000
+
+def calculate_pointer(short_pointer, bank):
+    """calculates the full address given a 4-byte pointer and bank byte"""
+    short_pointer = int(short_pointer)
+    bank = int(bank)
+    pointer = short_pointer - 0x4000 + (bank * 0x4000)
+    return pointer
+
+def parse_script_at(address):
+    """parses a script-engine script"""
+    return {}
+
+def parse_warp_bytes(some_bytes):
+    """parse some number of warps from the data"""
+    assert len(some_bytes) % warp_byte_size == 0, "wrong number of bytes"
+    warps = []
+    for bytes in grouper(some_bytes, count=warp_byte_size):
+        y = int(bytes[0], 16)
+        x = int(bytes[1], 16)
+        warp_to = int(bytes[2], 16)
+        map_group = int(bytes[3], 16)
+        map_id = int(bytes[4], 16)
+        warps.append({
+            "y": y,
+            "x": x,
+            "warp_to": warp_to,
+            "map_group": map_group,
+            "map_id": map_id,
+        })
+    return warps
+def parse_xy_trigger_bytes(some_bytes, bank=None):
+    """parse some number of triggers from the data"""
+    assert len(some_bytes) % trigger_byte_size == 0, "wrong number of bytes"
+    triggers = []
+    for bytes in grouper(some_bytes, count=trigger_byte_size):
+        trigger_number = int(bytes[0], 16)
+        y = int(bytes[1], 16)
+        x = int(bytes[2], 16)
+        unknown1 = int(bytes[3], 16) #XXX probably 00?
+        script_ptr_byte1 = int(bytes[4], 16)
+        script_ptr_byte2 = int(bytes[5], 16)
+        script_ptr = script_ptr_byte1 + (script_ptr_byte2 << 8)
+        script_address = None
+        script = None
+        if bank:
+            script_address = calculate_pointer(script_ptr, bank)
+            script = parse_script_at(script_address)
+        
+        triggers.append({
+            "trigger_number": trigger_number,
+            "y": y,
+            "x": x,
+            "unknown1": unknown1, #probably 00
+            "script_ptr": script_ptr,
+            "script_pointer": {"1": script_ptr_byte1, "2": script_ptr_byte2},
+            "script_address": script_address,
+            "script": script,
+        })
+    return triggers
+def parse_signpost_bytes(some_bytes, bank=None):
+    """parse some number of signposts from the data
+
+    [Y position][X position][Function][Script pointer (2byte)]
+
+    functions:
+        00      Sign can be read from all directions
+                script pointer to: script
+        01      Sign can only be read from below
+                script pointer to: script
+        02      Sign can only be read from above
+                script pointer to: script
+        03      Sign can only be read from right
+                script pointer to: script
+        04      Sign can only be read from left
+                script pointer to: script
+        05      If bit of BitTable1 is set then pointer is interpreted
+                script pointer to: [Bit-Nr. (2byte)][2byte pointer to script]
+        06      If bit of BitTable1 is not set then pointer is interpreted
+                script pointer to: [Bit-Nr. (2byte)][2byte pointer to script]
+        07      If bit of BitTable1 is set then item is given
+                script pointer to: [Bit-Nr. (2byte)][Item no.]
+        08      No Action
+                script pointer to: [Bit-Nr. (2byte)][??]
+    """
+    assert len(some_bytes) % signpost_byte_size == 0, "wrong number of bytes"
+    signposts = []
+    for bytes in grouper(some_bytes, count=signpost_byte_size):
+        y = int(bytes[0], 16)
+        x = int(bytes[1], 16)
+        func = int(bytes[2], 16)
+        script_ptr_byte1 = int(bytes[3], 16)
+        script_ptr_byte2 = int(bytes[4], 16)
+        script_pointer = script_ptr_byte1 + (script_ptr_byte2 << 8)
+        script_address = None
+        script = None
+        if bank:
+            script_address = calculate_pointer(script_pointer, bank)
+            script = parse_script_at(script)
+        signposts.append({
+            "y": y,
+            "x": x,
+            "func": func,
+            "script_ptr": script_pointer,
+            "script_pointer": {"1": script_ptr_byte1, "2": script_ptr_byte2},
+            "script_address": script_address,
+            "script": script,
+        })
+    return signposts
+def parse_people_event_bytes(some_bytes, address=None): #max of 14 people per map?
+    """parse some number of people-events from the data
+    see http://hax.iimarck.us/files/scriptingcodes_eng.htm#Scripthdr
+
+    For example, map 1.1 (group 1 map 1) has four person-events.
+    
+        37 05 07 06 00 FF FF 00 00 02 40 FF FF
+        3B 08 0C 05 01 FF FF 00 00 05 40 FF FF
+        3A 07 06 06 00 FF FF A0 00 08 40 FF FF
+        29 05 0B 06 00 FF FF 00 00 0B 40 FF FF
+    """
+    assert len(some_bytes) % people_event_byte_size == 0, "wrong number of bytes"
+
+    #address is not actually required for this function to work...
+    bank = None
+    if address:
+        bank = calculate_bank(address)
+
+    people_events = []
+    for bytes in grouper(some_bytes, count=people_event_byte_size):
+        pict = int(bytes[0], 16)
+        y = int(bytes[1], 16)    #y from top + 4
+        x = int(bytes[2], 16)    #x from left + 4
+        face = int(bytes[3], 16) #0-4 for regular, 6-9 for static facing
+        move = int(bytes[4], 16)
+        clock_time_byte1 = int(bytes[5], 16)
+        clock_time_byte2 = int(bytes[6], 16)
+        color_function_byte = int(bytes[7], 16) #Color|Function
+        trainer_sight_range = int(bytes[8], 16)
+        
+        #goldmap called these next two bytes "text_block" and "text_bank"?
+        script_pointer_byte1 = int(bytes[9], 16)
+        script_pointer_byte2 = int(bytes[10], 16)
+        script_pointer = script_pointer_byte1 + (script_pointer_byte2 << 8)
+        #calculate the full address by assuming it's in the current bank
+        #but what if it's not in the same bank?
+        script_address = None
+        script = None
+        if bank:
+            script_address = calculate_pointer(script_pointer, bank)
+            script = parse_script_at(script_address)
+
+        #take the script pointer
+
+        #XXX not sure what's going on here
+        #bit no. of bit table 1 (hidden if set)
+        #note: FFFF for none
+        when_byte = int(bytes[11], 16)
+        hide = int(bytes[12], 16)
+
+        people_events.append({
+            "pict": pict,
+            "y": y,                      #y from top + 4
+            "x": x,                      #x from left + 4
+            "face": face,                #0-4 for regular, 6-9 for static facing
+            "move": move,
+            "clock_time": {"1": clock_time_byte1,
+                           "2": clock_time_byte2},       #clock/time setting byte 1
+            "color_function_byte": color_function_byte,  #Color|Function
+            "trainer_sight_range": trainer_sight_range,  #trainer range of sight
+            "script_pointer": {"1": script_pointer_byte1,
+                               "2": script_pointer_byte2},
+            "script_address": script_address,
+            "script": script,            #parsed script.. hah!
+            #"text_block": text_block,   #script pointer byte 1
+            #"text_bank": text_bank,     #script pointer byte 2
+            "when_byte": when_byte,      #bit no. of bit table 1 (hidden if set)
+            "hide": hide,                #note: FFFF for none
+        })
+    return people_events
+
+class MapEventElement():
+    def __init__(self, *args, **kwargs):
+        if len(args) == 1:
+            if isinstance(args[0], list):
+                if len(args[0]) != self.__class__.standard_size:
+                    raise "input has the wrong size"
+                #convert all of the list elements to integers
+                ints = []
+                for byte in args[0]:
+                    ints.append(int(byte, 16))
+                #parse using the class default method
+                events = self.__class__.parse_func(ints)
+                for key, value in events[0]:
+                    setattr(self, key, value)
+            else:
+                raise "dunno how to handle this positional input"
+        elif len(kwargs.keys()) != 0:
+            for key, value in kwargs.items():
+                setattr(self, key, value)
+        else:
+            raise "dunno how to handle given input"
+class Warp(MapEventElement):
+    standard_size = warp_byte_size
+    parse_func    = parse_warp_bytes
+class Trigger(MapEventElement):
+    standard_size = trigger_byte_size
+    parse_func    = parse_xy_trigger_bytes
+class Signpost(MapEventElement):
+    standard_size = signpost_byte_size
+    parse_func    = parse_signpost_bytes
+class PeopleEvent(MapEventElement):
+    standard_size = people_event_byte_size
+    parse_func    = parse_people_event_bytes
+
+def parse_map_header_at(address):
+    """parses an arbitrary map header at some address"""
+    bytes = rom_interval(address, map_header_byte_size, strings=False)
+    bank = bytes[0]
+    tileset = bytes[1]
+    permission = bytes[2]
+    second_map_header_address = calculate_pointer(bytes[3] + (bytes[4] << 8), 0x25)
+    location_on_world_map = bytes[5] #pokégear world map location
+    music = bytes[6]
+    time_of_day = bytes[7]
+    fishing_group = bytes[8]
+
+    map_header = {
+        "bank": bank,
+        "tileset": tileset,
+        "permission": permission, #map type?
+        "second_map_header_pointer": {"1": bytes[3], "2": bytes[4]},
+        "second_map_header_address": second_map_header_address,
+        "location_on_world_map": location_on_world_map, #area
+        "music": music,
+        "time_of_day": time_of_day,
+        "fishing": fishing_group,
+    }
+    map_header.update(parse_second_map_header_at(second_map_header_address))
+    map_header.update(parse_map_event_header_at(map_header["event_address"]))
+    #maybe this next one should be under the "scripts" key?
+    map_header.update(parse_map_script_header_at(map_header["script_address"]))
+    return map_header
+
+def parse_second_map_header_at(address):
+    """each map has a second map header"""
+    bytes = rom_interval(address, second_map_header_byte_size, strings=False)
+    border_block = bytes[0]
+    height = bytes[1]
+    width = bytes[2]
+    blockdata_bank = bytes[3]
+    blockdata_pointer = bytes[4] + (bytes[5] << 8)
+    blockdata_address = calculate_pointer(blockdata_pointer, blockdata_bank)
+    script_bank = bytes[6]
+    script_pointer = bytes[7] + (bytes[8] << 8)
+    script_address = calculate_pointer(script_pointer, script_bank)
+    event_bank = script_bank
+    event_pointer = bytes[9] + (bytes[10] << 8)
+    event_address = calculate_pointer(event_pointer, event_bank)
+    connections = bytes[11]
+    return {
+        "border_block": border_block,
+        "height": height,
+        "width": width,
+        "blockdata_bank": blockdata_bank,
+        "blockdata_pointer": {"1": bytes[4], "2": bytes[5]},
+        "blockdata_address": blockdata_address,
+        "script_bank": script_bank,
+        "script_pointer": {"1": bytes[7], "2": bytes[8]},
+        "script_address": script_address,
+        "event_bank": event_bank,
+        "event_pointer": {"1": bytes[9], "2": bytes[10]},
+        "event_address": event_address,
+        "connections": connections,
+    }
+
+def parse_map_event_header_at(address):
+    """parse crystal map event header byte structure thing"""
+    returnable = {}
+
+    bank = calculate_bank(address)
+
+    print "event header address is: " + hex(address)
+    filler1 = ord(rom[address])
+    filler2 = ord(rom[address+1])
+    returnable.update({"1": filler1, "2": filler2})
+    
+    #warps
+    warp_count = ord(rom[address+2])
+    warp_byte_count = warp_byte_size * warp_count
+    warps = rom_interval(address+3, warp_byte_count)
+    after_warps = address + 3 + warp_byte_count
+    returnable.update({"warp_count": warp_count, "warps": parse_warp_bytes(warps)})
+    
+    #triggers (based on xy location)
+    trigger_count = ord(rom[after_warps])
+    trigger_byte_count = trigger_byte_size * trigger_count
+    triggers = rom_interval(after_warps+1, trigger_byte_count)
+    after_triggers = after_warps + 1 + trigger_byte_count
+    returnable.update({"xy_trigger_count": trigger_count, "xy_triggers": parse_xy_trigger_bytes(triggers, bank=bank)})
+    
+    #signposts
+    signpost_count = ord(rom[after_triggers])
+    signpost_byte_count = signpost_byte_size * signpost_count
+    signposts = rom_interval(after_triggers+1, signpost_byte_count)
+    after_signposts = after_triggers + 1 + signpost_byte_count
+    returnable.update({"signpost_count": signpost_count, "signposts": parse_signpost_bytes(signposts, bank=bank)})
+    
+    #people events
+    people_event_count = ord(rom[after_signposts])
+    people_event_byte_count = people_event_byte_size * people_event_count
+    people_events = rom_interval(after_signposts+1, people_event_byte_count)
+    returnable.update({"people_event_count": people_event_count, "people_events": parse_people_event_bytes(people_events, address=after_signposts+1)})
+
+    return returnable
+
+def parse_map_script_header_at(address):
+    """parses a script header
+    
+    This structure allows the game to have e.g. one-time only events on a map
+    or first enter events or permanent changes to the map or permanent script
+    calls.
+
+    This header a combination of a trigger script section and a callback script
+    section. I don't know if these 'trigger scripts' are the same as the others
+    referenced in the map event header, so this might need to be renamed very
+    soon.
+
+    trigger scripts: 
+    [[Number1 of pointers] Number1 * [2byte pointer to script][00][00]]
+
+    callback scripts:
+    [[Number2 of pointers] Number2 * [hook number][2byte pointer to script]]
+
+    hook byte choices:
+        01 - map data has already been loaded to ram, tileset and sprites still missing
+            map change (3rd step)
+            loading (2nd step)
+            map connection (3rd step)
+            after battle (1st step)
+        02 - map data, tileset and sprites are all loaded
+            map change (5th step)
+        03 - neither map data not tilesets nor sprites are loaded
+            map change (2nd step)
+            loading (1st step)
+            map connection (2nd step)
+        04 - map data and tileset loaded, sprites still missing
+            map change (4th step)
+            loading (3rd step)
+            sprite reload (1st step)
+            map connection (4th step)
+            after battle (2nd step)
+        05 - neither map data not tilesets nor sprites are loaded
+            map change (1st step)
+            map connection (1st step)
+
+    When certain events occur, the call backs will be called in this order (same info as above):
+        map change:
+            05, 03, 01, 04, 02
+        loading:
+            03, 01, 04
+        sprite reload:
+            04
+        map connection:
+            05, 03, 01, 04 note that #2 is not called (unlike "map change")
+        after battle:
+            01, 04
+    """
+    #[[Number1 of pointers] Number1 * [2byte pointer to script][00][00]]
+    ptr_line_size = 4 #[2byte pointer to script][00][00]
+    trigger_ptr_cnt = ord(rom[address])
+    trigger_pointers = grouper(rom_interval(address+1, trigger_ptr_cnt * ptr_line_size, strings=False), count=ptr_line_size)
+    triggers = {}
+    for index, trigger_pointer in enumerate(trigger_pointers):
+        byte1 = trigger_pointer[0]
+        byte2 = trigger_pointer[1]
+        ptr   = byte1 + (byte2 << 8)
+        trigger_address = calculate_pointer(ptr, calculate_bank(address))
+        trigger_script  = parse_script_at(trigger_address)
+        triggers[index] = {
+            "script": trigger_script,
+            "address": trigger_address,
+            "pointer": {"1": byte1, "2": byte2},
+        }
+    
+    #bump ahead in the byte stream
+    address += trigger_ptr_cnt * ptr_line_size + 1
+    
+    #[[Number2 of pointers] Number2 * [hook number][2byte pointer to script]]
+    callback_ptr_line_size = 3
+    callback_ptr_cnt = ord(rom[address])
+    callback_ptrs = grouper(rom_interval(address+1, callback_ptr_cnt * callback_ptr_line_size, strings=False), count=callback_ptr_line_size)
+    callback_pointers = {}
+    callbacks = {}
+    for index, callback_line in enumerate(callback_ptrs):
+        hook_byte = callback_line[0] #1, 2, 3, 4, 5
+        callback_byte1 = callback_line[1]
+        callback_byte2 = callback_line[2]
+        callback_ptr = callback_byte1 + (callback_byte2 << 8)
+        callback_address = calculate_pointer(callback_ptr, calculate_bank(address))
+        callback_script = parse_script_at(callback_address)
+        callback_pointers[len(callback_pointers.keys())] = [hook_byte, callback_ptr]
+        callbacks[index] = {
+            "script": callback_script,
+            "address": callback_address,
+            "pointer": {"1": callback_byte1, "2": callback_byte2},
+        }
+    
+    #XXX do these triggers/callbacks call asm or script engine scripts?
+    return {
+        #"trigger_ptr_cnt": trigger_ptr_cnt,
+        "trigger_pointers": trigger_pointers,
+        #"callback_ptr_cnt": callback_ptr_cnt,
+        #"callback_ptr_scripts": callback_ptrs,
+        "callback_pointers": callback_pointers,
+        "trigger_scripts": triggers,
+        "callback_scripts": callbacks,
+    }
+
+def parse_all_map_headers():
+    """calls parse_map_header_at for each map in each map group"""
+    global map_names
+    if not map_names[1].has_key("offset"):
+        raise "dunno what to do - map_names should have groups with pre-calculated offsets by now"
+    for group_id, group_data in map_names.items():
+        offset = group_data["offset"]
+        #we only care about the maps
+        del group_data["offset"]
+        for map_id, map_data in group_data.items():
+            map_header_offset = offset + ((map_id - 1) * map_header_byte_size)
+            print "map_group is: " + str(group_id) + "  map_id is: " + str(map_id)
+            parsed_map = parse_map_header_at(map_header_offset)
+            map_names[group_id][map_id].update(parsed_map)
+            map_names[group_id][map_id]["header_offset"] = map_header_offset
+
+#map names with no labels will be generated at the end of the structure 
+map_names = {
+    1: {
+        0x1: {"name": "Olivine Pokémon Center 1F",
+              "label": "OlivinePokeCenter1F"},
+        0x2: {"name": "Olivine Gym"},
+        0x3: {"name": "Olivine Voltorb House"},
+        0x4: {"name": "Olivine House Beta"},
+        0x5: {"name": "Olivine Punishment Speech House"},
+        0x6: {"name": "Olivine Good Rod House"},
+        0x7: {"name": "Olivine Cafe"},
+        0x8: {"name": "Olivine Mart"},
+        0x9: {"name": "Route 38 Ecruteak Gate"},
+        0xA: {"name": "Route 39 Barn"},
+        0xB: {"name": "Route 39 Farmhouse"},
+        0xC: {"name": "Route 38"},
+        0xD: {"name": "Route 39"},
+        0xE: {"name": "Olivine City"},
+       },
+    2: {
+        0x1: {"name": "Mahogany Red Gyarados Speech House"},
+        0x2: {"name": "Mahogany Gym"},
+        0x3: {"name": "Mahogany Pokémon Center 1F",
+              "label": "MahoganyPokeCenter1F"},
+        0x4: {"name": "Route 42 Ecruteak Gate"},
+        0x5: {"name": "Route 42"},
+        0x6: {"name": "Route 43"},
+        0x7: {"name": "Mahogany Town"},
+    },
+    3: {
+        0x1: {"name": "Sprout Tower 1F"},
+        0x2: {"name": "Sprout Tower 2F"},
+        0x3: {"name": "Sprout Tower 3F"},
+        0x4: {"name": "Tin Tower 1F"},
+        0x5: {"name": "Tin Tower 2F"},
+        0x6: {"name": "Tin Tower 3F"},
+        0x7: {"name": "Tin Tower 4F"},
+        0x8: {"name": "Tin Tower 5F"},
+        0x9: {"name": "Tin Tower 6F"},
+        0xA: {"name": "Tin Tower 7F"},
+        0xB: {"name": "Tin Tower 8F"},
+        0xC: {"name": "Tin Tower 9F"},
+        0xD: {"name": "Bured Tower 1F"},
+        0xE: {"name": "Burned Tower B1F"},
+        0xF: {"name": "National Park"},
+        0x10: {"name": "National Park Bug Contest"},
+        0x11: {"name": "Radio Tower 1F"},
+        0x12: {"name": "Radio Tower 2F"},
+        0x13: {"name": "Radio Tower 3F"},
+        0x14: {"name": "Radio Tower 4F"},
+        0x15: {"name": "Radio Tower 5F"},
+        0x16: {"name": "Ruins of Alph Outside"},
+        0x17: {"name": "Ruins of Alph Ho-oh Chamber"},
+        0x18: {"name": "Ruins of Alph Kabuto Chamber"},
+        0x19: {"name": "Ruins of Alph Omanyte Chamber"},
+        0x1A: {"name": "Ruins of Alph Aerodactyl Chamber"},
+        0x1B: {"name": "Ruins of Alph Inner Chamber"},
+        0x1C: {"name": "Ruins of Alph Research Center"},
+        0x1D: {"name": "Ruins of Alph Ho-oh Item Room"},
+        0x1E: {"name": "Ruins of Alph Kabuto Item Room"},
+        0x1F: {"name": "Ruins of Alph Omanyte Item Room"},
+        0x20: {"name": "Ruins of Alph Aerodactyl Item Room"},
+        0x21: {"name": "Ruins of Alph Ho-Oh Word Room"},
+        0x22: {"name": "Ruins of Alph Kabuto Word Room"},
+        0x23: {"name": "Ruins of Alph Omanyte Word Room"},
+        0x24: {"name": "Ruins of Alph Aerodactyl Word Room"},
+        0x25: {"name": "Union Cave 1F"},
+        0x26: {"name": "Union Cave B1F"},
+        0x27: {"name": "Union Cave B2F"},
+        0x28: {"name": "Slowpoke Well B1F"},
+        0x29: {"name": "Slowpoke Well B2F"},
+        0x2A: {"name": "Olivine Lighthouse 1F"},
+        0x2B: {"name": "Olivine Lighthouse 2F"},
+        0x2C: {"name": "Olivine Lighthouse 3F"},
+        0x2D: {"name": "Olivine Lighthouse 4F"},
+        0x2E: {"name": "Olivine Lighthouse 5F"},
+        0x2F: {"name": "Olivine Lighthouse 6F"},
+        0x30: {"name": "Mahogany Mart 1F"},
+        0x31: {"name": "Team Rocket Base B1F"},
+        0x32: {"name": "Team Rocket Base B2F"},
+        0x33: {"name": "Team Rocket Base B3F"},
+        0x34: {"name": "Ilex Forest"},
+        0x35: {"name": "Warehouse Entrance"},
+        0x36: {"name": "Underground Path Switch Room Entrances"},
+        0x37: {"name": "Goldenrod Dept Store B1F"},
+        0x38: {"name": "Underground Warehouse"},
+        0x39: {"name": "Mount Mortar 1F Outside"},
+        0x3A: {"name": "Mount Mortar 1F Inside"},
+        0x3B: {"name": "Mount Mortar 2F Inside"},
+        0x3C: {"name": "Mount Mortar B1F"},
+        0x3D: {"name": "Ice Path 1F"},
+        0x3E: {"name": "Ice Path B1F"},
+        0x3F: {"name": "Ice Path B2F Mahogany Side"},
+        0x40: {"name": "Ice Path B2F Blackthorn Side"},
+        0x41: {"name": "Ice Path B3F"},
+        0x42: {"name": "Whirl Island NW"},
+        0x43: {"name": "Whirl Island NE"},
+        0x44: {"name": "Whirl Island SW"},
+        0x45: {"name": "Whirl Island Cave"},
+        0x46: {"name": "Whirl Island SE"},
+        0x47: {"name": "Whirl Island B1F"},
+        0x48: {"name": "Whirl Island B2F"},
+        0x49: {"name": "Whirl Island Lugia Chamber"},
+        0x4A: {"name": "Silver Cave Room 1"},
+        0x4B: {"name": "Silver Cave Room 2"},
+        0x4C: {"name": "Silver Cave Room 3"},
+        0x4D: {"name": "Silver Cave Item Rooms"},
+        0x4E: {"name": "Dark Cave Violet Entrance"},
+        0x4F: {"name": "Dark Cave Blackthorn Entrance"},
+        0x50: {"name": "Dragon's Den 1F"},
+        0x51: {"name": "Dragon's Den B1F"},
+        0x52: {"name": "Dragon Shrine"},
+        0x53: {"name": "Tohjo Falls"},
+        0x54: {"name": "Diglett's Cave"},
+        0x55: {"name": "Mount Moon"},
+        0x56: {"name": "Underground"},
+        0x57: {"name": "Rock Tunnel 1F"},
+        0x58: {"name": "Rock Tunnel B1F"},
+        0x59: {"name": "Safari Zone Fuchsia Gate Beta"},
+        0x5A: {"name": "Safari Zone Beta"},
+        0x5B: {"name": "Victory Road"},
+    },
+    4: {
+        0x1: {"name": "Ecruteak House"}, #passage to Tin Tower
+        0x2: {"name": "Wise Trio's Room"},
+        0x3: {"name": "Ecruteak Pokémon Center 1F",
+              "label": "EcruteakPokeCenter1F"},
+        0x4: {"name": "Ecruteak Lugia Speech House"},
+        0x5: {"name": "Dance Theatre"},
+        0x6: {"name": "Ecruteak Mart"},
+        0x7: {"name": "Ecruteak Gym"},
+        0x8: {"name": "Ecruteak Itemfinder House"},
+        0x9: {"name": "Ecruteak City"},
+    },
+    5: {
+        0x1: {"name": "Blackthorn Gym 1F"},
+        0x2: {"name": "Blackthorn Gym 2F"},
+        0x3: {"name": "Blackthorn Dragon Speech House"},
+        0x4: {"name": "Blackthorn Dodrio Trade House"},
+        0x5: {"name": "Blackthorn Mart"},
+        0x6: {"name": "Blackthorn Pokémon Center 1F",
+              "label": "BlackthornPokeCenter1F"},
+        0x7: {"name": "Move Deleter's House"},
+        0x8: {"name": "Route 45"},
+        0x9: {"name": "Route 46"},
+        0xA: {"name": "Blackthorn City"},
+    },
+    6: {
+        0x1: {"name": "Cinnabar Pokémon Center 1F",
+              "label": "CinnabarPokeCenter1F"},
+        0x2: {"name": "Cinnabar Pokémon Center 2F Beta",
+              "label": "CinnabarPokeCenter2FBeta"},
+        0x3: {"name": "Route 19 - Fuchsia Gate"},
+        0x4: {"name": "Seafoam Gym"},
+        0x5: {"name": "Route 19"},
+        0x6: {"name": "Route 20"},
+        0x7: {"name": "Route 21"},
+        0x8: {"name": "Cinnabar Island"},
+    },
+    7: {
+        0x1: {"name": "Cerulean Gym Badge Speech House"},
+        0x2: {"name": "Cerulean Police Station"},
+        0x3: {"name": "Cerulean Trade Speech House"},
+        0x4: {"name": "Cerulean Pokémon Center 1F",
+              "label": "CeruleanPokeCenter1F"},
+        0x5: {"name": "Cerulean Pokémon Center 2F Beta",
+              "label": "CeruleanPokeCenter2FBeta"},
+        0x6: {"name": "Cerulean Gym"},
+        0x7: {"name": "Cerulean Mart"},
+        0x8: {"name": "Route 10 Pokémon Center 1F",
+              "label": "Route10PokeCenter1F"},
+        0x9: {"name": "Route 10 Pokémon Center 2F Beta",
+              "label": "Route10PokeCenter2FBeta"},
+        0xA: {"name": "Power Plant"},
+        0xB: {"name": "Bill's House"},
+        0xC: {"name": "Route 4"},
+        0xD: {"name": "Route 9"},
+        0xE: {"name": "Route 10"},
+        0xF: {"name": "Route 24"},
+        0x10: {"name": "Route 25"},
+        0x11: {"name": "Cerulean City"},
+    },
+    8: {
+        0x1: {"name": "Azalea Pokémon Center 1F",
+              "label": "AzaleaPokeCenter1F"},
+        0x2: {"name": "Charcoal Kiln"},
+        0x3: {"name": "Azalea Mart"},
+        0x4: {"name": "Kurt's House"},
+        0x5: {"name": "Azalea Gym"},
+        0x6: {"name": "Route 33"},
+        0x7: {"name": "Azalea Town"},
+    },
+    9: {
+        0x1: {"name": "Lake of Rage Hidden Power House"},
+        0x2: {"name": "Lake of Rage Magikarp House"},
+        0x3: {"name": "Route 43 Mahogany Gate"},
+        0x4: {"name": "Route 43 Gate"},
+        0x5: {"name": "Route 43"},
+        0x6: {"name": "Lake of Rage"},
+    },
+    10: {
+        0x1: {"name": "Route 32"},
+        0x2: {"name": "Route 35"},
+        0x3: {"name": "Route 36"},
+        0x4: {"name": "Route 37"},
+        0x5: {"name": "Violet City"},
+        0x6: {"name": "Violet Mart"},
+        0x7: {"name": "Violet Gym"},
+        0x8: {"name": "Earl's Pokémon Academy",
+              "label": "EarlsPokemonAcademy"},
+        0x9: {"name": "Violet Nickname Speech House"},
+        0xA: {"name": "Violet Pokémon Center 1F",
+              "label": "VioletPokeCenter1F"},
+        0xB: {"name": "Violet Onix Trade House"},
+        0xC: {"name": "Route 32 Ruins of Alph Gate"},
+        0xD: {"name": "Route 32 Pokémon Center 1F",
+              "label": "Route32PokeCenter1F"},
+        0xE: {"name": "Route 35 Goldenrod gate"},
+        0xF: {"name": "Route 35 National Park gate"},
+        0x10: {"name": "Route 36 Ruins of Alph gate"},
+        0x11: {"name": "Route 36 National Park gate"},
+    },
+    11: {
+        0x1: {"name": "Route 34"},
+        0x2: {"name": "Goldenrod City"},
+        0x3: {"name": "Goldenrod Gym"},
+        0x4: {"name": "Goldenrod Bike Shop"},
+        0x5: {"name": "Goldenrod Happiness Rater"},
+        0x6: {"name": "Goldenrod Bill's House"},
+        0x7: {"name": "Goldenrod Magnet Train Station"},
+        0x8: {"name": "Goldenrod Flower Shop"},
+        0x9: {"name": "Goldenrod PP Speech House"},
+        0xA: {"name": "Goldenrod Name Rater's House"},
+        0xB: {"name": "Goldenrod Dept Store 1F"},
+        0xC: {"name": "Goldenrod Dept Store 2F"},
+        0xD: {"name": "Goldenrod Dept Store 3F"},
+        0xE: {"name": "Goldenrod Dept Store 4F"},
+        0xF: {"name": "Goldenrod Dept Store 5F"},
+        0x10: {"name": "Goldenrod Dept Store 6F"},
+        0x11: {"name": "Goldenrod Dept Store Elevator"},
+        0x12: {"name": "Goldenrod Dept Store Roof"},
+        0x13: {"name": "Goldenrod Game Corner"},
+        0x14: {"name": "Goldenrod Pokémon Center 1F",
+               "label": "GoldenrodPokeCenter1F"},
+        0x15: {"name": "Goldenrod PokéCom Center 2F Mobile",
+               "label": "GoldenrodPokeComCenter2FMobile"},
+        0x16: {"name": "Ilex Forest Azalea Gate"},
+        0x17: {"name": "Route 34 Ilex Forest Gate"},
+        0x18: {"name": "Day Care"},
+    },
+    12: {
+        0x1: {"name": "Route 6"},
+        0x2: {"name": "Route 11"},
+        0x3: {"name": "Vermilion City"},
+        0x4: {"name": "Vermilion House Fishing Speech House"},
+        0x5: {"name": "Vermilion Pokémon Center 1F",
+              "label": "VermilionPokeCenter1F"},
+        0x6: {"name": "Vermilion Pokémon Center 2F Beta",
+              "label": "VermilionPokeCenter2FBeta"},
+        0x7: {"name": "Pokémon Fan Club"},
+        0x8: {"name": "Vermilion Magnet Train Speech House"},
+        0x9: {"name": "Vermilion Mart"},
+        0xA: {"name": "Vermilion House Diglett's Cave Speech House"},
+        0xB: {"name": "Vermilion Gym"},
+        0xC: {"name": "Route 6 Saffron Gate"},
+        0xD: {"name": "Route 6 Underground Entrance"},
+    },
+    13: {
+        0x1: {"name": "Route 1"},
+        0x2: {"name": "Pallet Town"},
+        0x3: {"name": "Red's House 1F"},
+        0x4: {"name": "Red's House 2F"},
+        0x5: {"name": "Blue's House"},
+        0x6: {"name": "Oak's Lab"},
+    },
+    14: {
+        0x1: {"name": "Route 3"},
+        0x2: {"name": "Pewter City"},
+        0x3: {"name": "Pewter Nidoran Speech House"},
+        0x4: {"name": "Pewter Gym"},
+        0x5: {"name": "Pewter Mart"},
+        0x6: {"name": "Pewter Pokémon Center 1F",
+              "label": "PewterPokeCenter1F"},
+        0x7: {"name": "Pewter Pokémon Center 2F Beta",
+              "label": "PewterPokeCEnter2FBeta"},
+        0x8: {"name": "Pewter Snooze Speech House"},
+    },
+    15: {
+        0x1: {"name": "Olivine Port"},
+        0x2: {"name": "Vermilion Port"},
+        0x3: {"name": "Fast Ship 1F"},
+        0x4: {"name": "Fast Ship Cabins NNW, NNE, NE",
+              "label": "FastShipCabins_NNW_NNE_NE"},
+        0x5: {"name": "Fast Ship Cabins SW, SSW, NW",
+              "label": "FastShipCabins_SW_SSW_NW"},
+        0x6: {"name": "Fast Ship Cabins SE, SSE, Captain's Cabin",
+              "label": "FastShipCabins_SE_SSE_CaptainsCabin"},
+        0x7: {"name": "Fast Ship B1F"},
+        0x8: {"name": "Olivine Port Passage"},
+        0x9: {"name": "Vermilion Port Passage"},
+        0xA: {"name": "Mount Moon Square"},
+        0xB: {"name": "Mount Moon Gift Shop"},
+        0xC: {"name": "Tin Tower Roof"},
+    },
+    16: {
+        0x1: {"name": "Route 23"},
+        0x2: {"name": "Indigo Plateau Pokémon Center 1F",
+              "label": "IndigoPlateauPokeCenter1F"},
+        0x3: {"name": "Will's Room"},
+        0x4: {"name": "Koga's Room"},
+        0x5: {"name": "Bruno's Room"},
+        0x6: {"name": "Karen's Room"},
+        0x7: {"name": "Lance's Room"},
+        0x8: {"name": "Hall of Fame",
+              "label": "HallOfFame"},
+    },
+    17: {
+        0x1: {"name": "Route 13"},
+        0x2: {"name": "Route 14"},
+        0x3: {"name": "Route 15"},
+        0x4: {"name": "Route 18"},
+        0x5: {"name": "Fuchsia City"},
+        0x6: {"name": "Fuchsia Mart"},
+        0x7: {"name": "Safari Zone Main Office"},
+        0x8: {"name": "Fuchsia Gym"},
+        0x9: {"name": "Fuchsia Bill Speech House"},
+        0xA: {"name": "Fuchsia Pokémon Center 1F",
+              "label": "FuchsiaPokeCenter1F"},
+        0xB: {"name": "Fuchsia Pokémon Center 2F Beta",
+              "label": "FuchsiaPokeCenter2FBeta"},
+        0xC: {"name": "Safari Zone Warden's Home"},
+        0xD: {"name": "Route 15 Fuchsia Gate"},
+    },
+    18: {
+        0x1: {"name": "Route 8"},
+        0x2: {"name": "Route 12"},
+        0x3: {"name": "Route 10"},
+        0x4: {"name": "Lavender Town"},
+        0x5: {"name": "Lavender Pokémon Center 1F",
+              "label": "LavenderPokeCenter1F"},
+        0x6: {"name": "Lavender Pokémon Center 2F Beta",
+              "label": "LavenderPokeCenter2FBeta"},
+        0x7: {"name": "Mr. Fuji's House"},
+        0x8: {"name": "Lavender Town Speech House"},
+        0x9: {"name": "Lavender Name Rater"},
+        0xA: {"name": "Lavender Mart"},
+        0xB: {"name": "Soul House"},
+        0xC: {"name": "Lav Radio Tower 1F"},
+        0xD: {"name": "Route 8 Saffron Gate"},
+        0xE: {"name": "Route 12 Super Rod House"},
+    },
+    19: {
+        0x1: {"name": "Route 28"},
+        0x2: {"name": "Silver Cave Outside"},
+        0x3: {"name": "Silver Cave Pokémon Center 1F",
+              "label": "SilverCavePokeCenter1F"},
+        0x4: {"name": "Route 28 Famous Speech House"},
+    },
+    20: {
+        0x1: {"name": "Pokémon Center 2F",
+              "label": "PokeCenter2F"},
+        0x2: {"name": "Trade Center"},
+        0x3: {"name": "Colosseum"},
+        0x4: {"name": "Time Capsule"},
+        0x5: {"name": "Mobile Trade Room Mobile"},
+        0x6: {"name": "Mobile Battle Room"},
+    },
+    21: {
+        0x1: {"name": "Route 7"},
+        0x2: {"name": "Route 16"},
+        0x3: {"name": "Route 17"},
+        0x4: {"name": "Celadon City"},
+        0x5: {"name": "Celadon Dept Store 1F"},
+        0x6: {"name": "Celadon Dept Store 2F"},
+        0x7: {"name": "Celadon Dept Store 3F"},
+        0x8: {"name": "Celadon Dept Store 4F"},
+        0x9: {"name": "Celadon Dept Store 5F"},
+        0xA: {"name": "Celadon Dept Store 6F"},
+        0xB: {"name": "Celadon Dept Store Elevator"},
+        0xC: {"name": "Celadon Mansion 1F"},
+        0xD: {"name": "Celadon Mansion 2F"},
+        0xE: {"name": "Celadon Mansion 3F"},
+        0xF: {"name": "Celadon Mansion Roof"},
+        0x10: {"name": "Celadon Mansion Roof House"},
+        0x11: {"name": "Celadon Pokémon Center 1F",
+               "label": "CeladonPokeCenter1F"},
+        0x12: {"name": "Celadon Pokémon Center 2F Beta",
+               "label": "CeladonPokeCenter2FBeta"},
+        0x13: {"name": "Celadon Game Corner"},
+        0x14: {"name": "Celadon Game Corner Prize Room"},
+        0x15: {"name": "Celadon Gym"},
+        0x16: {"name": "Celadon Cafe"},
+        0x17: {"name": "Route 16 Fuchsia Speech House"},
+        0x18: {"name": "Route 16 Gate"},
+        0x19: {"name": "Route 7 Saffron Gate"},
+        0x1A: {"name": "Route 17 18 Gate"},
+    },
+    22: {
+        0x1: {"name": "Route 40"},
+        0x2: {"name": "Route 41"},
+        0x3: {"name": "Cianwood City"},
+        0x4: {"name": "Mania's House"},
+        0x5: {"name": "Cianwood Gym"},
+        0x6: {"name": "Cianwood Pokémon Center 1F",
+              "label": "CianwoodPokeCenter1F"},
+        0x7: {"name": "Cianwood Pharmacy"},
+        0x8: {"name": "Cianwood City Photo Studio"},
+        0x9: {"name": "Cianwood Lugia Speech House"},
+        0xA: {"name": "Poke Seer's House"},
+        0xB: {"name": "Battle Tower 1F"},
+        0xC: {"name": "Battle Tower Battle Room"},
+        0xD: {"name": "Battle Tower Elevator"},
+        0xE: {"name": "Battle Tower Hallway"},
+        0xF: {"name": "Route 40 Battle Tower Gate"},
+        0x10: {"name": "Battle Tower Outside"},
+    },
+    23: {
+        0x1: {"name": "Route 2"},
+        0x2: {"name": "Route 22"},
+        0x3: {"name": "Viridian City"},
+        0x4: {"name": "Viridian Gym"},
+        0x5: {"name": "Viridian Nickname Speech House"},
+        0x6: {"name": "Trainer House 1F"},
+        0x7: {"name": "Trainer House B1F"},
+        0x8: {"name": "Viridian Mart"},
+        0x9: {"name": "Viridian Pokémon Center 1F",
+              "label": "ViridianPokeCenter1F"},
+        0xA: {"name": "Viridian Pokémon Center 2F Beta",
+              "label": "ViridianPokeCenter2FBeta"},
+        0xB: {"name": "Route 2 Nugget Speech House"},
+        0xC: {"name": "Route 2 Gate"},
+        0xD: {"name": "Victory Road Gate"},
+    },
+    24: {
+        0x1: {"name": "Route 26"},
+        0x2: {"name": "Route 27"},
+        0x3: {"name": "Route 29"},
+        0x4: {"name": "New Bark Town"},
+        0x5: {"name": "Elm's Lab"},
+        0x6: {"name": "Kris's House 1F"},
+        0x7: {"name": "Kris's House 2F"},
+        0x8: {"name": "Kris's Neighbor's House"},
+        0x9: {"name": "Elm's House"},
+        0xA: {"name": "Route 26 Heal Speech House"},
+        0xB: {"name": "Route 26 Day of Week Siblings House"},
+        0xC: {"name": "Route 27 Sandstorm House"},
+        0xD: {"name": "Route 29 46 Gate"},
+    },
+    25: {
+        0x1: {"name": "Route 5"},
+        0x2: {"name": "Saffron City"},
+        0x3: {"name": "Fighting Dojo"},
+        0x4: {"name": "Saffron Gym"},
+        0x5: {"name": "Saffron Mart"},
+        0x6: {"name": "Saffron Pokémon Center 1F",
+              "label": "SaffronPokeCenter1F"},
+        0x7: {"name": "Saffron Pokémon Center 2F Beta",
+              "label": "SaffronPokeCenter2FBeta"},
+        0x8: {"name": "Mr. Psychic's House"},
+        0x9: {"name": "Saffron Train Station"},
+        0xA: {"name": "Silph Co. 1F"},
+        0xB: {"name": "Copycat's House 1F"},
+        0xC: {"name": "Copycat's House 2F"},
+        0xD: {"name": "Route 5 Underground Entrance"},
+        0xE: {"name": "Route 5 Saffron City Gate"},
+        0xF: {"name": "Route 5 Cleanse Tag Speech House"},
+    },
+    26: {
+        0x1: {"name": "Route 30"},
+        0x2: {"name": "Route 31"},
+        0x3: {"name": "Cherrygrove City"},
+        0x4: {"name": "Cherrygrove Mart"},
+        0x5: {"name": "Cherrygrove Pokémon Center 1F",
+              "label": "CherrygrovePokeCenter1F"},
+        0x6: {"name": "Cherrygrove Gym Speech House"},
+        0x7: {"name": "Guide Gent's House"},
+        0x8: {"name": "Cherrygrove Evolution Speech House"},
+        0x9: {"name": "Route 30 Berry Speech House"},
+        0xA: {"name": "Mr. Pokémon's House"},
+        0xB: {"name": "Route 31 Violet Gate"},
+    },
+}
+#generate labels for each map name
+for map_group_id in map_names.keys():
+    map_group = map_names[map_group_id]
+    for map_id in map_group.keys():
+        #skip if we maybe already have the 'offset' label set in this map group
+        if map_id == "offset": continue
+        #skip if we provided a pre-set value for the map's label
+        if map_group[map_id].has_key("label"): continue
+        #convience alias
+        map_data = map_group[map_id]
+        #clean up the map name to be an asm label
+        cleaned_name = map_name_cleaner(map_data["name"])
+        #set the value in the original dictionary
+        map_names[map_group_id][map_id]["label"] = cleaned_name
+#read the rom and figure out the offsets for maps
+load_rom()
+load_map_group_offsets()
+#add the offsets into our map structure, why not (johto maps only)
+[map_names[map_group_id+1].update({"offset": offset}) for map_group_id, offset in enumerate(map_group_offsets)]
+#parse map header bytes for each map
+parse_all_map_headers()
+
+if __name__ == "__main__":
+    load_rom()
+    load_map_group_offsets()
--