summaryrefslogtreecommitdiff
path: root/pokemontools
diff options
context:
space:
mode:
Diffstat (limited to 'pokemontools')
-rw-r--r--pokemontools/__init__.py1
-rw-r--r--pokemontools/chars.py279
-rw-r--r--pokemontools/comparator.py267
-rw-r--r--pokemontools/crystal.py7629
-rw-r--r--pokemontools/disassemble_map_scripts.py151
-rwxr-xr-xpokemontools/dump_sections.py129
-rw-r--r--pokemontools/gbz80disasm.py945
-rw-r--r--pokemontools/gfx.py1671
-rw-r--r--pokemontools/graph.py169
-rw-r--r--pokemontools/interval_map.py103
-rw-r--r--pokemontools/item_constants.py240
-rw-r--r--pokemontools/labels.py171
-rw-r--r--pokemontools/map_names.py484
-rw-r--r--pokemontools/move_constants.py255
-rw-r--r--pokemontools/old_parse_scripts.py1885
-rw-r--r--pokemontools/overworldripper.py18
-rw-r--r--pokemontools/parse_consecutive_strings.py25
-rw-r--r--pokemontools/pksv.py314
-rw-r--r--pokemontools/pointers.py24
-rw-r--r--pokemontools/pokemon_constants.py255
-rw-r--r--pokemontools/romstr.py219
-rw-r--r--pokemontools/sym.py54
-rw-r--r--pokemontools/trainers.py108
-rw-r--r--pokemontools/type_constants.py21
-rw-r--r--pokemontools/wram.py125
25 files changed, 15542 insertions, 0 deletions
diff --git a/pokemontools/__init__.py b/pokemontools/__init__.py
new file mode 100644
index 0000000..833709b
--- /dev/null
+++ b/pokemontools/__init__.py
@@ -0,0 +1 @@
+import crystal
diff --git a/pokemontools/chars.py b/pokemontools/chars.py
new file mode 100644
index 0000000..fc69fc5
--- /dev/null
+++ b/pokemontools/chars.py
@@ -0,0 +1,279 @@
+# -*- coding: utf-8 -*-
+from copy import copy
+
+# this is straight out of ../preprocessor.py because i'm lazy
+# (also, it's flipped)
+# 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: "→",
+ 0xEC: "▷",
+ 0xED: "▶",
+ 0xEE: "▼",
+ 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
diff --git a/pokemontools/comparator.py b/pokemontools/comparator.py
new file mode 100644
index 0000000..6cc440c
--- /dev/null
+++ b/pokemontools/comparator.py
@@ -0,0 +1,267 @@
+# -*- coding: utf-8 -*-
+"""
+Find shared functions between red/crystal.
+"""
+
+from crystal import (
+ get_label_from_line,
+ get_address_from_line_comment,
+ AsmSection,
+ direct_load_rom,
+ direct_load_asm,
+)
+
+from romstr import (
+ RomStr,
+ AsmList,
+)
+
+def load_rom(path):
+ """
+ Load a ROM file into an abbreviated RomStr object.
+ """
+ return direct_load_rom(filename=path)
+
+def load_asm(path):
+ """
+ Load source ASM into an abbreviated AsmList object.
+ """
+ return direct_load_asm(filename=path)
+
+def findall_iter(sub, string):
+ # url: http://stackoverflow.com/a/3874760/687783
+
+ def next_index(length):
+ index = 0 - length
+ while True:
+ index = string.find(sub, index + length)
+ yield index
+
+ return iter(next_index(len(sub)).next, -1)
+
+class Address(int):
+ """
+ A simple int wrapper to take 0xFFFF and $FFFF addresses.
+ """
+
+ def __new__(cls, x=None, *args, **kwargs):
+ if type(x) == str:
+ if "$" in x:
+ x = x.replace("$", "0x")
+
+ if "0x" in str:
+ instance = int.__new__(cls, int(x, base=16), *args, **kwargs)
+ else:
+ msg = "Address.__new__ doesn't know how to parse this string"
+ raise Exception, msg
+ else:
+ instance = int.__new__(cls, x, *args, **kwargs)
+
+ return instance
+
+found_blobs = []
+
+class BinaryBlob(object):
+ """
+ Store a label, line number, and addresses of a function from Pokémon Red.
+
+ These details can be used to determine whether or not the function was
+ copied into Pokémon Crystal.
+ """
+
+ start_address = None
+ end_address = None
+ label = None
+ line_number = None
+ bytes = None
+ bank = None
+ debug = False
+ locations = None
+
+ def __init__(self, start_address=None, end_address=None, label=None, \
+ debug=None, line_number=None):
+ if not isinstance(start_address, Address):
+ start_address = Address(start_address)
+ if not isinstance(end_address, Address):
+ end_address = Address(end_address)
+
+ assert label != None, "label can't be none"
+ assert isinstance(label, str), "label must be a string"
+ assert line_number != None, "line_number must be provided"
+
+ self.start_address = start_address
+ self.end_address = end_address
+ self.label = label
+ self.line_number = line_number
+ self.bytes = []
+ self.locations = []
+ self.bank = start_address / 0x4000
+
+ if debug != None:
+ self.debug = debug
+
+ self.parse_from_red()
+ # self.find_in_crystal()
+ self.find_by_first_bytes()
+
+ def __repr__(self):
+ """
+ A beautiful poem.
+ """
+
+ r = "BinaryBlob("
+ r += "label=\""+self.label+"\", "
+ r += "start_address="+hex(self.start_address)+", "
+ r += "size="+str(self.end_address - self.start_address)+", "
+ locnum = len(self.locations)
+ if locnum == 1:
+ r += "located="+hex(self.locations[0])
+ elif locnum <= 5:
+ r += "located="+str([hex(x) for x in self.locations])
+ else:
+ r += "located="+str(locnum)
+ r += ")"
+
+ return r
+
+ def __str__(self):
+ return self.__repr__()
+
+ def parse_from_red(self):
+ """
+ Read bytes from Pokémon Red and stores them.
+ """
+
+ self.bytes = redrom[self.start_address : self.end_address + 1]
+
+ def pretty_bytes(self):
+ """
+ Returns a better looking range of bytes.
+ """
+
+ bytes = redrom.interval(self.start_address, \
+ self.end_address - self.start_address, \
+ strings=False, debug=True)
+
+ return bytes
+
+ def find_in_crystal(self):
+ """
+ Check whether or not the bytes appear in Pokémon Crystal.
+ """
+
+ finditer = findall_iter(self.bytes, cryrom)
+ self.locations = [match for match in finditer]
+
+ if len(self.locations) > 0:
+ found_blobs.append(self)
+
+ if self.debug:
+ print self.label + ": found " + str(len(self.locations)) + " matches."
+
+ def find_by_first_bytes(self):
+ """
+ Find this blob in Crystal based on the first n bytes.
+ """
+
+ # how many bytes to match
+ first_n = 3
+
+ # no match
+ if len(self.bytes) <= first_n:
+ return
+
+ finditer = findall_iter(self.bytes[0:first_n], cryrom)
+ self.locations = [match for match in finditer]
+
+ # filter out locations that suck
+ self.locations = [i for i in self.locations if abs(self.start_address - i) <= 0x8000]
+
+ if len(self.locations) > 0:
+ found_blobs.append(self)
+
+ if self.debug:
+ print self.label + ": found " + str(len(self.locations)) + " matches."
+
+pokecrystal_rom_path = "../baserom.gbc"
+pokecrystal_src_path = "../main.asm"
+pokered_rom_path = "../pokered-baserom.gbc"
+pokered_src_path = "../pokered-main.asm"
+
+cryrom = load_rom(pokecrystal_rom_path)
+crysrc = load_asm(pokecrystal_src_path)
+redrom = load_rom(pokered_rom_path)
+redsrc = load_asm(pokered_src_path)
+
+def scan_red_asm(bank_stop=3, debug=True):
+ """
+ Scan the ASM from Pokémon Red. Finds labels and objects. Does things.
+
+ Uses get_label_from_line and get_address_from_line_comment.
+ """
+
+ # whether or not to show the lines from redsrc
+ show_lines = False
+
+ line_number = 0
+ current_bank = 0
+
+ current_label = None
+ latest_label = "ignore me"
+ current_start_address = None
+ latest_start_address = 0
+ latest_line = ""
+
+ for line in redsrc:
+ if debug and show_lines:
+ print "processing a line from red: " + line
+
+ if line[0:7] == "SECTION":
+ thing = AsmSection(line)
+ current_bank = thing.bank_id
+
+ if debug:
+ print "scan_red_asm: switching to bank " + str(current_bank)
+
+ elif line[0:6] != "INCBIN":
+ if ":" in line and not ";XXX:" in line and not " ; XXX:" in line:
+ current_label = get_label_from_line(line)
+ current_start_address = get_address_from_line_comment(line, \
+ bank=current_bank)
+
+ if current_label != None and current_start_address != None and latest_start_address != None \
+ and current_start_address != 0 and current_start_address != latest_start_address \
+ and (current_start_address - latest_start_address) > 1:
+ if latest_label != None:
+ if latest_label not in ["Char52", "PokeCenterSignText", "DefaultNamesPlayer", "Unnamed_6a12"]:
+ blob = BinaryBlob(label=latest_label, \
+ start_address=latest_start_address, \
+ end_address=current_start_address, \
+ line_number=line_number)
+
+ if debug:
+ print "Created a new blob: " + str(blob) + " from line: " + str(latest_line)
+
+ latest_label = current_label
+ latest_start_address = current_start_address
+ latest_line = line
+
+ line_number += 1
+
+ if current_bank == bank_stop:
+ if debug:
+ print "scan_red_asm: stopping because current_bank >= " + \
+ str(bank_stop) + " (bank_stop)"
+
+ break
+
+scan_red_asm(bank_stop=3)
+
+print "================================"
+
+for blob in found_blobs:
+ print blob
+
+print "Found " + str(len(found_blobs)) + " possibly copied functions."
+
+print [hex(x) for x in found_blobs[10].locations]
diff --git a/pokemontools/crystal.py b/pokemontools/crystal.py
new file mode 100644
index 0000000..9ee6ec0
--- /dev/null
+++ b/pokemontools/crystal.py
@@ -0,0 +1,7629 @@
+# -*- coding: utf-8 -*-
+# utilities to help disassemble pokémon crystal
+import os
+import sys
+import inspect
+import hashlib
+import json
+from copy import copy, deepcopy
+import subprocess
+from new import classobj
+import random
+
+# for capwords
+import string
+
+# for python2.6
+if not hasattr(json, "dumps"):
+ json.dumps = json.write
+
+# New versions of json don't have read anymore.
+if not hasattr(json, "read"):
+ json.read = json.loads
+
+from labels import (
+ remove_quoted_text,
+ line_has_comment_address,
+ line_has_label,
+ get_label_from_line,
+ get_address_from_line_comment
+)
+
+spacing = "\t"
+
+lousy_dragon_shrine_hack = [0x18d079, 0x18d0a9, 0x18d061, 0x18d091]
+
+# 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"
+
+max_texts = 3
+text_count = 0
+texts = []
+
+# these appear outside of quotes (see pokered/extras/pretty_map_headers.py)
+# this doesn't do anything but is still used in TextScript
+constant_abbreviation_bytes = {}
+
+# Import the characters from its module.
+from chars import chars, jap_chars
+
+from trainers import (
+ trainer_group_pointer_table_address, # 0x39999
+ trainer_group_pointer_table_address_gs, # 0x3993E
+ trainer_group_names,
+)
+
+from move_constants import moves
+
+# for fixing trainer_group_names
+import re
+
+from interval_map import IntervalMap
+
+from pksv import (
+ pksv_gs,
+ pksv_crystal,
+ pksv_crystal_unknowns,
+ pksv_crystal_more_enders
+)
+
+# ---- script_parse_table explanation ----
+# This is an IntervalMap that keeps track of previously parsed scripts, texts
+# and other objects. Anything that has a location in the ROM should be mapped
+# to an interval (a range of addresses) in this structure. Each object that is
+# assigned to an interval should implement attributes or methods like:
+# ATTRIBUTE/METHOD EXPLANATION
+# label what the heck to call the object
+# address where it begins
+# to_asm() spit out asm (not including label)
+# keys are intervals "500..555" of byte addresses for each script
+# last byte is not inclusive(?) really? according to who??
+# this is how to make sure scripts are not recalculated
+script_parse_table = IntervalMap()
+
+def is_script_already_parsed_at(address):
+ """looks up whether or not a script is parsed at a certain address"""
+ if script_parse_table[address] == None:
+ return False
+ return True
+
+def script_parse_table_pretty_printer():
+ """helpful debugging output"""
+ for each in script_parse_table.items():
+ print each
+
+def map_name_cleaner(input):
+ """generate a valid asm label for a given map name"""
+ return input.replace(":", "").\
+ replace("(", "").\
+ replace(")", "").\
+ replace("'", "").\
+ replace("/", "").\
+ replace(",", "").\
+ replace(".", "").\
+ replace("Pokémon Center", "PokeCenter").\
+ replace("é", "e").\
+ replace("-", "").\
+ replace("Hooh", "HoOh").\
+ replace("hooh", "HoOh").\
+ replace(" ", "")
+
+from romstr import (
+ RomStr,
+ AsmList,
+)
+
+rom = RomStr(None)
+
+def direct_load_rom(filename="../baserom.gbc"):
+ """loads bytes into memory"""
+ global rom
+ file_handler = open(filename, "rb")
+ rom = RomStr(file_handler.read())
+ file_handler.close()
+ return rom
+
+def load_rom(filename="../baserom.gbc"):
+ """checks that the loaded rom matches the path
+ and then loads the rom if necessary."""
+ global rom
+ if rom != RomStr(None) and rom != None:
+ return rom
+ if not isinstance(rom, RomStr):
+ return direct_load_rom(filename=filename)
+ elif os.lstat(filename).st_size != len(rom):
+ return direct_load_rom(filename)
+
+def direct_load_asm(filename="../main.asm"):
+ """returns asm source code (AsmList) from a file"""
+ asm = open(filename, "r").read().split("\n")
+ asm = AsmList(asm)
+ return asm
+
+def load_asm(filename="../main.asm"):
+ """returns asm source code (AsmList) from a file (uses a global)"""
+ global asm
+ asm = direct_load_asm(filename=filename)
+ return asm
+
+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 is_valid_address(address):
+ """is_valid_rom_address"""
+ if address == None:
+ return False
+ if type(address) == str:
+ address = int(address, 16)
+ if 0 <= address <= 2097152:
+ return True
+ else:
+ return False
+
+def rom_interval(offset, length, strings=True, debug=True):
+ """returns hex values for the rom starting at offset until offset+length"""
+ global rom
+ return rom.interval(offset, length, strings=strings, debug=debug)
+
+def rom_until(offset, byte, strings=True, debug=True):
+ """returns hex values from rom starting at offset until the given byte"""
+ global rom
+ return rom.until(offset, byte, strings=strings, debug=debug)
+
+def how_many_until(byte, starting):
+ index = rom.find(byte, starting)
+ return index - starting
+
+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
+ map_group_offsets = [] # otherwise this method can only be used once
+ 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
+
+from pointers import (
+ calculate_bank,
+ calculate_pointer,
+)
+
+def calculate_pointer_from_bytes_at(address, bank=False):
+ """calculates a pointer from 2 bytes at a location
+ or 3-byte pointer [bank][2-byte pointer] if bank=True"""
+ if bank == True:
+ bank = ord(rom[address])
+ address += 1
+ elif bank == False or bank == None:
+ bank = calculate_bank(address)
+ elif bank == "reverse" or bank == "reversed":
+ bank = ord(rom[address+2])
+ elif type(bank) == int:
+ pass
+ else:
+ raise Exception("bad bank given to calculate_pointer_from_bytes_at")
+ byte1 = ord(rom[address])
+ byte2 = ord(rom[address+1])
+ temp = byte1 + (byte2 << 8)
+ if temp == 0:
+ return None
+ return calculate_pointer(temp, bank)
+
+def clean_up_long_info(long_info):
+ """cleans up some data from parse_script_engine_script_at formatting issues"""
+ long_info = str(long_info)
+ # get rid of the first newline
+ if long_info[0] == "\n":
+ long_info = long_info[1:]
+ # get rid of the last newline and any leftover space
+ if long_info.count("\n") > 0:
+ if long_info[long_info.rindex("\n")+1:].isspace():
+ long_info = long_info[:long_info.rindex("\n")]
+ # remove spaces+hash from the front of each line
+ new_lines = []
+ for line in long_info.split("\n"):
+ line = line.strip()
+ if line[0] == "#":
+ line = line[1:]
+ new_lines.append(line)
+ long_info = "\n".join(new_lines)
+ return long_info
+
+from pokemon_constants import pokemon_constants
+
+def get_pokemon_constant_by_id(id):
+ if id == 0:
+ return None
+ else:
+ return pokemon_constants[id]
+
+from item_constants import (
+ item_constants,
+ find_item_label_by_id,
+ generate_item_constants,
+)
+
+def command_debug_information(command_byte=None, map_group=None, map_id=None, address=0, info=None, long_info=None, pksv_name=None):
+ "used to help debug in parse_script_engine_script_at"
+ info1 = "parsing command byte " + hex(command_byte) + " for map " + \
+ str(map_group) + "." + str(map_id) + " at " + hex(address)
+ info1 += " pksv: " + str(pksv_name)
+ #info1 += " info: " + str(info)
+ #info1 += " long_info: " + long_info
+ return info1
+
+all_texts = []
+class TextScript:
+ """
+ A text is a sequence of bytes (and sometimes commands). It's not the same
+ thing as a Script. The bytes are translated into characters based on the
+ lookup table (see chars.py). The in-text commands are for including values
+ from RAM, playing sound, etc.
+
+ see: http://hax.iimarck.us/files/scriptingcodes_eng.htm#InText
+ """
+ base_label = "UnknownText_"
+ def __init__(self, address, map_group=None, map_id=None, debug=False, label=None, force=False, show=None):
+ self.address = address
+ # $91, $84, $82, $54, $8c
+ # 0x19768c is a a weird problem?
+ if address in [0x26ef, 0x26f2, 0x6ee, 0x1071, 0x5ce33, 0x69523, 0x7ee98, 0x72176, 0x7a578, 0x19c09b, 0x19768c]:
+ return None
+ self.map_group, self.map_id, self.debug = map_group, map_id, debug
+ self.dependencies = None
+ self.commands = None
+ self.force = force
+
+ if is_script_already_parsed_at(address) and not force:
+ raise Exception("TextScript already parsed at "+hex(address))
+
+ if not label:
+ label = self.base_label + hex(address)
+ self.label = Label(name=label, address=address, object=self)
+
+ self.parse()
+
+ def is_valid(self):
+ return not (self.address in [0x26ef, 0x26f2, 0x6ee, 0x1071, 0x5ce33, 0x69523, 0x7ee98, 0x72176, 0x7a578, 0x19c09b, 0x19768c])
+
+ # hmm this looks exactly like Script.get_dependencies (which makes sense..)
+ def get_dependencies(self, recompute=False, global_dependencies=set()):
+ if self.address in [0x26ef, 0x26f2, 0x6ee, 0x1071, 0x5ce33, 0x69523, 0x7ee98, 0x72176, 0x7a578, 0x19c09b, 0x19768c]:
+ return []
+
+ if self.dependencies != None and not recompute:
+ global_dependencies.update(self.dependencies)
+ return self.dependencies
+
+ dependencies = []
+
+ for command in self.commands:
+ deps = command.get_dependencies(recompute=recompute, global_dependencies=global_dependencies)
+ dependencies.extend(deps)
+
+ self.dependencies = dependencies
+ return self.dependencies
+
+ # this is almost an exact copy of Script.parse
+ # with the exception of using text_command_classes instead of command_classes
+ def parse(self):
+ if self.address in [0x26ef, 0x26f2, 0x6ee, 0x1071, 0x5ce33, 0x69523, 0x7ee98, 0x72176, 0x7a578, 0x19c09b, 0x19768c]:
+ return None
+
+ global text_command_classes, script_parse_table
+ current_address = copy(self.address)
+ start_address = copy(current_address)
+
+ # don't clutter up my screen
+ if self.debug:
+ print "NewTextScript.parse address="+hex(self.address)+" map_group="+str(self.map_group)+" map_id="+str(self.map_id)
+
+ # load up the rom if it hasn't been loaded already
+ load_rom()
+
+ # in the event that the script parsing fails.. it would be nice to leave evidence
+ script_parse_table[start_address:start_address+1] = "incomplete NewTextScript.parse"
+
+ # start with a blank script
+ commands = []
+
+ # use this to control the while loop
+ end = False
+
+ # for each command found...
+ while not end:
+ # get the current scripting byte
+ cur_byte = ord(rom[current_address])
+
+ # reset the command class (last command was probably different)
+ scripting_command_class = None
+
+ # match the command id byte to a scripting command class like MainText
+ for class_ in text_command_classes:
+ if class_[1].id == cur_byte:
+ scripting_command_class = class_[1]
+
+ if self.address == 0x9c00e and self.debug:
+ if current_address > 0x9c087:
+ print "self.commands is: " + str(commands)
+ print "command 0 address is: " + hex(commands[0].address) + " last_address="+hex(commands[0].last_address)
+ print "command 1 address is: " + hex(commands[1].address) + " last_address="+hex(commands[1].last_address)
+ raise Exception("going beyond the bounds for this text script")
+
+ # no matching command found
+ if scripting_command_class == None:
+ raise Exception("unable to parse text command $%.2x in the text script at %s at %s" % (cur_byte, hex(start_address), hex(current_address)))
+
+ # create an instance of the command class and let it parse its parameter bytes
+ cls = scripting_command_class(address=current_address, map_group=self.map_group, map_id=self.map_id, debug=self.debug, force=self.force)
+
+ if self.debug:
+ print cls.to_asm()
+
+ # store it in this script object
+ commands.append(cls)
+
+ # certain commands will end the scripting engine
+ end = cls.end
+
+ # skip past the command's parameter bytes to go to the next command
+ #current_address += cls.size
+ current_address = cls.last_address
+
+ # last byte belonging to script is last byte of last command,
+ # or the last byte of the last command's last parameter
+ # (actually i think this might be the next byte after??)
+ self.last_address = current_address
+
+ if self.debug:
+ print "cls.address is: " + hex(cls.address)
+ print "cls.size is: " + hex(cls.size)
+ print "cls.last_address is: " + hex(cls.last_address)
+ print "self.last_address is: " + hex(self.last_address)
+
+ assert self.last_address == (cls.address + cls.size), "the last address should equal the last command's (address + size)"
+ assert self.last_address == cls.last_address, "the last address of the TextScript should be the last_address of its last command"
+
+ # just some debugging..
+ if self.debug:
+ last_address = self.last_address
+ print "TextScript last_address == " + hex(last_address)
+ #assert last_address != 0x5db06, "TextScript.parse somehow has a text with a last_address of 0x5db06 instead of 0x5db07"
+
+ # store the script in the global table/map thing
+ script_parse_table[start_address:current_address] = self
+ all_texts.append(self)
+
+ if self.debug:
+ asm_output = "\n".join([command.to_asm() for command in commands])
+ print "--------------\n"+asm_output
+
+ # store the script
+ self.commands = commands
+
+ return commands
+
+ def to_asm(self):
+ if self.address in [0x26ef, 0x26f2, 0x6ee, 0x1071, 0x5ce33, 0x69523, 0x7ee98, 0x72176, 0x7a578, 0x19c09b, 0x19768c]:
+ return None
+
+ asm_output = "\n".join([command.to_asm() for command in self.commands])
+ return asm_output
+
+class OldTextScript:
+ "a text is a sequence of commands different from a script-engine script"
+ base_label = "UnknownText_"
+ def __init__(self, address, map_group=None, map_id=None, debug=True, show=True, force=False, label=None):
+ self.address = address
+ self.map_group, self.map_id, self.debug, self.show, self.force = map_group, map_id, debug, show, force
+ if not label:
+ label = self.base_label + hex(address)
+ self.label = Label(name=label, address=address, object=self)
+ self.dependencies = []
+ self.parse_text_at(address)
+
+ @staticmethod
+ def find_addresses():
+ """returns a list of text pointers
+ useful for testing parse_text_engine_script_at
+
+ Note that this list is not exhaustive. There are some texts that
+ are only pointed to from some script that a current script just
+ points to. So find_all_text_pointers_in_script_engine_script will
+ have to recursively follow through each script to find those.
+ .. it does this now :)
+ """
+ addresses = set()
+ # for each map group
+ for map_group in map_names:
+ # for each map id
+ for map_id in map_names[map_group]:
+ # skip the offset key
+ if map_id == "offset": continue
+ # dump this into smap
+ smap = map_names[map_group][map_id]
+ # signposts
+ signposts = smap["signposts"]
+ # for each signpost
+ for signpost in signposts:
+ if signpost["func"] in [0, 1, 2, 3, 4]:
+ # dump this into script
+ script = signpost["script"]
+ elif signpost["func"] in [05, 06]:
+ script = signpost["script"]
+ else: continue
+ # skip signposts with no bytes
+ if len(script) == 0: continue
+ # find all text pointers in script
+ texts = find_all_text_pointers_in_script_engine_script(script, smap["event_bank"])
+ # dump these addresses in
+ addresses.update(texts)
+ # xy triggers
+ xy_triggers = smap["xy_triggers"]
+ # for each xy trigger
+ for xy_trigger in xy_triggers:
+ # dump this into script
+ script = xy_trigger["script"]
+ # find all text pointers in script
+ texts = find_all_text_pointers_in_script_engine_script(script, smap["event_bank"])
+ # dump these addresses in
+ addresses.update(texts)
+ # trigger scripts
+ triggers = smap["trigger_scripts"]
+ # for each trigger
+ for (i, trigger) in triggers.items():
+ # dump this into script
+ script = trigger["script"]
+ # find all text pointers in script
+ texts = find_all_text_pointers_in_script_engine_script(script, calculate_bank(trigger["address"]))
+ # dump these addresses in
+ addresses.update(texts)
+ # callback scripts
+ callbacks = smap["callback_scripts"]
+ # for each callback
+ for (k, callback) in callbacks.items():
+ # dump this into script
+ script = callback["script"]
+ # find all text pointers in script
+ texts = find_all_text_pointers_in_script_engine_script(script, calculate_bank(callback["address"]))
+ # dump these addresses in
+ addresses.update(texts)
+ # people-events
+ events = smap["people_events"]
+ # for each event
+ for event in events:
+ if event["event_type"] == "script":
+ # dump this into script
+ script = event["script"]
+ # find all text pointers in script
+ texts = find_all_text_pointers_in_script_engine_script(script, smap["event_bank"])
+ # dump these addresses in
+ addresses.update(texts)
+ if event["event_type"] == "trainer":
+ trainer_data = event["trainer_data"]
+ addresses.update([trainer_data["text_when_seen_ptr"]])
+ addresses.update([trainer_data["text_when_trainer_beaten_ptr"]])
+ trainer_bank = calculate_bank(event["trainer_data_address"])
+ script1 = trainer_data["script_talk_again"]
+ texts1 = find_all_text_pointers_in_script_engine_script(script1, trainer_bank)
+ addresses.update(texts1)
+ script2 = trainer_data["script_when_lost"]
+ texts2 = find_all_text_pointers_in_script_engine_script(script2, trainer_bank)
+ addresses.update(texts2)
+ return addresses
+
+ def parse_text_at(self, address):
+ """parses a text-engine script ("in-text scripts")
+ http://hax.iimarck.us/files/scriptingcodes_eng.htm#InText
+
+ This is presently very broken.
+
+ see parse_text_at2, parse_text_at, and process_00_subcommands
+ """
+ global rom, text_count, max_texts, texts, script_parse_table
+ if rom == None:
+ direct_load_rom()
+ if address == None:
+ return "not a script"
+ map_group, map_id, debug, show, force = self.map_group, self.map_id, self.debug, self.show, self.force
+ commands = {}
+
+ if is_script_already_parsed_at(address) and not force:
+ print "text is already parsed at this location: " + hex(address)
+ raise Exception("text is already parsed, what's going on ?")
+ return script_parse_table[address]
+
+ total_text_commands = 0
+ command_counter = 0
+ original_address = address
+ offset = address
+ end = False
+ script_parse_table[original_address:original_address+1] = "incomplete text"
+ while not end:
+ address = offset
+ command = {}
+ command_byte = ord(rom[address])
+ if debug:
+ print "TextScript.parse_script_at has encountered a command byte " + hex(command_byte) + " at " + hex(address)
+ end_address = address + 1
+ if command_byte == 0:
+ # read until $57, $50 or $58
+ jump57 = how_many_until(chr(0x57), offset)
+ jump50 = how_many_until(chr(0x50), offset)
+ jump58 = how_many_until(chr(0x58), offset)
+
+ # whichever command comes first
+ jump = min([jump57, jump50, jump58])
+
+ end_address = offset + jump # we want the address before $57
+
+ lines = process_00_subcommands(offset+1, end_address, debug=debug)
+
+ if show and debug:
+ text = parse_text_at2(offset+1, end_address-offset+1, debug=debug)
+ print text
+
+ command = {"type": command_byte,
+ "start_address": offset,
+ "end_address": end_address,
+ "size": jump,
+ "lines": lines,
+ }
+
+ offset += jump
+ elif command_byte == 0x17:
+ # TX_FAR [pointer][bank]
+ pointer_byte1 = ord(rom[offset+1])
+ pointer_byte2 = ord(rom[offset+2])
+ pointer_bank = ord(rom[offset+3])
+
+ pointer = (pointer_byte1 + (pointer_byte2 << 8))
+ pointer = extract_maps.calculate_pointer(pointer, pointer_bank)
+
+ text = TextScript(pointer, map_group=self.map_group, map_id=self.amp_id, debug=self.debug, \
+ show=self.debug, force=self.debug, label="Target"+self.label.name)
+ if text.is_valid():
+ self.dependencies.append(text)
+
+ command = {"type": command_byte,
+ "start_address": offset,
+ "end_address": offset + 3, # last byte belonging to this command
+ "pointer": pointer, # parameter
+ "text": text,
+ }
+
+ offset += 3 + 1
+ elif command_byte == 0x50 or command_byte == 0x57 or command_byte == 0x58: # end text
+ command = {"type": command_byte,
+ "start_address": offset,
+ "end_address": offset,
+ }
+
+ # this byte simply indicates to end the script
+ end = True
+
+ # this byte simply indicates to end the script
+ if command_byte == 0x50 and ord(rom[offset+1]) == 0x50: # $50$50 means end completely
+ end = True
+ commands[command_counter+1] = command
+
+ # also save the next byte, before we quit
+ commands[command_counter+1]["start_address"] += 1
+ commands[command_counter+1]["end_address"] += 1
+ add_command_byte_to_totals(command_byte)
+ elif command_byte == 0x50: # only end if we started with $0
+ if len(commands.keys()) > 0:
+ if commands[0]["type"] == 0x0: end = True
+ elif command_byte == 0x57 or command_byte == 0x58: # end completely
+ end = True
+ offset += 1 # go past this 0x50
+ elif command_byte == 0x1:
+ # 01 = text from RAM. [01][2-byte pointer]
+ size = 3 # total size, including the command byte
+ pointer_byte1 = ord(rom[offset+1])
+ pointer_byte2 = ord(rom[offset+2])
+
+ command = {"type": command_byte,
+ "start_address": offset+1,
+ "end_address": offset+2, # last byte belonging to this command
+ "pointer": [pointer_byte1, pointer_byte2], # RAM pointer
+ }
+
+ # view near these bytes
+ # subsection = rom[offset:offset+size+1] #peak ahead
+ #for x in subsection:
+ # print hex(ord(x))
+ #print "--"
+
+ offset += 2 + 1 # go to the next byte
+
+ # use this to look at the surrounding bytes
+ if debug:
+ print "next command is: " + hex(ord(rom[offset])) + " ... we are at command number: " + str(command_counter) + " near " + hex(offset) + " on map_id=" + str(map_id)
+ elif command_byte == 0x7:
+ # 07 = shift texts 1 row above (2nd line becomes 1st line); address for next text = 2nd line. [07]
+ size = 1
+ command = {"type": command_byte,
+ "start_address": offset,
+ "end_address": offset,
+ }
+ offset += 1
+ elif command_byte == 0x3:
+ # 03 = set new address in RAM for text. [03][2-byte RAM address]
+ size = 3
+ command = {"type": command_byte, "start_address": offset, "end_address": offset+2}
+ offset += size
+ elif command_byte == 0x4: # draw box
+ # 04 = draw box. [04][2-Byte pointer][height Y][width X]
+ size = 5 # including the command
+ command = {
+ "type": command_byte,
+ "start_address": offset,
+ "end_address": offset + size,
+ "pointer_bytes": [ord(rom[offset+1]), ord(rom[offset+2])],
+ "y": ord(rom[offset+3]),
+ "x": ord(rom[offset+4]),
+ }
+ offset += size + 1
+ elif command_byte == 0x5:
+ # 05 = write text starting at 2nd line of text-box. [05][text][ending command]
+ # read until $57, $50 or $58
+ jump57 = how_many_until(chr(0x57), offset)
+ jump50 = how_many_until(chr(0x50), offset)
+ jump58 = how_many_until(chr(0x58), offset)
+
+ # whichever command comes first
+ jump = min([jump57, jump50, jump58])
+
+ end_address = offset + jump # we want the address before $57
+
+ lines = process_00_subcommands(offset+1, end_address, debug=debug)
+
+ if show and debug:
+ text = parse_text_at2(offset+1, end_address-offset+1, debug=debug)
+ print text
+
+ command = {"type": command_byte,
+ "start_address": offset,
+ "end_address": end_address,
+ "size": jump,
+ "lines": lines,
+ }
+ offset = end_address + 1
+ elif command_byte == 0x6:
+ # 06 = wait for keypress A or B (put blinking arrow in textbox). [06]
+ command = {"type": command_byte, "start_address": offset, "end_address": offset}
+ offset += 1
+ elif command_byte == 0x7:
+ # 07 = shift texts 1 row above (2nd line becomes 1st line); address for next text = 2nd line. [07]
+ command = {"type": command_byte, "start_address": offset, "end_address": offset}
+ offset += 1
+ elif command_byte == 0x8:
+ # 08 = asm until whenever
+ command = {"type": command_byte, "start_address": offset, "end_address": offset}
+ offset += 1
+ end = True
+ elif command_byte == 0x9:
+ # 09 = write hex-to-dec number from RAM to textbox [09][2-byte RAM address][byte bbbbcccc]
+ # bbbb = how many bytes to read (read number is big-endian)
+ # cccc = how many digits display (decimal)
+ #(note: max of decimal digits is 7,i.e. max number correctly displayable is 9999999)
+ ram_address_byte1 = ord(rom[offset+1])
+ ram_address_byte2 = ord(rom[offset+2])
+ read_byte = ord(rom[offset+3])
+
+ command = {
+ "type": command_byte,
+ "address": [ram_address_byte1, ram_address_byte2],
+ "read_byte": read_byte, # split this up when we make a macro for this
+ }
+
+ offset += 4
+ else:
+ #if len(commands) > 0:
+ # print "Unknown text command " + hex(command_byte) + " at " + hex(offset) + ", script began with " + hex(commands[0]["type"])
+ if debug:
+ print "Unknown text command at " + hex(offset) + " - command: " + hex(ord(rom[offset])) + " on map_id=" + str(map_id)
+
+ # end at the first unknown command
+ end = True
+ commands[command_counter] = command
+ command_counter += 1
+ total_text_commands += len(commands)
+
+ text_count += 1
+ #if text_count >= max_texts:
+ # sys.exit()
+
+ self.commands = commands
+ self.last_address = offset
+ script_parse_table[original_address:offset] = self
+ all_texts.append(self)
+ self.size = self.byte_count = self.last_address - original_address
+ return commands
+
+ def get_dependencies(self, recompute=False, global_dependencies=set()):
+ #if recompute:
+ # raise NotImplementedError(bryan_message)
+ global_dependencies.update(self.dependencies)
+ return self.dependencies
+
+ def to_asm(self, label=None):
+ address = self.address
+ start_address = address
+ if label == None: label = self.label.name
+ # using deepcopy because otherwise additional @s get appended each time
+ # like to the end of the text for TextScript(0x5cf3a)
+ commands = deepcopy(self.commands)
+ # apparently this isn't important anymore?
+ needs_to_begin_with_0 = True
+ # start with zero please
+ byte_count = 0
+ # where we store all output
+ output = ""
+ had_text_end_byte = False
+ had_text_end_byte_57_58 = False
+ had_db_last = False
+ xspacing = ""
+ # reset this pretty fast..
+ first_line = True
+ # for each command..
+ for this_command in commands.keys():
+ if not "lines" in commands[this_command].keys():
+ command = commands[this_command]
+ if not "type" in command.keys():
+ print "ERROR in command: " + str(command)
+ continue # dunno what to do here?
+
+ if command["type"] == 0x1: # TX_RAM
+ p1 = command["pointer"][0]
+ p2 = command["pointer"][1]
+
+ # remember to account for big endian -> little endian
+ output += "\n" + xspacing + "TX_RAM $%.2x%.2x" %(p2, p1)
+ byte_count += 3
+ had_db_last = False
+ elif command["type"] == 0x17: # TX_FAR
+ #p1 = command["pointer"][0]
+ #p2 = command["pointer"][1]
+ output += "\n" + xspacing + "TX_FAR _" + label + " ; " + hex(command["pointer"])
+ byte_count += 4 # $17, bank, address word
+ had_db_last = False
+ elif command["type"] == 0x9: # TX_RAM_HEX2DEC
+ # address, read_byte
+ output += "\n" + xspacing + "TX_NUM $%.2x%.2x, $%.2x" % (command["address"][1], command["address"][0], command["read_byte"])
+ had_db_last = False
+ byte_count += 4
+ elif command["type"] == 0x50 and not had_text_end_byte:
+ # had_text_end_byte helps us avoid repeating $50s
+ if had_db_last:
+ output += ", $50"
+ else:
+ output += "\n" + xspacing + "db $50"
+ byte_count += 1
+ had_db_last = True
+ elif command["type"] in [0x57, 0x58] and not had_text_end_byte_57_58:
+ if had_db_last:
+ output += ", $%.2x" % (command["type"])
+ else:
+ output += "\n" + xspacing + "db $%.2x" % (command["type"])
+ byte_count += 1
+ had_db_last = True
+ elif command["type"] in [0x57, 0x58] and had_text_end_byte_57_58:
+ pass # this is ok
+ elif command["type"] == 0x50 and had_text_end_byte:
+ pass # this is also ok
+ elif command["type"] == 0x0b:
+ if had_db_last:
+ output += ", $0b"
+ else:
+ output += "\n" + xspacing + "db $0B"
+ byte_count += 1
+ had_db_last = True
+ elif command["type"] == 0x11:
+ if had_db_last:
+ output += ", $11"
+ else:
+ output += "\n" + xspacing + "db $11"
+ byte_count += 1
+ had_db_last = True
+ elif command["type"] == 0x6: # wait for keypress
+ if had_db_last:
+ output += ", $6"
+ else:
+ output += "\n" + xspacing + "db $6"
+ byte_count += 1
+ had_db_last = True
+ else:
+ print "ERROR in command: " + hex(command["type"])
+ had_db_last = False
+
+ # everything else is for $0s, really
+ continue
+ lines = commands[this_command]["lines"]
+
+ # reset this in case we have non-$0s later
+ had_db_last = False
+
+ # add the ending byte to the last line- always seems $57
+ # this should already be in there, but it's not because of a bug in the text parser
+ lines[len(lines.keys())-1].append(commands[len(commands.keys())-1]["type"])
+
+ first = True # first byte
+ for line_id in lines:
+ line = lines[line_id]
+ output += xspacing + "db "
+ if first and needs_to_begin_with_0:
+ output += "$0, "
+ first = False
+ byte_count += 1
+
+ quotes_open = False
+ first_byte = True
+ was_byte = False
+ for byte in line:
+ if byte == 0x50:
+ had_text_end_byte = True # don't repeat it
+ if byte in [0x58, 0x57]:
+ had_text_end_byte_57_58 = True
+
+ if byte in chars:
+ if not quotes_open and not first_byte: # start text
+ output += ", \""
+ quotes_open = True
+ first_byte = False
+ if not quotes_open and first_byte: # start text
+ output += "\""
+ quotes_open = True
+ output += chars[byte]
+ elif byte in constant_abbreviation_bytes:
+ if quotes_open:
+ output += "\""
+ quotes_open = False
+ if not first_byte:
+ output += ", "
+ output += constant_abbreviation_bytes[byte]
+ else:
+ if quotes_open:
+ output += "\""
+ quotes_open = False
+
+ # if you want the ending byte on the last line
+ #if not (byte == 0x57 or byte == 0x50 or byte == 0x58):
+ if not first_byte:
+ output += ", "
+
+ output += "$" + hex(byte)[2:]
+ was_byte = True
+
+ # add a comma unless it's the end of the line
+ #if byte_count+1 != len(line):
+ # output += ", "
+
+ first_byte = False
+ byte_count += 1
+ # close final quotes
+ if quotes_open:
+ output += "\""
+ quotes_open = False
+
+ output += "\n"
+ #include_newline = "\n"
+ #if len(output)!=0 and output[-1] == "\n":
+ # include_newline = ""
+ #output += include_newline + "; " + hex(start_address) + " + " + str(byte_count) + " bytes = " + hex(start_address + byte_count)
+ if len(output) > 0 and output[-1] == "\n":
+ output = output[:-1]
+ self.size = self.byte_count = byte_count
+ return output
+
+def parse_text_engine_script_at(address, map_group=None, map_id=None, debug=True, show=True, force=False):
+ """parses a text-engine script ("in-text scripts")
+ http://hax.iimarck.us/files/scriptingcodes_eng.htm#InText
+ see parse_text_at2, parse_text_at, and process_00_subcommands
+ """
+ if is_script_already_parsed_at(address) and not force:
+ return script_parse_table[address]
+ return TextScript(address, map_group=map_group, map_id=map_id, debug=debug, show=show, force=force)
+
+def find_text_addresses():
+ """returns a list of text pointers
+ useful for testing parse_text_engine_script_at"""
+ return TextScript.find_addresses()
+
+class EncodedText:
+ """a sequence of bytes that, when decoded, represent readable text
+ based on the chars table from preprocessor.py and other places"""
+ base_label = "UnknownRawText_"
+
+ def __init__(self, address, bank=None, map_group=None, map_id=None, debug=True, label=None):
+ self.address = address
+ if bank:
+ self.bank = bank
+ else:
+ self.bank = calculate_bank(address)
+ self.map_group, self.map_id, self.debug = map_group, map_id, debug
+ if not label:
+ label = self.base_label + hex(address)
+ self.label = Label(name=label, address=address, object=self)
+ self.dependencies = None
+ self.parse()
+ script_parse_table[self.address : self.last_address] = self
+
+ def get_dependencies(self, recompute=False, global_dependencies=set()):
+ return []
+
+ def parse(self):
+ offset = self.address
+
+ # read until $57, $50 or $58
+ jump57 = how_many_until(chr(0x57), offset)
+ jump50 = how_many_until(chr(0x50), offset)
+ jump58 = how_many_until(chr(0x58), offset)
+
+ # whichever command comes first
+ jump = min([jump57, jump50, jump58])
+
+ end_address = offset + jump # we want the address before $57
+
+ text = parse_text_at2(offset, end_address-offset, debug=self.debug)
+
+ if jump == jump50:
+ text += "@"
+
+ self.text = text
+
+ self.last_address = self.end_address = end_address
+
+ def to_asm(self):
+ return "\""+self.text+"\""
+
+ @staticmethod
+ def process_00_subcommands(start_address, end_address, debug=True):
+ """split this text up into multiple lines
+ based on subcommands ending each line"""
+ if debug:
+ print "process_00_subcommands(" + hex(start_address) + ", " + hex(end_address) + ")"
+ lines = {}
+ subsection = rom[start_address:end_address]
+
+ line_count = 0
+ current_line = []
+ for pbyte in subsection:
+ byte = ord(pbyte)
+ current_line.append(byte)
+ if byte == 0x4f or byte == 0x51 or byte == 0x55:
+ lines[line_count] = current_line
+ current_line = []
+ line_count += 1
+
+ # don't forget the last line
+ lines[line_count] = current_line
+ line_count += 1
+ return lines
+
+ @staticmethod
+ def from_bytes(bytes, debug=True, japanese=False):
+ """assembles a string based on bytes looked up in the chars table"""
+ line = ""
+ if japanese: charset = jap_chars
+ else: charset = chars
+ for byte in bytes:
+ if type(byte) != int:
+ byte = ord(byte)
+ if byte in charset.keys():
+ line += charset[byte]
+ elif debug:
+ print "byte not known: " + hex(byte)
+ return line
+
+ @staticmethod
+ def parse_text_at(address, count=10, debug=True, japanese=False):
+ """returns a string of text from an address
+ this does not handle text commands"""
+ output = ""
+ commands = process_00_subcommands(address, address+count, debug=debug)
+ for (line_id, line) in commands.items():
+ output += parse_text_from_bytes(line, debug=debug, japanese=japanese)
+ texts.append([address, output])
+ return output
+
+
+def process_00_subcommands(start_address, end_address, debug=True):
+ """split this text up into multiple lines
+ based on subcommands ending each line"""
+ return EncodedText.process_00_subcommands(start_address, end_address, debug=debug)
+
+def parse_text_from_bytes(bytes, debug=True, japanese=False):
+ """assembles a string based on bytes looked up in the chars table"""
+ return EncodedText.from_bytes(bytes, debug=debug, japanese=japanese)
+
+def parse_text_at(address, count=10, debug=True):
+ """returns a list of bytes from an address
+ see parse_text_at2 for pretty printing"""
+ return parse_text_from_bytes(rom_interval(address, count, strings=False), debug=debug)
+
+def parse_text_at2(address, count=10, debug=True, japanese=False):
+ """returns a string of text from an address
+ this does not handle text commands"""
+ return EncodedText.parse_text_at(address, count, debug=debug, japanese=japanese)
+
+def parse_text_at3(address, map_group=None, map_id=None, debug=False):
+ deh = script_parse_table[address]
+ if deh:
+ return deh
+ else:
+ text = TextScript(address, map_group=map_group, map_id=map_id, debug=debug)
+ if text.is_valid():
+ return text
+ else:
+ return None
+
+def rom_text_at(address, count=10):
+ """prints out raw text from the ROM
+ like for 0x112110"""
+ return "".join([chr(x) for x in rom_interval(address, count, strings=False)])
+
+def get_map_constant_label(map_group=None, map_id=None):
+ """returns PALLET_TOWN for some map group/id pair"""
+ if map_group == None:
+ raise Exception("need map_group")
+ if map_id == None:
+ raise Exception("need map_id")
+
+ global map_internal_ids
+ for (id, each) in map_internal_ids.items():
+ if each["map_group"] == map_group and each["map_id"] == map_id:
+ return each["label"]
+ return None
+
+def get_map_constant_label_by_id(global_id):
+ """returns a map constant label for a particular map id"""
+ global map_internal_ids
+ return map_internal_ids[global_id]["label"]
+
+def get_id_for_map_constant_label(label):
+ """returns some global id for a given map constant label
+ PALLET_TOWN = 1, for instance."""
+ global map_internal_ids
+ for (id, each) in map_internal_ids.items():
+ if each["label"] == label:
+ return id
+ return None
+
+def generate_map_constant_labels():
+ """generates the global for this script
+ mapping ids to map groups/ids/labels"""
+ global map_internal_ids
+ map_internal_ids = {}
+ i = 0
+ for map_group in map_names.keys():
+ for map_id in map_names[map_group].keys():
+ if map_id == "offset": continue
+ cmap = map_names[map_group][map_id]
+ name = cmap["name"]
+ name = name.replace("Pokémon Center", "PokeCenter").\
+ replace(" ", "_").\
+ replace("-", "_").\
+ replace("é", "e")
+ constant_label = map_name_cleaner(name).upper()
+ map_internal_ids[i] = {"label": constant_label,
+ "map_id": map_id,
+ "map_group": map_group}
+ i += 1
+ return map_internal_ids
+
+# see generate_map_constant_labels() later
+def generate_map_constants():
+ """generates content for constants.asm
+ this will generate two macros: GROUP and MAP"""
+ global map_internal_ids
+ if map_internal_ids == None or map_internal_ids == {}:
+ generate_map_constant_labels()
+ globals, groups, maps = "", "", ""
+ for (id, each) in map_internal_ids.items():
+ label = each["label"].replace("-", "_").replace("é", "e").upper()
+
+ groups += "GROUP_"+ label + " EQU $%.2x" % (each["map_group"])
+ groups += "\n"
+ maps += "MAP_"+ label + " EQU $%.2x" % (each["map_id"])
+ maps += "\n"
+ globals += label + " EQU $%.2x" % (id)
+ globals += "\n"
+ #for multi-byte constants:
+ #print each["label"] + " EQUS \"$%.2x,$%.2x\"" % (each["map_group"], each["map_id"])
+ print globals
+ print groups
+ print maps
+
+def generate_map_constants_dimensions():
+ """
+ Generate _WIDTH and _HEIGHT properties.
+ """
+ global map_internal_ids
+ output = ""
+ if map_internal_ids == None or map_internal_ids == {}:
+ generate_map_constant_labels()
+ for (id, each) in map_internal_ids.items():
+ map_group = each["map_group"]
+ map_id = each["map_id"]
+ label = each["label"].replace("-", "_").replace("é", "e").upper()
+ output += label + "_HEIGHT EQU %d\n" % (map_names[map_group][map_id]["header_new"].second_map_header.height.byte)
+ output += label + "_WIDTH EQU %d\n" % (map_names[map_group][map_id]["header_new"].second_map_header.width.byte)
+ return output
+
+def transform_wildmons(asm):
+ """
+ Converts a wildmons section to use map constants.
+ input: wildmons text.
+ """
+ asmlines = asm.split("\n")
+ returnlines = []
+ for line in asmlines:
+ if "; " in line and not ("day" in line or "morn" in line or "nite" in line or "0x" in line or "encounter" in line) \
+ and line != "" and line.split("; ")[0] != "":
+ map_group = int(line.split("\tdb ")[1].split(",")[0].replace("$", "0x"), base=16)
+ map_id = int(line.split("\tdb ")[1].split(",")[1].replace("$", "0x").split("; ")[0], base=16)
+ label = get_map_constant_label(map_group=map_group, map_id=map_id)
+ returnlines.append("\tdb GROUP_"+label+", MAP_"+label) #+" ; " + line.split(";")[1])
+ else:
+ returnlines.append(line)
+ return "\n".join(returnlines)
+
+def parse_script_asm_at(*args, **kwargs):
+ # XXX TODO
+ return None
+
+def find_all_text_pointers_in_script_engine_script(script, bank=None, debug=False):
+ """returns a list of text pointers
+ based on each script-engine script command"""
+ # TODO: recursively follow any jumps in the script
+ if script == None:
+ return []
+ addresses = set()
+ for (k, command) in enumerate(script.commands):
+ if debug:
+ print "command is: " + str(command)
+ if command.id == 0x4B:
+ addresses.add(command.params[0].parsed_address)
+ elif command.id == 0x4C:
+ addresses.add(command.params[0].parsed_address)
+ elif command.id == 0x51:
+ addresses.add(command.params[0].parsed_address)
+ elif command.id == 0x53:
+ addresses.add(command.params[0].parsed_address)
+ elif command.id == 0x64:
+ addresses.add(command.params[0].parsed_address)
+ addresses.add(command.params[1].parsed_address)
+ return addresses
+
+def translate_command_byte(crystal=None, gold=None):
+ """takes a command byte from either crystal or gold
+ returns the command byte in the other (non-given) game
+
+ The new commands are values 0x52 and 0x9F. This means:
+ Crystal's 0x00–0x51 correspond to Gold's 0x00–0x51
+ Crystal's 0x53–0x9E correspond to Gold's 0x52–0x9D
+ Crystal's 0xA0–0xA5 correspond to Gold's 0x9E–0xA3
+
+ see: http://www.pokecommunity.com/showpost.php?p=4347261
+ """
+ if crystal != None: # convert to gold
+ if crystal <= 0x51: return crystal
+ if crystal == 0x52: return None
+ if 0x53 <= crystal <= 0x9E: return crystal-1
+ if crystal == 0x9F: return None
+ if 0xA0 <= crystal <= 0xA5: return crystal-2
+ if crystal > 0xA5:
+ raise Exception("dunno yet if crystal has new insertions after crystal:0xA5 (gold:0xA3)")
+ elif gold != None: # convert to crystal
+ if gold <= 0x51: return gold
+ if 0x52 <= gold <= 0x9D: return gold+1
+ if 0x9E <= gold <= 0xA3: return gold+2
+ if gold > 0xA3:
+ raise Exception("dunno yet if crystal has new insertions after gold:0xA3 (crystal:0xA5)")
+ else:
+ raise Exception("translate_command_byte needs either a crystal or gold command")
+
+class SingleByteParam():
+ """or SingleByte(CommandParam)"""
+ size = 1
+ should_be_decimal = False
+ byte_type = "db"
+
+ def __init__(self, *args, **kwargs):
+ for (key, value) in kwargs.items():
+ setattr(self, key, value)
+ # check address
+ if not hasattr(self, "address"):
+ raise Exception("an address is a requirement")
+ elif self.address == None:
+ raise Exception("address must not be None")
+ elif not is_valid_address(self.address):
+ raise Exception("address must be valid")
+ # check size
+ if not hasattr(self, "size") or self.size == None:
+ raise Exception("size is probably 1?")
+ # parse bytes from ROM
+ self.parse()
+
+ def parse(self): self.byte = ord(rom[self.address])
+
+ def get_dependencies(self, recompute=False, global_dependencies=set()):
+ return []
+
+ def to_asm(self):
+ if not self.should_be_decimal:
+ return hex(self.byte).replace("0x", "$")
+ else:
+ return str(self.byte)
+
+class DollarSignByte(SingleByteParam):
+ def to_asm(self):
+ return hex(self.byte).replace("0x", "$")
+HexByte=DollarSignByte
+
+class ItemLabelByte(DollarSignByte):
+ def to_asm(self):
+ label = find_item_label_by_id(self.byte)
+ if label:
+ return label
+ elif not label:
+ return DollarSignByte.to_asm(self)
+
+
+class DecimalParam(SingleByteParam):
+ should_be_decimal = True
+
+
+class MultiByteParam():
+ """or MultiByte(CommandParam)"""
+ size = 2
+ should_be_decimal = False
+ byte_type = "dw"
+
+ def __init__(self, *args, **kwargs):
+ self.prefix = "$" # default.. feel free to set 0x in kwargs
+ for (key, value) in kwargs.items():
+ setattr(self, key, value)
+ # check address
+ if not hasattr(self, "address") or self.address == None:
+ raise Exception("an address is a requirement")
+ elif not is_valid_address(self.address):
+ raise Exception("address must be valid")
+ # check size
+ if not hasattr(self, "size") or self.size == None:
+ raise Exception("don't know how many bytes to read (size)")
+ self.parse()
+
+ def parse(self):
+ self.bytes = rom_interval(self.address, self.size, strings=False)
+ self.parsed_number = self.bytes[0] + (self.bytes[1] << 8)
+ if hasattr(self, "bank"):
+ self.parsed_address = calculate_pointer_from_bytes_at(self.address, bank=self.bank)
+ else:
+ self.parsed_address = calculate_pointer_from_bytes_at(self.address, bank=None)
+
+ def get_dependencies(self, recompute=False, global_dependencies=set()):
+ return []
+
+ # you won't actually use this to_asm because it's too generic
+ #def to_asm(self): return ", ".join([(self.prefix+"%.2x")%x for x in self.bytes])
+ def to_asm(self):
+ if not self.should_be_decimal:
+ return self.prefix+"".join([("%.2x")%x for x in reversed(self.bytes)])
+ elif self.should_be_decimal:
+ decimal = int("0x"+"".join([("%.2x")%x for x in reversed(self.bytes)]), 16)
+ return str(decimal)
+
+
+class PointerLabelParam(MultiByteParam):
+ # default size is 2 bytes
+ default_size = 2
+ size = 2
+ # default is to not parse out a bank
+ bank = False
+ force = False
+ debug = False
+
+ def __init__(self, *args, **kwargs):
+ self.dependencies = None
+ # bank can be overriden
+ if "bank" in kwargs.keys():
+ if kwargs["bank"] != False and kwargs["bank"] != None and kwargs["bank"] in [True, "reverse"]:
+ # not +=1 because child classes set size=3 already
+ self.size = self.default_size + 1
+ self.given_bank = kwargs["bank"]
+ #if kwargs["bank"] not in [None, False, True, "reverse"]:
+ # raise Exception("bank cannot be: " + str(kwargs["bank"]))
+ if self.size > 3:
+ raise Exception("param size is too large")
+ # continue instantiation.. self.bank will be set down the road
+ MultiByteParam.__init__(self, *args, **kwargs)
+
+ def parse(self):
+ self.parsed_address = calculate_pointer_from_bytes_at(self.address, bank=self.bank)
+ MultiByteParam.parse(self)
+
+ def get_dependencies(self, recompute=False, global_dependencies=set()):
+ dependencies = []
+ if self.parsed_address == self.address:
+ return dependencies
+ if self.dependencies != None and not recompute:
+ global_dependencies.update(self.dependencies)
+ return self.dependencies
+ thing = script_parse_table[self.parsed_address]
+ if thing and thing.address == self.parsed_address and not (thing is self):
+ #if self.debug:
+ # print "parsed address is: " + hex(self.parsed_address) + " with label: " + thing.label.name + " of type: " + str(thing.__class__)
+ dependencies.append(thing)
+ if not thing in global_dependencies:
+ global_dependencies.add(thing)
+ more = thing.get_dependencies(recompute=recompute, global_dependencies=global_dependencies)
+ dependencies.extend(more)
+ self.dependencies = dependencies
+ return dependencies
+
+ def to_asm(self):
+ bank = self.bank
+ # we pass bank= for whether or not to include a bank byte when reading
+ #.. it's not related to caddress
+ caddress = None
+ if not (hasattr(self, "parsed_address") and self.parsed_address != None):
+ caddress = calculate_pointer_from_bytes_at(self.address, bank=self.bank)
+ else:
+ caddress = self.parsed_address
+ label = get_label_for(caddress)
+ pointer_part = label # use the label, if it is found
+
+ # check that the label actually points to the right place
+ result = script_parse_table[caddress]
+ if result != None and hasattr(result, "label"):
+ if result.label.name != label:
+ label = None
+ elif result.address != caddress:
+ label = None
+ elif result != None:
+ label = None
+
+ # setup output bytes if the label was not found
+ if not label:
+ #pointer_part = (", ".join([(self.prefix+"%.2x")%x for x in reversed(self.bytes[1:])]))
+ pointer_part = self.prefix+("%.2x"%self.bytes[1])+("%.2x"%self.bytes[0])
+
+ # bank positioning matters!
+ if bank == True or bank == "reverse": # bank, pointer
+ # possibly use BANK(LABEL) if we know the bank
+ if not label:
+ bank_part = ((self.prefix+"%.2x")%bank)
+ else:
+ if "$" in label:
+ if 0x4000 <= caddress <= 0x7FFF:
+ #bank_part = "$%.2x" % (calculate_bank(self.parent.parent.address))
+ bank_part = "1"
+ else:
+ bank_part = "$%.2x" % (calculate_bank(caddress))
+ else:
+ bank_part = "BANK("+label+")"
+ # return the asm based on the order the bytes were specified to be in
+ if bank == "reverse": # pointer, bank
+ return pointer_part+", "+bank_part
+ elif bank == True: # bank, pointer
+ return bank_part+", "+pointer_part
+ else:
+ raise Exception("this should never happen")
+ raise Exception("this should never happen")
+ # this next one will either return the label or the raw bytes
+ elif bank == False or bank == None: # pointer
+ return pointer_part # this could be the same as label
+ else:
+ #raise Exception("this should never happen")
+ return pointer_part # probably in the same bank ?
+ raise Exception("this should never happen")
+
+class PointerLabelBeforeBank(PointerLabelParam):
+ bank = True # bank appears first, see calculate_pointer_from_bytes_at
+ size = 3
+ byte_type = "dw"
+
+class PointerLabelAfterBank(PointerLabelParam):
+ bank = "reverse" # bank appears last, see calculate_pointer_from_bytes_at
+ size = 3
+
+
+class ScriptPointerLabelParam(PointerLabelParam): pass
+
+
+class ScriptPointerLabelBeforeBank(PointerLabelBeforeBank): pass
+
+
+class ScriptPointerLabelAfterBank(PointerLabelAfterBank): pass
+
+
+def _parse_script_pointer_bytes(self, debug = False):
+ PointerLabelParam.parse(self)
+ if debug: print "_parse_script_pointer_bytes - calculating the pointer located at " + hex(self.address)
+ address = calculate_pointer_from_bytes_at(self.address, bank=self.bank)
+ if address != None and address > 0x4000:
+ if debug: print "_parse_script_pointer_bytes - the pointer is: " + hex(address)
+ self.script = parse_script_engine_script_at(address, debug=self.debug, force=self.force, map_group=self.map_group, map_id=self.map_id)
+ScriptPointerLabelParam.parse = _parse_script_pointer_bytes
+ScriptPointerLabelBeforeBank.parse = _parse_script_pointer_bytes
+ScriptPointerLabelAfterBank.parse = _parse_script_pointer_bytes
+
+class PointerLabelToScriptPointer(PointerLabelParam):
+ def parse(self):
+ PointerLabelParam.parse(self)
+ address = calculate_pointer_from_bytes_at(self.parsed_address, bank=self.bank)
+ address2 = calculate_pointer_from_bytes_at(address, bank=True)
+ self.script = parse_script_engine_script_at(address2, origin=False, map_group=self.map_group, map_id=self.map_id, force=self.force, debug=self.debug)
+
+
+class AsmPointerParam(PointerLabelBeforeBank):
+ def parse(self):
+ PointerLabelBeforeBank.parse(self)
+ address = calculate_pointer_from_bytes_at(self.address, bank=self.bank) # 3-byte pointer
+ self.asm = parse_script_asm_at(address, map_group=self.map_group, map_id=self.map_id, force=self.force, debug=self.debug) # might end in some specific way?
+
+
+class PointerToAsmPointerParam(PointerLabelParam):
+ def parse(self):
+ PointerLabelParam.parse(self)
+ address = calculate_pointer_from_bytes_at(self.address, bank=self.bank) # 2-byte pointer
+ address2 = calculate_pointer_from_bytes_at(address, bank="reverse") # maybe not "reverse"?
+ self.asm = parse_script_asm_at(address, map_group=self.map_group, map_id=self.map_id, force=self.force, debug=self.debug) # might end in some specific way?
+
+
+class RAMAddressParam(MultiByteParam):
+ def to_asm(self):
+ address = calculate_pointer_from_bytes_at(self.address, bank=False)
+ label = get_ram_label(address)
+ if label:
+ return label
+ else:
+ return "$"+"".join(["%.2x"%x for x in reversed(self.bytes)])+""
+
+
+class MoneyByteParam(MultiByteParam):
+ size = 3
+ max_value = 0x0F423F
+ should_be_decimal = True
+ def parse(self):
+ MultiByteParam.parse(self)
+ # in the rom as xxyyzz
+ self.x = self.bytes[2]
+ self.y = self.bytes[1]
+ self.z = self.bytes[0]
+ def to_asm(self):
+ return str(self.x + (self.y << 8) + (self.z << 16))
+
+ # this is used by the preprocessor
+ @staticmethod
+ def from_asm(value):
+ # max is 0F423F
+ # z = 0x0F ; y = 0x42 ; x = 0x3F
+ # 999999 = x + (y << 8) + (z << 16)
+
+ value = int(value)
+
+ x = (value & 0x0000FF)
+ y = (value & 0x00FF00) >> 8
+ z = (value & 0xFF0000) >> 16
+
+ return str(z) + "\ndb "+str(y)+"\ndb "+str(x)
+
+def read_money(address, dohex=False):
+ z = ord(rom[address])
+ y = ord(rom[address+1])
+ x = ord(rom[address+2])
+ answer = x + (y << 8) + (z << 16)
+ if not dohex:
+ return answer
+ else:
+ return hex(answer)
+
+def write_money(money):
+ value = money
+ x = (value & 0x0000FF)
+ y = (value & 0x00FF00) >> 8
+ z = (value & 0xFF0000) >> 16
+ return "db "+str(z)+"\ndb "+str(y)+"\ndb "+str(x)
+
+class CoinByteParam(MultiByteParam):
+ size = 2
+ max_value = 0x270F
+ should_be_decimal = True
+
+
+class MapGroupParam(SingleByteParam):
+ def to_asm(self):
+ map_id = ord(rom[self.address+1])
+ map_constant_label = get_map_constant_label(map_id=map_id, map_group=self.byte) # like PALLET_TOWN
+ if map_constant_label == None:
+ return str(self.byte)
+ #else: return "GROUP("+map_constant_label+")"
+ else:
+ return "GROUP_"+map_constant_label
+
+
+class MapIdParam(SingleByteParam):
+ def parse(self):
+ SingleByteParam.parse(self)
+ self.map_group = ord(rom[self.address-1])
+
+ def to_asm(self):
+ map_group = ord(rom[self.address-1])
+ map_constant_label = get_map_constant_label(map_id=self.byte, map_group=map_group)
+ if map_constant_label == None:
+ return str(self.byte)
+ #else: return "MAP("+map_constant_label+")"
+ else:
+ return "MAP_"+map_constant_label
+
+
+class MapGroupIdParam(MultiByteParam):
+ def parse(self):
+ MultiByteParam.parse(self)
+ self.map_group = self.bytes[0]
+ self.map_id = self.bytes[1]
+
+ def to_asm(self):
+ map_group = self.map_group
+ map_id = self.map_id
+ label = get_map_constant_label(map_group=map_group, map_id=map_id)
+ return label
+
+
+class PokemonParam(SingleByteParam):
+ def to_asm(self):
+ pokemon_constant = get_pokemon_constant_by_id(self.byte)
+ if pokemon_constant:
+ return pokemon_constant
+ else:
+ return str(self.byte)
+
+
+class PointerParamToItemAndLetter(MultiByteParam):
+ # [2F][2byte pointer to item no + 0x20 bytes letter text]
+ #raise NotImplementedError(bryan_message)
+ pass
+
+
+class TrainerIdParam(SingleByteParam):
+ def to_asm(self):
+ # find the group id by first finding the param type id
+ i = 0
+ foundit = None
+ for (k, v) in self.parent.param_types.items():
+ if v["class"] == TrainerGroupParam:
+ foundit = i
+ break
+ i += 1
+
+ if foundit == None:
+ raise Exception("didn't find a TrainerGroupParam in this command??")
+
+ # now get the trainer group id
+ trainer_group_id = self.parent.params[foundit].byte
+
+ # check the rule to see whether to use an id or not
+ if ("uses_numeric_trainer_ids" in trainer_group_names[trainer_group_id].keys()) or \
+ (not "trainer_names" in trainer_group_names[trainer_group_id].keys()):
+ return str(self.byte)
+ else:
+ return trainer_group_names[trainer_group_id]["trainer_names"][self.byte-1]
+
+class TrainerGroupParam(SingleByteParam):
+ def to_asm(self):
+ trainer_group_id = self.byte
+ return trainer_group_names[trainer_group_id]["constant"]
+
+class MoveParam(SingleByteParam):
+ def to_asm(self):
+ if self.byte in moves.keys():
+ return moves[self.byte]
+ else:
+ # this happens for move=0 (no move) in trainer headers
+ return str(self.byte)
+
+class MenuDataPointerParam(PointerLabelParam):
+ # read menu data at the target site
+ #raise NotImplementedError(bryan_message)
+ pass
+
+
+string_to_text_texts = []
+class RawTextPointerLabelParam(PointerLabelParam):
+ # not sure if these are always to a text script or raw text?
+ def parse(self):
+ PointerLabelParam.parse(self)
+ #bank = calculate_bank(self.address)
+ address = calculate_pointer_from_bytes_at(self.address, bank=False)
+ self.calculated_address = address
+ #self.text = parse_text_at3(address, map_group=self.map_group, map_id=self.map_id, debug=self.debug)
+ #self.text = TextScript(address, map_group=self.map_group, map_id=self.map_id, debug=self.debug)
+ self.text = parse_text_engine_script_at(address, map_group=self.map_group, map_id=self.map_id, debug=self.debug)
+
+ def get_dependencies(self, recompute=False, global_dependencies=set()):
+ global_dependencies.add(self.text)
+ return [self.text]
+
+class EncodedTextLabelParam(PointerLabelParam):
+ def parse(self):
+ PointerLabelParam.parse(self)
+
+ address = calculate_pointer_from_bytes_at(self.address, bank=False)
+ self.parsed_address = address
+ self.text = EncodedText(address, map_group=self.map_group, map_id=self.map_id, debug=self.debug)
+
+ if isinstance(self.text, EncodedText):
+ string_to_text_texts.append(self.text)
+
+ def get_dependencies(self, recompute=False, global_dependencies=set()):
+ global_dependencies.add(self.text)
+ return [self.text]
+
+class TextPointerLabelParam(PointerLabelParam):
+ """this is a pointer to a text script"""
+ bank = False
+ text = None
+ def parse(self):
+ PointerLabelParam.parse(self)
+ address = calculate_pointer_from_bytes_at(self.address, bank=self.bank)
+ if address != None and address != 0:
+ self.text = parse_text_engine_script_at(address, map_group=self.map_group, map_id=self.map_id, force=self.force, debug=self.debug)
+ if not self.text:
+ self.text = script_parse_table[address]
+
+ def get_dependencies(self, recompute=False, global_dependencies=set()):
+ if self.text:
+ global_dependencies.add(self.text)
+ return [self.text]
+ else:
+ return []
+
+class TextPointerLabelAfterBankParam(PointerLabelAfterBank):
+ text = None
+ def parse(self):
+ PointerLabelAfterBank.parse(self)
+ address = calculate_pointer_from_bytes_at(self.address, bank=self.bank)
+ if address != None and address != 0:
+ self.text = parse_text_engine_script_at(address, map_group=self.map_group, map_id=self.map_id, force=self.force, debug=self.debug)
+ if not self.text:
+ self.text = script_parse_table[address]
+
+ def get_dependencies(self, recompute=False, global_dependencies=set()):
+ if self.text:
+ global_dependencies.add(self.text)
+ return [self.text]
+ else:
+ return []
+
+class MovementPointerLabelParam(PointerLabelParam):
+ def parse(self):
+ PointerLabelParam.parse(self)
+ if is_script_already_parsed_at(self.parsed_address):
+ self.movement = script_parse_table[self.parsed_address]
+ else:
+ self.movement = ApplyMovementData(self.parsed_address, map_group=self.map_group, map_id=self.map_id, debug=self.debug)
+
+ def get_dependencies(self, recompute=False, global_dependencies=set()):
+ if hasattr(self, "movement") and self.movement:
+ global_dependencies.add(self.movement)
+ return [self.movement] + self.movement.get_dependencies(recompute=recompute, global_dependencies=global_dependencies)
+ else:
+ raise Exception("MovementPointerLabelParam hasn't been parsed yet")
+
+class MapDataPointerParam(PointerLabelParam):
+ pass
+
+class Command:
+ """
+ Note: when dumping to asm, anything in script_parse_table that directly
+ inherits Command should not be .to_asm()'d.
+ """
+ # use this when the "byte id" doesn't matter
+ # .. for example, a non-script command doesn't use the "byte id"
+ override_byte_check = False
+ base_label = "UnseenLabel_"
+
+ def __init__(self, address=None, *pargs, **kwargs):
+ """params:
+ address - where the command starts
+ force - whether or not to force the script to be parsed (default False)
+ debug - are we in debug mode? default False
+ map_group
+ map_id
+ """
+ defaults = {"force": False, "debug": False, "map_group": None, "map_id": None}
+ if not is_valid_address(address):
+ raise Exception("address is invalid")
+ # set up some variables
+ self.address = address
+ self.last_address = None
+ # setup the label based on base_label if available
+ label = self.base_label + hex(self.address)
+ self.label = Label(name=label, address=address, object=self)
+ # params are where this command's byte parameters are stored
+ self.params = {}
+ self.dependencies = None
+ # override default settings
+ defaults.update(kwargs)
+ # set everything
+ for (key, value) in defaults.items():
+ setattr(self, key, value)
+ # but also store these kwargs
+ self.args = defaults
+ # start parsing this command's parameter bytes
+ self.parse()
+
+ def get_dependencies(self, recompute=False, global_dependencies=set()):
+ dependencies = []
+ #if self.dependencies != None and not recompute:
+ # global_dependencies.update(self.dependencies)
+ # return self.dependencies
+ for (key, param) in self.params.items():
+ if hasattr(param, "get_dependencies") and param != self:
+ deps = param.get_dependencies(recompute=recompute, global_dependencies=global_dependencies)
+ if deps != None and not self in deps:
+ dependencies.extend(deps)
+ self.dependencies = dependencies
+ return dependencies
+
+ def to_asm(self):
+ # start with the rgbasm macro name for this command
+ output = ""
+ #if len(self.macro_name) > 0 and self.macro_name[0].isdigit():
+ # output += "_"
+ output += self.macro_name
+ # return if there are no params
+ if len(self.param_types.keys()) == 0:
+ return output
+ # first one will have no prefixing comma
+ first = True
+ # start reading the bytes after the command byte
+ if not self.override_byte_check:
+ current_address = self.address+1
+ else:
+ current_address = self.address
+ #output = self.macro_name + ", ".join([param.to_asm() for (key, param) in self.params.items()])
+ # add each param
+ for (key, param) in self.params.items():
+ name = param.name
+ # the first param shouldn't have ", " prefixed
+ if first:
+ output += " "
+ first = False
+ # but all other params should
+ else: output += ", "
+ # now add the asm-compatible param string
+ output += param.to_asm()
+ current_address += param.size
+ #for param_type in self.param_types:
+ # name = param_type["name"]
+ # klass = param_type["klass"]
+ # # create an instance of this type
+ # # tell it to begin parsing at this latest byte
+ # obj = klass(address=current_address)
+ # # the first param shouldn't have ", " prefixed
+ # if first: first = False
+ # # but all other params should
+ # else: output += ", "
+ # # now add the asm-compatible param string
+ # output += obj.to_asm()
+ # current_address += obj.size
+ return output
+
+ def parse(self):
+ # id, size (inclusive), param_types
+ #param_type = {"name": each[1], "class": each[0]}
+ if not self.override_byte_check:
+ current_address = self.address+1
+ else:
+ current_address = self.address
+ byte = ord(rom[self.address])
+ if not self.override_byte_check and (not byte == self.id):
+ raise Exception("byte ("+hex(byte)+") != self.id ("+hex(self.id)+")")
+ i = 0
+ for (key, param_type) in self.param_types.items():
+ name = param_type["name"]
+ klass = param_type["class"]
+ # make an instance of this class, like SingleByteParam()
+ # or ItemLabelByte.. by making an instance, obj.parse() is called
+ obj = klass(address=current_address, name=name, parent=self, **dict([(k,v) for (k, v) in self.args.items() if k not in ["parent"]]))
+ # save this for later
+ self.params[i] = obj
+ # increment our counters
+ current_address += obj.size
+ i += 1
+ self.last_address = current_address
+ return True
+
+
+class GivePoke(Command):
+ id = 0x2D
+ macro_name = "givepoke"
+ size = 4 # minimum
+ end = False
+ param_types = {
+ 0: {"name": "pokemon", "class": PokemonParam},
+ 1: {"name": "level", "class": DecimalParam},
+ 2: {"name": "item", "class": ItemLabelByte},
+ 3: {"name": "trainer", "class": DecimalParam},
+ 4: {"name": "trainer_name_pointer", "class": MultiByteParam}, # should probably use TextLabelParam
+ 5: {"name": "pkmn_nickname", "class": MultiByteParam}, # XXX TextLabelParam ?
+ }
+ allowed_lengths = [4, 6]
+
+ def parse(self):
+ self.params = {}
+ byte = ord(rom[self.address])
+ if not byte == self.id:
+ raise Exception("this should never happen")
+ current_address = self.address+1
+ i = 0
+ self.size = 1
+ for (key, param_type) in self.param_types.items():
+ # stop executing after the 4th byte unless it == 0x1
+ if i == 4: print "self.params[3].byte is: " + str(self.params[3].byte)
+ if i == 4 and self.params[3].byte != 1: break
+ name = param_type["name"]
+ klass = param_type["class"]
+ # make an instance of this class, like SingleByteParam()
+ # or ItemLabelByte.. by making an instance, obj.parse() is called
+ obj = klass(address=current_address, name=name)
+ # save this for later
+ self.params[i] = obj
+ # increment our counters
+ current_address += obj.size
+ self.size += obj.size
+ i += 1
+ self.last_address = current_address
+ return True
+
+class DataByteWordMacro(Command):
+ """
+ Only used by the preprocessor.
+ """
+
+ id = None
+ macro_name = "dbw"
+ size = 3
+ override_byte_check = True
+
+ param_types = {
+ 0: {"name": "db value", "class": DecimalParam},
+ 1: {"name": "dw value", "class": PointerLabelParam},
+ }
+
+ def __init__(self): pass
+ def parse(self): pass
+ def to_asm(self): pass
+
+class MovementCommand(Command):
+ # the vast majority of movement commands do not end the movement script
+ end = False
+
+ # this is only used for e.g. macros that don't appear as a byte in the ROM
+ # don't use the override because all movements are specified with a byte
+ override_byte_check = False
+
+ # most commands have size=1 but one or two have a single parameter (gasp)
+ size = 1
+
+ param_types = {}
+ params = []
+
+ # most movement commands won't have any dependencies
+ # get_dependencies on Command will look at the values of params
+ # so this doesn't need to be specified by MovementCommand as long as it extends Command
+ #def get_dependencies(self, recompute=False, global_dependencies=set()):
+ # return []
+
+ def parse(self):
+ if ord(rom[self.address]) < 0x45:
+ # this is mostly handled in to_asm
+ pass
+ else:
+ Command.parse(self)
+
+ def to_asm(self):
+ # return "db $%.2x"%(self.byte)
+ return Command.to_asm(self)
+
+class MovementDBCommand(Command):
+ end = False
+ macro_name = "db"
+ override_byte_check = True
+ id = None
+ byte = None
+ size = 1
+ param_types = {
+ 0: {"name": "db value", "class": SingleByteParam},
+ }
+ params = []
+
+ def to_asm(self):
+ asm = Command.to_asm(self)
+ return asm + " ; movement"
+
+# down, up, left, right
+movement_command_bases = {
+ 0x00: "turn_head",
+ 0x04: "half_step",
+ 0x08: "slow_step", # small_step?
+ 0x0C: "step",
+ 0x10: "big_step", # fast_step?
+ 0x14: "slow_slide_step",
+ 0x18: "slide_step",
+ 0x1C: "fast_slide_step",
+ 0x20: "turn_away",
+ 0x24: "turn_in", # towards?
+ 0x28: "turn_waterfall", # what??
+ 0x2C: "slow_jump_step",
+ 0x30: "jump_step",
+ 0x34: "fast_jump_step",
+
+ # tauwasser says the pattern stops at $45 but $38 looks more realistic?
+ 0x3A: "remove_fixed_facing",
+ 0x3B: "fix_facing",
+ 0x3D: "hide_person",
+ 0x3E: "show_person",
+ 0x45: "accelerate_last",
+ 0x46: ["step_sleep", ["duration", DecimalParam]],
+ 0x47: "step_end",
+ 0x49: "hide_person",
+
+ # do these next two have any params ??
+ 0x4C: "teleport_from",
+ 0x4D: "teleport_to",
+
+ 0x4E: "skyfall",
+ 0x4F: "step_wait5",
+ 0x53: "hide_emote",
+ 0x54: "show_emote",
+ 0x55: ["step_shake", ["displacement", DecimalParam]],
+}
+
+# create MovementCommands from movement_command_bases
+def create_movement_commands(debug=False):
+ """
+ Creates MovementCommands from movement_command_bases. This is just a cheap
+ trick instead of manually defining all of those classes.
+ """
+ #movement_command_classes = inspect.getmembers(sys.modules[__name__], \
+ # lambda obj: inspect.isclass(obj) and \
+ # issubclass(obj, MovementCommand) and \
+ # not (obj is MovementCommand))
+ movement_command_classes2 = []
+ for (byte, cmd) in movement_command_bases.items():
+ if type(cmd) == str:
+ cmd = [cmd]
+ cmd_name = cmd[0].replace(" ", "_")
+ params = {"id": byte, "size": 1, "end": byte is 0x47, "macro_name": cmd_name}
+ params["param_types"] = {}
+ if len(cmd) > 1:
+ param_types = cmd[1:]
+ for (i, each) in enumerate(param_types):
+ thing = {"name": each[0], "class": each[1]}
+ params["param_types"][i] = thing
+ if debug:
+ print "each is: " + str(each)
+ print "thing[class] is: " + str(thing["class"])
+ params["size"] += thing["class"].size
+
+ if byte <= 0x34:
+ for x in range(0, 4):
+
+ direction = None
+ if x == 0:
+ direction = "down"
+ elif x == 1:
+ direction = "up"
+ elif x == 2:
+ direction = "left"
+ elif x == 3:
+ direction = "right"
+ else:
+ raise Exception("this should never happen")
+
+ cmd_name = cmd[0].replace(" ", "_") + "_" + direction
+ klass_name = cmd_name+"Command"
+ params["id"] = copy(byte)
+ params["macro_name"] = cmd_name
+ klass = classobj(copy(klass_name), (MovementCommand,), deepcopy(params))
+ globals()[klass_name] = klass
+ movement_command_classes2.append(klass)
+
+ byte += 1
+ del cmd_name
+ del params
+ del klass_name
+ else:
+ klass_name = cmd_name+"Command"
+ klass = classobj(klass_name, (MovementCommand,), params)
+ globals()[klass_name] = klass
+ movement_command_classes2.append(klass)
+ # later an individual klass will be instantiated to handle something
+ return movement_command_classes2
+
+movement_command_classes = create_movement_commands()
+
+all_movements = []
+class ApplyMovementData:
+ base_label = "MovementData_"
+
+ def __init__(self, address, map_group=None, map_id=None, debug=False, label=None, force=False):
+ self.address = address
+ self.map_group = map_group
+ self.map_id = map_id
+ self.debug = debug
+ self.force = force
+
+ if not label:
+ label = self.base_label + hex(address)
+ self.label = Label(name=label, address=address, object=self)
+
+ self.dependencies = []
+ self.commands = []
+
+ self.parse()
+
+ # this is almost an exact copy of Script.parse
+ # with the exception of using text_command_classes instead of command_classes
+ def parse(self):
+ global movement_command_classes, script_parse_table
+ address = self.address
+
+ # i feel like checking myself
+ assert is_valid_address(address), "ApplyMovementData.parse must be given a valid address"
+
+ current_address = copy(self.address)
+ start_address = copy(current_address)
+
+ # don't clutter up my screen
+ if self.debug:
+ print "ApplyMovementData.parse address="+hex(self.address)+" map_group="+str(self.map_group)+" map_id="+str(self.map_id)
+
+ # load up the rom if it hasn't been loaded already
+ load_rom()
+
+ # in the event that the script parsing fails.. it would be nice to leave evidence
+ script_parse_table[start_address:start_address+1] = "incomplete ApplyMovementData.parse"
+
+ # start with a blank script
+ commands = []
+
+ # use this to control the while loop
+ end = False
+
+ # for each command found...
+ while not end:
+ # get the current scripting byte
+ cur_byte = ord(rom[current_address])
+
+ # reset the command class (last command was probably different)
+ scripting_command_class = None
+
+ # match the command id byte to a scripting command class like "step half"
+ for class_ in movement_command_classes:
+ # allow lists of ids
+ if (type(class_.id) == list and cur_byte in class_.id) \
+ or class_.id == cur_byte:
+ scripting_command_class = class_
+
+ # temporary fix for applymovement scripts
+ if ord(rom[current_address]) == 0x47:
+ end = True
+
+ # no matching command found
+ xyz = None
+ if scripting_command_class == None:
+ scripting_command_class = MovementDBCommand
+ #scripting_command_class = deepcopy(MovementCommand)
+ #scripting_command_class.id = scripting_command_class.byte = ord(rom[current_address])
+ #scripting_command_class.macro_name = "db"
+ #scripting_command_class.size = 1
+ #scripting_command_class.override_byte_check = True
+ #scripting_command_class.id = None
+ #scripting_command_class.param_types = {0: {"name": "db value", "class": DecimalParam}}
+
+ xyz = True
+
+ # create an instance of the command class and let it parse its parameter bytes
+ cls = scripting_command_class(address=current_address, map_group=self.map_group, map_id=self.map_id, debug=self.debug, force=self.force)
+
+ if self.debug:
+ print cls.to_asm()
+
+ # store it in this script object
+ commands.append(cls)
+
+ # certain commands will end the movement engine
+ end = cls.end
+
+ # skip past the command's parameter bytes to go to the next command
+ current_address += cls.size
+
+ # last byte belonging to script is last byte of last command,
+ # or the last byte of the last command's last parameter
+ # (actually i think this might be the next byte after??)
+ self.last_address = current_address
+
+ # store the script in the global table/map thing
+ all_movements.append(self)
+ script_parse_table[start_address:current_address] = self
+
+ if self.debug:
+ asm_output = "\n".join([command.to_asm() for command in commands])
+ print "--------------\n"+asm_output
+
+ # store the script
+ self.commands = commands
+ return commands
+
+ def to_asm(self):
+ asm_output = "\n".join([command.to_asm() for command in self.commands])
+ return asm_output
+
+ # TODO: get_dependencies doesn't work if ApplyMovementData uses labels in the future
+ def get_dependencies(self, recompute=False, global_dependencies=set()):
+ return []
+
+def print_all_movements():
+ for each in all_movements:
+ print each.to_asm()
+ print "------------------"
+ print "done"
+
+class TextCommand(Command):
+ # an individual text command will not end it
+ end = False
+
+ # this is only used for e.g. macros that don't appear as a byte in the ROM
+ # don't use the override because all text commands are specified with a byte
+ override_byte_check = False
+
+ # in the case of text/asm commands, size is unknown until after parsing
+ # some text commands can specify this upfront but not $0
+ size = None
+
+ param_types = {}
+ params = []
+
+ # most text commands won't have any dependencies
+ # .. except for that one that points to another location for text
+ # get_dependencies on Command will look at the values of params
+ # so this doesn't need to be specified by TextCommand as long as it extends Command
+ #def get_dependencies(self, recompute=False, global_dependencies=set()):
+ # return []
+
+# this is a regular command in a TextScript for writing text
+# but unlike other macros that preprocessor.py handles,
+# the preprocessor-parser is custom and MainText is not
+# used as a macro by main.asm - however, MainText is
+# treated as a macro for the sake of parsing the ROM because
+# it is called with $0. This is very similar to how Script
+# is parsed and handled. But again, script command macros
+# are quite different.. preprocessor.py allows some of them
+# to handle how they should be parsed from main.asm, in
+# addition to their regular "parse()" method.
+class MainText(TextCommand):
+ "Write text. Structure: [00][Text][0x50 (ends code)]"
+ id = 0x0
+ macro_name = "do_text"
+ use_zero = True
+
+ def parse(self):
+ offset = self.address
+
+ # the code below assumes we're jumping past a $0 byte
+ if self.use_zero == False:
+ offset = offset
+ else:
+ offset = offset + 1
+
+ # read until $50, $57 or $58 (not sure about $58...)
+ jump57 = how_many_until(chr(0x57), offset)
+ jump50 = how_many_until(chr(0x50), offset)
+ jump58 = how_many_until(chr(0x58), offset)
+
+ # pick whichever one comes first
+ jump = min([jump57, jump50, jump58])
+
+ # if $57 appears first then this command is the last in this text script
+ if jump == jump57 or jump == jump58:
+ self.end = True
+
+ jump += 1
+
+ # we want the address after the $57
+ # ("last_address" is misnamed everywhere)
+ end_address = offset + jump
+ self.last_address = self.end_address = end_address
+
+ # read the text bytes into a structure
+ # skip the first offset byte because that's the command byte
+ self.bytes = rom_interval(offset, jump, strings=False)
+
+ # include the original command in the size calculation
+ self.size = jump
+
+ if self.use_zero:
+ self.last_address = self.address + jump + 1
+ self.size = self.last_address - self.address
+
+ if self.address == 0x9c00e and self.debug:
+ if self.last_address != 0x9c086:
+ print "self.address is: " + hex(self.address)
+ print "jump is: " + str(jump)
+ print "bytes are: " + str(self.bytes)
+ print "self.size is: " + str(self.size)
+ print "self.last_address is: " + hex(self.last_address)
+ raise Exception("last_address is wrong for 0x9c00e")
+
+ def to_asm(self):
+ if self.size < 2 or len(self.bytes) < 1:
+ raise Exception("$0 text command can't end itself with no follow-on bytes")
+
+ if self.use_zero:
+ output = "db $0"
+ else:
+ output = ""
+
+ # db $0, $57 or db $0, $50 or w/e
+ if self.size == 2 and len(self.bytes) == 1:
+ output += ", $%.2x" % (self.bytes[0])
+ return output
+
+ # whether or not quotes are open
+ in_quotes = False
+
+ # whether or not to print "db " next
+ new_line = False
+
+ # whether or not there was a ", " last..
+ # this is useful outside of quotes
+ was_comma = False
+
+ # has a $50 or $57 been passed yet?
+ end = False
+
+ if not self.use_zero:
+ new_line = True
+ was_comma = False
+
+ for byte in self.bytes:
+ if end:
+ raise Exception("the text ended due to a $50 or $57 but there are more bytes?")
+
+ if new_line:
+ if in_quotes:
+ raise Exception("can't be in_quotes on a newline")
+ elif was_comma:
+ raise Exception("last line's last character can't be a comma")
+
+ output += "db "
+
+ # $4f, $51 and $55 can end a line
+ if byte in [0x4f, 0x51, 0x55]:
+ assert not new_line, "can't have $4f, $51, $55 as the first character on a newline"
+
+ if in_quotes:
+ output += "\", $%.2x\n" % (byte)
+ elif not in_quotes:
+ if not was_comma:
+ output += ", "
+ output += "$%.2x\n" % (byte)
+
+ # reset everything
+ in_quotes = False
+ new_line = True
+ was_comma = False
+ elif byte == 0x50:
+ # technically you could have this i guess... db "@"
+ # but in most situations it will be added to the end of the previous line
+ #assert not new_line, "can't have $50 or '@' as the first character on a newline in the text at "+hex(self.address)
+
+ if in_quotes:
+ output += "@\"\n"
+ new_line = True
+ elif not in_quotes:
+ if not was_comma and not new_line:
+ output += ", "
+ output += "\"@\"\n"
+
+ # reset everything
+ in_quotes = False
+ new_line = True
+ was_comma = False
+ end = True
+
+ # self.end should be set in parse or constructor
+ # so this is very useless here.. but it's a truism i guess
+ self.end = True
+ elif byte == 0x57 or byte == 0x58:
+ # close any quotes
+ if in_quotes:
+ output += "\""
+ was_comma = False
+
+ if not was_comma and not new_line:
+ output += ", "
+
+ output += "$%.2x\n" % (byte)
+
+ in_quotes = False
+ new_line = True
+ was_comma = False
+ end = True
+
+ # dunno if $58 should end a text script or not
+ # also! self.end should be set in parse not in to_asm
+ # so this is pretty useless overall...
+ if byte == 0x58:
+ self.end = True
+ elif byte in chars.keys():
+ # figure out what the character actually is
+ char = chars[byte]
+
+ # oh wait.. quotes isn't a valid character in the first place :(
+ if char == "\"":
+ if in_quotes:
+ output += "\""
+ in_quotes = False
+ elif not in_quotes:
+ if new_line:
+ output += "\""
+ elif not new_line:
+ if not was_comma:
+ output += ", "
+ output += "\""
+ in_quotes = True
+
+ # the above if statement is probably never called
+ else:
+ if not in_quotes:
+ if not new_line and not was_comma:
+ output += ", "
+ output += "\""
+ in_quotes = True
+
+ output += char
+
+ new_line = False
+ was_comma = False
+ end = False
+ else:
+ # raise Exception("unknown byte in text script ($%.2x)" % (byte))
+ # just add an unknown byte directly to the text.. what's the worse that can happen?
+
+ if in_quotes:
+ output += "\", $%.2x" % (byte)
+
+ in_quotes = False
+ was_comma = False
+ new_line = False
+ elif not in_quotes:
+ if not was_comma and not new_line:
+ output += ", "
+ output += "$%.2x" % (byte)
+
+ # reset things
+ in_quotes = False
+ new_line = False
+ was_comma = False
+
+ # this shouldn't happen because of the rom_until calls in the parse method
+ if not end:
+ raise Exception("ran out of bytes without the script ending? starts at "+hex(self.address))
+
+ # last character may or may not be allowed to be a newline?
+ # Script.to_asm() has command.to_asm()+"\n"
+ if output[-1] == "\n":
+ output = output[:-1]
+
+ return output
+
+class PokedexText(MainText):
+ use_zero = False
+
+class WriteTextFromRAM(TextCommand):
+ """
+ Write text from ram. Structure: [01][Ram address (2byte)]
+ For valid ram addresses see Glossary. This enables use of variable text strings.
+ """
+ id = 0x1
+ macro_name = "text_from_ram"
+ size = 3
+ param_types = {
+ 0: {"name": "pointer", "class": MultiByteParam},
+ }
+class WriteNumberFromRAM(TextCommand):
+ """
+ 02 = Write number from ram. Structure: [02][Ram address (2byte)][Byte]
+
+ Byte:
+
+ Bit5:Bit6:Bit7
+ 1: 1: 1 = PokéDollar| Don’t write zeros
+ 0: 1: 1 = Don’t write zeros
+ 0: 0: 1 = Spaces instead of zeros
+ 0: 0: 0 = Write zeros
+ 0: 1: 0 = Write zeros
+ 1: 0: 0 = PokéDollar
+ 1: 1: 0 = PokéDollar
+ 1: 0: 1 = Spaces instead of zeros| PokéDollar
+
+ Number of figures = Byte AND 0x1F *2
+ No Hex --> Dec Conversio
+ """
+ id = 0x2
+ macro_name = "number_from_ram"
+ size = 4
+ param_types = {
+ 0: {"name": "pointer", "class": PointerLabelParam},
+ 1: {"name": "config", "class": HexByte},
+ }
+class SetWriteRAMLocation(TextCommand):
+ "Define new ram address to write to. Structure: [03][Ram address (2byte)]"
+ id = 0x3
+ macro_name = "store_at"
+ size = 3
+ param_types = {
+ 0: {"name": "ram address", "class": PointerLabelParam},
+ }
+class ShowBoxWithValueAt(TextCommand):
+ "04 = Write a box. Structure: [04][Ram address (2byte)][Y][X]"
+ id = 0x4
+ macro_name = "text_box"
+ size = 5
+ param_types = {
+ 0: {"name": "ram address", "class": PointerLabelParam},
+ 1: {"name": "y", "class": DecimalParam},
+ 2: {"name": "x", "class": DecimalParam},
+ }
+class Populate2ndLineOfTextBoxWithRAMContents(TextCommand):
+ "05 = New ram address to write to becomes 2nd line of a text box. Structure: [05]"
+ id = 0x5
+ macro_name = "text_dunno1"
+ size = 1
+class ShowArrowsAndButtonWait(TextCommand):
+ "06 = Wait for key down + show arrows. Structure: [06]"
+ id = 0x6
+ macro_name = "text_waitbutton"
+ size = 1
+class Populate2ndLine(TextCommand):
+ """
+ 07 = New ram address to write to becomes 2nd line of a text box
+ Textbox + show arrows. Structure: [07]
+ """
+ id = 0x7
+ macro_name = "text_dunno2"
+ size = 1
+class TextInlineAsm(TextCommand):
+ "08 = After the code an ASM script starts. Structure: [08][Script]"
+ id = 0x8
+ macro_name = "start_asm"
+ end = True
+ size = 1
+ # TODO: parse the following asm with gbz80disasm
+class WriteDecimalNumberFromRAM(TextCommand):
+ """
+ 09 = Write number from rom/ram in decimal. Structure: [09][Ram address/Pointer (2byte)][Byte]
+ Byte:
+
+ Is split: 1. 4 bits = Number of bytes to load. 0 = 3, 1 = 1, 2 = 2
+ 2. 4 bits = Number of figures of displayed number
+ 0 = Don’t care
+ 1 = Don’t care
+ >=2 = Number
+ """
+ id = 0x9
+ macro_name = "deciram"
+ size = 4
+ param_types = {
+ 0: {"name": "pointer?", "class": PointerLabelParam},
+ 1: {"name": "config", "class": HexByte},
+ }
+class InterpretDataStream(TextCommand):
+ """
+ 0A = Interpret Data stream. Structure: [0A]
+ see: http://hax.iimarck.us/files/scriptingcodes_eng.htm#Marke88
+ """
+ id = 0xA
+ macro_name = "interpret_data"
+ size = 1
+class Play0thSound(TextCommand):
+ "0B = Play sound 0x0000. Structure: [0B]"
+ id = 0xB
+ sound_num = 0
+ macro_name = "sound0"
+ size = 1
+class LimitedIntrepretDataStream(TextCommand):
+ """
+ 0C = Interpret Data stream. Structure: [0C][Number of codes to interpret]
+ For every interpretation there is a“…“ written
+ """
+ id = 0xC
+ macro_name = "limited_interpret_data"
+ size = 2
+ param_types = {
+ 0: {"name": "number of codes to interpret", "class": DecimalParam},
+ }
+class WaitForKeyDownDisplayArrow(ShowArrowsAndButtonWait):
+ """
+ 0D = Wait for key down display arrow. Structure: [0D]
+ """
+ id = 0xD
+ macro_name = "waitbutton2"
+ size = 1
+class Play9thSound(Play0thSound):
+ id = 0xE
+ sound_num = 9
+ macro_name = "sound0x09"
+ size = 1
+class Play1stSound(Play0thSound):
+ id = 0xF
+ sound_num = 1
+ macro_name = "sound0x0F"
+ size = 1
+class Play2ndSound(Play0thSound):
+ id = 0x10
+ sound_num = 2
+ macro_name = "sound0x02"
+ size = 1
+class Play10thSound(Play0thSound):
+ id = 0x11
+ sound_num = 10
+ macro_name = "sound0x0A"
+ size = 1
+class Play45thSound(Play0thSound):
+ id = 0x12
+ sound_num = 0x2D
+ macro_name = "sound0x2D"
+ size = 1
+class Play44thSound(Play0thSound):
+ id = 0x13
+ sound_num = 0x2C
+ macro_name = "sound0x2C"
+ size = 1
+class DisplayByteFromRAMAt(TextCommand):
+ """
+ 14 = Display MEMORY. Structure: [14][Byte]
+
+ Byte:
+
+ 00 = MEMORY1
+ 01 = MEMORY2
+ 02 = MEMORY
+ 04 = TEMPMEMORY2
+ 05 = TEMPMEMORY1
+ """
+ id = 0x14
+ macro_name = "show_byte_at"
+ size = 2
+ param_types = {
+ 1: {"name": "memory byte id", "class": DecimalParam},
+ }
+class WriteCurrentDay(TextCommand):
+ "15 = Write current day. Structure: [15]"
+ id = 0x15
+ macro_name = "current_day"
+ size = 1
+class TextJump(TextCommand):
+ "16 = 3byte pointer to new text follows. Structure: [16][2byte pointer][bank]"
+ id = 0x16
+ macro_name = "text_jump"
+ size = 4
+ param_types = {
+ 0: {"name": "text", "class": TextPointerLabelAfterBankParam},
+ }
+# this is needed because sometimes a script ends with $50 $50
+class TextEndingCommand(TextCommand):
+ id = 0x50
+ macro_name = "db"
+ override_byte_check = False
+ size = 1
+ end = True
+ def to_asm(self):
+ return "db $50"
+
+text_command_classes = inspect.getmembers(sys.modules[__name__], \
+ lambda obj: inspect.isclass(obj) and \
+ issubclass(obj, TextCommand) and \
+ obj != TextCommand and obj != PokedexText)
+
+# byte: [name, [param1 name, param1 type], [param2 name, param2 type], ...]
+# 0x9E: ["verbosegiveitem", ["item", ItemLabelByte], ["quantity", SingleByteParam]],
+pksv_crystal_more = {
+ 0x00: ["2call", ["pointer", ScriptPointerLabelParam]],
+ 0x01: ["3call", ["pointer", ScriptPointerLabelBeforeBank]],
+ 0x02: ["2ptcall", ["pointer", RAMAddressParam]],
+ 0x03: ["2jump", ["pointer", ScriptPointerLabelParam]],
+ 0x04: ["3jump", ["pointer", ScriptPointerLabelBeforeBank]],
+ 0x05: ["2ptjump", ["pointer", RAMAddressParam]],
+ 0x06: ["if equal", ["byte", SingleByteParam], ["pointer", ScriptPointerLabelParam]],
+ 0x07: ["if not equal", ["byte", SingleByteParam], ["pointer", ScriptPointerLabelParam]],
+ 0x08: ["iffalse", ["pointer", ScriptPointerLabelParam]],
+ 0x09: ["iftrue", ["pointer", ScriptPointerLabelParam]],
+ 0x0A: ["if less than", ["byte", SingleByteParam], ["pointer", ScriptPointerLabelParam]],
+ 0x0B: ["if greater than", ["byte", SingleByteParam], ["pointer", ScriptPointerLabelParam]],
+ 0x0C: ["jumpstd", ["predefined_script", MultiByteParam]],
+ 0x0D: ["callstd", ["predefined_script", MultiByteParam]],
+ 0x0E: ["3callasm", ["asm", AsmPointerParam]],
+ 0x0F: ["special", ["predefined_script", MultiByteParam]],
+ 0x10: ["2ptcallasm", ["asm", RAMAddressParam]],
+ # should map_group/map_id be dealt with in some special way in the asm?
+ 0x11: ["checkmaptriggers", ["map_group", SingleByteParam], ["map_id", SingleByteParam]],
+ 0x12: ["domaptrigger", ["map_group", MapGroupParam], ["map_id", MapIdParam], ["trigger_id", SingleByteParam]],
+ 0x13: ["checktriggers"],
+ 0x14: ["dotrigger", ["trigger_id", SingleByteParam]],
+ 0x15: ["writebyte", ["value", SingleByteParam]],
+ 0x16: ["addvar", ["value", SingleByteParam]],
+ 0x17: ["random", ["input", SingleByteParam]],
+ 0x18: ["checkver"],
+ 0x19: ["copybytetovar", ["address", RAMAddressParam]],
+ 0x1A: ["copyvartobyte", ["address", RAMAddressParam]],
+ 0x1B: ["loadvar", ["address", RAMAddressParam], ["value", SingleByteParam]],
+ 0x1C: ["checkcode", ["variable_id", SingleByteParam]],
+ 0x1D: ["writevarcode", ["variable_id", SingleByteParam]],
+ 0x1E: ["writecode", ["variable_id", SingleByteParam], ["value", SingleByteParam]],
+ 0x1F: ["giveitem", ["item", ItemLabelByte], ["quantity", SingleByteParam]],
+ 0x20: ["takeitem", ["item", ItemLabelByte], ["quantity", DecimalParam]],
+ 0x21: ["checkitem", ["item", ItemLabelByte]],
+ 0x22: ["givemoney", ["account", SingleByteParam], ["money", MoneyByteParam]],
+ 0x23: ["takemoney", ["account", SingleByteParam], ["money", MoneyByteParam]],
+ 0x24: ["checkmoney", ["account", SingleByteParam], ["money", MoneyByteParam]],
+ 0x25: ["givecoins", ["coins", CoinByteParam]],
+ 0x26: ["takecoins", ["coins", CoinByteParam]],
+ 0x27: ["checkcoins", ["coins", CoinByteParam]],
+ # 0x28-0x2A not from pksv
+ 0x28: ["addcellnum", ["person", SingleByteParam]],
+ 0x29: ["delcellnum", ["person", SingleByteParam]],
+ 0x2A: ["checkcellnum", ["person", SingleByteParam]],
+ # back on track...
+ 0x2B: ["checktime", ["time", SingleByteParam]],
+ 0x2C: ["checkpoke", ["pkmn", PokemonParam]],
+#0x2D: ["givepoke", ], .... see GivePoke class
+ 0x2E: ["giveegg", ["pkmn", PokemonParam], ["level", DecimalParam]],
+ 0x2F: ["givepokeitem", ["pointer", PointerParamToItemAndLetter]],
+ 0x30: ["checkpokeitem", ["pointer", PointerParamToItemAndLetter]], # not pksv
+ 0x31: ["checkbit1", ["bit_number", MultiByteParam]],
+ 0x32: ["clearbit1", ["bit_number", MultiByteParam]],
+ 0x33: ["setbit1", ["bit_number", MultiByteParam]],
+ 0x34: ["checkbit2", ["bit_number", MultiByteParam]],
+ 0x35: ["clearbit2", ["bit_number", MultiByteParam]],
+ 0x36: ["setbit2", ["bit_number", MultiByteParam]],
+ 0x37: ["wildoff"],
+ 0x38: ["wildon"],
+ 0x39: ["xycompare", ["pointer", MultiByteParam]],
+ 0x3A: ["warpmod", ["warp_id", SingleByteParam], ["map_group", MapGroupParam], ["map_id", MapIdParam]],
+ 0x3B: ["blackoutmod", ["map_group", MapGroupParam], ["map_id", MapIdParam]],
+ 0x3C: ["warp", ["map_group", MapGroupParam], ["map_id", MapIdParam], ["x", SingleByteParam], ["y", SingleByteParam]],
+ 0x3D: ["readmoney", ["account", SingleByteParam], ["memory", SingleByteParam]], # not pksv
+ 0x3E: ["readcoins", ["memory", SingleByteParam]], # not pksv
+ 0x3F: ["RAM2MEM", ["memory", SingleByteParam]], # not pksv
+ 0x40: ["pokenamemem", ["pokemon", PokemonParam], ["memory", SingleByteParam]], # not pksv
+ 0x41: ["itemtotext", ["item", ItemLabelByte], ["memory", SingleByteParam]],
+ 0x42: ["mapnametotext", ["memory", SingleByteParam]], # not pksv
+ 0x43: ["trainertotext", ["trainer_id", TrainerGroupParam], ["trainer_group", TrainerIdParam], ["memory", SingleByteParam]],
+ 0x44: ["stringtotext", ["text_pointer", EncodedTextLabelParam], ["memory", SingleByteParam]],
+ 0x45: ["itemnotify"],
+ 0x46: ["pocketisfull"],
+ 0x47: ["loadfont"],
+ 0x48: ["refreshscreen", ["dummy", SingleByteParam]],
+ 0x49: ["loadmovesprites"],
+ 0x4A: ["loadbytec1ce", ["byte", SingleByteParam]], # not pksv
+ 0x4B: ["3writetext", ["text_pointer", PointerLabelBeforeBank]],
+ 0x4C: ["2writetext", ["text_pointer", RawTextPointerLabelParam]],
+ 0x4D: ["repeattext", ["byte", SingleByteParam], ["byte", SingleByteParam]], # not pksv
+ 0x4E: ["yesorno"],
+ 0x4F: ["loadmenudata", ["data", MenuDataPointerParam]],
+ 0x50: ["writebackup"],
+ 0x51: ["jumptextfaceplayer", ["text_pointer", RawTextPointerLabelParam]],
+ 0x52: ["3jumptext", ["text_pointer", PointerLabelBeforeBank]],
+ 0x53: ["jumptext", ["text_pointer", RawTextPointerLabelParam]],
+ 0x54: ["closetext"],
+ 0x55: ["keeptextopen"],
+ 0x56: ["pokepic", ["pokemon", PokemonParam]],
+ 0x57: ["pokepicyesorno"],
+ 0x58: ["interpretmenu"],
+ 0x59: ["interpretmenu2"],
+# not pksv
+ 0x5A: ["loadpikachudata"],
+ 0x5B: ["battlecheck"],
+ 0x5C: ["loadtrainerdata"],
+# back to pksv..
+ 0x5D: ["loadpokedata", ["pokemon", PokemonParam], ["level", DecimalParam]],
+ 0x5E: ["loadtrainer", ["trainer_group", TrainerGroupParam], ["trainer_id", TrainerIdParam]],
+ 0x5F: ["startbattle"],
+ 0x60: ["returnafterbattle"],
+ 0x61: ["catchtutorial", ["byte", SingleByteParam]],
+# not pksv
+ 0x62: ["trainertext", ["which_text", SingleByteParam]],
+ 0x63: ["trainerstatus", ["action", SingleByteParam]],
+# back to pksv..
+ 0x64: ["winlosstext", ["win_text_pointer", TextPointerLabelParam], ["loss_text_pointer", TextPointerLabelParam]],
+ 0x65: ["scripttalkafter"], # not pksv
+ 0x66: ["talkaftercancel"],
+ 0x67: ["talkaftercheck"],
+ 0x68: ["setlasttalked", ["person", SingleByteParam]],
+ 0x69: ["applymovement", ["person", SingleByteParam], ["data", MovementPointerLabelParam]],
+ 0x6A: ["applymovement2", ["data", MovementPointerLabelParam]], # not pksv
+ 0x6B: ["faceplayer"],
+ 0x6C: ["faceperson", ["person1", SingleByteParam], ["person2", SingleByteParam]],
+ 0x6D: ["variablesprite", ["byte", SingleByteParam], ["sprite", SingleByteParam]],
+ 0x6E: ["disappear", ["person", SingleByteParam]], # hideperson
+ 0x6F: ["appear", ["person", SingleByteParam]], # showperson
+ 0x70: ["follow", ["person2", SingleByteParam], ["person1", SingleByteParam]],
+ 0x71: ["stopfollow"],
+ 0x72: ["moveperson", ["person", SingleByteParam], ["x", SingleByteParam], ["y", SingleByteParam]],
+ 0x73: ["writepersonxy", ["person", SingleByteParam]], # not pksv
+ 0x74: ["loademote", ["bubble", SingleByteParam]],
+ 0x75: ["showemote", ["bubble", SingleByteParam], ["person", SingleByteParam], ["time", DecimalParam]],
+ 0x76: ["spriteface", ["person", SingleByteParam], ["facing", SingleByteParam]],
+ 0x77: ["follownotexact", ["person2", SingleByteParam], ["person1", SingleByteParam]],
+ 0x78: ["earthquake", ["param", DecimalParam]],
+ 0x79: ["changemap", ["map_data_pointer", MapDataPointerParam]],
+ 0x7A: ["changeblock", ["x", SingleByteParam], ["y", SingleByteParam], ["block", SingleByteParam]],
+ 0x7B: ["reloadmap"],
+ 0x7C: ["reloadmappart"],
+ 0x7D: ["writecmdqueue", ["queue_pointer", MultiByteParam]],
+ 0x7E: ["delcmdqueue", ["byte", SingleByteParam]],
+ 0x7F: ["playmusic", ["music_pointer", MultiByteParam]],
+ 0x80: ["playrammusic"],
+ 0x81: ["musicfadeout", ["music", MultiByteParam], ["fadetime", SingleByteParam]],
+ 0x82: ["playmapmusic"],
+ 0x83: ["reloadmapmusic"],
+ 0x84: ["cry", ["cry_id", MultiByteParam]], # XXX maybe it should use PokemonParam
+ 0x85: ["playsound", ["sound_pointer", MultiByteParam]],
+ 0x86: ["waitbutton"],
+ 0x87: ["warpsound"],
+ 0x88: ["specialsound"],
+ 0x89: ["passtoengine", ["data_pointer", PointerLabelBeforeBank]],
+ 0x8A: ["newloadmap", ["which_method", SingleByteParam]],
+ 0x8B: ["pause", ["length", DecimalParam]],
+ 0x8C: ["deactivatefacing", ["time", SingleByteParam]],
+ 0x8D: ["priorityjump", ["pointer", ScriptPointerLabelParam]],
+ 0x8E: ["warpcheck"],
+ 0x8F: ["ptpriorityjump", ["pointer", ScriptPointerLabelParam]],
+ 0x90: ["return"],
+ 0x91: ["end"],
+ 0x92: ["reloadandreturn"],
+ 0x93: ["resetfuncs"],
+ 0x94: ["pokemart", ["dialog_id", SingleByteParam], ["mart_id", MultiByteParam]], # maybe it should be a pokemark constant id/label?
+ 0x95: ["elevator", ["floor_list_pointer", PointerLabelParam]],
+ 0x96: ["trade", ["trade_id", SingleByteParam]],
+ 0x97: ["askforphonenumber", ["number", SingleByteParam]],
+ 0x98: ["phonecall", ["caller_name", RawTextPointerLabelParam]],
+ 0x99: ["hangup"],
+ 0x9A: ["describedecoration", ["byte", SingleByteParam]],
+ 0x9B: ["fruittree", ["tree_id", SingleByteParam]],
+ 0x9C: ["specialphonecall", ["call_id", MultiByteParam]],
+ 0x9D: ["checkphonecall"],
+ 0x9E: ["verbosegiveitem", ["item", ItemLabelByte], ["quantity", DecimalParam]],
+ 0x9F: ["verbosegiveitem2", ["item", ItemLabelByte], ["var", SingleByteParam]],
+ 0xA0: ["loadwilddata", ["map_group", MapGroupParam], ["map_id", MapIdParam]],
+ 0xA1: ["halloffame"],
+ 0xA2: ["credits"],
+ 0xA3: ["warpfacing", ["facing", SingleByteParam], ["map_group", MapGroupParam], ["map_id", MapIdParam], ["x", SingleByteParam], ["y", SingleByteParam]],
+ 0xA4: ["storetext", ["pointer", PointerLabelBeforeBank], ["memory", SingleByteParam]],
+ 0xA5: ["displaylocation", ["id", SingleByteParam]],
+ 0xA6: ["unknown0xa6"],
+ 0xA7: ["unknown0xa7"],
+ 0xA8: ["unknown0xa8", ["unknown", SingleByteParam]],
+ 0xA9: ["unknown0xa9"],
+ 0xAA: ["unknown0xaa"],
+}
+def create_command_classes(debug=False):
+ """creates some classes for each command byte"""
+ # don't forget to add any manually created script command classes
+ # .. except for Warp, Signpost and some others that aren't found in scripts
+ klasses = [GivePoke]
+ for (byte, cmd) in pksv_crystal_more.items():
+ cmd_name = cmd[0].replace(" ", "_")
+ params = {"id": byte, "size": 1, "end": byte in pksv_crystal_more_enders, "macro_name": cmd_name}
+ params["param_types"] = {}
+ if len(cmd) > 1:
+ param_types = cmd[1:]
+ for (i, each) in enumerate(param_types):
+ thing = {"name": each[0], "class": each[1]}
+ params["param_types"][i] = thing
+ if debug:
+ print "each is: " + str(each)
+ print "thing[class] is: " + str(thing["class"])
+ params["size"] += thing["class"].size
+ klass_name = cmd_name+"Command"
+ klass = classobj(klass_name, (Command,), params)
+ globals()[klass_name] = klass
+ klasses.append(klass)
+ # later an individual klass will be instantiated to handle something
+ return klasses
+command_classes = create_command_classes()
+
+
+
+music_commands_new = {
+ 0xD0: ["octave8"],
+ 0xD1: ["octave7"],
+ 0xD2: ["octave6"],
+ 0xD3: ["octave5"],
+ 0xD4: ["octave4"],
+ 0xD5: ["octave3"],
+ 0xD6: ["octave2"],
+ 0xD7: ["octave1"],
+ 0xD8: ["notetype", ["note_length", SingleByteParam], ["intensity", SingleByteParam]], # only 1 param on ch3
+ 0xD9: ["forceoctave", ["octave", SingleByteParam]],
+ 0xDA: ["tempo", ["tempo", MultiByteParam]],
+ 0xDB: ["dutycycle", ["duty_cycle", SingleByteParam]],
+ 0xDC: ["intensity", ["intensity", SingleByteParam]],
+ 0xDD: ["soundinput", ["input", SingleByteParam]],
+ 0xDE: ["unknownmusic0xde", ["unknown", SingleByteParam]], # also updates duty cycle
+ 0xDF: ["unknownmusic0xdf"],
+ 0xE0: ["unknownmusic0xe0", ["unknown", SingleByteParam], ["unknown", SingleByteParam]],
+ 0xE1: ["vibrato", ["delay", SingleByteParam], ["extent", SingleByteParam]],
+ 0xE2: ["unknownmusic0xe2", ["unknown", SingleByteParam]],
+ 0xE3: ["togglenoise", ["id", SingleByteParam]], # this can have 0-1 params!
+ 0xE4: ["panning", ["tracks", SingleByteParam]],
+ 0xE5: ["volume", ["volume", SingleByteParam]],
+ 0xE6: ["tone", ["tone", MultiByteParam]], # big endian
+ 0xE7: ["unknownmusic0xe7", ["unknown", SingleByteParam]],
+ 0xE8: ["unknownmusic0xe8", ["unknown", SingleByteParam]],
+ 0xE9: ["globaltempo", ["value", MultiByteParam]],
+ 0xEA: ["restartchannel", ["address", PointerLabelParam]],
+ 0xEB: ["newsong", ["id", MultiByteParam]],
+ 0xEC: ["sfxpriorityon"],
+ 0xED: ["sfxpriorityoff"],
+ 0xEE: ["unknownmusic0xee", ["address", PointerLabelParam]],
+ 0xEF: ["stereopanning", ["tracks", SingleByteParam]],
+ 0xF0: ["sfxtogglenoise", ["id", SingleByteParam]], # 0-1 params
+ 0xF1: ["music0xf1"], # nothing
+ 0xF2: ["music0xf2"], # nothing
+ 0xF3: ["music0xf3"], # nothing
+ 0xF4: ["music0xf4"], # nothing
+ 0xF5: ["music0xf5"], # nothing
+ 0xF6: ["music0xf6"], # nothing
+ 0xF7: ["music0xf7"], # nothing
+ 0xF8: ["music0xf8"], # nothing
+ 0xF9: ["unknownmusic0xf9"],
+ 0xFA: ["setcondition", ["condition", SingleByteParam]],
+ 0xFB: ["jumpif", ["condition", SingleByteParam], ["address", PointerLabelParam]],
+ 0xFC: ["jumpchannel", ["address", PointerLabelParam]],
+ 0xFD: ["loopchannel", ["count", SingleByteParam], ["address", PointerLabelParam]],
+ 0xFE: ["callchannel", ["address", PointerLabelParam]],
+ 0xFF: ["endchannel"],
+}
+
+music_command_enders = [0xEA, 0xEB, 0xEE, 0xFC, 0xFF,]
+# special case for 0xFD (if loopchannel.count = 0, break)
+
+def create_music_command_classes(debug=False):
+ klasses = []
+ for (byte, cmd) in music_commands_new.items():
+ cmd_name = cmd[0].replace(" ", "_")
+ params = {"id": byte, "size": 1, "end": byte in music_command_enders, "macro_name": cmd_name}
+ params["param_types"] = {}
+ if len(cmd) > 1:
+ param_types = cmd[1:]
+ for (i, each) in enumerate(param_types):
+ thing = {"name": each[0], "class": each[1]}
+ params["param_types"][i] = thing
+ if debug:
+ print "each is: " + str(each)
+ print "thing[class] is: " + str(thing["class"])
+ params["size"] += thing["class"].size
+ klass_name = cmd_name+"Command"
+ klass = classobj(klass_name, (Command,), params)
+ globals()[klass_name] = klass
+ if klass.macro_name == "notetype":
+ klass.allowed_lengths = [1, 2]
+ elif klass.macro_name in ["togglenoise", "sfxtogglenoise"]:
+ klass.allowed_lengths = [0, 1]
+ klasses.append(klass)
+ # later an individual klass will be instantiated to handle something
+ return klasses
+music_classes = create_music_command_classes()
+
+
+
+effect_commands = {
+ 0x1: ['checkturn'],
+ 0x2: ['checkobedience'],
+ 0x3: ['usedmovetext'],
+ 0x4: ['doturn'],
+ 0x5: ['critical'],
+ 0x6: ['damagestats'],
+ 0x7: ['stab'],
+ 0x8: ['damagevariation'],
+ 0x9: ['checkhit'],
+ 0xa: ['effect0x0a'],
+ 0xb: ['effect0x0b'],
+ 0xc: ['effect0x0c'],
+ 0xd: ['resulttext'],
+ 0xe: ['checkfaint'],
+ 0xf: ['criticaltext'],
+ 0x10: ['supereffectivetext'],
+ 0x11: ['checkdestinybond'],
+ 0x12: ['buildopponentrage'],
+ 0x13: ['poisontarget'],
+ 0x14: ['sleeptarget'],
+ 0x15: ['draintarget'],
+ 0x16: ['eatdream'],
+ 0x17: ['burntarget'],
+ 0x18: ['freezetarget'],
+ 0x19: ['paralyzetarget'],
+ 0x1a: ['selfdestruct'],
+ 0x1b: ['mirrormove'],
+ 0x1c: ['statup'],
+ 0x1d: ['statdown'],
+ 0x1e: ['payday'],
+ 0x1f: ['conversion'],
+ 0x20: ['resetstats'],
+ 0x21: ['storeenergy'],
+ 0x22: ['unleashenergy'],
+ 0x23: ['forceswitch'],
+ 0x24: ['endloop'],
+ 0x25: ['flinchtarget'],
+ 0x26: ['ohko'],
+ 0x27: ['recoil'],
+ 0x28: ['mist'],
+ 0x29: ['focusenergy'],
+ 0x2a: ['confuse'],
+ 0x2b: ['confusetarget'],
+ 0x2c: ['heal'],
+ 0x2d: ['transform'],
+ 0x2e: ['screen'],
+ 0x2f: ['poison'],
+ 0x30: ['paralyze'],
+ 0x31: ['substitute'],
+ 0x32: ['rechargenextturn'],
+ 0x33: ['mimic'],
+ 0x34: ['metronome'],
+ 0x35: ['leechseed'],
+ 0x36: ['splash'],
+ 0x37: ['disable'],
+ 0x38: ['cleartext'],
+ 0x39: ['charge'],
+ 0x3a: ['checkcharge'],
+ 0x3b: ['traptarget'],
+ 0x3c: ['effect0x3c'],
+ 0x3d: ['rampage'],
+ 0x3e: ['checkrampage'],
+ 0x3f: ['constantdamage'],
+ 0x40: ['counter'],
+ 0x41: ['encore'],
+ 0x42: ['painsplit'],
+ 0x43: ['snore'],
+ 0x44: ['conversion2'],
+ 0x45: ['lockon'],
+ 0x46: ['sketch'],
+ 0x47: ['defrostopponent'],
+ 0x48: ['sleeptalk'],
+ 0x49: ['destinybond'],
+ 0x4a: ['spite'],
+ 0x4b: ['falseswipe'],
+ 0x4c: ['healbell'],
+ 0x4d: ['kingsrock'],
+ 0x4e: ['triplekick'],
+ 0x4f: ['kickcounter'],
+ 0x50: ['thief'],
+ 0x51: ['arenatrap'],
+ 0x52: ['nightmare'],
+ 0x53: ['defrost'],
+ 0x54: ['curse'],
+ 0x55: ['protect'],
+ 0x56: ['spikes'],
+ 0x57: ['foresight'],
+ 0x58: ['perishsong'],
+ 0x59: ['startsandstorm'],
+ 0x5a: ['endure'],
+ 0x5b: ['checkcurl'],
+ 0x5c: ['rolloutpower'],
+ 0x5d: ['effect0x5d'],
+ 0x5e: ['furycutter'],
+ 0x5f: ['attract'],
+ 0x60: ['happinesspower'],
+ 0x61: ['present'],
+ 0x62: ['damagecalc'],
+ 0x63: ['frustrationpower'],
+ 0x64: ['safeguard'],
+ 0x65: ['checksafeguard'],
+ 0x66: ['getmagnitude'],
+ 0x67: ['batonpass'],
+ 0x68: ['pursuit'],
+ 0x69: ['clearhazards'],
+ 0x6a: ['healmorn'],
+ 0x6b: ['healday'],
+ 0x6c: ['healnite'],
+ 0x6d: ['hiddenpower'],
+ 0x6e: ['startrain'],
+ 0x6f: ['startsun'],
+ 0x70: ['attackup'],
+ 0x71: ['defenseup'],
+ 0x72: ['speedup'],
+ 0x73: ['specialattackup'],
+ 0x74: ['specialdefenseup'],
+ 0x75: ['accuracyup'],
+ 0x76: ['evasionup'],
+ 0x77: ['attackup2'],
+ 0x78: ['defenseup2'],
+ 0x79: ['speedup2'],
+ 0x7a: ['specialattackup2'],
+ 0x7b: ['specialdefenseup2'],
+ 0x7c: ['accuracyup2'],
+ 0x7d: ['evasionup2'],
+ 0x7e: ['attackdown'],
+ 0x7f: ['defensedown'],
+ 0x80: ['speeddown'],
+ 0x81: ['specialattackdown'],
+ 0x82: ['specialdefensedown'],
+ 0x83: ['accuracydown'],
+ 0x84: ['evasiondown'],
+ 0x85: ['attackdown2'],
+ 0x86: ['defensedown2'],
+ 0x87: ['speeddown2'],
+ 0x88: ['specialattackdown2'],
+ 0x89: ['specialdefensedown2'],
+ 0x8a: ['accuracydown2'],
+ 0x8b: ['evasiondown2'],
+ 0x8c: ['statmessageuser'],
+ 0x8d: ['statmessagetarget'],
+ 0x8e: ['statupfailtext'],
+ 0x8f: ['statdownfailtext'],
+ 0x90: ['effectchance'],
+ 0x91: ['effect0x91'],
+ 0x92: ['effect0x92'],
+ 0x93: ['switchturn'],
+ 0x94: ['fakeout'],
+ 0x95: ['bellydrum'],
+ 0x96: ['psychup'],
+ 0x97: ['rage'],
+ 0x98: ['doubleflyingdamage'],
+ 0x99: ['doubleundergrounddamage'],
+ 0x9a: ['mirrorcoat'],
+ 0x9b: ['checkfuturesight'],
+ 0x9c: ['futuresight'],
+ 0x9d: ['doubleminimizedamage'],
+ 0x9e: ['skipsuncharge'],
+ 0x9f: ['thunderaccuracy'],
+ 0xa0: ['teleport'],
+ 0xa1: ['beatup'],
+ 0xa2: ['ragedamage'],
+ 0xa3: ['effect0xa3'],
+ 0xa4: ['allstatsup'],
+ 0xa5: ['effect0xa5'],
+ 0xa6: ['effect0xa6'],
+ 0xa7: ['effect0xa7'],
+ 0xa8: ['effect0xa8'],
+ 0xa9: ['clearmissdamage'],
+ 0xaa: ['wait'],
+ 0xab: ['hittarget'],
+ 0xac: ['tristatuschance'],
+ 0xad: ['supereffectivelooptext'],
+ 0xae: ['startloop'],
+ 0xaf: ['curl'],
+ 0xfe: ['endturn'],
+ 0xff: ['endmove'],
+}
+
+effect_command_enders = [0xFF,]
+
+def create_effect_command_classes(debug=False):
+ klasses = []
+ for (byte, cmd) in effect_commands.items():
+ cmd_name = cmd[0].replace(" ", "_")
+ params = {
+ "id": byte,
+ "size": 1,
+ "end": byte in effect_command_enders,
+ "macro_name": cmd_name
+ }
+ params["param_types"] = {}
+ if len(cmd) > 1:
+ param_types = cmd[1:]
+ for (i, each) in enumerate(param_types):
+ thing = {"name": each[0], "class": each[1]}
+ params["param_types"][i] = thing
+ if debug:
+ print "each is: " + str(each)
+ print "thing[class] is: " + str(thing["class"])
+ params["size"] += thing["class"].size
+ klass_name = cmd_name+"Command"
+ klass = classobj(klass_name, (Command,), params)
+ globals()[klass_name] = klass
+ klasses.append(klass)
+ # later an individual klass will be instantiated to handle something
+ return klasses
+
+effect_classes = create_effect_command_classes()
+
+
+
+def generate_macros(filename="../script_macros.asm"):
+ """generates all macros based on commands
+ this is dumped into script_macros.asm"""
+ output = "; This file is generated by generate_macros.\n"
+ for command in command_classes:
+ output += "\n"
+ #if command.macro_name[0].isdigit():
+ # output += "_"
+ output += command.macro_name + ": MACRO\n"
+ output += spacing + "db $%.2x\n"%(command.id)
+ current_param = 1
+ for (index, each) in command.param_types.items():
+ if issubclass(each["class"], SingleByteParam):
+ output += spacing + "db \\" + str(current_param) + "\n"
+ elif issubclass(each["class"], MultiByteParam):
+ output += spacing + "dw \\" + str(current_param) + "\n"
+ current_param += 1
+ output += spacing + "ENDM\n"
+
+ fh = open(filename, "w")
+ fh.write(output)
+ fh.close()
+
+ return output
+
+# use this to keep track of commands without pksv names
+pksv_no_names = {}
+def pretty_print_pksv_no_names():
+ """just some nice debugging output
+ use this to keep track of commands without pksv names
+ pksv_no_names is created in parse_script_engine_script_at"""
+ for (command_byte, addresses) in pksv_no_names.items():
+ if command_byte in pksv_crystal_unknowns: continue
+ print hex(command_byte) + " appearing in these scripts: "
+ for address in addresses:
+ print " " + hex(address)
+
+recursive_scripts = set([])
+def rec_parse_script_engine_script_at(address, origin=None, debug=True):
+ """this is called in parse_script_engine_script_at for recursion
+ when this works it should be flipped back to using the regular
+ parser."""
+ recursive_scripts.add((address, origin))
+ return parse_script_engine_script_at(address, origin=origin, debug=debug)
+
+def find_broken_recursive_scripts(output=False, debug=True):
+ """well.. these at least have a chance of maybe being broken?"""
+ for r in list(recursive_scripts):
+ script = {}
+ length = "not counted here"
+ if is_script_already_parsed_at(r[0]):
+ script = script_parse_table[r[0]]
+ length = str(len(script))
+ if len(script) > 20 or script == {}:
+ print "******************* begin"
+ print "script at " + hex(r[0]) + " from main script " + hex(r[1]) + " with length: " + length
+ if output:
+ parse_script_engine_script_at(r[0], force=True, debug=True)
+ print "==================== end"
+
+
+stop_points = [0x1aafa2,
+ 0x9f58f, # battle tower
+ 0x9f62f, # battle tower
+ ]
+class Script:
+ base_label = "UnknownScript_"
+ def __init__(self, *args, **kwargs):
+ self.address = None
+ self.commands = None
+ if len(kwargs) == 0 and len(args) == 0:
+ raise Exception("Script.__init__ must be given some arguments")
+ # first positional argument is address
+ if len(args) == 1:
+ address = args[0]
+ if type(address) == str:
+ address = int(address, 16)
+ elif type(address) != int:
+ raise Exception("address must be an integer or string")
+ self.address = address
+ elif len(args) > 1:
+ raise Exception("don't know what to do with second (or later) positional arguments")
+ self.dependencies = None
+ if "label" in kwargs.keys():
+ label = kwargs["label"]
+ else:
+ label = None
+ if not label:
+ label = self.base_label + hex(self.address)
+ self.label = Label(name=label, address=address, object=self)
+ if "map_group" in kwargs.keys():
+ self.map_group = kwargs["map_group"]
+ if "map_id" in kwargs.keys():
+ self.map_id = kwargs["map_id"]
+ if "parent" in kwargs.keys():
+ self.parent = kwargs["parent"]
+ # parse the script at the address
+ if "use_old_parse" in kwargs.keys() and kwargs["use_old_parse"] == True:
+ self.old_parse(**kwargs)
+ else:
+ self.parse(self.address, **kwargs)
+
+ def pksv_list(self):
+ """shows a list of pksv names for each command in the script"""
+ items = []
+ if type(self.commands) == dict:
+ for (id, command) in self.commands.items():
+ if command["type"] in pksv_crystal:
+ items.append(pksv_crystal[command["type"]])
+ else:
+ items.append(hex(command["type"]))
+ else:
+ for command in self.commands:
+ items.append(command.macro_name)
+ return items
+
+
+ def to_pksv(self):
+ """returns a string of pksv command names"""
+ pksv = self.pksv_list()
+ output = "script starting at: "+hex(self.address)+" .. "
+ first = True
+ for item in pksv:
+ item = str(item)
+ if first:
+ output += item
+ first = False
+ else:
+ output += ", "+item
+ return output
+
+ def show_pksv(self):
+ """prints a list of pksv command names in this script"""
+ print self.to_pksv()
+
+ def parse(self, start_address, force=False, map_group=None, map_id=None, force_top=True, origin=True, debug=False):
+ """parses a script using the Command classes
+ as an alternative to the old method using hard-coded commands
+
+ force_top just means 'force the main script to get parsed, but not any subscripts'
+ """
+ global command_classes, rom, script_parse_table
+ current_address = start_address
+ if debug: print "Script.parse address="+hex(self.address) +" map_group="+str(map_group)+" map_id="+str(map_id)
+ if start_address in stop_points and force == False:
+ if debug: print "script parsing is stopping at stop_point=" + hex(start_address) + " at map_group="+str(map_group)+" map_id="+str(map_id)
+ return None
+ if start_address < 0x4000 and start_address not in [0x26ef, 0x114, 0x1108]:
+ if debug: print "address is less than 0x4000.. address is: " + hex(start_address)
+ sys.exit(1)
+ if is_script_already_parsed_at(start_address) and not force and not force_top:
+ raise Exception("this script has already been parsed before, please use that instance ("+hex(start_address)+")")
+
+ # load up the rom if it hasn't been loaded already
+ load_rom()
+
+ # in the event that the script parsing fails.. it would be nice to leave evidence
+ script_parse_table[start_address:start_address+1] = "incomplete parse_script_with_command_classes"
+
+ # start with a blank script
+ commands = []
+
+ # use this to control the while loop
+ end = False
+
+ # for each command found..
+ while not end:
+ # get the current scripting byte
+ cur_byte = ord(rom[current_address])
+
+ # reset the command class (last command was probably different)
+ scripting_command_class = None
+
+ # match the command id byte to a scripting command class like GivePoke
+ for class_ in command_classes:
+ if class_.id == cur_byte:
+ scripting_command_class = class_
+
+ # no matching command found (not implemented yet)- just end this script
+ # NOTE: might be better to raise an exception and end the program?
+ if scripting_command_class == None:
+ if debug: print "parsing script; current_address is: " + hex(current_address)
+ current_address += 1
+ asm_output = "\n".join([command.to_asm() for command in commands])
+ end = True
+ continue
+ # maybe the program should exit with failure instead?
+ #raise Exception("no command found? id: " + hex(cur_byte) + " at " + hex(current_address) + " asm is:\n" + asm_output)
+
+ # create an instance of the command class and let it parse its parameter bytes
+ #print "about to parse command(script@"+hex(start_address)+"): " + str(scripting_command_class.macro_name)
+ cls = scripting_command_class(address=current_address, force=force, map_group=map_group, map_id=map_id, parent=self)
+
+ #if self.debug:
+ # print cls.to_asm()
+
+ # store it in this script object
+ commands.append(cls)
+
+ # certain commands will end the scripting engine
+ end = cls.end
+
+ # skip past the command's parameter bytes to go to the next command
+ #current_address = cls.last_address + 1
+ current_address += cls.size
+
+ # last byte belonging to script is last byte of last command,
+ # or the last byte of the last command's last parameter
+ self.last_address = current_address
+
+ # store the script in the global table/map thing
+ script_parse_table[start_address:current_address] = self
+
+ asm_output = "\n".join([command.to_asm() for command in commands])
+ if debug: print "--------------\n"+asm_output
+
+ # store the script
+ self.commands = commands
+
+ return commands
+
+ def get_dependencies(self, recompute=False, global_dependencies=set()):
+ if self.dependencies != None and not recompute:
+ global_dependencies.update(self.dependencies)
+ return self.dependencies
+ dependencies = []
+ for command in self.commands:
+ deps = command.get_dependencies(recompute=recompute, global_dependencies=global_dependencies)
+ dependencies.extend(deps)
+ self.dependencies = dependencies
+ return dependencies
+
+ def to_asm(self):
+ asm_output = "".join([command.to_asm()+"\n" for command in self.commands])
+ if asm_output[-1] == "\n":
+ asm_output = asm_output[:-1]
+ return asm_output
+
+ def old_parse(self, *args, **kwargs):
+ """included from old_parse_scripts"""
+from old_parse_scripts import old_parse
+Script.old_parse = old_parse
+
+def parse_script_engine_script_at(address, map_group=None, map_id=None, force=False, debug=True, origin=True):
+ if is_script_already_parsed_at(address) and not force:
+ return script_parse_table[address]
+ return Script(address, map_group=map_group, map_id=map_id, force=force, debug=debug, origin=origin)
+
+def compare_script_parsing_methods(address):
+ """
+ compares the parsed scripts using the new method and the old method
+ The new method is Script.parse, the old method is Script.old_parse.
+
+ There are likely to be problems with the new script parser, the one
+ that uses the command classes to parse bytes. To look for these
+ problems, you can compare the output of one parsing method to the
+ output of the other. When there's a difference, there is something
+ worth correcting. Probably by each command's "macro_name" attribute.
+ """
+ load_rom()
+ separator = "################ compare_script_parsing_methods"
+ # first do it the old way
+ print separator
+ print "parsing the script at " + hex(address) + " using the old method"
+ oldscript = Script(address, debug=True, force=True, origin=True, use_old_parse=True)
+ # and now the old way
+ print separator
+ print "parsing the script at " + hex(address) + " using the new method"
+ newscript = Script(address, debug=True, force=True, origin=True)
+ # let the comparison begin..
+ errors = 0
+ print separator + " COMPARISON RESULTS"
+ if not len(oldscript.commands.keys()) == len(newscript.commands):
+ print "the two scripts don't have the same number of commands"
+ errors += 1
+ for (id, oldcommand) in oldscript.commands.items():
+ newcommand = newscript.commands[id]
+ oldcommand_pksv_name = pksv_crystal[oldcommand["type"]].replace(" ", "_")
+ if oldcommand["start_address"] != newcommand.address:
+ print "the two addresses (command id="+str(id)+") do not match old="+hex(oldcommand["start_address"]) + " new="+hex(newcommand.address)
+ errors += 1
+ if oldcommand_pksv_name != newcommand.macro_name:
+ print "the two commands (id="+str(id)+") do not have the same name old="+oldcommand_pksv_name+" new="+newcommand.macro_name
+ errors += 1
+ print "total comparison errors: " + str(errors)
+ return oldscript, newscript
+
+
+class Warp(Command):
+ """only used outside of scripts"""
+ size = warp_byte_size
+ macro_name = "warp_def"
+ param_types = {
+ 0: {"name": "y", "class": HexByte},
+ 1: {"name": "x", "class": HexByte},
+ 2: {"name": "warp_to", "class": DecimalParam},
+ 3: {"name": "map_bank", "class": MapGroupParam},
+ 4: {"name": "map_id", "class": MapIdParam},
+ }
+ override_byte_check = True
+
+ def __init__(self, *args, **kwargs):
+ self.id = kwargs["id"]
+ script_parse_table[kwargs["address"] : kwargs["address"] + self.size] = self
+ Command.__init__(self, *args, **kwargs)
+
+ def get_dependencies(self, recompute=False, global_dependencies=set()):
+ return []
+
+all_warps = []
+def parse_warps(address, warp_count, bank=None, map_group=None, map_id=None, debug=True):
+ warps = []
+ current_address = address
+ for each in range(warp_count):
+ warp = Warp(address=current_address, id=each, bank=bank, map_group=map_group, map_id=map_id, debug=debug)
+ current_address += warp_byte_size
+ warps.append(warp)
+ all_warps.extend(warps)
+ return warps
+
+def old_parse_warp_bytes(some_bytes, debug=True):
+ """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
+
+class XYTrigger(Command):
+ size = trigger_byte_size
+ macro_name = "xy_trigger"
+ param_types = {
+ 0: {"name": "number", "class": DecimalParam},
+ 1: {"name": "y", "class": HexByte},
+ 2: {"name": "x", "class": HexByte},
+ 3: {"name": "unknown1", "class": SingleByteParam},
+ 4: {"name": "script", "class": ScriptPointerLabelParam},
+ 5: {"name": "unknown2", "class": SingleByteParam},
+ 6: {"name": "unknown3", "class": SingleByteParam},
+ }
+ override_byte_check = True
+
+ def __init__(self, *args, **kwargs):
+ self.id = kwargs["id"]
+ self.dependencies = None
+ Command.__init__(self, *args, **kwargs)
+
+ def get_dependencies(self, recompute=False, global_dependencies=set()):
+ dependencies = []
+ if self.dependencies != None and not recompute:
+ global_dependencies.update(self.dependencies)
+ return self.dependencies
+ thing = script_parse_table[self.params[4].parsed_address]
+ if thing and thing != self.params[4]:
+ dependencies.append(thing)
+ global_dependencies.add(thing)
+ self.dependencies = dependencies
+ return dependencies
+
+all_xy_triggers = []
+def parse_xy_triggers(address, trigger_count, bank=None, map_group=None, map_id=None, debug=True):
+ xy_triggers = []
+ current_address = address
+ for each in range(trigger_count):
+ xy_trigger = XYTrigger(address=current_address, id=each, bank=bank, map_group=map_group, map_id=map_id, debug=debug)
+ current_address += trigger_byte_size
+ xy_triggers.append(xy_trigger)
+ all_xy_triggers.extend(xy_triggers)
+ return xy_triggers
+
+def old_parse_xy_trigger_bytes(some_bytes, bank=None, map_group=None, map_id=None, debug=True):
+ """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)
+ print "******* parsing xy trigger byte scripts... x=" + str(x) + " y=" + str(y)
+ script = parse_script_engine_script_at(script_address, map_group=map_group, map_id=map_id)
+
+ 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
+
+
+class ItemFragment(Command):
+ """used by ItemFragmentParam and PeopleEvent
+ (for items placed on a map)"""
+ size = 2
+ macro_name = "db"
+ base_label = "ItemFragment_"
+ override_byte_check = True
+ param_types = {
+ 0: {"name": "item", "class": ItemLabelByte},
+ 1: {"name": "quantity", "class": DecimalParam},
+ }
+
+ def __init__(self, address=None, bank=None, map_group=None, map_id=None, debug=False, label=None):
+ assert is_valid_address(address), "PeopleEvent must be given a valid address"
+ self.address = address
+ self.last_address = address + self.size
+ self.bank = bank
+ if not label:
+ label = self.base_label + hex(address)
+ self.label = Label(name=label, address=address, object=self)
+ self.map_group = map_group
+ self.map_id = map_id
+ self.debug = debug
+ self.params = {}
+ self.dependencies = None
+ self.args = {"debug": debug, "map_group": map_group, "map_id": map_id, "bank": bank}
+ script_parse_table[self.address : self.last_address] = self
+ self.parse()
+
+
+class ItemFragmentParam(PointerLabelParam):
+ """used by PeopleEvent"""
+
+ def parse(self):
+ PointerLabelParam.parse(self)
+
+ address = calculate_pointer_from_bytes_at(self.address, bank=self.bank)
+ self.calculated_address = address
+
+ itemfrag = ItemFragment(address=address, map_group=self.map_group, map_id=self.map_id, debug=self.debug)
+ self.itemfrag = itemfrag
+
+ def get_dependencies(self, recompute=False, global_dependencies=set()):
+ if self.dependencies != None and not recompute:
+ global_dependencies.update(self.dependencies)
+ return self.dependencies
+ self.dependencies = [self.itemfrag].extend(self.itemfrag.get_dependencies(recompute=recompute, global_dependencies=global_dependencies))
+ global_dependencies.add(self.itemfrag)
+ return self.dependencies
+
+trainer_group_maximums = {}
+class TrainerFragment(Command):
+ """used by TrainerFragmentParam and PeopleEvent for trainer data
+
+ Maybe this shouldn't be a Command. The output might sprawl
+ over multiple lines, and maybe it should be commented in to_asm?
+
+ [Bit no. (2byte)][Trainer group][Trainer]
+ [2byte pointer to Text when seen]
+ [2byte pointer to text when trainer beaten]
+ [2byte pointer to script when lost (0000=Blackout)]
+ [2byte pointer to script if won/talked to again]
+
+ The bit number tell the game later on if the trainer has been
+ beaten already (bit = 1) or not (bit = 0). All Bit number of BitTable1.
+
+ 03 = Nothing
+ 04 = Nothing
+ 05 = Nothing
+ 06 = Nothing
+ """
+ size = 12
+ macro_name = "trainer_def"
+ base_label = "Trainer_"
+ override_byte_check = True
+ param_types = {
+ 0: {"name": "bit_number", "class": MultiByteParam},
+ 1: {"name": "trainer_group", "class": TrainerGroupParam},
+ 2: {"name": "trainer_id", "class": TrainerIdParam},
+ 3: {"name": "text_when_seen", "class": TextPointerLabelParam},
+ 4: {"name": "text_when_trainer_beaten", "class": TextPointerLabelParam},
+ 5: {"name": "script_when_lost", "class": ScriptPointerLabelParam},
+ 6: {"name": "script_talk_again", "class": ScriptPointerLabelParam},
+ }
+
+ def __init__(self, *args, **kwargs):
+ address = kwargs["address"]
+ print "TrainerFragment address=" + hex(address)
+ self.address = address
+ self.last_address = self.address + self.size
+ if not is_valid_address(address) or address in [0x26ef]:
+ self.include_in_asm = False
+ return
+ script_parse_table[self.address : self.last_address] = self
+ self.dependencies = None
+ Command.__init__(self, *args, **kwargs)
+
+ def get_dependencies(self, recompute=False, global_dependencies=set()):
+ deps = []
+ if not is_valid_address(self.address):
+ return deps
+ if self.dependencies != None and not recompute:
+ global_dependencies.update(self.dependencies)
+ return self.dependencies
+ #deps.append(self.params[3])
+ deps.extend(self.params[3].get_dependencies(recompute=recompute, global_dependencies=global_dependencies))
+ #deps.append(self.params[4])
+ deps.extend(self.params[4].get_dependencies(recompute=recompute, global_dependencies=global_dependencies))
+ #deps.append(self.params[5])
+ deps.extend(self.params[5].get_dependencies(recompute=recompute, global_dependencies=global_dependencies))
+ #deps.append(self.params[6])
+ deps.extend(self.params[6].get_dependencies(recompute=recompute, global_dependencies=global_dependencies))
+ self.dependencies = deps
+ return deps
+
+ def parse(self):
+ Command.parse(self)
+
+ # get the trainer group id
+ trainer_group = self.params[1].byte
+
+ # get the trainer id
+ trainer_id = self.params[2].byte
+
+ if not trainer_group in trainer_group_maximums.keys():
+ trainer_group_maximums[trainer_group] = set([trainer_id])
+ else:
+ trainer_group_maximums[trainer_group].add(trainer_id)
+
+ # give this object a possibly better label
+ label = "Trainer"
+ if ("uses_numeric_trainer_ids" in trainer_group_names[trainer_group].keys()) \
+ or ("trainer_names" not in trainer_group_names[trainer_group].keys()):
+ label += string.capwords(trainer_group_names[trainer_group]["constant"])
+ if "trainer_names" in trainer_group_names[trainer_group].keys() \
+ and len(trainer_group_names[trainer_group]["trainer_names"]) > 1:
+ label += str(trainer_id)
+ else:
+ label += string.capwords(trainer_group_names[trainer_group]["constant"]) + \
+ string.capwords(trainer_group_names[trainer_group]["trainer_names"][trainer_id-1])
+
+ label = label.replace("Gruntm", "GruntM").replace("Gruntf", "GruntF").replace("Lt_surge", "LtSurge")
+
+ self.label = Label(name=label, address=self.address, object=self)
+
+ # ---- give better labels to the objects created by TrainerFragment ----
+
+ text_when_seen_text = script_parse_table[self.params[3].parsed_address]
+ if text_when_seen_text != None:
+ text_when_seen_label = Label(name=label + "WhenSeenText", address=text_when_seen_text.address, object=text_when_seen_text)
+ text_when_seen_text.label = text_when_seen_label
+
+ text_when_beaten_text = script_parse_table[self.params[4].parsed_address]
+ if text_when_beaten_text != None:
+ text_when_beaten_label = Label(name=label + "WhenBeatenText", address=text_when_beaten_text.address, object=text_when_beaten_text)
+ text_when_beaten_text.label = text_when_beaten_label
+
+ script_when_lost = script_parse_table[self.params[5].parsed_address]
+ if script_when_lost != None:
+ script_when_lost_label = Label(name=label + "WhenLostScript", address=script_when_lost.address, object=script_when_lost)
+ script_when_lost.label = script_when_lost_label
+
+ script_talk_again = script_parse_table[self.params[6].parsed_address]
+ if script_talk_again != None:
+ script_talk_again_label = Label(name=label + "WhenTalkScript", address=script_talk_again.address, object=script_talk_again)
+ script_talk_again.label = script_talk_again_label
+
+ def to_asm(self):
+ xspacing = ""
+ output = ""
+ output += xspacing + "; bit/flag number\n"
+ output += xspacing + "dw $%.2x"%(self.params[0].parsed_number)
+ output += "\n\n"+xspacing+"; trainer group && trainer id\n"
+ output += xspacing + "db %s, %s" % (self.params[1].to_asm(), self.params[2].to_asm())
+ output += "\n\n"+xspacing+"; text when seen\n"
+ output += xspacing + "dw " + self.params[3].to_asm()
+ output += "\n\n"+xspacing+"; text when trainer beaten\n"
+ output += xspacing + "dw " + self.params[4].to_asm()
+ output += "\n\n"+xspacing+"; script when lost\n"
+ output += xspacing + "dw " + self.params[5].to_asm()
+ output += "\n\n"+xspacing+"; script when talk again\n"
+ output += xspacing + "dw " + self.params[6].to_asm()
+ return output
+
+class TrainerFragmentParam(PointerLabelParam):
+ """used by PeopleEvent to point to trainer data"""
+ def parse(self):
+ address = calculate_pointer_from_bytes_at(self.address, bank=self.bank)
+ self.calculated_address = address
+ if address == 0x26ef:
+ self.trainerfrag = None
+ else:
+ trainerfrag = TrainerFragment(address=address, map_group=self.map_group, map_id=self.map_id, debug=self.debug)
+ self.trainerfrag = trainerfrag
+ PointerLabelParam.parse(self)
+
+ def get_dependencies(self, recompute=False, global_dependencies=set()):
+ deps = []
+ if self.dependencies != None and not recompute:
+ global_dependencies.update(self.dependencies)
+ return self.dependencies
+ if self.trainerfrag:
+ global_dependencies.add(self.trainerfrag)
+ deps.append(self.trainerfrag)
+ deps.extend(self.trainerfrag.get_dependencies(recompute=recompute, global_dependencies=global_dependencies))
+ self.dependencies = deps
+ return deps
+
+trainer_group_table = None
+class TrainerGroupTable:
+ """
+ A list of pointers.
+
+ This should probably be called TrainerGroupPointerTable.
+ """
+
+ def __init__(self):
+ assert 0x43 in trainer_group_maximums.keys(), "TrainerGroupTable should onyl be created after all the trainers have been found"
+ self.address = trainer_group_pointer_table_address
+ self.bank = calculate_bank(trainer_group_pointer_table_address)
+ self.label = Label(name="TrainerGroupPointerTable", address=self.address, object=self)
+ self.size = None
+ self.last_address = None
+ self.dependencies = None
+ self.headers = []
+ self.parse()
+
+ script_parse_table[self.address : self.last_address] = self
+
+ def get_dependencies(self, recompute=False, global_dependencies=set()):
+ global_dependencies.update(self.headers)
+ if recompute == True and self.dependencies != None and self.dependencies != []:
+ return self.dependencies
+ dependencies = copy(self.headers)
+ for header in self.headers:
+ dependencies.extend(header.get_dependencies(recompute=recompute, global_dependencies=global_dependencies))
+ return dependencies
+
+ def parse(self):
+ size = 0
+ for (key, kvalue) in trainer_group_names.items():
+ # calculate the location of this trainer group header from its pointer
+ pointer_bytes_location = kvalue["pointer_address"]
+ parsed_address = calculate_pointer_from_bytes_at(pointer_bytes_location, bank=self.bank)
+ trainer_group_names[key]["parsed_address"] = parsed_address
+
+ # parse the trainer group header at this location
+ name = kvalue["name"]
+ trainer_group_header = TrainerGroupHeader(address=parsed_address, group_id=key, group_name=name)
+ trainer_group_names[key]["header"] = trainer_group_header
+ self.headers.append(trainer_group_header)
+
+ # keep track of the size of this pointer table
+ size += 2
+ self.size = size
+ self.last_address = self.address + self.size
+
+ def to_asm(self):
+ output = "".join([str("dw "+get_label_for(header.address)+"\n") for header in self.headers])
+ return output
+
+class TrainerGroupHeader:
+ """
+ A trainer group header is a repeating list of individual trainer headers.
+
+ <Trainer Name> <0x50> <Data type> <Pokémon Data>+ <0xFF>
+
+ Data type <0x00>: Pokémon Data is <Level> <Species>. Used by most trainers.
+ Data type <0x01>: Pokémon Data is <Level> <Pokémon> <Move1> <Move2> <Move3> <Move4>. Used often for Gym Leaders.
+ Data type <0x02>: Pokémon Data is <Level> <Pokémon> <Held Item>. Used mainly by Pokéfans.
+ Data type <0x03>: Pokémon Data is <Level> <Pokémon> <Held Item> <Move1> <Move2> <Move3> <Move4>. Used by a few Cooltrainers.
+ """
+
+ def __init__(self, address=None, group_id=None, group_name=None):
+ assert address!=None, "TrainerGroupHeader requires an address"
+ assert group_id!=None, "TrainerGroupHeader requires a group_id"
+ assert group_name!=None, "TrainerGroupHeader requires a group_name"
+
+ self.address = address
+ self.group_id = group_id
+ self.group_name = group_name
+ self.dependencies = None
+ self.individual_trainer_headers = []
+ self.label = Label(name=group_name+"TrainerGroupHeader", address=self.address, object=self)
+ self.parse()
+
+ script_parse_table[address : self.last_address] = self
+
+ def get_dependencies(self, recompute=False, global_dependencies=set()):
+ """
+ TrainerGroupHeader has no dependencies.
+ """
+ # TODO: possibly include self.individual_trainer_headers
+ if recompute or self.dependencies == None:
+ self.dependencies = []
+ return self.dependencies
+
+ def parse(self):
+ """
+ how do i know when there's no more data for this header?
+ do a global analysis of the rom and figure out the max ids
+ this wont work for rom hacks of course
+ see find_trainer_ids_from_scripts
+ """
+ size = 0
+ current_address = self.address
+
+ if self.group_id not in trainer_group_maximums.keys():
+ self.size = 0
+ self.last_address = current_address
+ return
+
+ # create an IndividualTrainerHeader for each id in range(min id, max id + 1)
+ min_id = min(trainer_group_maximums[self.group_id])
+ max_id = max(trainer_group_maximums[self.group_id])
+
+ if self.group_id == 0x0C:
+ # CAL appears a third time with third-stage evos (meganium, typhlosion, feraligatr)
+ max_id += 1
+ elif self.group_id == 0x29:
+ # there's a missing supernerd :(
+ max_id += 1
+ elif self.group_id == 0x2D:
+ # missing bikers
+ max_id += 2
+ elif self.group_id == 0x31:
+ # missing jugglers
+ max_id += 3
+ elif self.group_id == 0x32:
+ # blackbelt wai
+ max_id += 1
+ elif self.group_id == 0x3C:
+ # kimono girl miki
+ max_id += 1
+ elif self.group_id == 0x3D:
+ # twins lea & pia
+ max_id += 1
+
+ for trainer_id in range(min_id, max_id+1):
+ trainer_header = TrainerHeader(address=current_address, trainer_group_id=self.group_id, trainer_id=trainer_id, parent=self)
+ self.individual_trainer_headers.append(trainer_header)
+ # current_address += trainer_header.size
+ current_address = trainer_header.last_address
+ size += trainer_header.size
+
+ self.last_address = current_address
+ self.size = size
+
+ def to_asm(self):
+ output = "\n\n".join(["; "+header.make_constant_name()+" ("+str(header.trainer_id)+") at "+hex(header.address)+"\n"+header.to_asm() for header in self.individual_trainer_headers])
+ return output
+
+class TrainerHeader:
+ """
+ <Trainer Name> <0x50> <Data type> <Pokémon Data>+ <0xFF>
+
+ Data type <0x00>: Pokémon Data is <Level> <Species>. Used by most trainers.
+ Data type <0x01>: Pokémon Data is <Level> <Pokémon> <Move1> <Move2> <Move3> <Move4>. Used often for Gym Leaders.
+ Data type <0x02>: Pokémon Data is <Level> <Pokémon> <Held Item>. Used mainly by Pokéfans.
+ Data type <0x03>: Pokémon Data is <Level> <Pokémon> <Held Item> <Move1> <Move2> <Move3> <Move4>. Used by a few Cooltrainers.
+ """
+
+ def __init__(self, address=None, trainer_group_id=None, trainer_id=None, parent=None):
+ self.parent = parent
+ self.address = address
+ self.trainer_group_id = trainer_group_id
+ self.trainer_id = trainer_id
+ self.dependencies = []
+ self.size = None
+ self.last_address = None
+ self.parse()
+ self.label = Label(name=self.make_name(), address=self.address, object=self)
+ # this shouldn't be added to script_parse_table because
+ # TrainerGroupHeader covers its address range
+
+ def make_name(self):
+ """
+ Must occur after parse() is called.
+ Constructs a name based on self.parent.group_name and self.name.
+ """
+ if self.trainer_group_id in [0x14, 0x16, 0x17, 0x18, 0x19, 0x1B, 0x1C, 0x1D, 0x1E, 0x20, 0x21, 0x22, 0x24, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2B, 0x2C, 0x2D, 0x2F, 0x30, 0x31, 0x32, 0x34, 0x35, 0x36, 0x38, 0x39, 0x3A, 0x3B, 0x3C, 0x3D, 0x3E, 0x41]:
+ return self.parent.group_name.upper() + "_" + self.name[:-1]
+ else:
+ return self.parent.group_name + "_" + str(self.trainer_id)
+
+ def make_constant_name(self):
+ if hasattr(self, "seed_constant_name"):
+ seed = self.seed_constant_name
+ else:
+ seed = self.name
+
+ if "?" in seed:
+ if seed[-2].isdigit():
+ x = 2
+ else:
+ x = 1
+ seed = trainer_group_names[self.trainer_group_id]["name"]+"_"+seed[-x:]
+ elif self.trainer_group_id == 0x1f and "EXECUTIVE" in seed:
+ seed = "GRUNT_"+seed
+ elif self.trainer_group_id == 0x2d and "BENNY" in seed.upper():
+ seed = "BIKER_BENNY"
+ elif self.trainer_group_id == 0x24 and "BENNY" in seed.upper():
+ seed = "BUG_CATCHER_BENNY"
+
+ return string.capwords(seed).\
+ replace("@", "").\
+ replace(" & ", "AND").\
+ replace(" ", "").\
+ replace(".", "_").\
+ upper()
+
+ def get_dependencies(self, recompute=False, global_dependencies=set()):
+ if recompute or self.dependencies == None:
+ self.dependencies = []
+ return self.dependencies
+
+ def parse(self):
+ address = self.address
+
+ # figure out how many bytes until 0x50 "@"
+ jump = how_many_until(chr(0x50), address)
+
+ # parse the "@" into the name
+ self.name = parse_text_at(address, jump+1)
+
+ # where is the next byte?
+ current_address = address + jump + 1
+
+ # figure out the pokemon data type
+ self.data_type = ord(rom[current_address])
+
+ current_address += 1
+
+ # figure out which partymon parser to use for this trainer header
+ party_mon_parser = None
+ for monparser in trainer_party_mon_parsers:
+ if monparser.id == self.data_type:
+ party_mon_parser = monparser
+ break
+
+ if party_mon_parser == None:
+ raise Exception("no trainer party mon parser found to parse data type " + hex(self.data_type))
+
+ self.party_mons = party_mon_parser(address=current_address, group_id=self.trainer_group_id, trainer_id=self.trainer_id, parent=self)
+
+ # let's have everything in trainer_party_mon_parsers handle the last $FF
+ #self.size = self.party_mons.size + 1 + len(self.name)
+ self.size = self.party_mons.last_address - self.address
+ self.last_address = self.party_mons.last_address
+
+ def to_asm(self):
+ output = "db \""+self.name+"\"\n"
+ output += "db $%.2x ; data type\n" % (self.data_type)
+ output += self.party_mons.to_asm()
+ output += "\n; last_address="+hex(self.last_address)+" size="+str(self.size)
+ return output
+
+class TrainerPartyMonParser:
+ """
+ Just a generic trainer party mon parser.
+ Don't use this directly. Only use the child classes.
+ """
+ id = None
+ dependencies = None
+ param_types = None
+
+ # could go either way on this one.. TrainerGroupHeader.parse would need to be changed
+ # so as to not increase current_address by one after reading "data_type"
+ override_byte_check = True
+
+ def __init__(self, address=None, group_id=None, trainer_id=None, parent=None):
+ self.address = address
+ self.group_id = group_id
+ self.trainer_id = trainer_id
+ self.parent = parent
+ self.args = {}
+ self.mons = {}
+ self.parse()
+
+ # pick up the $FF at the end
+ self.last_address += 1
+
+ def parse(self):
+ current_address = self.address
+ pkmn = 0
+ continuer = True
+ while continuer:
+ self.mons[pkmn] = {}
+ i = 0
+ for (key, param_type) in self.param_types.items():
+ name = param_type["name"]
+ klass = param_type["class"]
+ # make an instance of this class, like SingleByteParam()
+ # or ItemLabelByte.. by making an instance, obj.parse() is called
+ obj = klass(address=current_address, name=name, parent=self, **dict([(k,v) for (k, v) in self.args.items() if k not in ["parent"]]))
+ # save this for later
+ self.mons[pkmn][i] = obj
+ # increment our counters
+ current_address += obj.size
+ i += 1
+ pkmn += 1
+ if ord(rom[current_address]) == 0xFF:
+ break
+ self.last_address = current_address
+ return True
+
+ def to_asm(self):
+ output = ""
+ #output = "; " + ", ".join([param_type["name"] for (key, param_type) in self.param_types.items()]) + "\n"
+ for mon in self.mons:
+ output += "db " + ", ".join([param.to_asm() for (name, param) in self.mons[mon].items()])
+ output += "\n"
+ output += "db $ff ; end trainer party mons"
+ return output
+
+class TrainerPartyMonParser0(TrainerPartyMonParser):
+ """
+ Data type <0x00>: Pokémon Data is <Level> <Species>. Used by most trainers.
+ """
+ id = 0
+ size = 2 + 1
+ param_types = {
+ 0: {"name": "level", "class": DecimalParam},
+ 1: {"name": "species", "class": PokemonParam},
+ }
+class TrainerPartyMonParser1(TrainerPartyMonParser):
+ """
+ Data type <0x01>: Pokémon Data is <Level> <Pokémon> <Move1> <Move2> <Move3>
+ <Move4>. Used often for Gym Leaders.
+ """
+ id = 1
+ size = 6 + 1
+ param_types = {
+ 0: {"name": "level", "class": DecimalParam},
+ 1: {"name": "species", "class": PokemonParam},
+ 2: {"name": "move1", "class": MoveParam},
+ 3: {"name": "move2", "class": MoveParam},
+ 4: {"name": "move3", "class": MoveParam},
+ 5: {"name": "move4", "class": MoveParam},
+ }
+class TrainerPartyMonParser2(TrainerPartyMonParser):
+ """
+ Data type <0x02>: Pokémon Data is <Level> <Pokémon> <Held Item>. Used mainly by Pokéfans.
+ """
+ id = 2
+ size = 3 + 1
+ param_types = {
+ 0: {"name": "level", "class": DecimalParam},
+ 1: {"name": "species", "class": PokemonParam},
+ 2: {"name": "item", "class": ItemLabelByte},
+ }
+class TrainerPartyMonParser3(TrainerPartyMonParser):
+ """
+ Data type <0x03>: Pokémon Data is <Level> <Pokémon> <Held Item> <Move1>
+ <Move2> <Move3> <Move4>.
+ Used by a few Cooltrainers.
+ """
+ id = 3
+ size = 7 + 1
+ param_types = {
+ 0: {"name": "level", "class": DecimalParam},
+ 1: {"name": "species", "class": PokemonParam},
+ 2: {"name": "item", "class": ItemLabelByte},
+ 3: {"name": "move1", "class": MoveParam},
+ 4: {"name": "move2", "class": MoveParam},
+ 5: {"name": "move3", "class": MoveParam},
+ 6: {"name": "move4", "class": MoveParam},
+ }
+
+trainer_party_mon_parsers = [TrainerPartyMonParser0, TrainerPartyMonParser1, TrainerPartyMonParser2, TrainerPartyMonParser3]
+
+def find_trainer_ids_from_scripts():
+ """
+ Looks through all scripts to find trainer group numbers and trainer numbers.
+
+ This can be used with trainer_group_maximums to figure out the current number of
+ trainers in each of the originating trainer groups.
+ """
+ total_unreferenced_trainers = 0
+
+ # look at each possibly relevant script
+ for item in script_parse_table.items():
+ object = item[1]
+ if isinstance(object, Script):
+ check_script_has_trainer_data(object)
+
+ # make a set of each list of trainer ids to avoid dupes
+ # this will be used later in TrainerGroupTable
+ for item in trainer_group_maximums.items():
+ key = item[0]
+ value = set(item[1])
+ trainer_group_maximums[key] = value
+
+def report_unreferenced_trainer_ids():
+ """
+ Reports on the number of unreferenced trainer ids in each group.
+
+ This should be called after find_trainer_ids_from_scripts.
+
+ These are trainer groups with "unused" trainer ids. The
+ "find_trainer_ids_from_scripts" function analyzes each script in the game,
+ and each map header in the game (because of code in TrainerFragment), and
+ finds all references to trainers. But, if there are any trainers that are
+ referenced in raw ASM, this method does not detect them. Each instance of a
+ trainer reference is added to a global table called
+ "trainer_group_maximums". Next, "find_trainer_ids_from_scripts" looks at
+ the trainer IDs referenced for each group and takes the minimum number and
+ the maximum number. To find whether or not there are any unused trainers,
+ it takes the minimum and maximum ids and then sees which intermediate
+ numbers are missing from the list of "referenced" trainer ids.
+ """
+ for item in trainer_group_maximums.items():
+ key = item[0]
+ value = item[1]
+
+ # i'm curious: are there any missing trainer ids in this group?
+ min_id = min(value)
+ max_id = max(value)
+ expectables = range(min_id, max_id+1)
+
+ unreferenced = set()
+
+ for expectable in expectables:
+ if not expectable in value:
+ unreferenced.add(expectable)
+
+ if len(unreferenced) > 0:
+ total_unreferenced_trainers += len(unreferenced)
+ output = "trainer group "+hex(key)+" (\""+trainer_group_names[key]["name"]+"\")"
+ output += " (min="+str(min_id)+", max="+str(max_id)+")"
+ output += " has "+str(len(unreferenced))+" unreferenced trainer ids"
+ output += ": " + str(unreferenced)
+ print output
+ print "total unreferenced trainers: " + str(total_unreferenced_trainers)
+
+def check_script_has_trainer_data(script):
+ """
+ see find_trainer_ids_from_scripts
+ """
+ for command in script.commands:
+ trainer_group = None
+ trainer_id = None
+
+ if command.id == 0x43:
+ trainer_group = command.params[0].byte
+ trainer_id = command.params[1].byte
+ elif command.id == 0x5E:
+ trainer_group = command.params[0].byte
+ trainer_id = command.params[1].byte
+
+ if trainer_group != None and trainer_id != None:
+ if trainer_group in trainer_group_maximums.keys():
+ trainer_group_maximums[trainer_group].add(trainer_id)
+ else:
+ trainer_group_maximums[trainer_group] = set([trainer_id])
+
+def trainer_name_from_group(group_id, trainer_id=0):
+ """This doesn't actually work for trainer_id > 0."""
+ bank = calculate_bank(0x39999)
+ ptr_address = 0x39999 + ((group_id - 1)*2)
+ address = calculate_pointer_from_bytes_at(ptr_address, bank=bank)
+ text = parse_text_at2(address, how_many_until(chr(0x50), address))
+ return text
+
+def trainer_group_report():
+ """
+ Reports how many trainer ids are used in each trainer group.
+ """
+ output = ""
+ total = 0
+ for trainer_group_id in trainer_group_maximums.keys():
+ group_name = trainer_group_names[trainer_group_id]["name"]
+ first_name = trainer_name_from_group(trainer_group_id).replace("\n", "")
+ trainers = len(trainer_group_maximums[trainer_group_id])
+ total += trainers
+ output += "group "+hex(trainer_group_id)+":\n"
+ output += "\tname: "+group_name+"\n"
+ output += "\tfirst: "+first_name+"\n"
+ output += "\ttrainer count:\t"+str(trainers)+"\n\n"
+ output += "total trainers: " + str(total)
+ return output
+
+def make_trainer_group_name_trainer_ids(trainer_group_table, debug=True):
+ """
+ Edits trainer_group_names and sets the trainer names.
+ For instance, "AMY & MAY" becomes "AMY_AND_MAY1" and "AMY_AND_MAY2"
+
+ This should only be used after TrainerGroupTable.parse has been called.
+ """
+ assert trainer_group_table != None, "TrainerGroupTable must be called before setting the trainer names"
+
+ if debug:
+ print "starting to make trainer names and give ids to repeated trainer names"
+
+ i = 1
+ for header in trainer_group_table.headers:
+ trainer_names = [] # (name, trainer_header)
+ dupes = set()
+ group_id = i
+ group_name = header.group_name
+ for trainer_header in header.individual_trainer_headers:
+ if trainer_header.name in [x[0] for x in trainer_names]:
+ dupes.add(trainer_header.name)
+ trainer_names.append([trainer_header.name, trainer_header])
+
+ # now fix trainers with duplicate names by appending an id
+ if len(dupes) > 0:
+ for dupe in dupes:
+ culprits = [trainer_header for trainer_header in header.individual_trainer_headers if trainer_header.name == dupe]
+ for (id, culprit) in enumerate(culprits):
+ culprit.seed_constant_name = culprit.name.replace("@", "") + str(id+1)
+ culprit.constant_name = culprit.make_constant_name()
+
+ # now add the trainer names to trainer_group_names
+ trainer_group_names[i]["trainer_names"] = [theader.make_constant_name() for theader in header.individual_trainer_headers]
+
+ i += 1
+
+ if debug:
+ print "done improving trainer names"
+
+def pretty_print_trainer_id_constants():
+ """
+ Prints out some constants for trainer ids, for "constants.asm".
+
+ make_trainer_group_name_trainer_ids must be called prior to this.
+ """
+ assert trainer_group_table != None, "must make trainer_group_table first"
+ assert trainer_group_names != None, "must have trainer_group_names available"
+ assert "trainer_names" in trainer_group_names[1].keys(), "trainer_names must be set in trainer_group_names"
+
+ output = ""
+ for (key, value) in trainer_group_names.items():
+ if "uses_numeric_trainer_ids" in trainer_group_names[key].keys():
+ continue
+ id = key
+ group = value
+ header = group["header"]
+ name = group["name"]
+ trainer_names = group["trainer_names"]
+ output += "; " + name + "\n"
+ for (id, name) in enumerate(trainer_names):
+ output += name.upper() + " EQU $%.2x"%(id+1) + "\n"
+ output += "\n"
+ return output
+
+class PeopleEvent(Command):
+ size = people_event_byte_size
+ macro_name = "person_event"
+ base_label = "PeopleEvent_"
+ override_byte_check = True
+ param_types = {
+ 0: {"name": "sprite", "class": HexByte},
+ 1: {"name": "y from top+4", "class": DecimalParam},
+ 2: {"name": "x from top+4", "class": DecimalParam},
+ 3: {"name": "facing", "class": HexByte},
+ 4: {"name": "movement", "class": HexByte},
+ 5: {"name": "clock_hour", "class": DecimalParam},
+ 6: {"name": "clock_daytime", "class": DecimalParam},
+ 7: {"name": "color_function", "class": HexByte},
+ 8: {"name": "sight_range", "class": DecimalParam},
+ 9: {"name": "pointer", "class": PointerLabelParam}, # or ScriptPointerLabelParam or ItemLabelParam
+ 10: {"name": "BitTable1 bit number", "class": MultiByteParam},
+ }
+
+ def xto_asm(self):
+ output = "\n; person-event\n; picture, y, x, facing, movement, clock_hour, clock_daytime, color_function, sight_range\n"
+ output += "db $%.2x, %d, %d, $%.2x, $%.2x, %d, %d, $%.2x, %d\n" % (self.params[0].byte, self.params[1].byte, self.params[2].byte, self.params[3].byte, self.params[4].byte, self.params[5].byte, self.params[6].byte, self.params[7].byte, self.params[8].byte)
+ output += "; pointer\ndw %s\n" % (self.params[9].to_asm())
+ output += "; BitTable1 bit number\ndw %s" % (self.params[10].to_asm())
+ return output
+
+ def __init__(self, address, id, bank=None, map_group=None, map_id=None, debug=False, label=None, force=False):
+ assert is_valid_address(address), "PeopleEvent must be given a valid address"
+ self.address = address
+ self.last_address = address + people_event_byte_size
+ self.id = id
+ self.bank = bank
+ if not label:
+ label = self.base_label + hex(address)
+ self.label = Label(name=label, address=address, object=self)
+ self.map_group = map_group
+ self.map_id = map_id
+ self.debug = debug
+ self.force = force
+ self.params = {}
+ self.dependencies = None
+ # PeopleEvent should probably not be in the global script_parse_table
+ #script_parse_table[self.address : self.last_address] = self
+ self.parse()
+
+ def parse(self):
+ address = self.address
+ bank = self.bank
+
+ color_function_byte = None
+ lower_bits = None
+ higher_bits = None
+ is_regular_script = None
+ is_give_item = None
+ is_trainer = None
+
+ self.params = {}
+ current_address = self.address
+ i = 0
+ self.size = 1
+ color_function_byte = None
+ for (key, param_type) in self.param_types.items():
+ if i == 9:
+ if is_give_item:
+ name = "item_fragment_pointer"
+ klass = ItemFragmentParam
+ elif is_regular_script:
+ name = "script_pointer"
+ klass = ScriptPointerLabelParam
+ elif is_trainer:
+ name = "trainer"
+ #klass = MultiByteParam
+ klass = TrainerFragmentParam
+ else:
+ name = "unknown"
+ klass = MultiByteParam
+ else:
+ name = param_type["name"]
+ klass = param_type["class"]
+ obj = klass(address=current_address, name=name, debug=self.debug, force=self.force, map_group=self.map_group, map_id=self.map_id, bank=self.bank)
+ self.params[i] = obj
+ if i == 7:
+ color_function_byte = ord(rom[current_address])
+ lower_bits = color_function_byte & 0xF
+ higher_bits = color_function_byte >> 4
+ is_regular_script = lower_bits == 00
+ is_give_item = lower_bits == 01
+ is_trainer = lower_bits == 02
+ current_address += obj.size
+ self.size += obj.size
+ i += 1
+ self.last_address = current_address
+ self.is_trainer = is_trainer
+ self.is_give_item = is_give_item
+ self.is_regular_script = is_regular_script
+ self.y = self.params[1].byte
+ self.x = self.params[2].byte
+ self.facing = self.params[3].byte
+ self.movement = self.params[4].byte
+ self.clock_hour = self.params[5].byte
+ self.clock_daytime = self.params[6].byte
+ self.color_function = self.params[7].byte
+ self.sight_range = self.params[8].byte
+ self.pointer = self.params[9].bytes
+ self.bit_number = self.params[10].bytes
+ return True
+
+
+all_people_events = []
+def parse_people_events(address, people_event_count, bank=None, map_group=None, map_id=None, debug=False, force=False):
+ # people_event_byte_size
+ people_events = []
+ current_address = address
+ id = 0
+ for each in range(people_event_count):
+ pevent = PeopleEvent(address=current_address, id=id, bank=bank, map_group=map_group, map_id=map_id, debug=debug, force=force)
+ current_address += people_event_byte_size
+ people_events.append(pevent)
+ id += 1
+ all_people_events.extend(people_events)
+ return people_events
+
+def old_parse_people_event_bytes(some_bytes, address=None, map_group=None, map_id=None, debug=True):
+ """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)
+
+ lower_bits = color_function_byte & 0xF
+ #lower_bits_high = lower_bits >> 2
+ #lower_bits_low = lower_bits & 3
+ higher_bits = color_function_byte >> 4
+ #higher_bits_high = higher_bits >> 2
+ #higher_bits_low = higher_bits & 3
+
+ is_regular_script = lower_bits == 00
+ # pointer points to script
+ is_give_item = lower_bits == 01
+ # pointer points to [Item no.][Amount]
+ is_trainer = lower_bits == 02
+ # pointer points to trainer header
+
+ # 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?
+ extra_portion = {}
+ if bank:
+ ptr_address = calculate_pointer(script_pointer, bank)
+ if is_regular_script:
+ print "parsing a person-script at x=" + str(x-4) + " y=" + str(y-4) + " address="+hex(ptr_address)
+ script = parse_script_engine_script_at(ptr_address, map_group=map_group, map_id=map_id)
+ extra_portion = {
+ "script_address": ptr_address,
+ "script": script,
+ "event_type": "script",
+ }
+ if is_give_item:
+ print "... not parsing give item event... [item id][quantity]"
+ extra_portion = {
+ "event_type": "give_item",
+ "give_item_data_address": ptr_address,
+ "item_id": ord(rom[ptr_address]),
+ "item_qty": ord(rom[ptr_address+1]),
+ }
+ if is_trainer:
+ print "parsing a trainer (person-event) at x=" + str(x) + " y=" + str(y)
+ parsed_trainer = parse_trainer_header_at(ptr_address, map_group=map_group, map_id=map_id)
+ extra_portion = {
+ "event_type": "trainer",
+ "trainer_data_address": ptr_address,
+ "trainer_data": parsed_trainer,
+ }
+
+ # 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)
+
+ bit_number_of_bit_table1_byte2 = int(bytes[11], 16)
+ bit_number_of_bit_table1_byte1 = int(bytes[12], 16)
+ bit_number_of_bit_table1 = bit_number_of_bit_table1_byte1 + (bit_number_of_bit_table1_byte2 << 8)
+
+ people_event = {
+ "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},
+
+ #"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
+
+ "is_trainer": is_trainer,
+ "is_regular_script": is_regular_script,
+ "is_give_item": is_give_item,
+ }
+ people_event.update(extra_portion)
+ people_events.append(people_event)
+ return people_events
+
+
+class SignpostRemoteBase:
+ def __init__(self, address, bank=None, map_group=None, map_id=None, signpost=None, debug=False, label=None):
+ self.address = address
+ self.last_address = address + self.size
+ script_parse_table[self.address : self.last_address] = self
+ self.bank = bank
+ self.map_group = map_group
+ self.map_id = map_id
+ self.signpost = signpost
+ self.debug = debug
+ self.params = []
+ if not label:
+ label = self.base_label + hex(address)
+ self.label = Label(name=label, address=address, object=self)
+ self.dependencies = None
+ self.parse()
+
+ def get_dependencies(self, recompute=False, global_dependencies=set()):
+ dependencies = []
+ if self.dependencies != None and not recompute:
+ global_dependencies.update(self.dependencies)
+ return self.dependencies
+ for p in self.params:
+ deps = p.get_dependencies(recompute=recompute, global_dependencies=global_dependencies)
+ dependencies.extend(deps)
+ self.dependencies = dependencies
+ return dependencies
+
+ def to_asm(self):
+ """very similar to Command.to_asm"""
+ if len(self.params) == 0:
+ return ""
+ #output = ", ".join([p.to_asm() for p in self.params])
+ output = ""
+ for param in self.params:
+ if issubclass(param.__class__, SingleByteParam):
+ output += "db "
+ else:
+ output += "dw "
+ output += param.to_asm() + "\n"
+ return output
+
+
+class SignpostRemoteScriptChunk(SignpostRemoteBase):
+ """
+ a signpost might point to [Bit-Nr. (2byte)][2byte pointer to script]
+ """
+ base_label = "SignpostRemoteScript_"
+ size = 4
+
+ def parse(self):
+ address = self.address
+ bank = self.bank
+
+ #bit_table_byte1 = ord(rom[address])
+ #bit_table_byte2 = ord(rom[address+1])
+ bit_table = MultiByteParam(address=address, map_group=self.map_group, map_id=self.map_id, debug=self.debug)
+ self.params.append(bit_table)
+
+ #script_address = calculate_pointer_from_bytes_at(address+2, bank=bank)
+ #script = parse_script_engine_script_at(script_address, map_group=self.map_group, map_id=self.map_id, debug=self.debug)
+ script_param = ScriptPointerLabelParam(address=address+2, map_group=self.map_group, map_id=self.map_id, debug=self.debug, force=False)
+ self.params.append(script_param)
+ self.script = script_param.script
+ self.signpost.remote_script = self.script
+
+ #self.bit_table_bytes = [bit_table_byte1, bit_table_byte2]
+ #self.script_address = script_address
+ #self.script = script
+
+
+class SignpostRemoteItemChunk(SignpostRemoteBase):
+ """
+ a signpost might point to [Bit-Nr. (2byte)][Item no.]
+ """
+ base_label = "SignpostRemoteItem_"
+ size = 3
+
+ def parse(self):
+ address = self.address
+ bank = self.bank
+
+ bit_table = MultiByteParam(address=address, map_group=self.map_group, map_id=self.map_id, debug=self.debug)
+ self.params.append(bit_table)
+
+ item = ItemLabelByte(address=address+2)
+ self.params.append(item)
+ self.item = item
+
+
+class SignpostRemoteUnknownChunk(SignpostRemoteBase):
+ """
+ a signpost might point to [Bit-Nr. (2byte)][??]
+ """
+ base_label = "SignpostRemoteUnknown_"
+ size = 3
+
+ def parse(self):
+ address = self.address
+ bank = self.bank
+
+ bit_table = MultiByteParam(address=address, bank=self.bank, map_group=self.map_group, map_id=self.map_id, debug=self.debug)
+ self.params.append(bit_table)
+
+ byte = SingleByteParam(address=address+2)
+ self.params.append(byte)
+
+
+# this could potentially extend Command
+# see how class Warp does this
+class Signpost(Command):
+ """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)][??]
+ """
+ size = 5
+ macro_name = "signpost"
+ override_byte_check = True
+
+ # preprocessor uses this
+ param_types = {
+ 0: {"name": "y", "class": DecimalParam},
+ 1: {"name": "x", "class": DecimalParam},
+ 2: {"name": "function", "class": HexByte},
+ 3: {"name": "pointer", "class": PointerLabelParam},
+ }
+
+ def __init__(self, address, id, bank=None, map_group=None, map_id=None, debug=True, label=None):
+ self.address = address
+ self.id = id
+ if label == None:
+ label = "UnknownSignpost_"+str(map_group)+"Map"+str(map_id)+"_"+hex(address)
+ self.label = Label(name=label, address=address, object=self)
+ self.map_group = map_group
+ self.map_id = map_id
+ self.debug = debug
+ self.bank = bank
+ self.last_address = self.address + self.size
+ self.y, self.x, self.func = None, None, None
+ # Signpost should probably not be in the globals
+ #script_parse_table[self.address : self.last_address] = self
+ self.remotes = []
+ self.params = []
+ self.dependencies = None
+ self.parse()
+
+ def parse(self):
+ """parse just one signpost"""
+ address = self.address
+ bank = self.bank
+ self.last_address = self.address + self.size
+ bytes = rom_interval(self.address, self.size) #, signpost_byte_size)
+
+ self.y = int(bytes[0], 16)
+ self.x = int(bytes[1], 16)
+ self.func = int(bytes[2], 16)
+ y, x, func = self.y, self.x, self.func
+
+ # y
+ self.params.append(DecimalParam(address=address, bank=self.bank, map_group=self.map_group, map_id=self.map_id, debug=self.debug))
+ # x
+ self.params.append(DecimalParam(address=address+1, bank=self.bank, map_group=self.map_group, map_id=self.map_id, debug=self.debug))
+ # func
+ self.params.append(HexByte(address=address+2, bank=self.bank, map_group=self.map_group, map_id=self.map_id, debug=self.debug))
+
+ output = "******* parsing signpost "+str(self.id)+" at: "
+ output += "x="+str(x)+" y="+str(y)+" on map_group="
+ output += str(self.map_group)+" map_id="+str(self.map_id)
+
+ if func in [0, 1, 2, 3, 4]:
+ # signpost's script pointer points to a script
+ 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 = calculate_pointer(script_pointer, bank)
+ output += " script@"+hex(script_address)
+ print output
+
+ param = ScriptPointerLabelParam(address=self.address+3, map_group=self.map_group, map_id=self.map_id, debug=self.debug, force=False)
+ self.params.append(param)
+ param = script_parse_table[param.parsed_address]
+ param.label = Label(address=param.address, object=param, name="Map"+map_names[self.map_group][self.map_id]["label"]+"Signpost"+str(self.id)+"Script")
+
+ #self.script_address = script_address
+ #self.script = script
+ elif func in [5, 6]:
+ # signpost's script pointer points to [Bit-Nr. (2byte)][2byte pointer to script]
+ ptr_byte1 = int(bytes[3], 16)
+ ptr_byte2 = int(bytes[4], 16)
+ pointer = ptr_byte1 + (ptr_byte2 << 8)
+ address = calculate_pointer(pointer, bank)
+
+ bit_table_byte1 = ord(rom[address])
+ bit_table_byte2 = ord(rom[address+1])
+ script_ptr_byte1 = ord(rom[address+2])
+ script_ptr_byte2 = ord(rom[address+3])
+ script_address = calculate_pointer_from_bytes_at(address+2, bank=bank)
+
+ output += " remote_chunk@"+hex(address)+" remote_script@"+hex(script_address)
+ print output
+
+ r1 = SignpostRemoteScriptChunk(address, signpost=self, \
+ bank=self.bank, map_group=self.map_group, map_id=self.map_id, \
+ debug=self.debug)
+ self.remotes.append(r1)
+
+ # give a better label to the SignpostRemoteScriptChunk
+ r1.label = Label(address=r1.address, object=r1, name="Map"+map_names[self.map_group][self.map_id]["label"]+"SignpostPtr"+str(self.id))
+
+ mb = PointerLabelParam(address=self.address+3, map_group=self.map_group, map_id=self.map_id, debug=self.debug)
+ self.params.append(mb)
+
+ # update the remote script address
+ param = script_parse_table[script_address]
+ param.label = Label(address=param.address, object=param, name="Map"+map_names[self.map_group][self.map_id]["label"]+"Signpost"+str(self.id)+"Script")
+
+ elif func == 7:
+ # signpost's script pointer points to [Bit-Nr. (2byte)][Item no.]
+ ptr_byte1 = int(bytes[3], 16)
+ ptr_byte2 = int(bytes[4], 16)
+ pointer = ptr_byte1 + (ptr_byte2 << 8)
+ address = calculate_pointer(pointer, bank)
+
+ item_id = ord(rom[address+2])
+ output += " item_id="+str(item_id)
+ print output
+
+ r1 = SignpostRemoteItemChunk(address, signpost=self, \
+ bank=self.bank, map_group=self.map_group, map_id=self.map_id, \
+ debug=self.debug)
+ self.remotes.append(r1)
+ r1.label = Label(address=r1.address, object=r1, name="Map"+map_names[self.map_group][self.map_id]["label"]+"SignpostItem"+str(self.id))
+
+ mb = PointerLabelParam(address=self.address+3, map_group=self.map_group, map_id=self.map_id, debug=self.debug)
+ self.params.append(mb)
+
+ #bit_table_byte1 = ord(rom[address])
+ #bit_table_byte2 = ord(rom[address+1])
+ #self.bit_table_bytes = [bit_table_byte1, bit_table_byte2]
+ #self.item_id = item_id
+ elif func == 8:
+ # signpost's script pointer points to [Bit-Nr. (2byte)][??]
+ ptr_byte1 = int(bytes[3], 16)
+ ptr_byte2 = int(bytes[4], 16)
+ pointer = ptr_byte1 + (ptr_byte2 << 8)
+ address = calculate_pointer(pointer, bank)
+
+ output += " remote unknown chunk at="+hex(address)
+ print output
+
+ r1 = SignpostRemoteUnknownChunk(address, signpost=self, \
+ bank=self.bank, map_group=self.map_group, map_id=self.map_id, \
+ debug=self.debug)
+ self.remotes.append(r1)
+
+ mb = PointerLabelParam(address=self.address+3, map_group=self.map_group, map_id=self.map_id, debug=self.debug)
+ self.params.append(mb)
+ else:
+ raise Exception("unknown signpost type byte="+hex(func) + " signpost@"+hex(self.address))
+
+ def get_dependencies(self, recompute=False, global_dependencies=set()):
+ dependencies = []
+ if self.dependencies != None and not recompute:
+ global_dependencies.update(self.dependencies)
+ return self.dependencies
+ for p in self.params:
+ dependencies.extend(p.get_dependencies(recompute=recompute, global_dependencies=global_dependencies))
+ self.dependencies = dependencies
+ return dependencies
+
+ def to_asm(self):
+ output = self.macro_name + " "
+ if self.params == []:
+ raise Exception("signpost has no params?")
+ output += ", ".join([p.to_asm() for p in self.params])
+ return output
+
+all_signposts = []
+def parse_signposts(address, signpost_count, bank=None, map_group=None, map_id=None, debug=True):
+ if bank == None:
+ raise Exception("signposts need to know their bank")
+ signposts = []
+ current_address = address
+ id = 0
+ for each in range(signpost_count):
+ signpost = Signpost(current_address, id, bank=bank, map_group=map_group, map_id=map_id, debug=debug)
+ current_address += signpost_byte_size # i think ??
+ signposts.append(signpost)
+ id += 1
+ all_signposts.extend(signposts)
+ return signposts
+
+def old_parse_signpost_bytes(some_bytes, bank=None, map_group=None, map_id=None, debug=True):
+ 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)
+
+ additional = {}
+ if func in [0, 1, 2, 3, 4]:
+ print "******* parsing signpost script.. signpost is at: x=" + str(x) + " y=" + str(y)
+ 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
+
+ script_address = calculate_pointer(script_pointer, bank)
+ script = parse_script_engine_script_at(script_address, map_group=map_group, map_id=map_id)
+
+ additional = {
+ "script_ptr": script_pointer,
+ "script_pointer": {"1": script_ptr_byte1, "2": script_ptr_byte2},
+ "script_address": script_address,
+ "script": script,
+ }
+ elif func in [5, 6]:
+ print "******* parsing signpost script.. signpost is at: x=" + str(x) + " y=" + str(y)
+ ptr_byte1 = int(bytes[3], 16)
+ ptr_byte2 = int(bytes[4], 16)
+ pointer = ptr_byte1 + (ptr_byte2 << 8)
+ address = calculate_pointer(pointer, bank)
+ bit_table_byte1 = ord(rom[address])
+ bit_table_byte2 = ord(rom[address+1])
+ script_ptr_byte1 = ord(rom[address+2])
+ script_ptr_byte2 = ord(rom[address+3])
+ script_address = calculate_pointer_from_bytes_at(address+2, bank=bank)
+ script = parse_script_engine_script_at(script_address, map_group=map_group, map_id=map_id)
+
+ additional = {
+ "bit_table_bytes": {"1": bit_table_byte1, "2": bit_table_byte2},
+ "script_ptr": script_ptr_byte1 + (script_ptr_byte2 << 8),
+ "script_pointer": {"1": script_ptr_byte1, "2": script_ptr_byte2},
+ "script_address": script_address,
+ "script": script,
+ }
+ else:
+ print ".. type 7 or 8 signpost not parsed yet."
+
+ spost = {
+ "y": y,
+ "x": x,
+ "func": func,
+ }
+ spost.update(additional)
+ signposts.append(spost)
+ return signposts
+
+
+class MapHeader:
+ base_label = "MapHeader_"
+
+ def __init__(self, address, map_group=None, map_id=None, debug=True, label=None, bank=0x25):
+ print "creating a MapHeader at "+hex(address)+" map_group="+str(map_group)+" map_id="+str(map_id)
+ self.address = address
+ self.map_group = map_group
+ self.map_id = map_id
+ self.bank = bank
+ self.debug = debug
+ self.dependencies = None
+ label = self.make_label()
+ self.label = Label(name=label, address=address, object=self)
+ self.last_address = address + 9
+ script_parse_table[address : self.last_address] = self
+ self.parse()
+
+ def make_label(self):
+ return map_names[self.map_group][self.map_id]["label"] + "_MapHeader"
+
+ def parse(self):
+ address = self.address
+ print "parsing a MapHeader at " + hex(address)
+ self.bank = HexByte(address=address)
+ self.tileset = HexByte(address=address+1)
+ self.permission = DecimalParam(address=address+2)
+ self.second_map_header_address = calculate_pointer(ord(rom[address+3])+(ord(rom[address+4])<<8), self.bank.byte)
+ # TODO: is the bank really supposed to be 0x25 all the time ??
+ self.second_map_header = SecondMapHeader(self.second_map_header_address, map_group=self.map_group, map_id=self.map_id, debug=self.debug)
+ all_second_map_headers.append(self.second_map_header)
+ self.location_on_world_map = HexByte(address=address+5)
+ self.music = HexByte(address=address+6)
+ self.time_of_day = DecimalParam(address=address+7)
+ self.fishing_group = DecimalParam(address=address+8)
+
+ def get_dependencies(self, recompute=False, global_dependencies=set()):
+ if self.dependencies != None and not recompute:
+ global_dependencies.update(self.dependencies)
+ return self.dependencies
+ dependencies = [self.second_map_header]
+ global_dependencies.add(self.second_map_header)
+ dependencies.append(self.second_map_header.get_dependencies(recompute=recompute, global_dependencies=global_dependencies))
+ self.dependencies = dependencies
+ return dependencies
+
+ def to_asm(self):
+ output = "; bank, tileset, permission\n"
+ output += "db " + ", ".join(["BANK(" + self.second_map_header.label.name + ")", self.tileset.to_asm(), self.permission.to_asm()])
+ output += "\n\n; second map header\n"
+ output += "dw " + PointerLabelParam(address=self.address+3).to_asm() # TODO: should we include bank=self.bank.byte ??
+ output += "\n\n; location on world map, music, time of day, fishing group\n"
+ output += "db " + ", ".join([self.location_on_world_map.to_asm(), self.music.to_asm(), self.time_of_day.to_asm(), self.fishing_group.to_asm()])
+ return output
+
+
+all_map_headers = []
+def parse_map_header_at(address, map_group=None, map_id=None, debug=True):
+ """parses an arbitrary map header at some address"""
+ print "parsing a map header at: " + hex(address)
+ map_header = MapHeader(address, map_group=map_group, map_id=map_id, debug=debug)
+ all_map_headers.append(map_header)
+ return map_header
+
+def old_parse_map_header_at(address, map_group=None, map_id=None, debug=True):
+ """parses an arbitrary map header at some address"""
+ print "parsing a map header at: " + hex(address)
+ bytes = rom_interval(address, map_header_byte_size, strings=False, debug=debug)
+ 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] # pokegear 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,
+ }
+ print "second map header address is: " + hex(second_map_header_address)
+ map_header["second_map_header"] = old_parse_second_map_header_at(second_map_header_address, debug=debug)
+ event_header_address = map_header["second_map_header"]["event_address"]
+ script_header_address = map_header["second_map_header"]["script_address"]
+ # maybe event_header and script_header should be put under map_header["second_map_header"]
+ map_header["event_header"] = old_parse_map_event_header_at(event_header_address, map_group=map_group, map_id=map_id, debug=debug)
+ map_header["script_header"] = old_parse_map_script_header_at(script_header_address, map_group=map_group, map_id=map_id, debug=debug)
+ return map_header
+
+
+def get_direction(connection_byte, connection_id):
+ """
+ Given a connection byte and a connection id, which direction is this
+ connection?
+
+ example:
+ The 0th connection of $5 is SOUTH and the 1st connection is
+ EAST.
+ """
+ connection_options = [0b1000, 0b0100, 0b0010, 0b0001]
+ results = ["NORTH", "SOUTH", "WEST", "EAST"]
+
+ for option in connection_options:
+ if (option & connection_byte) == 0:
+ results[connection_options.index(option)] = ""
+
+ # prune results
+ while "" in results:
+ results.remove("")
+
+ return results[connection_id]
+
+class SecondMapHeader:
+ base_label = "SecondMapHeader_"
+
+ def __init__(self, address, map_group=None, map_id=None, debug=True, bank=None, label=None):
+ print "creating a SecondMapHeader at " + hex(address)
+ self.address = address
+ self.map_group = map_group
+ self.map_id = map_id
+ self.debug = debug
+ self.bank = bank
+ self.dependencies = None
+ label = self.make_label()
+ self.label = Label(name=label, address=address, object=self)
+
+ # the minimum number of bytes is 12
+ self.last_address = address+12
+ self.size = 12
+
+ script_parse_table[address : self.last_address] = self
+ self.parse()
+
+ def make_label(self):
+ return map_names[self.map_group][self.map_id]["label"] + "_SecondMapHeader"
+
+ def parse(self):
+ address = self.address
+ bytes = rom_interval(address, second_map_header_byte_size, strings=False)
+ size = second_map_header_byte_size
+
+ # for later
+ self.connections = []
+
+ self.border_block = HexByte(address=address)
+ self.height = DecimalParam(address=address+1)
+ self.width = DecimalParam(address=address+2)
+
+ # bank appears first
+ ###self.blockdata_address = PointerLabelBeforeBank(address+3)
+ self.blockdata_address = calculate_pointer_from_bytes_at(address+3, bank=True)
+ xyz = script_parse_table[self.blockdata_address]
+ if xyz == None:
+ self.blockdata = MapBlockData(self.blockdata_address, map_group=self.map_group, map_id=self.map_id, debug=self.debug, width=self.width, height=self.height)
+ else:
+ self.blockdata = xyz
+
+ # bank appears first
+ ###self.script_address = PointerLabelBeforeBank(address+6)
+ self.script_header_address = calculate_pointer_from_bytes_at(address+6, bank=True)
+ self.script_header = MapScriptHeader(self.script_header_address, map_group=self.map_group, map_id=self.map_id, debug=self.debug)
+ all_map_script_headers.append(self.script_header)
+
+ self.event_bank = ord(rom[address+6])
+ self.event_header_address = calculate_pointer_from_bytes_at(address+9, bank=ord(rom[address+6]))
+ self.event_header = MapEventHeader(self.event_header_address, map_group=self.map_group, map_id=self.map_id, debug=self.debug)
+ self.connection_byte = DecimalParam(address=address+11)
+ all_map_event_headers.append(self.event_header)
+
+ self.size = size
+
+ if self.connection_byte == 0:
+ return True
+
+ current_address = address+12
+
+ # short alias
+ cb = self.connection_byte.byte
+
+ # east = 1, west = 2, south = 4, north = 8 (or'd together)
+ east = ((cb & 0x1) != 0)
+ west = ((cb & 0x2) != 0)
+ south = ((cb & 0x4) != 0)
+ north = ((cb & 0x8) != 0)
+ directions = [east, west, south, north]
+ connection_count = directions.count(True)
+
+ for connection in range(0, connection_count):
+ direction = get_direction(self.connection_byte.byte, connection)
+ connection = Connection(current_address, direction=direction, map_group=self.map_group, map_id=self.map_id, debug=self.debug, smh=self)
+ self.connections.append(connection)
+
+ # 12 bytes each?
+ current_address += connection.size
+
+ self.last_address = current_address
+
+ return True
+
+ def get_dependencies(self, recompute=False, global_dependencies=set()):
+ if self.dependencies != None and not recompute:
+ global_dependencies.update(self.dependencies)
+ return self.dependencies
+ dependencies = [self.script_header, self.event_header, self.blockdata]
+ global_dependencies.update(dependencies)
+ dependencies.append(self.script_header.get_dependencies(recompute=recompute, global_dependencies=global_dependencies))
+ dependencies.append(self.event_header.get_dependencies(recompute=recompute, global_dependencies=global_dependencies))
+ self.dependencies = dependencies
+ return dependencies
+
+ def to_asm(self):
+ self_constant_label = get_map_constant_label(map_group=self.map_group, map_id=self.map_id)
+ output = "; border block\n"
+ output += "db " + self.border_block.to_asm() + "\n\n"
+ output += "; height, width\n"
+ output += "db " + self_constant_label + "_HEIGHT, " + self_constant_label + "_WIDTH\n\n"
+ output += "; blockdata (bank-then-pointer)\n"
+ thing = ScriptPointerLabelBeforeBank(address=self.address+3, map_group=self.map_group, map_id=self.map_id, debug=self.debug).to_asm()
+ output += "dbw " + thing.split(", ")[0] + ", "+thing.split(", ")[1] + "\n\n"
+ output += "; script header (bank-then-pointer)\n"
+ thing = ScriptPointerLabelBeforeBank(address=self.address+6, map_group=self.map_group, map_id=self.map_id, debug=self.debug).to_asm()
+ output += "dbw " + thing.split(", ")[0] + ", " + thing.split(", ")[1] + "\n\n"
+ output += "; map event header (bank-then-pointer)\n"
+ output += "dw " + PointerLabelParam(address=self.address+9, bank=self.event_bank, map_group=self.map_group, map_id=self.map_id, debug=self.debug).to_asm() + "\n\n"
+
+ output += "; connections\n"
+ dir_results = []
+ connection_options = [0b1000, 0b0100, 0b0010, 0b0001]
+ dirs = ["NORTH", "SOUTH", "WEST", "EAST"]
+ for (id, each) in enumerate(dirs):
+ if ((connection_options[id] & self.connection_byte.byte) != 0):
+ dir_results.append(each)
+ output += "db " + " | ".join(dir_results)
+ if len(dir_results) == 0:
+ output += "0"
+
+ if self.connection_byte.byte == 0 or len(dir_results) == 0:
+ return output
+ else:
+ output += "\n\n"
+
+ connections = "\n\n".join([connection.to_asm() for connection in self.connections])
+ output += connections
+
+ return output
+
+strip_pointer_data = []
+strip_destination_data = []
+connections = []
+wrong_norths = []
+wrong_easts = []
+wrong_souths = []
+wrong_wests = []
+
+class Connection:
+ size = 12
+
+ def __init__(self, address, direction=None, map_group=None, map_id=None, debug=True, smh=None):
+ self.address = address
+ self.direction = direction.lower()
+ self.map_group = map_group
+ self.map_id = map_id
+ self.debug = debug
+ self.smh = smh
+ self.last_address = address + self.size
+ connections.append(self)
+
+ self.parse()
+
+ def parse(self):
+ current_address = self.address
+
+ is_vertical = ((self.direction == "north") or (self.direction == "south"))
+ is_horizontal = ((self.direction == "east") or (self.direction == "west"))
+
+ connected_map_group_id = ord(rom[current_address])
+ self.connected_map_group_id = connected_map_group_id
+ current_address += 1
+
+ connected_map_id = ord(rom[current_address])
+ self.connected_map_id = connected_map_id
+ current_address += 1
+
+ # window (use JohtoMap's calculation, not this)
+ # up: C701h + height_of_connected_map * (width_of_connected_map + 6)
+ # left: C706h + 2 * width_of_connected_map
+ # down/right: C707h + width_of_connected_map
+ #
+ # 2 bytes (flipped) - X position of starting point for intermediate
+ # tiles (scrolls through connected map line-by-line. this way you can
+ # change Y position also)
+ #
+ # According to JohtoMap, the calculation for tile data pointer is:
+ # int p = otherMap.tileDataLocation;
+ # int h = (otherMap.width - otherMap.height)
+ # if (h > 0)
+ # p += (h * otherMap.height) + (otherMap.height * 3) + (otherMap.height + 3)
+ # else
+ # p += (otherMap.height * otherMap.width) - (otherMap.width * 3);
+ # c.tileDataPointer = gb.Get2BytePointer(p);
+ #
+ # tauwasser calls this "connection strip pointer"
+ tile_data_pointer = ord(rom[current_address]) + (ord(rom[current_address+1]) << 8)
+ strip_pointer = tile_data_pointer
+ self.strip_pointer = tile_data_pointer
+ current_address += 2
+
+ # 10:19 <comet> memoryotherpointer is <comet> what johtomap calls OMDL (in ram where the tiles start getting pulled from the other map)
+ memory_other_pointer = ord(rom[current_address]) + (ord(rom[current_address+1]) << 8)
+ # 10:42 <comet> it would be a good idea to rename otherpointer strippointer or striploc
+ # 10:42 <comet> since thats more accurate
+ # 11:05 <comet> Above: C803h + xoffset
+ # 11:05 <comet> Below: C803h + (m.height + 3) * (m.width + 6) + xoffset
+ # 11:05 <comet> Left: C800h + (m.width + 6) * (yoffset + 3)
+ # 11:05 <comet> Right: C7FDh + (m.width + 6) * (yoffset + 4)
+ #
+ # tauwasser calls this "connection strip destination" and lin calls this "memoryOtherPointer"
+ # Points to the upper left block of the connection strip
+ # (The bank the Blockdata is in, is loaded out of the Mapheader of the connected Map.)
+ # The connection strip is always 3 Blocks high resp. wide
+ # (depending on the connection's direction)
+ strip_destination = memory_other_pointer
+ self.strip_destination = memory_other_pointer
+ current_address += 2
+
+ # length of the connection strip in blocks
+ connection_strip_length = ord(rom[current_address])
+ current_address += 1
+ connected_map_width = ord(rom[current_address])
+ current_address += 1
+
+ self.connection_strip_length = connection_strip_length
+ self.connected_map_width = connected_map_width
+
+ y_position_after_map_change = ord(rom[current_address])
+ yoffset = y_position_after_map_change
+ current_address += 1
+
+ x_position_after_map_change = ord(rom[current_address])
+ xoffset = x_position_after_map_change
+ current_address += 1
+
+ # in pokered these were called alignments? same thing?
+ self.yoffset = y_position_after_map_change
+ self.xoffset = x_position_after_map_change
+
+ # tauwasser calls this "window" and lin calls this "memoryCurrentPointer"
+ # Position of the upper left block after entering the Map
+ #
+ # tauwasser's formula for windows:
+ # Above: C701h + Height_of_connected_map * (Width_of_connected_map + 6)
+ # Left: C706h + 2 * Width_of_connected_map
+ # Below/Right: C707h + Width_of_connected_map
+ window = ord(rom[current_address]) + (ord(rom[current_address+1]) << 8)
+ current_address += 2
+
+ self.window = window
+
+ current_map_height = self.smh.height.byte
+ current_map_width = self.smh.width.byte
+
+ if "header_new" in map_names[connected_map_group_id][connected_map_id].keys():
+ # the below code ensures that there's an equation to handle strip_pointer
+
+ ldirection = self.direction.lower()
+ connected_map_header = map_names[connected_map_group_id][connected_map_id]["header_new"]
+ connected_second_map_header = connected_map_header.second_map_header
+ connected_map_height = connected_second_map_header.height.byte
+ connected_map_width = connected_second_map_header.width.byte
+ p = connected_second_map_header.blockdata.address
+ h = None
+ method = "default"
+
+ if ldirection == "north":
+ h = connected_map_width - self.smh.width.byte
+ if ((p + ((connected_map_height * connected_map_width) - (connected_map_width * 3)))%0x4000)+0x4000 == strip_pointer:
+ # lin's equation:
+ # p += (otherMap.height * otherMap.width) - (otherMap.width * 3)
+ p += (connected_map_height * connected_map_width) - (connected_map_width * 3)
+ method = "north1"
+ elif ((p + connected_map_width + xoffset + (16 * connected_map_height) - 16)%0x4000)+0x4000 == strip_pointer:
+ p += connected_map_width + xoffset + (16 * connected_map_height) - 16
+ method = "north2"
+ elif p != strip_pointer:
+ # worst case scenario: we don't know how to calculate p, so we'll just set it as a constant
+ # example: Route10North north to Route9 (strip_pointer=0x7eae, connected map's blockdata=0x7de9)
+ p = strip_pointer
+ method = "north3"
+ else:
+ # this doesn't seem to ever happen
+ # or just do nothing (value is already ok)
+ method = "north4"
+ elif ldirection == "west":
+ h = connected_map_height - self.smh.height.byte
+ if ((p + (h * connected_map_width) - (connected_map_width * 3) + (connected_map_width - 1) - 2)%0x4000)+0x4000 == strip_pointer:
+ # lin's method:
+ # p += (h * otherMap.width) - (otherMap.width * 3) + (otherMap.width - 3)
+ p += (h * connected_map_width) - (connected_map_width * 3) + (connected_map_width - 1) - 2
+ method = "west1"
+ elif ((p + connected_map_width - 3)%0x4000)+0x4000 == strip_pointer:
+ print "west h <= 0"
+ # lin's method:
+ # p += otherMap.width - 3
+ p += connected_map_width - 3
+ method = "west2"
+ elif ((p + xoffset + (current_map_height * 2))%0x4000 + 0x4000) == strip_pointer:
+ method = "west3"
+ p += xoffset + (current_map_height * 2)
+ elif (p%0x4000)+0x4000 != strip_pointer:
+ # worst case scenario: dunno what to do
+ method = "west4"
+ p = strip_pointer
+ else:
+ # this doesn't seem to ever happen
+ # do nothing
+ method = "west5"
+ elif ldirection == "south":
+ print "south.. dunno what to do?"
+
+ if (p%0x4000)+0x4000 == strip_pointer:
+ # do nothing
+ method = "south1"
+ elif ((p + (xoffset - connection_strip_length + self.smh.width.byte) / 2)%0x4000)+0x4000 == strip_pointer:
+ # comet's method
+ method = "south2"
+ p += (xoffset - connection_strip_length + self.smh.width.byte) / 2
+ elif ((p + ((xoffset - connection_strip_length + self.smh.width.byte) / 2) - 1)%0x4000)+0x4000 == strip_pointer:
+ method = "south3"
+ p += ((xoffset - connection_strip_length + self.smh.width.byte) / 2) - 1
+ elif ldirection == "east":
+ if (p%0x4000)+0x4000 == strip_pointer:
+ # do nothing
+ method = "east1"
+ elif ((p + (connected_map_height - connection_strip_length) * connected_map_width)%0x4000)+0x4000 == strip_pointer:
+ p += (connected_map_height - connection_strip_length) * connected_map_width
+ method = "east2"
+ elif ((p + 100 - 4 * connected_map_width)%0x4000) + 0x4000 == strip_pointer:
+ method = "east3"
+ p += 100 - 4 * connected_map_width
+ elif ((p + 2 * (100 - 4 * connected_map_width))%0x4000) + 0x4000 == strip_pointer:
+ method = "east4"
+ # the "2" is possibly ( connected_map_height / current_map_height )
+ # or current_map_width/yoffset or connected_map_width/yoffset
+ p += 2 * (100 - 4 * connected_map_width)
+
+ # convert the address to a 2-byte pointer
+ intermediate_p = p
+ p = (p % 0x4000) + 0x4000
+
+ data = {
+ "strip_pointer": strip_pointer,
+ "strip_length": connection_strip_length,
+ "other_blockdata_address": connected_second_map_header.blockdata.address,
+ "other_blockdata_pointer": (connected_second_map_header.blockdata.address%0x4000)+0x4000,
+
+ "xoffset": xoffset,
+ "yoffset": yoffset,
+
+ "connected_map_height": connected_map_height,
+ "connected_map_width": connected_map_width,
+ "connected_map_group_id": connected_map_group_id,
+ "connected_map_id": connected_map_id,
+ "connected_map_label": map_names[connected_map_group_id][connected_map_id]["label"],
+
+ "current_map_width": self.smh.width.byte,
+ "current_map_height": self.smh.height.byte,
+ "current_map_label": map_names[self.smh.map_group][self.smh.map_id]["label"],
+ "current_map_group_id": self.smh.map_group,
+ "current_map_id": self.smh.map_id,
+
+ "difference": strip_pointer - ((connected_second_map_header.blockdata.address%0x4000)+0x4000),
+ "direction": ldirection,
+ "method": method,
+ }
+ strip_pointer_data.append(data)
+
+ if p != strip_pointer:
+ print "method: " + method + " direction: " + ldirection
+ print "other map blockdata address: " + hex(connected_second_map_header.blockdata.address)
+ print "h = " + str(h)
+ print "initial p = " + hex(connected_second_map_header.blockdata.address)
+ print "intermediate p = " + hex(intermediate_p)
+ print "final p = " + hex(p)
+ print "connection length = " + str(connection_strip_length)
+ print "strip_pointer = " + hex(strip_pointer)
+ print "other map height = " + str(connected_map_height)
+ print "other map width = " + str(connected_map_width)
+ o = "other map group_id="+hex(connected_map_group_id) + " map_id="+hex(connected_map_id)+" "+map_names[connected_map_group_id][connected_map_id]["label"] + " smh="+hex(connected_second_map_header.address)
+ o += " width="+str(connected_second_map_header.width.byte)+" height="+str(connected_second_map_header.height.byte)
+ print o
+
+ o = "current map group_id="+hex(self.map_group)+" map_id="+hex(self.map_id)+" "+map_names[self.map_group][self.map_id]["label"]+" smh="+hex(self.smh.address)
+ o += " width="+str(self.smh.width.byte)+" height="+str(self.smh.height.byte)
+ print o
+
+ if ldirection == "east":
+ wrong_easts.append(data)
+ elif ldirection == "west":
+ wrong_wests.append(data)
+ elif ldirection == "south":
+ wrong_souths.append(data)
+ elif ldirection == "north":
+ wrong_norths.append(data)
+
+ # this will only happen if there's a bad formula
+ raise Exception("tauwasser strip_pointer calculation was wrong? strip_pointer="+hex(strip_pointer) + " p="+hex(p))
+
+ calculated_destination = None
+ method = "strip_destination_default"
+ x_movement_of_the_connection_strip_in_blocks = None
+ y_movement_of_the_connection_strip_in_blocks = None
+
+ # the below code makes sure there's an equation to calculate strip_destination
+ # 11:05 <comet> Above: C803h + xoffset
+ # 11:05 <comet> Below: C803h + (m.height + 3) * (m.width + 6) + xoffset
+ # 11:05 <comet> Left: C800h + (m.width + 6) * (yoffset + 3)
+ # 11:05 <comet> Right: C7FDh + (m.width + 6) * (yoffset + 4)
+ #
+ # tauwasser calls this "connection strip destination" and lin calls this "memoryOtherPointer"
+ # Points to the upper left block of the connection strip
+ # (The bank the Blockdata is in, is loaded out of the Mapheader of the connected Map.)
+ # The connection strip is always 3 Blocks high resp. wide
+ # (depending on the connection's direction)
+ if ldirection == "north":
+ x_movement_of_the_connection_strip_in_blocks = strip_destination - 0xC703
+ print "(north) x_movement_of_the_connection_strip_in_blocks is: " + str(x_movement_of_the_connection_strip_in_blocks)
+ if x_movement_of_the_connection_strip_in_blocks < 0:
+ raise Exception("x_movement_of_the_connection_strip_in_blocks is wrong? " + str(x_movement_of_the_connection_strip_in_blocks))
+ elif ldirection == "south":
+ # strip_destination =
+ # 0xc703 + (current_map_height + 3) * (current_map_width + 6) + x_movement_of_the_connection_strip_in_blocks
+ x_movement_of_the_connection_strip_in_blocks = strip_destination - (0xc703 + (current_map_height + 3) * (current_map_width + 6))
+ print "(south) x_movement_of_the_connection_strip_in_blocks is: " + str(x_movement_of_the_connection_strip_in_blocks)
+ elif ldirection == "east":
+ # strip_destination =
+ # 0xc700 + (current_map_width + 6) * (y_movement_of_the_connection_strip_in_blocks + 3)
+ y_movement_of_the_connection_strip_in_blocks = (strip_destination - 0xc700) / (current_map_width + 6) - 3
+ print "(east) y_movement_of_the_connection_strip_in_blocks is: " + str(y_movement_of_the_connection_strip_in_blocks)
+ elif ldirection == "west":
+ # strip_destination =
+ # 0xc6fd + (current_map_width + 6) * (y_movement_of_the_connection_strip_in_blocks + 4)
+ y_movement_of_the_connection_strip_in_blocks = (strip_destination - 0xc6fd) / (current_map_width + 6) - 4
+ print "(west) y_movement_of_the_connection_strip_in_blocks is: " + str(y_movement_of_the_connection_strip_in_blocks)
+
+ # let's also check the window equations
+ # tauwasser calls this "window" and lin calls this "memoryCurrentPointer"
+ # Position of the upper left block after entering the Map
+ #
+ # tauwasser's formula for windows:
+ # Above: C701h + Height_of_connected_map * (Width_of_connected_map + 6)
+ # Left: C706h + 2 * Width_of_connected_map
+ # Below/Right: C707h + Width_of_connected_map
+ window_worked = False
+ if ldirection == "north":
+ # tauwasser's formula: 0xc701 + connected_map_height * (connected_map_width + 6)
+ window_start = 0xc801
+ if window == window_start + (connected_map_height * 6) + (connected_map_height * connected_map_width):
+ window_worked = True
+ elif ldirection == "east":
+ window_start = 0xc807
+ if window == (window_start + connected_map_width):
+ window_worked = True
+ elif ldirection == "south":
+ window_start = 0xc807
+ if window == (window_start + connected_map_width):
+ window_worked = True
+ elif ldirection == "west":
+ window_start = 0xc807
+ if window == (window_start + xoffset):
+ window_worked = True
+
+ data = {
+ "window": window,
+ "window_start": window_start,
+ "window_diff": window - window_start,
+ "window_worked": window_worked,
+ "strip_destination": strip_destination,
+ "strip_length": connection_strip_length,
+ "other_blockdata_address": connected_second_map_header.blockdata.address,
+ "other_blockdata_pointer": (connected_second_map_header.blockdata.address%0x4000)+0x4000,
+
+ "xoffset": xoffset,
+ "yoffset": yoffset,
+
+ "connected_map_height": connected_map_height,
+ "connected_map_width": connected_map_width,
+ "connected_map_group_id": connected_map_group_id,
+ "connected_map_id": connected_map_id,
+ "connected_map_label": map_names[connected_map_group_id][connected_map_id]["label"],
+
+ "current_map_width": self.smh.width.byte,
+ "current_map_height": self.smh.height.byte,
+ "current_map_label": map_names[self.smh.map_group][self.smh.map_id]["label"],
+ "current_map_group_id": self.smh.map_group,
+ "current_map_id": self.smh.map_id,
+
+ "y_movement_of_the_connection_strip_in_blocks": y_movement_of_the_connection_strip_in_blocks,
+ "x_movement_of_the_connection_strip_in_blocks": x_movement_of_the_connection_strip_in_blocks,
+
+ "direction": ldirection,
+ "method": method,
+ }
+ strip_destination_data.append(data)
+
+ def to_asm(self):
+ output = ""
+ ldirection = self.direction.lower()
+
+ connected_map_group_id = self.connected_map_group_id
+ connected_map_id = self.connected_map_id
+
+ connected_map_header = map_names[connected_map_group_id][connected_map_id]["header_new"]
+ connected_second_map_header = connected_map_header.second_map_header
+ connected_map_height = connected_second_map_header.height.byte
+ connected_map_width = connected_second_map_header.width.byte
+
+ connection_strip_length = self.connection_strip_length
+ connected_map_width = self.connected_map_width
+
+ current_map_height = self.smh.height.byte
+ current_map_width = self.smh.width.byte
+
+ map_constant_label = get_map_constant_label(map_group=connected_map_group_id, map_id=connected_map_id)
+ self_constant_label = get_map_constant_label(map_group=self.smh.map_group, map_id=self.smh.map_id)
+ if map_constant_label != None:
+ map_group_label = "GROUP_" + map_constant_label
+ map_label = "MAP_" + map_constant_label
+ else:
+ map_group_label = str(connected_map_group_id)
+ map_label = str(connected_map_id)
+
+ output += "; " + self.direction.upper() + " to " \
+ + map_names[connected_map_group_id][connected_map_id]["name"] \
+ + "\n"
+
+ output += "db %s, %s ; connected map (group, id)\n" % (map_group_label, map_label)
+
+ yoffset = self.yoffset
+ xoffset = self.xoffset
+
+ # According to JohtoMap, the calculation for tile data pointer is:
+ # int p = otherMap.tileDataLocation;
+ # int h = (otherMap.width - otherMap.height)
+ # if (h > 0)
+ # p += (h * otherMap.height) + (otherMap.height * 3) + (otherMap.height + 3)
+ # else
+ # p += (otherMap.height * otherMap.width) - (otherMap.width * 3);
+ # c.tileDataPointer = gb.Get2BytePointer(p);
+ strip_pointer = self.strip_pointer
+
+ p = connected_second_map_header.blockdata.address
+
+ output += "dw "
+
+ if ldirection == "north":
+ h = connected_map_width - self.smh.width.byte
+ if ((p + ((connected_map_height * connected_map_width) - (connected_map_width * 3)))%0x4000)+0x4000 == strip_pointer:
+ # lin's equation:
+ # p += (otherMap.height * otherMap.width) - (otherMap.width * 3)
+ p += (connected_map_height * connected_map_width) - (connected_map_width * 3)
+ method = "north1"
+ output += "(" + get_label_for(connected_second_map_header.blockdata.address) + " + (" + map_constant_label + "_HEIGHT * " + map_constant_label + "_WIDTH) - (" + map_constant_label + "_WIDTH * 3))"
+ elif ((p + connected_map_width + xoffset + (16 * connected_map_height) - 16)%0x4000)+0x4000 == strip_pointer:
+ p += connected_map_width + xoffset + (16 * connected_map_height) - 16
+ method = "north2"
+ output += "(" + get_label_for(connected_second_map_header.blockdata.address) + " + " + map_constant_label + "_WIDTH + " + str(xoffset) + " + (16 * " + map_constant_label + "_HEIGHT) - 16)"
+ elif p != strip_pointer:
+ # worst case scenario: we don't know how to calculate p, so we'll just set it as a constant
+ # example: Route10North north to Route9 (strip_pointer=0x7eae, connected map's blockdata=0x7de9)
+ p = strip_pointer
+ method = "north3"
+ output += "$%.2x" % (p)
+ else:
+ # this doesn't seem to ever happen
+ # or just do nothing (value is already ok)
+ method = "north4"
+ output += "(" + get_label_for(connected_second_map_header.blockdata.address) + ")"
+ elif ldirection == "west":
+ h = connected_map_height - self.smh.height.byte
+ h_out = "(" + map_constant_label +"_HEIGHT - " + self_constant_label +"_HEIGHT)"
+ if ((p + (h * connected_map_width) - (connected_map_width * 3) + (connected_map_width - 1) - 2)%0x4000)+0x4000 == strip_pointer:
+ # lin's method:
+ # p += (h * otherMap.width) - (otherMap.width * 3) + (otherMap.width - 3)
+ p += (h * connected_map_width) - (connected_map_width * 3) + (connected_map_width - 1) - 2
+ method = "west1"
+ this_part = "((" + h_out + " * " + map_constant_label + "_WIDTH) - (" + map_constant_label + "_WIDTH * 3) + (" + map_constant_label + "_WIDTH - 1) - 2)"
+ output += "(" + get_label_for(connected_second_map_header.blockdata.address) + " + " + this_part + ")"
+ elif ((p + connected_map_width - 3)%0x4000)+0x4000 == strip_pointer:
+ print "west h <= 0"
+ # lin's method:
+ # p += otherMap.width - 3
+ p += connected_map_width - 3
+ method = "west2"
+ output += "(" + get_label_for(connected_second_map_header.blockdata.address) + " + " + map_constant_label + "_WIDTH - 3)"
+ elif ((p + xoffset + (current_map_height * 2))%0x4000 + 0x4000) == strip_pointer:
+ method = "west3"
+ p += xoffset + (current_map_height * 2)
+ output += "(" + get_label_for(connected_second_map_header.blockdata.address) + " + " + str(xoffset) + " + (" + map_constant_label + "_HEIGHT * 2))"
+ elif (p%0x4000)+0x4000 != strip_pointer:
+ # worst case scenario: dunno what to do
+ method = "west4"
+ p = strip_pointer
+ output += "$%.2x" % ((p%0x4000)+0x4000)
+ else:
+ # this doesn't seem to ever happen
+ # do nothing
+ method = "west5"
+ output += "(" + get_label_for(connected_second_map_header.blockdata.address) + ")"
+ elif ldirection == "south":
+ if (p%0x4000)+0x4000 == strip_pointer:
+ # do nothing
+ method = "south1"
+ output += "(" + get_label_for(connected_second_map_header.blockdata.address) + ")"
+ elif ((p + (xoffset - connection_strip_length + self.smh.width.byte) / 2)%0x4000)+0x4000 == strip_pointer:
+ # comet's method
+ method = "south2"
+ p += (xoffset - connection_strip_length + self.smh.width.byte) / 2
+ this_part = "((" + str(xoffset) + " - " + str(connection_strip_length) + " + " + self_constant_label + "_WIDTH) / 2)"
+ output += "(" + get_label_for(connected_second_map_header.blockdata.address) + " + " + this_part + ")"
+ elif ((p + ((xoffset - connection_strip_length + self.smh.width.byte) / 2) - 1)%0x4000)+0x4000 == strip_pointer:
+ method = "south3"
+ p += ((xoffset - connection_strip_length + self.smh.width.byte) / 2) - 1
+ this_part = "(((" + str(xoffset) + " - " + str(connection_strip_length) + " + " + self_constant_label + "_WIDTH) / 2) - 1)"
+ output += "(" + get_label_for(connected_second_map_header.blockdata.address) + " + " + this_part + ")"
+ elif ldirection == "east":
+ if (p%0x4000)+0x4000 == strip_pointer:
+ # do nothing
+ method = "east1"
+ output += "(" + get_label_for(connected_second_map_header.blockdata.address) + ")"
+ elif ((p + (connected_map_height - connection_strip_length) * connected_map_width)%0x4000)+0x4000 == strip_pointer:
+ p += (connected_map_height - connection_strip_length) * connected_map_width
+ method = "east2"
+ this_part = "((" + map_constant_label + "_HEIGHT - " + str(connection_strip_length) + ") * " + map_constant_label + "_WIDTH)"
+ output += "(" + get_label_for(connected_second_map_header.blockdata.address) + " + " + this_part + ")"
+ elif ((p + 100 - 4 * connected_map_width)%0x4000) + 0x4000 == strip_pointer:
+ method = "east3"
+ p += 100 - 4 * connected_map_width
+ output += "(" + get_label_for(connected_second_map_header.blockdata.address) + " + 100 - (" + map_constant_label + "_WIDTH * 4))"
+ elif ((p + 2 * (100 - 4 * connected_map_width))%0x4000) + 0x4000 == strip_pointer:
+ method = "east4"
+ # the "2" is possibly ( connected_map_height / current_map_height )
+ # or current_map_width/yoffset or connected_map_width/yoffset
+ p += 2 * (100 - 4 * connected_map_width)
+ output += "(" + get_label_for(connected_second_map_header.blockdata.address) + " + ((100 - (" + map_constant_label + "_WIDTH * 4)) * 2))"
+
+ output += " ; strip pointer\n"
+
+ # tauwasser calls this "connection strip destination" and lin calls this "memoryOtherPointer"
+ # Points to the upper left block of the connection strip
+ # (The bank the Blockdata is in, is loaded out of the Mapheader of the connected Map.)
+ # The connection strip is always 3 Blocks high resp. wide
+ # (depending on the connection's direction)
+ strip_destination = self.strip_destination
+
+ output += "dw "
+
+ # i am not convinced about these calculations
+ if ldirection == "north":
+ x_movement_of_the_connection_strip_in_blocks = strip_destination - 0xC703
+ xmov = x_movement_of_the_connection_strip_in_blocks
+ output += "($C703 + " + str(xmov) + ")"
+ elif ldirection == "south":
+ # strip_destination =
+ # 0xc703 + (current_map_height + 3) * (current_map_width + 6) + x_movement_of_the_connection_strip_in_blocks
+ x_movement_of_the_connection_strip_in_blocks = strip_destination - (0xc703 + (current_map_height + 3) * (current_map_width + 6))
+ xmov = x_movement_of_the_connection_strip_in_blocks
+ #output += "($C703 + (((" + self_constant_label + "_HEIGHT + 3) * (" + self_constant_label + "_WIDTH + 6)) + " + str(xmov) + "))"
+
+ # xmov = strip_destination - (0xc703 + (current_map_height + 3) * (current_map_width + 6))
+ #difference = 0xC715 + xmov + 6*current_map_height + 3*current_map_width + current_map_width*current_map_height
+ #difference = 50965 + ymov + 6*current_map_height + 3*current_map_width + current_map_width*current_map_height
+
+ output += "($C703 + " + str(xmov) + " + ((" + self_constant_label + "_HEIGHT + 3) * (" + self_constant_label + "_WIDTH + 6)))"
+ elif ldirection == "east":
+ # strip_destination =
+ # 0xc700 + (current_map_width + 6) * (y_movement_of_the_connection_strip_in_blocks + 3)
+ y_movement_of_the_connection_strip_in_blocks = (strip_destination - 0xc700) / (current_map_width + 6) - 3
+ ymov = y_movement_of_the_connection_strip_in_blocks
+ #output += "($C700 + ((" + self_constant_label + "_WIDTH + 6) * (" + str(ymov) + " + 3)) + "+str(ymov)+")"
+ output += "$%.2x" % (strip_destination)
+ elif ldirection == "west":
+ # strip_destination =
+ # 0xc6fd + (current_map_width + 6) * (y_movement_of_the_connection_strip_in_blocks + 4)
+ y_movement_of_the_connection_strip_in_blocks = (strip_destination - 0xc700) / (current_map_width + 6) - 3
+ ymov = y_movement_of_the_connection_strip_in_blocks
+ #output += "($C700 + ((" + self_constant_label + "_WIDTH + 6) * (" + str(ymov) + " + 4)) - 4)"
+ output += "$%.2x" % (strip_destination)
+ output += " ; strip destination\n"
+
+ output += "db " + str(connection_strip_length,) + ", " + map_constant_label + "_WIDTH ; (connection strip length, connected map width)\n"
+
+ #if ldirection in ["east", "west"]:
+ # Y_movement_of_connection_strip_in_blocks =
+ #elif direction in ["north", "south"]:
+ # X_movement_of_connection_strip_in_blocks =
+
+ # Above: (Height_of_connected_map * 2) - 1
+ # Below: 0
+ # Left/Right: (Y_movement_of_connection_strip_in_blocks * -2)
+ yoffset = self.yoffset # y_position_after_map_change
+
+ if ldirection == "south" and yoffset != 0:
+ raise Exception("tauwasser was wrong about yoffset=0 for south? it's: " + str(yoffset))
+ elif ldirection == "north" and yoffset != ((connected_map_height * 2) - 1):
+ raise Exception("tauwasser was wrong about yoffset for north? it's: " + str(yoffset))
+ #elif not ((yoffset % -2) == 0):
+ # raise Exception("tauwasser was wrong about yoffset for west/east? it's not divisible by -2: " + str(yoffset))
+
+ # Left: (Width_of_connected_map * 2) - 1
+ # Right: 0
+ # Above/Below: (X_movement_of_connection_strip_in_blocks * -2)
+ xoffset = self.xoffset # x_position_after_map_change
+
+ if ldirection == "east" and xoffset != 0:
+ raise Exception("tauwasser was wrong about xoffset=0 for east? it's: " + str(xoffset))
+ elif ldirection == "west" and xoffset != ((connected_map_width * 2) - 1):
+ raise Exception("tauwasser was wrong about xoffset for west? it's: " + str(xoffset))
+ #elif not ((xoffset % -2) == 0):
+ # raise Exception("tauwasser was wrong about xoffset for north/south? it's not divisible by -2: " + str(xoffset))
+
+ output += "db "
+
+ if ldirection == "south":
+ output += "0"
+ elif ldirection == "north":
+ output += "((" + map_constant_label + "_HEIGHT * 2) - 1)"
+ else:
+ output += str(yoffset)
+
+ output += ", "
+
+ if ldirection == "east":
+ output += "0"
+ elif ldirection == "west":
+ output += "((" + map_constant_label + "_WIDTH * 2) - 1)"
+ else:
+ output += str(xoffset)
+
+ output += " ; yoffset, xoffset\n"
+
+ window = self.window
+
+ output += "dw "
+
+ # let's also check the window equations
+ # tauwasser calls this "window" and lin calls this "memoryCurrentPointer"
+ # Position of the upper left block after entering the Map
+ #
+ # tauwasser's formula for windows:
+ # Above: C701h + Height_of_connected_map * (Width_of_connected_map + 6)
+ # Left: C706h + 2 * Width_of_connected_map
+ # Below/Right: C707h + Width_of_connected_map
+ window_worked = False
+ if ldirection == "north":
+ # tauwasser's formula: 0xc701 + connected_map_height * (connected_map_width + 6)
+ window_start = 0xc801
+ if window == window_start + (connected_map_height * 6) + (connected_map_height * connected_map_width):
+ window_worked = True
+ output += "($C801 + ((" + map_constant_label + "_HEIGHT * 6) + (" + map_constant_label + "_HEIGHT * " + map_constant_label + "_WIDTH)))"
+ elif ldirection == "east":
+ window_start = 0xc807
+ if window == (window_start + connected_map_width):
+ window_worked = True
+ output += "($C807 + " + map_constant_label + "_WIDTH)"
+ elif ldirection == "south":
+ window_start = 0xc807
+ if window == (window_start + connected_map_width):
+ window_worked = True
+ output += "($C807 + " + map_constant_label + "_WIDTH)"
+ elif ldirection == "west":
+ window_start = 0xc807
+ if window == (window_start + xoffset):
+ window_worked = True
+ output += "($C807 + " + str(xoffset) + ")"
+
+ output += " ; window"
+
+ return output
+
+all_second_map_headers = []
+def parse_second_map_header_at(address, map_group=None, map_id=None, debug=True):
+ """each map has a second map header"""
+ smh = SecondMapHeader(address, map_group=map_group, map_id=map_id, debug=debug)
+ all_second_map_headers.append(smh)
+ return smh
+
+def old_parse_second_map_header_at(address, map_group=None, map_id=None, debug=True):
+ """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,
+ }
+
+
+class MapBlockData:
+ base_label = "MapBlockData_"
+ maps_path = os.path.realpath(os.path.join(os.path.realpath("."), "../maps"))
+
+ def __init__(self, address, map_group=None, map_id=None, debug=True, bank=None, label=None, width=None, height=None):
+ self.address = address
+ self.map_group = map_group
+ self.map_id = map_id
+ self.map_name = map_names[map_group][map_id]["label"]
+ self.map_path = os.path.join(self.maps_path, self.map_name + ".blk")
+ self.debug = debug
+ self.bank = bank
+ if width and height:
+ self.width = width
+ self.height = height
+ else:
+ raise Exception("MapBlockData needs to know the width/height of its map")
+ label = self.make_label()
+ self.label = Label(name=label, address=address, object=self)
+ self.last_address = self.address + (self.width.byte * self.height.byte)
+ script_parse_table[address : self.last_address] = self
+ self.parse()
+
+ def make_label(self):
+ return map_names[self.map_group][self.map_id]["label"] + "_BlockData"
+
+ def save_to_file(self):
+ # check if the file exists already
+ map_path = self.map_path
+ if not os.path.exists(self.maps_path):
+ os.mkdir(self.maps_path)
+ if not os.path.exists(map_path):
+ # dump to file
+ #bytes = rom_interval(self.address, self.width.byte*self.height.byte, strings=True)
+ bytes = rom[self.address : self.address + self.width.byte*self.height.byte]
+ file_handler = open(map_path, "w")
+ file_handler.write(bytes)
+ file_handler.close()
+
+ def parse(self):
+ self.save_to_file()
+
+ def to_asm(self):
+ return "INCBIN \"maps/"+self.map_name+".blk\""
+
+
+class MapEventHeader:
+ base_label = "MapEventHeader_"
+
+ def __init__(self, address, map_group=None, map_id=None, debug=True, bank=None, label=None):
+ print "making a MapEventHeader at "+hex(address)+" map_group="+str(map_group)+" map_id="+str(map_id)
+ self.address = address
+ self.map_group = map_group
+ self.map_id = map_id
+ self.debug = debug
+ self.bank = bank
+ self.dependencies = None
+ label = self.make_label()
+ self.label = Label(name=label, address=address, object=self)
+ self.parse()
+ script_parse_table[address : self.last_address] = self
+
+ def make_label(self):
+ return map_names[self.map_group][self.map_id]["label"] + "_MapEventHeader"
+
+ def parse(self):
+ map_group, map_id, debug = self.map_group, self.map_id, self.debug
+ address = self.address
+ bank = calculate_bank(self.address) # or use self.bank
+ print "event header address is: " + hex(address)
+
+ filler1 = ord(rom[address])
+ filler2 = ord(rom[address+1])
+ self.fillers = [filler1, filler2]
+
+ # warps
+ warp_count = ord(rom[address+2])
+ warp_byte_count = warp_byte_size * warp_count
+ after_warps = address + 3 + warp_byte_count
+ warps = parse_warps(address+3, warp_count, bank=bank, map_group=map_group, map_id=map_id, debug=debug)
+ self.warp_count = warp_count
+ self.warps = warps
+
+ # triggers (based on xy location)
+ xy_trigger_count = ord(rom[after_warps])
+ trigger_byte_count = trigger_byte_size * xy_trigger_count
+ xy_triggers = parse_xy_triggers(after_warps+1, xy_trigger_count, bank=bank, map_group=map_group, map_id=map_id, debug=debug)
+ after_triggers = after_warps + 1 + trigger_byte_count
+ self.xy_trigger_count = xy_trigger_count
+ self.xy_triggers = xy_triggers
+
+ # signposts
+ signpost_count = ord(rom[after_triggers])
+ signpost_byte_count = signpost_byte_size * signpost_count
+ # signposts = rom_interval(after_triggers+1, signpost_byte_count)
+ signposts = parse_signposts(after_triggers+1, signpost_count, bank=bank, map_group=map_group, map_id=map_id, debug=debug)
+ after_signposts = after_triggers + 1 + signpost_byte_count
+ self.signpost_count = signpost_count
+ self.signposts = signposts
+
+ # people events
+ people_event_count = ord(rom[after_signposts])
+ people_event_byte_count = people_event_byte_size * people_event_count
+ # people_events_bytes = rom_interval(after_signposts+1, people_event_byte_count)
+ # people_events = parse_people_event_bytes(people_events_bytes, address=after_signposts+1, map_group=map_group, map_id=map_id)
+ people_events = parse_people_events(after_signposts+1, people_event_count, bank=calculate_bank(after_signposts+2), map_group=map_group, map_id=map_id, debug=debug)
+ self.people_event_count = people_event_count
+ self.people_events = people_events
+
+ if people_event_count > 0:
+ self.last_address = people_events[-1].last_address
+ else:
+ self.last_address = after_signposts+1
+ return True
+
+ def get_dependencies(self, recompute=False, global_dependencies=set()):
+ if self.dependencies != None and not recompute:
+ global_dependencies.update(self.dependencies)
+ return self.dependencies
+ bases = []
+ bases += self.people_events
+ bases += self.signposts
+ bases += self.xy_triggers
+ bases += self.warps
+
+ dependencies = []
+ for p in bases:
+ dependencies.extend(p.get_dependencies(recompute=recompute, global_dependencies=global_dependencies))
+ self.dependencies = dependencies
+ return dependencies
+
+ def to_asm(self):
+ xspacing = "" # was =spacing
+ output = "; filler\n"
+ output += "db %d, %d\n\n" % (self.fillers[0], self.fillers[1])
+
+ output += xspacing + "; warps\n"
+ output += xspacing + "db %d"%(self.warp_count)
+ if len(self.warps) > 0:
+ output += "\n"
+ output += "\n".join([xspacing+warp.to_asm() for warp in self.warps])
+
+ output += "\n\n"
+ output += xspacing + "; xy triggers\n"
+ output += xspacing + "db %d"%(self.xy_trigger_count)
+ if len(self.xy_triggers) > 0:
+ output += "\n"
+ output += "\n".join([xspacing+xy_trigger.to_asm() for xy_trigger in self.xy_triggers])
+
+ output += "\n\n"
+ output += xspacing + "; signposts\n"
+ output += xspacing + "db %d"%(self.signpost_count)
+ if len(self.signposts) > 0:
+ output += "\n"
+ output += "\n".join([xspacing+signpost.to_asm() for signpost in self.signposts])
+
+ output += "\n\n"
+ output += xspacing + "; people-events\n"
+ output += xspacing + "db %d"%(self.people_event_count)
+ if len(self.people_events) > 0:
+ output += "\n"
+
+ for people_event in self.people_events:
+ output += xspacing
+ output += people_event.to_asm()
+ output += "\n"
+
+ if output[-1] == "\n":
+ output = output[:-1]
+ return output
+
+all_map_event_headers = []
+def parse_map_event_header_at(address, map_group=None, map_id=None, debug=True, bank=None):
+ """parse crystal map event header byte structure thing"""
+ ev = MapEventHeader(address, map_group=map_group, map_id=map_id, debug=debug, bank=bank)
+ all_map_event_headers.append(ev)
+ return ev
+
+def old_parse_map_event_header_at(address, map_group=None, map_id=None, debug=True):
+ """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": old_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": old_parse_xy_trigger_bytes(triggers, bank=bank, map_group=map_group, map_id=map_id)})
+
+ # 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": old_parse_signpost_bytes(signposts, bank=bank, map_group=map_group, map_id=map_id)})
+
+ # people events
+ people_event_count = ord(rom[after_signposts])
+ people_event_byte_count = people_event_byte_size * people_event_count
+ people_events_bytes = rom_interval(after_signposts+1, people_event_byte_count)
+ people_events = old_parse_people_event_bytes(people_events_bytes, address=after_signposts+1, map_group=map_group, map_id=map_id)
+ returnable.update({"people_event_count": people_event_count, "people_events": people_events})
+
+ return returnable
+
+
+class MapScriptHeader:
+ """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. The scripts in MapEventHeader are called XYTrigger.
+
+ 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
+ """
+ base_label = "MapScriptHeader_"
+
+ def __init__(self, address, map_group=None, map_id=None, debug=True, bank=None, label=None):
+ print "creating a MapScriptHeader at " + hex(address) + " map_group="+str(map_group)+" map_id="+str(map_id)
+ self.address = address
+ self.map_group = map_group
+ self.map_id = map_id
+ self.debug = debug
+ self.bank = bank
+ self.dependencies = None
+ label = self.make_label()
+ self.label = Label(name=label, address=address, object=self)
+ self.parse()
+ script_parse_table[address : self.last_address] = self
+
+ def make_label(self):
+ return map_names[self.map_group][self.map_id]["label"] + "_MapScriptHeader"
+
+ def parse(self):
+ address = self.address
+ map_group = self.map_group
+ map_id = self.map_id
+ debug = self.debug
+ #[[Number1 of pointers] Number1 * [2byte pointer to script][00][00]]
+ self.trigger_count = ord(rom[address])
+ self.triggers = []
+ ptr_line_size = 4
+ groups = grouper(rom_interval(address+1, self.trigger_count * ptr_line_size, strings=False), count=ptr_line_size)
+ current_address = address+1
+ for (index, trigger_bytes) in enumerate(groups):
+ print "parsing a map trigger script at "+hex(current_address)+" map_group="+str(map_group)+" map_id="+str(map_id)
+ script = ScriptPointerLabelParam(address=current_address, map_group=map_group, map_id=map_id, debug=debug)
+ extra_bytes = MultiByteParam(address=current_address+2, map_group=map_group, map_id=map_id, debug=debug)
+ self.triggers.append([script, extra_bytes])
+ current_address += ptr_line_size
+ current_address = address + (self.trigger_count * ptr_line_size) + 1
+ #[[Number2 of pointers] Number2 * [hook number][2byte pointer to script]]
+ callback_ptr_line_size = 3
+ self.callback_count = DecimalParam(address=current_address)
+ self.callback_count = self.callback_count.byte
+ current_address += 1
+ self.callbacks = []
+ for index in range(self.callback_count):
+ print "parsing a callback script at "+hex(current_address)+" map_group="+str(map_group)+" map_id="+str(map_id)
+ hook_byte = HexByte(address=current_address)
+ callback = ScriptPointerLabelParam(address=current_address+1, map_group=map_group, map_id=map_id, debug=debug)
+ self.callbacks.append({"hook": hook_byte, "callback": callback})
+ current_address += 3 # i think?
+ self.last_address = current_address
+ print "done parsing a MapScriptHeader map_group="+str(map_group)+" map_id="+str(map_id)
+ return True
+
+ def get_dependencies(self, recompute=False, global_dependencies=set()):
+ if self.dependencies != None and not recompute:
+ global_dependencies.update(self.dependencies)
+ return self.dependencies
+ dependencies = []
+ for p in list(self.triggers):
+ # dependencies.append(p[0])
+ dependencies.extend(p[0].get_dependencies(recompute=recompute, global_dependencies=global_dependencies))
+ for callback in self.callbacks:
+ dependencies.append(callback["callback"])
+ global_dependencies.add(callback["callback"])
+ dependencies.extend(callback["callback"].get_dependencies(recompute=recompute, global_dependencies=global_dependencies))
+ self.dependencies = dependencies
+ return dependencies
+
+ def to_asm(self):
+ output = ""
+ output += "; trigger count\n"
+ output += "db %d\n"%self.trigger_count
+ if len(self.triggers) > 0:
+ output += "\n; triggers\n"
+ output += "\n".join([str("dw "+p[0].to_asm()+", "+p[1].to_asm()) for p in self.triggers])
+ output += "\n"
+ output += "\n; callback count\n"
+ output += "db %d"%self.callback_count
+ if len(self.callbacks) > 0:
+ output += "\n\n; callbacks\n\n"
+ output += "\n\n".join(["dbw "+str(p["hook"].byte)+", "+p["callback"].to_asm() for p in self.callbacks])
+ return output
+
+all_map_script_headers = []
+def parse_map_script_header_at(address, map_group=None, map_id=None, debug=True):
+ evv = MapScriptHeader(address, map_group=map_group, map_id=map_id, debug=debug)
+ all_map_script_headers.append(evv)
+ return evv
+
+def old_parse_map_script_header_at(address, map_group=None, map_id=None, debug=True):
+ print "starting to parse the map's script header.."
+ #[[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):
+ print "parsing a trigger header..."
+ byte1 = trigger_pointer[0]
+ byte2 = trigger_pointer[1]
+ ptr = byte1 + (byte2 << 8)
+ trigger_address = calculate_pointer(ptr, calculate_bank(address))
+ trigger_script = parse_script_engine_script_at(trigger_address, map_group=map_group, map_id=map_id)
+ 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):
+ print "parsing a callback header..."
+ 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_engine_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 old_parse_trainer_header_at(address, map_group=None, map_id=None, debug=True):
+ bank = calculate_bank(address)
+ bytes = rom_interval(address, 12, strings=False)
+ bit_number = bytes[0] + (bytes[1] << 8)
+ trainer_group = bytes[2]
+ trainer_id = bytes[3]
+ text_when_seen_ptr = calculate_pointer_from_bytes_at(address+4, bank=bank)
+ text_when_seen = parse_text_engine_script_at(text_when_seen_ptr, map_group=map_group, map_id=map_id, debug=debug)
+ text_when_trainer_beaten_ptr = calculate_pointer_from_bytes_at(address+6, bank=bank)
+ text_when_trainer_beaten = parse_text_engine_script_at(text_when_trainer_beaten_ptr, map_group=map_group, map_id=map_id, debug=debug)
+
+ if [ord(rom[address+8]), ord(rom[address+9])] == [0, 0]:
+ script_when_lost_ptr = 0
+ script_when_lost = None
+ else:
+ print "parsing script-when-lost"
+ script_when_lost_ptr = calculate_pointer_from_bytes_at(address+8, bank=bank)
+ script_when_lost = None
+ silver_avoids = [0xfa53]
+ if script_when_lost_ptr > 0x4000 and not script_when_lost_ptr in silver_avoids:
+ script_when_lost = parse_script_engine_script_at(script_when_lost_ptr, map_group=map_group, map_id=map_id, debug=debug)
+
+ print "parsing script-talk-again" # or is this a text?
+ script_talk_again_ptr = calculate_pointer_from_bytes_at(address+10, bank=bank)
+ script_talk_again = None
+ if script_talk_again_ptr > 0x4000:
+ script_talk_again = parse_script_engine_script_at(script_talk_again_ptr, map_group=map_group, map_id=map_id, debug=debug)
+
+ return {
+ "bit_number": bit_number,
+ "trainer_group": trainer_group,
+ "trainer_id": trainer_id,
+ "text_when_seen_ptr": text_when_seen_ptr,
+ "text_when_seen": text_when_seen,
+ "text_when_trainer_beaten_ptr": text_when_trainer_beaten_ptr,
+ "text_when_trainer_beaten": text_when_trainer_beaten,
+ "script_when_lost_ptr": script_when_lost_ptr,
+ "script_when_lost": script_when_lost,
+ "script_talk_again_ptr": script_talk_again_ptr,
+ "script_talk_again": script_talk_again,
+ }
+
+def old_parse_people_event_bytes(some_bytes, address=None, map_group=None, map_id=None, debug=True):
+ """parse some number of people-events from the data
+ see PeopleEvent
+ 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
+
+ max of 14 people per map?
+ """
+ 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)
+
+ lower_bits = color_function_byte & 0xF
+ #lower_bits_high = lower_bits >> 2
+ #lower_bits_low = lower_bits & 3
+ higher_bits = color_function_byte >> 4
+ #higher_bits_high = higher_bits >> 2
+ #higher_bits_low = higher_bits & 3
+
+ is_regular_script = lower_bits == 00
+ # pointer points to script
+ is_give_item = lower_bits == 01
+ # pointer points to [Item no.][Amount]
+ is_trainer = lower_bits == 02
+ # pointer points to trainer header
+
+ # 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?
+ extra_portion = {}
+ if bank:
+ ptr_address = calculate_pointer(script_pointer, bank)
+ if is_regular_script:
+ print "parsing a person-script at x=" + str(x-4) + " y=" + str(y-4) + " address="+hex(ptr_address)
+ script = parse_script_engine_script_at(ptr_address, map_group=map_group, map_id=map_id)
+ extra_portion = {
+ "script_address": ptr_address,
+ "script": script,
+ "event_type": "script",
+ }
+ if is_give_item:
+ print "... not parsing give item event... [item id][quantity]"
+ extra_portion = {
+ "event_type": "give_item",
+ "give_item_data_address": ptr_address,
+ "item_id": ord(rom[ptr_address]),
+ "item_qty": ord(rom[ptr_address+1]),
+ }
+ if is_trainer:
+ print "parsing a trainer (person-event) at x=" + str(x) + " y=" + str(y)
+ parsed_trainer = old_parse_trainer_header_at(ptr_address, map_group=map_group, map_id=map_id)
+ extra_portion = {
+ "event_type": "trainer",
+ "trainer_data_address": ptr_address,
+ "trainer_data": parsed_trainer,
+ }
+
+ # 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)
+
+ bit_number_of_bit_table1_byte2 = int(bytes[11], 16)
+ bit_number_of_bit_table1_byte1 = int(bytes[12], 16)
+ bit_number_of_bit_table1 = bit_number_of_bit_table1_byte1 + (bit_number_of_bit_table1_byte2 << 8)
+
+ people_event = {
+ "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},
+
+ #"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
+
+ "is_trainer": is_trainer,
+ "is_regular_script": is_regular_script,
+ "is_give_item": is_give_item,
+ }
+ people_event.update(extra_portion)
+ people_events.append(people_event)
+ return people_events
+
+def parse_map_header_by_id(*args, **kwargs):
+ """convenience function to parse a specific map"""
+ map_group, map_id = None, None
+ if "map_group" in kwargs.keys():
+ map_group = kwargs["map_group"]
+ if "map_id" in kwargs.keys():
+ map_id = kwargs["map_id"]
+ if (map_group == None and map_id != None) or \
+ (map_group != None and map_id == None):
+ raise Exception("map_group and map_id must both be provided")
+ elif map_group == None and map_id == None and len(args) == 0:
+ raise Exception("must be given an argument")
+ elif len(args) == 1 and type(args[0]) == str:
+ map_group = int(args[0].split(".")[0])
+ map_id = int(args[0].split(".")[1])
+ elif map_group == None and map_id == None:
+ raise Exception("dunno what to do with input")
+ offset = map_names[map_group]["offset"]
+ map_header_offset = offset + ((map_id - 1) * map_header_byte_size)
+ return parse_map_header_at(map_header_offset, map_group=map_group, map_id=map_id)
+
+def parse_all_map_headers(debug=True):
+ """calls parse_map_header_at for each map in each map group"""
+ global map_names
+ if not map_names[1].has_key("offset"):
+ raise Exception("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():
+ if map_id == "offset": continue # skip the "offset" address for this map group
+ if debug: print "map_group is: " + str(group_id) + " map_id is: " + str(map_id)
+ map_header_offset = offset + ((map_id - 1) * map_header_byte_size)
+ map_names[group_id][map_id]["header_offset"] = map_header_offset
+
+ new_parsed_map = parse_map_header_at(map_header_offset, map_group=group_id, map_id=map_id, debug=debug)
+ map_names[group_id][map_id]["header_new"] = new_parsed_map
+ old_parsed_map = old_parse_map_header_at(map_header_offset, map_group=group_id, map_id=map_id, debug=debug)
+ map_names[group_id][map_id]["header_old"] = old_parsed_map
+
+class PokedexEntryPointerTable:
+ """
+ A list of pointers.
+ """
+
+ def __init__(self):
+ self.address = 0x44378
+ self.target_bank = calculate_bank(0x181695)
+ self.label = Label(name="PokedexDataPointerTable", address=self.address, object=self)
+ self.size = None
+ self.last_address = None
+ self.dependencies = None
+ self.entries = []
+ self.parse()
+
+ script_parse_table[self.address : self.last_address] = self
+
+ def get_dependencies(self, recompute=False, global_dependencies=set()):
+ global_dependencies.update(self.entries)
+ dependencies = []
+ [dependencies.extend(entry.get_dependencies(recompute=recompute, global_dependencies=global_dependencies)) for entry in self.entries]
+ return dependencies
+
+ def parse(self):
+ size = 0
+ lastpointer = 0
+ for i in range(251):
+ # Those are consecutive in GS!
+ if i == 0x40:
+ self.target_bank = 0x6e
+ elif i == 0x80:
+ self.target_bank = 0x73
+ elif i == 0xc0:
+ self.target_bank = 0x74
+ loc = self.address+(i*2)
+ pointer = calculate_pointer_from_bytes_at(loc, bank=self.target_bank)
+ #print(hex(pointer))
+ #if pointer < lastpointer:
+ # self.target_bank += 1
+ # pointer += 0x4000
+ self.entries.append(PokedexEntry(pointer, i+1))
+
+ size += 2
+ self.size = size
+ self.last_address = self.address + self.size
+
+ def to_asm(self):
+ output = "".join([str("dw "+get_label_for(entry.address)+"\n") for entry in self.entries])
+ return output
+
+class PokedexEntry:
+ def __init__(self, address, pokemon_id):
+ self.address = address
+ self.dependencies = None
+ #label = self.make_label()
+ if pokemon_id in pokemon_constants:
+ pokename = string.capwords(pokemon_constants[pokemon_id].replace("__", " ").replace("_", " ")).replace(" ", "")
+ else:
+ pokename = "Pokemon{0}".format(pokemon_id)
+ self.label = Label(name=pokename+"PokedexEntry", address=self.address, object=self)
+ self.parse()
+ script_parse_table[address : self.last_address] = self
+
+ def get_dependencies(self, recompute=False, global_dependencies=set()):
+ return []
+
+ def parse(self):
+ # eww.
+ address = self.address
+ jump = how_many_until(chr(0x50), address)
+ self.species = parse_text_at(address, jump+1)
+ address = address + jump + 1
+
+ self.weight = ord(rom[address ]) + (ord(rom[address+1]) << 8)
+ self.height = ord(rom[address+2]) + (ord(rom[address+3]) << 8)
+ address += 4
+
+ jump = how_many_until(chr(0x50), address)
+ self.page1 = PokedexText(address)
+ address = address + jump + 1
+ jump = how_many_until(chr(0x50), address)
+ self.page2 = PokedexText(address)
+
+ self.last_address = address + jump + 1
+ #print(self.to_asm())
+ return True
+
+ def to_asm(self):
+ output = """\
+ db "{0}" ; species name
+ dw {1}, {2} ; height, weight
+
+ {3}
+ {4}""".format(self.species, self.weight, self.height, self.page1.to_asm(), self.page2.to_asm())
+ return output
+
+from map_names import map_names
+
+# map names with no labels will be generated
+# 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
+# generate map constants (like 1=PALLET_TOWN)
+generate_map_constant_labels()
+
+#### asm utilities ####
+# these are pulled in from pokered/extras/analyze_incbins.py
+
+# store each line of source code here
+asm = None
+
+# store each incbin line separately
+incbin_lines = []
+
+# storage for processed incbin lines
+processed_incbins = {}
+
+def to_asm(some_object, use_asm_rules=False):
+ """shows an object's asm with a label and an ending comment
+ showing the next byte address"""
+ if isinstance(some_object, int):
+ some_object = script_parse_table[some_object]
+ # add one to the last_address to show where the next byte is in the file
+ last_address = some_object.last_address
+ # create a line like "label: ; 0x10101"
+ asm = some_object.label.name + ": ; " + hex(some_object.address) + "\n"
+ # now add the inner/actual asm
+ #asm += spacing + some_object.to_asm().replace("\n", "\n"+spacing).replace("\n"+spacing+"\n"+spacing, "\n\n"+spacing)
+ asmr = some_object.to_asm()
+ asmr = asmr.replace("\n", "\n"+spacing)
+ asmr = asmr.replace("\n"+spacing+"\n", "\n\n"+spacing)
+ asmr = asmr.replace("\n\n"+spacing+spacing, "\n\n"+spacing)
+ asm += spacing + asmr
+ if use_asm_rules:
+ asm = asm.replace("\n" + spacing + "; ", "\n; ")
+ asm = asm.replace("\n" + spacing + ".asm_", "\n.asm_")
+ # show the address of the next byte below this
+ asm += "\n; " + hex(last_address)
+ return asm
+
+def flattener(x):
+ "flattens a list of sublists into just one list (generator)"
+ try:
+ it = iter(x)
+ except TypeError:
+ yield x
+ else:
+ for i in it:
+ for j in flattener(i):
+ yield j
+
+def flatten(x):
+ "flattens a list of sublists into just one list"
+ return list(flattener(x))
+
+def get_dependencies_for(some_object, recompute=False, global_dependencies=set()):
+ """
+ calculates which labels need to be satisfied for an object
+ to be inserted into the asm and compile successfully.
+
+ You could also choose to not insert labels into the asm, but
+ then you're losing out on the main value of having asm in the
+ first place.
+ """
+ try:
+ if isinstance(some_object, int):
+ some_object = script_parse_table[some_object]
+ if some_object.dependencies != None and not recompute:
+ global_dependencies.update(some_object.dependencies)
+ else:
+ some_object.get_dependencies(recompute=recompute, global_dependencies=global_dependencies)
+ return global_dependencies
+ except RuntimeError, e:
+ # 1552, 1291, 2075, 1552, 1291...
+ print "some_object is: " + str(some_object)
+ print "class type: " + str(some_object.__class__)
+ print "label name: " + str(some_object.label.name)
+ print "address: " + str(some_object.address)
+ print "asm is: \n\n" + to_asm(some_object)
+ raise e
+
+def isolate_incbins():
+ "find each incbin line"
+ global incbin_lines, asm
+ incbin_lines = []
+ for line in asm:
+ if line == "": continue
+ if line.count(" ") == len(line): continue
+
+ # clean up whitespace at beginning of line
+ while line[0] == " ":
+ line = line[1:]
+
+ if line[0:6] == "INCBIN" and "baserom.gbc" in line:
+ incbin_lines.append(line)
+ return incbin_lines
+
+def process_incbins():
+ "parse incbin lines into memory"
+ global asm, incbin_lines, processed_incbins
+ # load asm if it isn't ready yet
+ if asm == [] or asm == None:
+ load_asm()
+ # get a list of incbins if that hasn't happened yet
+ if incbin_lines == [] or incbin_lines == None:
+ isolate_incbins()
+ # reset the global that this function creates
+ processed_incbins = {}
+ # for each incbin..
+ for incbin in incbin_lines:
+ # reset this entry
+ processed_incbin = {}
+ # get the line number from the global asm line list
+ line_number = asm.index(incbin)
+ # forget about all the leading characters
+ partial_start = incbin[21:]
+ start = partial_start.split(",")[0].replace("$", "0x")
+ start = eval(start)
+ start_hex = hex(start).replace("0x", "$")
+
+ partial_interval = incbin[21:].split(",")[1]
+ partial_interval = partial_interval.replace(";", "#")
+ partial_interval = partial_interval.replace("$", "0x").replace("0xx", "0x")
+ interval = eval(partial_interval)
+ interval_hex = hex(interval).replace("0x", "$").replace("x", "")
+
+ end = start + interval
+ end_hex = hex(end).replace("0x", "$")
+
+ processed_incbin = {"line_number": line_number,
+ "line": incbin,
+ "start": start,
+ "interval": interval,
+ "end": end, }
+ # don't add this incbin if the interval is 0
+ if interval != 0:
+ processed_incbins[line_number] = processed_incbin
+ return processed_incbins
+
+def reset_incbins():
+ "reset asm before inserting another diff"
+ global asm, incbin_lines, processed_incbins
+ asm = None
+ incbin_lines = []
+ processed_incbins = {}
+ load_asm()
+ isolate_incbins()
+ process_incbins()
+
+def find_incbin_to_replace_for(address, debug=False, rom_file="../baserom.gbc"):
+ """returns a line number for which incbin to edit
+ if you were to insert bytes into main.asm"""
+ if type(address) == str: address = int(address, 16)
+ if not (0 <= address <= os.lstat(rom_file).st_size):
+ raise IndexError("address is out of bounds")
+ for incbin_key in processed_incbins.keys():
+ incbin = processed_incbins[incbin_key]
+ start = incbin["start"]
+ end = incbin["end"]
+ if debug:
+ print "start is: " + str(start)
+ print "end is: " + str(end)
+ print "address is: " + str(type(address))
+ print "checking.... " + hex(start) + " <= " + hex(address) + " <= " + hex(end)
+ if start <= address <= end:
+ return incbin_key
+ return None
+
+def split_incbin_line_into_three(line, start_address, byte_count, rom_file="../baserom.gbc"):
+ """
+ splits an incbin line into three pieces.
+ you can replace the middle one with the new content of length bytecount
+
+ start_address: where you want to start inserting bytes
+ byte_count: how many bytes you will be inserting
+ """
+ if type(start_address) == str: start_address = int(start_address, 16)
+ if not (0 <= start_address <= os.lstat(rom_file).st_size):
+ raise IndexError("start_address is out of bounds")
+ if len(processed_incbins) == 0:
+ raise Exception("processed_incbins must be populated")
+
+ original_incbin = processed_incbins[line]
+ start = original_incbin["start"]
+ end = original_incbin["end"]
+
+ # start, end1, end2 (to be printed as start, end1 - end2)
+ if start_address - start > 0:
+ first = (start, start_address, start)
+ else:
+ first = (None) # skip this one because we're not including anything
+
+ # this is the one you will replace with whatever content
+ second = (start_address, byte_count)
+
+ third = (start_address + byte_count, end - (start_address + byte_count))
+
+ output = ""
+
+ if first:
+ output += "INCBIN \"baserom.gbc\",$" + hex(first[0])[2:] + ",$" + hex(first[1])[2:] + " - $" + hex(first[2])[2:] + "\n"
+ output += "INCBIN \"baserom.gbc\",$" + hex(second[0])[2:] + "," + str(byte_count) + "\n"
+ output += "INCBIN \"baserom.gbc\",$" + hex(third[0])[2:] + ",$" + hex(third[1])[2:] # no newline
+ return output
+
+def generate_diff_insert(line_number, newline, debug=False):
+ """generates a diff between the old main.asm and the new main.asm
+ note: requires python2.7 i think? b/c of subprocess.check_output"""
+ global asm
+ original = "\n".join(line for line in asm)
+ newfile = deepcopy(asm)
+ newfile[line_number] = newline # possibly inserting multiple lines
+ newfile = "\n".join(line for line in newfile)
+
+ # make sure there's a newline at the end of the file
+ if newfile[-1] != "\n":
+ newfile += "\n"
+
+ original_filename = "ejroqjfoad.temp"
+ newfile_filename = "fjiqefo.temp"
+
+ original_fh = open(original_filename, "w")
+ original_fh.write(original)
+ original_fh.close()
+
+ newfile_fh = open(newfile_filename, "w")
+ newfile_fh.write(newfile)
+ newfile_fh.close()
+
+ try:
+ from subprocess import CalledProcessError
+ except ImportError:
+ CalledProcessError = None
+
+ try:
+ diffcontent = subprocess.check_output("diff -u ../main.asm " + newfile_filename, shell=True)
+ except (AttributeError, CalledProcessError):
+ p = subprocess.Popen(["diff", "-u", "../main.asm", newfile_filename], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
+ out, err = p.communicate()
+ diffcontent = out
+
+ os.system("rm " + original_filename)
+ os.system("rm " + newfile_filename)
+
+ if debug: print diffcontent
+ return diffcontent
+
+def apply_diff(diff, try_fixing=True, do_compile=True):
+ print "... Applying diff."
+
+ # write the diff to a file
+ fh = open("temp.patch", "w")
+ fh.write(diff)
+ fh.close()
+
+ # apply the patch
+ os.system("cp ../main.asm ../main1.asm")
+ os.system("patch ../main.asm temp.patch")
+
+ # remove the patch
+ os.system("rm temp.patch")
+
+ # confirm it's working
+ if do_compile:
+ try:
+ subprocess.check_call("cd ../; make clean; make", shell=True)
+ return True
+ except Exception, exc:
+ if try_fixing:
+ os.system("mv ../main1.asm ../main.asm")
+ return False
+
+class AsmLine:
+ # TODO: parse label lines
+ def __init__(self, line, bank=None):
+ self.line = line
+ self.bank = bank
+ def to_asm(self):
+ return self.line
+
+class Incbin:
+ def __init__(self, line, bank=None, debug=False):
+ self.line = line
+ self.bank = bank
+ self.replace_me = False
+ self.debug = debug
+ self.parse()
+ def parse(self):
+ incbin = self.line
+ partial_start = incbin[21:]
+ start = partial_start.split(",")[0].replace("$", "0x")
+
+ if self.debug:
+ print "Incbin.parse -- line is: " + self.line
+ print "Incbin.parse -- partial_start is: " + partial_start
+ print "Incbin.parse -- start is: " + start
+ try:
+ start = eval(start)
+ except Exception, e:
+ print "start is: " + str(start)
+ raise Exception("problem with evaluating interval range: " + str(e))
+
+ start_hex = hex(start).replace("0x", "$")
+
+ partial_interval = incbin[21:].split(",")[1]
+ partial_interval = partial_interval.replace(";", "#")
+ partial_interval = partial_interval.replace("$", "0x").replace("0xx", "0x")
+ interval = eval(partial_interval)
+ interval_hex = hex(interval).replace("0x", "$").replace("x", "")
+
+ end = start + interval
+ end_hex = hex(end).replace("0x", "$")
+
+ self.address = start
+ self.start_address = start
+ self.end_address = end
+ self.last_address = end
+ self.interval = interval
+ def to_asm(self):
+ if self.interval > 0:
+ return self.line
+ else:
+ return ""
+ def split(self, start_address, byte_count):
+ """splits this incbin into three separate incbins"""
+ if start_address < self.start_address or start_address > self.end_address:
+ raise Exception("this incbin doesn't handle this address")
+ incbins = []
+
+ if self.debug:
+ print "splitting an incbin ("+self.line+") into three at "+hex(start_address)+" for "+str(byte_count)+" bytes"
+
+ # start, end1, end2 (to be printed as start, end1 - end2)
+ if (start_address - self.start_address) > 0:
+ first = (self.start_address, start_address, self.start_address)
+ incbins.append(Incbin("INCBIN \"baserom.gbc\",$%.2x,$%.2x - $%.2x" % (first[0], first[1], first[2])))
+ if self.debug:
+ print " " + incbins[0].line
+ else:
+ # skip this one because we're not including anything
+ first = None
+
+ # this is the one you will replace with whatever content
+ second = (start_address, byte_count)
+ incbins.append(Incbin("INCBIN \"baserom.gbc\",$%.2x,$%.2x" % (start_address, byte_count)))
+ incbins[-1].replace_me = True
+ if self.debug:
+ print " " + incbins[-1].line
+
+ if (self.last_address - (start_address + byte_count)) > 0:
+ third = (start_address + byte_count, self.last_address - (start_address + byte_count))
+ incbins.append(Incbin("INCBIN \"baserom.gbc\",$%.2x,$%.2x" % (third[0], third[1])))
+ if self.debug:
+ print " " + incbins[-1].line
+
+ return incbins
+
+class AsmSection:
+ def __init__(self, line):
+ self.bank_id = None
+ self.line = line
+ self.parse()
+ def parse(self):
+ line = self.line
+
+ if not "bank" in line:
+ self.bank_id = -1
+ self.address = -1
+ self.last_address = None
+ self.end_address = None
+ return
+
+ bank_id = int(line.split("\"")[1].split("bank")[1], 16)
+ self.bank_id = bank_id
+ start_address = bank_id * 0x4000
+ end_address = (bank_id * 0x4000) + 0x4000 - 1
+
+ self.address = self.start_address = start_address
+ self.last_address = None
+ self.end_address = None
+ # this entity doesn't actually take up this space..
+ # although it could be argued that lines should exist under this object
+ #self.address = self.start_address = start_address
+ #self.last_address = self.end_address = end_address
+ def to_asm(self):
+ return self.line
+
+new_asm = None
+def load_asm2(filename="../main.asm", force=False):
+ """loads the asm source code into memory"""
+ global new_asm
+ if new_asm == None or force:
+ new_asm = Asm(filename=filename)
+ return new_asm
+
+class Asm:
+ """controls the overall asm output"""
+ def __init__(self, filename="../main.asm", debug=True):
+ self.parts = []
+ self.labels = []
+ self.filename = filename
+ self.debug = debug
+ self.load_and_parse()
+ def load_and_parse(self):
+ self.parts = []
+ asm = open(self.filename, "r").read().split("\n")
+ asm_list = AsmList(asm)
+ bank = 0
+ for line in asm_list:
+ if (line[0:6] == "INCBIN" or line[1:6] == "INCBIN") and not any([contaminant+"\"" in line for contaminant in [".2bpp", ".1bpp", ".asm", ".lz"]]):
+ thing = Incbin(line, bank=bank)
+ elif line[0:7] == "SECTION":
+ thing = AsmSection(line)
+ bank = thing.bank_id
+ else:
+ thing = AsmLine(line, bank=bank)
+ label = get_label_from_line(line)
+ if label:
+ laddress = get_address_from_line_comment(line)
+ thing.label = Label(name=label, address=laddress, object=thing, add_to_globals=False)
+ self.labels.append(thing.label)
+ self.parts.append(thing)
+ def is_label_name_in_file(self, label_name):
+ for llabel in self.labels:
+ if llabel.name == label_name:
+ return llabel
+ return False
+ def does_address_have_label(self, address):
+ """
+ Checks if an address has a label.
+ """
+ # either something will directly have the address
+ # or- it's possibel that no label was given
+ # or there will be an Incbin that covers the range
+ for part in self.parts:
+ if isinstance(part, Incbin) and part.start_address <= address <= part.end_address:
+ return False
+ elif hasattr(part, "address") and part.address == address and hasattr(part, "label"):
+ return part.label
+
+ return None
+ def insert(self, new_object):
+ if isinstance(new_object, ScriptPointerLabelParam):
+ # its' probably being injected in some get_dependencies() somewhere
+ print "don't know why ScriptPointerLabelParam is getting to this point?"
+ return
+
+ # first some validation
+ if not hasattr(new_object, "address"):
+ print "object needs to have an address property: " + str(new_object)
+ return
+
+ start_address = new_object.address
+
+ # skip this dragon shrine script calling itself
+ # what about other scripts that call themselves ?
+ if start_address in lousy_dragon_shrine_hack:
+ print "skipping 0x18d079 in dragon shrine for a lousy hack"
+ return
+
+ if not hasattr(new_object, "label") and hasattr(new_object, "is_valid") and not new_object.is_valid():
+ return
+
+ debugmsg = "object is " + new_object.label.name + " type="+str(new_object.__class__)+" new_object="+str(new_object)
+ debugmsg += " label = " + new_object.label.name
+ debugmsg += " start_address="+hex(start_address)#+" end_address="+hex(end_address)
+
+ if not hasattr(new_object, "last_address"):
+ print debugmsg
+ raise Exception("object needs to have a last_address property")
+ end_address = new_object.last_address
+ debugmsg += " last_address="+hex(end_address)
+
+ # check if the object is already inserted
+ if new_object in self.parts:
+ print "object was previously inserted ("+str(new_object)+"; " + hex(new_object.address) + ")"
+ return
+ # check by label
+ other_obj = self.is_label_name_in_file(new_object.label.name)
+ if other_obj:
+ other_obj = other_obj.object
+ print "object was previously inserted ("+new_object.label.name+" at "+hex(new_object.address)+") by "+other_obj.label.name+" at "+hex(other_obj.address)
+ return
+ # check by address
+ #if self.does_address_have_label(new_object.address):
+ # print "object's address is already used ("+str(new_object)+") at "+hex(new_object.address)+" label="+new_object.label.name
+ # return
+
+ if self.debug:
+ print debugmsg
+ del debugmsg
+ if (end_address < start_address) or ((end_address - start_address) < 0):
+ if not self.debug:
+ print "object is new_object="+str(new_object)
+ print "start_address="+hex(start_address)+" end_address="+hex(end_address)
+ if hasattr(new_object, "to_asm"):
+ print to_asm(new_object)
+ raise Exception("Asm.insert was given an object with a bad address range")
+
+ # 1) find which object needs to be replaced
+ # or
+ # 2) find which object goes after it
+ found = False
+ for object in list(self.parts):
+ # skip objects without a defined interval (like a comment line)
+ if not hasattr(object, "address") or not hasattr(object, "last_address"):
+ continue
+ # skip an AsmSection
+ if isinstance(object, AsmSection):
+ continue
+ # replace an incbin with three incbins, replace middle incbin with whatever
+ elif isinstance(object, Incbin) and (object.address <= start_address < object.last_address):
+ # split up the incbin into three segments
+ incbins = object.split(start_address, end_address - start_address)
+ # figure out which incbin to replace with the new object
+ if incbins[0].replace_me:
+ index = 0
+ else: # assume incbins[1].replace_me (the middle one)
+ index = 1
+ # replace that index with the new_object
+ incbins[index] = new_object
+ # insert these incbins into self.parts
+ gindex = self.parts.index(object)
+ self.parts = self.parts[:gindex] + incbins + self.parts[gindex:]
+ self.parts.remove(object)
+ found = True
+ break
+ elif object.address <= start_address < object.last_address:
+ print "this is probably a script that is looping back on itself?"
+ found = True
+ break
+ # insert before the current object
+ elif object.address > end_address:
+ #insert_before = index of object
+ index = self.parts.index(object)
+ self.parts.insert(index, new_object)
+ found = True
+ break
+ if not found:
+ raise Exception("unable to insert object into Asm")
+ self.labels.append(new_object.label)
+ return True
+ def insert_with_dependencies(self, input):
+ if type(input) == list:
+ input_objects = input
+ else:
+ input_objects = [input]
+
+ for object0 in input_objects:
+ global_dependencies = set([object0])
+ poopbutt = get_dependencies_for(object0, global_dependencies=global_dependencies, recompute=False)
+ objects = global_dependencies
+ objects.update(poopbutt)
+ new_objects = copy(objects)
+ for object in objects:
+ if hasattr(object, "dependencies") and object.dependencies == None:
+ new_objects.update(object.get_dependencies())
+ for object in new_objects:
+ if isinstance(object, ScriptPointerLabelParam):
+ continue
+ #if object in self.parts:
+ # if self.debug:
+ # print "already inserted -- object.__class__="+str(object.__class__)+" object is: "+str(object)+\
+ # " for object.__class__="+str(object0.__class__)+" object="+str(object0)
+ # continue
+ if self.debug:
+ print " object is: " + str(object)
+ self.insert(object)
+
+ # just some old debugging
+ #if object.label.name == "UnknownText_0x60128":
+ # raise Exception("debugging...")
+ #elif object.label.name == "UnknownScript_0x60011":
+ # raise Exception("debugging.. dependencies are: " + str(object.dependencies) + " versus: " + str(object.get_dependencies()))
+ def insert_single_with_dependencies(self, object):
+ self.insert_with_dependencies(object)
+ def insert_multiple_with_dependencies(self, objects):
+ self.insert_with_dependencies(objects)
+ def insert_all(self, limit=100):
+ count = 0
+ for each in script_parse_table.items():
+ if count == limit: break
+ object = each[1]
+ if type(object) == str: continue
+ self.insert_single_with_dependencies(object)
+ count += 1
+ def insert_and_dump(self, limit=100, filename="output.txt"):
+ self.insert_all(limit=limit)
+ self.dump(filename=filename)
+ def dump(self, filename="output.txt"):
+ fh = open(filename, "w")
+ newlines_before_next_obj_requested = 0
+ newlines_before_next_obj_given = 0
+
+ current_requested_newlines_before = 0
+ current_requested_newlines_after = 0
+ previous_requested_newlines_before = 0
+ previous_requested_newlines_after = 0
+
+ written_newlines = 0
+ write_something = False
+ first = True
+ last = None
+ for each in self.parts:
+ asm = ""
+ previous_requested_newlines_after = current_requested_newlines_after
+ current_requested_newlines_before = current_requested_newlines_after
+
+ write_something = True
+ if (isinstance(each, str) and each == "") or (isinstance(each, AsmLine) and each.line == ""):
+ current_requested_newlines_before = 0
+ if current_requested_newlines_after < 2:
+ current_requested_newlines_after += 1
+ write_something = False
+ elif (isinstance(each, str) and each != "") or (isinstance(each, AsmLine) and each.line != ""):
+ if isinstance(each, AsmLine):
+ asm = each.to_asm()
+ elif isinstance(each, str):
+ asm = each
+ current_requested_newlines_before = 0
+ current_requested_newlines_after = 1
+ elif isinstance(each, AsmSection) or isinstance(each, Incbin) or hasattr(each, "to_asm"):
+ if isinstance(each, AsmSection) or isinstance(each, Incbin):
+ asm = each.to_asm()
+ else:
+ asm = to_asm(each)
+ current_requested_newlines_before = 2
+ current_requested_newlines_after = 2
+ else:
+ raise Exception("dunno what to do with("+str(each)+") in Asm.parts")
+
+ if write_something:
+ if not first:
+ newlines_before = max([current_requested_newlines_before, previous_requested_newlines_after])
+ while written_newlines < newlines_before:
+ fh.write("\n")
+ written_newlines += 1
+ else:
+ first = False
+ fh.write(asm)
+ written_newlines = 0
+ last = each
+
+ # make sure the file ends with a newline
+ fh.write("\n")
+
+def list_things_in_bank(bank):
+ objects = []
+ for blah in script_parse_table.items():
+ object = blah[1]
+ if hasattr(object, "address") and calculate_bank(object.address) == bank:
+ objects.append(object)
+ return objects
+
+def list_texts_in_bank(bank):
+ """
+ Narrows down the list of objects that you will be inserting into Asm.
+ """
+ if len(all_texts) == 0:
+ raise Exception("all_texts is blank.. run_main() will populate it")
+
+ assert bank != None, "list_texts_in_banks must be given a particular bank"
+
+ assert 0 <= bank < 0x80, "bank doesn't exist in the ROM"
+
+ texts = []
+ for text in all_texts:
+ if calculate_bank(text.address) == bank:
+ texts.append(text)
+
+ return texts
+
+def list_movements_in_bank(bank):
+ """
+ Narrows down the list of objects to speed up Asm insertion.
+ """
+ if len(all_movements) == 0:
+ raise Exception("all_movements is blank.. run_main() will populate it")
+
+ assert bank != None, "list_movements_in_bank must be given a particular bank"
+ assert 0 <= bank < 0x80, "bank doesn't exist in the ROM (out of bounds)"
+
+ movements = []
+ for movement in all_movements:
+ if calculate_bank(movement.address) == bank:
+ movements.append(movement)
+ return movements
+
+def dump_asm_for_texts_in_bank(bank, start=50, end=100):
+ """
+ Simple utility to help with dumping texts into a particular bank. This is
+ helpful for figuring out which text is breaking that bank.
+ """
+ # load and parse the ROM if necessary
+ if rom == None or len(rom) <= 4:
+ load_rom()
+ run_main()
+
+ # get all texts
+ # first 100 look okay?
+ texts = list_texts_in_bank(bank)[start:end]
+
+ # create a new dump
+ asm = Asm()
+
+ # start the insertion process
+ asm.insert_multiple_with_dependencies(texts)
+
+ # start dumping
+ asm.dump()
+
+ print "done dumping texts for bank $%.2x" % (bank)
+
+def dump_asm_for_movements_in_bank(bank, start=0, end=100):
+ if rom == None or len(rom) <= 4:
+ load_rom()
+ run_main()
+
+ movements = list_movements_in_bank(bank)[start:end]
+
+ asm = Asm()
+ asm.insert_with_dependencies(movements)
+ asm.dump()
+ print "done dumping movements for bank $%.2x" % (bank)
+
+def dump_things_in_bank(bank, start=50, end=100):
+ """
+ is helpful for figuring out which object is breaking that bank.
+ """
+ # load and parse the ROM if necessary
+ if rom == None or len(rom) <= 4:
+ load_rom()
+ run_main()
+
+ things = list_things_in_bank(bank)[start:end]
+
+ # create a new dump
+ asm = Asm()
+
+ # start the insertion process
+ asm.insert_with_dependencies(things)
+
+ # start dumping
+ asm.dump()
+
+ print "done dumping things for bank $%.2x" % (bank)
+
+def index(seq, f):
+ """return the index of the first item in seq
+ where f(item) == True."""
+ return next((i for i in xrange(len(seq)) if f(seq[i])), None)
+
+def analyze_intervals():
+ """find the largest baserom.gbc intervals"""
+ global asm, processed_incbins
+ if asm == None:
+ load_asm()
+ if processed_incbins == {}:
+ isolate_incbins()
+ process_incbins()
+ results = []
+ ordered_keys = sorted(processed_incbins, key=lambda entry: processed_incbins[entry]["interval"])
+ ordered_keys.reverse()
+ for key in ordered_keys:
+ results.append(processed_incbins[key])
+ return results
+
+all_labels = []
+def write_all_labels(all_labels, filename="labels.json"):
+ fh = open(filename, "w")
+ fh.write(json.dumps(all_labels))
+ fh.close()
+ return True
+
+from wram import wram_labels
+def get_ram_label(address):
+ """returns a label assigned to a particular ram address"""
+ if address in wram_labels.keys():
+ return wram_labels[address][-1]
+ return None
+
+def get_label_for(address):
+ """returns a label assigned to a particular rom address"""
+ global all_labels
+
+ if address == None:
+ return None
+ if type(address) != int:
+ raise Exception("get_label_for requires an integer address, got: " + str(type(address)))
+
+ # lousy hack to get around recursive scripts in dragon shrine
+ if address in lousy_dragon_shrine_hack:
+ return None
+
+ # the old way
+ for thing in all_labels:
+ if thing["address"] == address:
+ return thing["label"]
+
+ # the new way
+ obj = script_parse_table[address]
+ if obj:
+ if hasattr(obj, "label"):
+ return obj.label.name
+ else:
+ return "AlreadyParsedNoDefaultUnknownLabel_" + hex(address)
+
+ #return "NotYetParsed_"+hex(address)
+ if address > 0x7FFF:
+ value = 0x4000 + (address % 0x4000)
+ return "$%.2x"%(value)
+ else:
+ return "$%.2x"%(address)
+
+# all_new_labels is a temporary replacement for all_labels,
+# at least until the two approaches are merged in the code base.
+all_new_labels = []
+
+class Label:
+ """
+ Every object in script_parse_table is given a label.
+
+ This label is simply a way to keep track of what objects have
+ been previously written to file.
+ """
+ def __init__(self, name=None, address=None, line_number=None, object=None, is_in_file=None, address_is_in_file=None, add_to_globals=True):
+ assert address != None, "need an address"
+ assert is_valid_address(address), "address must be valid"
+ assert object != None, "need an object to relate with"
+
+ self.address = address
+ self.object = object
+
+ # label might not be in the file yet
+ self.line_number = line_number
+
+ # -- These were some old attempts to check whether the label
+ # -- was already in use. They work, but the other method is
+ # -- better.
+ #
+ # check if the label is in the file already
+ # check if the address of this label is already in use
+
+ self.is_in_file = is_in_file
+
+ self.address_is_in_file = address_is_in_file
+
+ if name == None:
+ name = object.base_label + "_" + hex(object.address)
+
+ self.name = name
+
+ if add_to_globals:
+ all_new_labels.append(self)
+
+ def check_is_in_file(self):
+ """
+ This method checks if the label appears in the file based on the
+ entries to the Asm.parts list.
+ """
+ # assert new_asm != None, "new_asm should be an instance of Asm"
+ load_asm2()
+ is_in_file = new_asm.is_label_name_in_file(self.name)
+ self.is_in_file = is_in_file
+ return is_in_file
+
+ def check_address_is_in_file(self):
+ """
+ Checks if the address is in use by another label.
+ """
+ load_asm2()
+ self.address_is_in_file = new_asm.does_address_have_label(self.address)
+ return self.address_is_in_file
+
+ def old_check_address_is_in_file(self):
+ """
+ Checks whether or not the address of the object is already in the file.
+ This might happen if the label name is different but the address is the
+ same. Another scenario is that the label is already used, but at a
+ different address.
+
+ This method works by looking at the INCBINs. When there is
+ an INCBIN that covers this address in the file, then there
+ is no label at this address yet (or there is, but we can
+ easily add another label in front of the incbin or something),
+ and when there is no INCBIN that has this address, then we
+ know that something is already using this address.
+ """
+ if processed_incbins == {}:
+ process_incbins()
+
+ incbin = find_incbin_to_replace_for(self.address)
+
+ if incbin == None:
+ return True
+ else:
+ return False
+
+ def make_label(self):
+ """
+ Generates a label name based on parents and self.object.
+ """
+ object = self.object
+ name = object.make_label()
+ return name
+
+def find_labels_without_addresses():
+ """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
+
+label_errors = ""
+def get_labels_between(start_line_id, end_line_id, bank):
+ labels = []
+ #label = {
+ # "line_number": 15,
+ # "bank": 32,
+ # "label": "PalletTownText1",
+ # "offset": 0x5315,
+ # "address": 0x75315,
+ #}
+ if asm == None:
+ load_asm()
+ sublines = asm[start_line_id : end_line_id + 1]
+ for (current_line_offset, line) in enumerate(sublines):
+ # skip lines without labels
+ if not line_has_label(line): continue
+ # reset some variables
+ line_id = start_line_id + current_line_offset
+ line_label = get_label_from_line(line)
+ address = None
+ offset = None
+ # setup a place to store return values from line_has_comment_address
+ returnable = {}
+ # get the address from the comment
+ has_comment = line_has_comment_address(line, returnable=returnable, bank=bank)
+ # skip this line if it has no address in the comment
+ if not has_comment: continue
+ # parse data from line_has_comment_address
+ address = returnable["address"]
+ bank = returnable["bank"]
+ offset = returnable["offset"]
+ # dump all this info into a single structure
+ label = {
+ "line_number": line_id,
+ "bank": bank,
+ "label": line_label,
+ "offset": offset,
+ "address": address,
+ }
+ # store this structure
+ labels.append(label)
+ return labels
+
+def scan_for_predefined_labels(debug=False):
+ """looks through the asm file for labels at specific addresses,
+ this relies on the label having its address after. ex:
+
+ ViridianCity_h: ; 0x18357 to 0x18384 (45 bytes) (bank=6) (id=1)
+ PalletTownText1: ; 4F96 0x18f96
+ ViridianCityText1: ; 0x19102
+
+ It would be more productive to use rgbasm to spit out all label
+ addresses, but faster to write this script. rgbasm would be able
+ to grab all label addresses better than this script..
+ """
+ global all_labels
+ all_labels = []
+ bank_intervals = {}
+
+ if asm == None:
+ load_asm()
+
+ # figure out line numbers for each bank
+ for bank_id in range(0x7F+1):
+ abbreviation = ("%.x" % (bank_id)).upper()
+ abbreviation_next = ("%.x" % (bank_id+1)).upper()
+ if bank_id == 0:
+ abbreviation = "0"
+ abbreviation_next = "1"
+
+ # calculate the start/stop line numbers for this bank
+ start_line_id = index(asm, lambda line: "\"bank" + abbreviation.lower() + "\"" in line.lower())
+ if bank_id != 0x7F:
+ end_line_id = index(asm, lambda line: "\"bank" + abbreviation_next.lower() + "\"" in line.lower())
+ end_line_id += 1
+ else:
+ end_line_id = len(asm) - 1
+
+ if debug:
+ output = "bank" + abbreviation + " starts at "
+ output += str(start_line_id)
+ output += " to "
+ output += str(end_line_id)
+ print output
+
+ # store the start/stop line number for this bank
+ bank_intervals[bank_id] = {"start": start_line_id,
+ "end": end_line_id,}
+ # for each bank..
+ for bank_id in bank_intervals.keys():
+ # get the start/stop line number
+ bank_data = bank_intervals[bank_id]
+ start_line_id = bank_data["start"]
+ end_line_id = bank_data["end"]
+ # get all labels between these two lines
+ labels = get_labels_between(start_line_id, end_line_id, bank_id)
+ # bank_intervals[bank_id]["labels"] = labels
+ all_labels.extend(labels)
+ write_all_labels(all_labels)
+ return all_labels
+
+def run_main():
+ # read the rom and figure out the offsets for maps
+ direct_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()
+
+ # find trainers based on scripts and map headers
+ # this can only happen after parsing the entire map and map scripts
+ find_trainer_ids_from_scripts()
+
+ # and parse the main TrainerGroupTable once we know the max number of trainers
+ #global trainer_group_table
+ trainer_group_table = TrainerGroupTable()
+
+ # improve duplicate trainer names
+ make_trainer_group_name_trainer_ids(trainer_group_table)
+
+# just a helpful alias
+main = run_main
+
+# when you load the module.. parse everything
+if __name__ == "crystal":
+ pass
diff --git a/pokemontools/disassemble_map_scripts.py b/pokemontools/disassemble_map_scripts.py
new file mode 100644
index 0000000..21df569
--- /dev/null
+++ b/pokemontools/disassemble_map_scripts.py
@@ -0,0 +1,151 @@
+# -*- encoding: utf-8 -*-
+"""
+Dump out asm for scripting things in bank $25. This script will modify main.asm
+and insert all scripting commands.
+"""
+
+import crystal
+from gbz80disasm import output_bank_opcodes
+
+rom = crystal.load_rom()
+roml = [ord(x) for x in rom]
+
+script_command_table_address = 0x96cb1
+script_command_count = 170
+
+# a list of addresses for each script command
+command_pointers = [crystal.calculate_pointer_from_bytes_at(script_command_table_address + (id * 2), bank=0x25) for id in range(0, 170)]
+
+# a list of hex addresses for each script command in bank $25
+command_pointers_hex = ["$%.2x" % (x % 0x4000 + 0x4000) for x in command_pointers]
+
+commands = {}
+
+# force data into a more usable form
+for command in crystal.command_classes:
+ name = "Script_" + command.macro_name
+ id = command.id
+ params = {}
+
+ for (id2, param_type) in command.param_types.items():
+ param = {
+ "name": param_type["name"],
+ "type": param_type["class"].__name__,
+ }
+ params[id2] = param
+
+ if id <= 0xa9:
+ commands[id] = {
+ "name": name,
+ "params": params,
+ "address": command_pointers[id],
+ }
+
+avoid = [
+ 0x974b0,
+ 0x974be,
+ 0x9754b,
+ 0x97556,
+ 0x97562,
+ 0x9756e,
+ 0x97540,
+
+ 0x96f8e, # verbosegiveitem2
+]
+
+class DisassembledScriptCommand():
+ """
+ Just a temporary object to store information about a script command's asm.
+ This is used by some of the infrastructure in crystal.py to automatically
+ insert asm into main.asm, rather than having someone do it manually.
+ """
+ dependencies = None
+
+ def __init__(self, label=None, id=None, address=None, params=None):
+ self.id = id
+ self.label = crystal.Label(name=label, address=address, object=self)
+ self.address = address
+ self.params = params
+
+ max_byte_count = 0x4000
+
+ # Some of these scripts need to be truncated before insertion, because
+ # output_bank_opcodes doesn't know anything about stopping if some of
+ # the local labels are not resolved yet.
+
+ # Script_if_equal
+ if address == 0x97540:
+ max_byte_count = 86
+
+ # disassemble and laso get the last address
+ (asm, last_address, last_hl_address, last_a_address, used_3d97) = output_bank_opcodes(address, max_byte_count=max_byte_count, stop_at=command_pointers, include_last_address=False)
+
+ # remove indentation
+ asm = asm.replace("\n\t", "\n")
+ if asm[0] == "\t":
+ asm = asm[1:]
+
+ # remove the last two newlines
+ while asm[-1] == "\n":
+ asm = asm[:-1]
+
+ self.asm = asm
+ self.last_address = last_address
+
+ # make sure this gets dumped into main.asm
+ #if crystal.script_parse_table[self.address] == None and crystal.script_parse_table[self.last_address] == None:
+ crystal.script_parse_table[self.address : self.last_address] = self
+ #else:
+ # print ".. hm, something is already at " + hex(self.address) + " for " + self.label.name
+
+ def to_asm(self):
+ #output += self.label + ": ; " + hex(self.address) + "\n"
+ output = "; script command " + hex(self.id) + "\n"
+ if len(self.params) > 0:
+ output += "; parameters:\n"
+ for (id2, param) in self.params.items():
+ output += "; " + param["name"] + " (" + param["type"] + ")\n"
+ output += "\n"
+ output += self.asm
+ return output
+
+ def get_dependencies(*args, **kwargs):
+ return []
+
+# make instances of DisassembledScriptCommand
+for (id, command) in commands.items():
+ name = command["name"]
+ params = command["params"]
+ address = command["address"]
+
+ script_asm = DisassembledScriptCommand(label=name, id=id, address=address, params=params)
+ #print script_asm.to_asm()
+ #print crystal.to_asm(script_asm, use_asm_rules=True)
+
+class ScriptCommandTable():
+ address = script_command_table_address
+ last_address = script_command_table_address + (2 * 170)
+ dependencies = None
+
+ def __init__(self):
+ self.label = crystal.Label(name="ScriptCommandTable", address=self.address, object=self)
+
+ # make sure this gets dumped into main.asm
+ crystal.script_parse_table[self.address : self.last_address] = self
+
+ def get_dependencies(*args, **kwargs):
+ return []
+
+ def to_asm(self):
+ output = ""
+ for (id, command) in commands.items():
+ output += "dw " + command["name"] + "; " + hex(command["address"]) + "\n"
+ if output[-1] == "\n":
+ output = output[:-1]
+ return output
+script_command_table = ScriptCommandTable()
+#print crystal.to_asm(script_command_table, use_asm_rules=True)
+
+# automatic asm insertion
+asm = crystal.Asm()
+asm.insert_and_dump(limit=500)
diff --git a/pokemontools/dump_sections.py b/pokemontools/dump_sections.py
new file mode 100755
index 0000000..fef35d8
--- /dev/null
+++ b/pokemontools/dump_sections.py
@@ -0,0 +1,129 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+"""
+Use this tool to dump an asm file for a new source code or disassembly project.
+
+usage:
+
+ from dump_sections import dump_sections
+
+ output = dump_sections("../../butt.gbc")
+
+ file_handler = open("main.asm", "w")
+ file_handler.write(output)
+ file_handler.close()
+
+You can also use this script from the shell, where it will look for
+"baserom.gbc" in the current working path or whatever file path you pass in the
+first positional argument.
+"""
+
+import os
+import sys
+import argparse
+
+def upper_hex(input):
+ """
+ Converts the input to an uppercase hex string.
+ """
+ if input in [0, "0"]:
+ return "0"
+ elif input <= 0xF:
+ return ("%.x" % (input)).upper()
+ else:
+ return ("%.2x" % (input)).upper()
+
+def format_bank_number(address, bank_size=0x4000):
+ """
+ Returns a str of the hex number of the bank based on the address.
+ """
+ return upper_hex(address / bank_size)
+
+def calculate_bank_quantity(path, bank_size=0x4000):
+ """
+ Returns the number of 0x4000 banks in the file at path.
+ """
+ return float(os.lstat(path).st_size) / bank_size
+
+def dump_section(bank_number, separator="\n\n"):
+ """
+ Returns a str of a section header for the asm file.
+ """
+ output = "SECTION \""
+ if bank_number in [0, "0"]:
+ output += "bank0\",HOME"
+ else:
+ output += "bank"
+ output += bank_number
+ output += "\",DATA,BANK[$"
+ output += bank_number
+ output += "]"
+ output += separator
+ return output
+
+def dump_incbin_for_section(address, bank_size=0x4000, baserom="baserom.gbc", separator="\n\n"):
+ """
+ Returns a str for an INCBIN line for an entire section.
+ """
+ output = "INCBIN \""
+ output += baserom
+ output += "\",$"
+ output += upper_hex(address)
+ output += ",$"
+ output += upper_hex(bank_size)
+ output += separator
+ return output
+
+def dump_sections(path, bank_size=0x4000, initial_bank=0, last_bank=None, separator="\n\n"):
+ """
+ Returns a str of assembly source code. The source code delineates each
+ SECTION and includes bytes from the file specified by baserom.
+ """
+ if not last_bank:
+ last_bank = calculate_bank_quantity(path, bank_size=bank_size)
+
+ if last_bank < initial_bank:
+ raise Exception("last_bank must be greater than or equal to initial_bank")
+
+ baserom_name = os.path.basename(path)
+
+ output = ""
+
+ banks = range(initial_bank, last_bank)
+
+ for bank_number in banks:
+ address = bank_number * bank_size
+
+ # get a formatted hex number of the bank based on the address
+ formatted_bank_number = format_bank_number(address, bank_size=bank_size)
+
+ # SECTION
+ output += dump_section(formatted_bank_number, separator=separator)
+
+ # INCBIN a range of bytes from the ROM
+ output += dump_incbin_for_section(address, bank_size=bank_size, baserom=baserom_name, separator=separator)
+
+ # clean up newlines at the end of the output
+ if output[-2:] == "\n\n":
+ output = output[:-2]
+ output += "\n"
+
+ return output
+
+if __name__ == "__main__":
+ parser = argparse.ArgumentParser()
+ parser.add_argument("rompath", nargs="?", metavar="rompath", type=str)
+ args = parser.parse_args()
+
+ # default to "baserom.gbc" in the current working directory
+ baserom = "baserom.gbc"
+
+ # but let the user override the default
+ if args.rompath:
+ baserom = args.rompath
+
+ # generate some asm
+ output = dump_sections(baserom)
+
+ # dump everything to stdout
+ sys.stdout.write(output)
diff --git a/pokemontools/gbz80disasm.py b/pokemontools/gbz80disasm.py
new file mode 100644
index 0000000..6759a4a
--- /dev/null
+++ b/pokemontools/gbz80disasm.py
@@ -0,0 +1,945 @@
+# -*- coding: utf-8 -*-
+
+import os
+import sys
+from copy import copy, deepcopy
+from ctypes import c_int8
+import random
+import json
+from wram import *
+
+# New versions of json don't have read anymore.
+if not hasattr(json, "read"):
+ json.read = json.loads
+
+def load_rom(filename="../baserom.gbc"):
+ """
+ Load the specified rom.
+
+ If no rom is given, load "../baserom.gbc".
+ """
+ global rom
+ rom = bytearray(open(filename,'rb').read())
+ return rom
+
+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]
+
+all_labels = {}
+def load_labels(filename="labels.json"):
+ """
+ Load labels from specified file.
+
+ If no filename is given, loads 'labels.json'.
+ """
+ global all_labels
+
+ # don't re-load labels each time
+ if all_labels != {}:
+ return
+
+ if os.path.exists(filename):
+ all_labels = json.read(open(filename, "r").read())
+ else:
+ print "You must run crystal.scan_for_predefined_labels() to create \"labels.json\". Trying..."
+ import crystal
+ crystal.scan_for_predefined_labels()
+
+def find_label(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 all_labels:
+ if get_local_address(label_entry["address"]) == local_address:
+ if label_entry["bank"] == bank_id or label_entry["bank"] == 0:
+ return label_entry["label"]
+ if local_address in wram_labels.keys():
+ return wram_labels[local_address][-1]
+ for constants in [gbhw_constants, hram_constants]:
+ if local_address in constants.keys() and local_address >= 0xff00:
+ return constants[local_address]
+ return None
+
+def find_address_from_label(label):
+ for label_entry in all_labels:
+ if label == label_entry["label"]:
+ return label_entry["address"]
+ return None
+
+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 output_bank_opcodes(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.
+ """
+
+ load_labels()
+ load_rom()
+
+ 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
+
+ global 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 = 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 = 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)
+
+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
+
+
+
+if __name__ == "__main__":
+ load_labels()
+ addr = sys.argv[1]
+ if ":" in addr:
+ addr = addr.split(":")
+ addr = int(addr[0], 16)*0x4000+(int(addr[1], 16)%0x4000)
+ else:
+ label_addr = find_address_from_label(addr)
+ if label_addr:
+ addr = label_addr
+ else:
+ addr = int(addr, 16)
+ print output_bank_opcodes(addr)[0]
diff --git a/pokemontools/gfx.py b/pokemontools/gfx.py
new file mode 100644
index 0000000..c632ac0
--- /dev/null
+++ b/pokemontools/gfx.py
@@ -0,0 +1,1671 @@
+# -*- coding: utf-8 -*-
+
+import os
+import sys
+import png
+from math import sqrt, floor, ceil
+
+from crystal import load_rom
+
+from pokemon_constants import pokemon_constants
+from trainers import trainer_group_names
+
+
+if __name__ != "__main__":
+ rom = load_rom()
+
+
+def mkdir_p(path):
+ """
+ Make a directory at a given path.
+ """
+ try:
+ os.makedirs(path)
+ except OSError as exc: # Python >2.5
+ if exc.errno == errno.EEXIST:
+ pass
+ else: raise
+
+
+def hex_dump(input, debug=True):
+ """
+ Display hex dump in rows of 16 bytes.
+ """
+
+ dump = ''
+ output = ''
+ stream = ''
+ address = 0x00
+ margin = 2 + len(hex(len(input))[2:])
+
+ # dump
+ for byte in input:
+ cool = hex(byte)[2:].zfill(2)
+ dump += cool + ' '
+ if debug: stream += cool
+
+ # convenient for testing quick edits in bgb
+ if debug: output += stream + '\n'
+
+ # get dump info
+ bytes_per_line = 16
+ chars_per_byte = 3 # '__ '
+ chars_per_line = bytes_per_line * chars_per_byte
+ num_lines = int(ceil(float(len(dump)) / float(chars_per_line)))
+
+ # top
+ # margin
+ for char in range(margin):
+ output += ' '
+
+ for byte in range(bytes_per_line):
+ output += hex(byte)[2:].zfill(2) + ' '
+ output = output[:-1] # last space
+
+ # print hex
+ for line in range(num_lines):
+ # address
+ output += '\n' + hex(address)[2:].zfill(margin - 2) + ': '
+ # contents
+ start = line * chars_per_line
+ end = chars_per_line + start - 1 # ignore last space
+ output += dump[start:end]
+ address += 0x10
+
+ return output
+
+
+def get_tiles(image):
+ """
+ Split a 2bpp image into 8x8 tiles.
+ """
+ tiles = []
+ tile = []
+ bytes_per_tile = 16
+
+ cur_byte = 0
+ for byte in image:
+ # build tile
+ tile.append(byte)
+ cur_byte += 1
+ # done building?
+ if cur_byte >= bytes_per_tile:
+ # push completed tile
+ tiles.append(tile)
+ tile = []
+ cur_byte = 0
+ return tiles
+
+
+def connect(tiles):
+ """
+ Combine 8x8 tiles into a 2bpp image.
+ """
+ out = []
+ for tile in tiles:
+ for byte in tile:
+ out.append(byte)
+ return out
+
+
+def transpose(tiles):
+ """
+ Transpose a tile arrangement along line y=x.
+ """
+
+ # horizontal <-> vertical
+ # 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
+ # etc
+
+ flipped = []
+ t = 0 # which tile we're on
+ w = int(sqrt(len(tiles))) # assume square image
+ for tile in tiles:
+ flipped.append(tiles[t])
+ t += w
+ # end of row?
+ if t >= w*w:
+ # wrap around
+ t -= w*w
+ # next row
+ t += 1
+ return flipped
+
+
+def to_file(filename, data):
+ file = open(filename, 'wb')
+ for byte in data:
+ file.write('%c' % byte)
+ file.close()
+
+
+
+
+# basic rundown of crystal's compression scheme:
+
+# a control command consists of
+# the command (bits 5-7)
+# and the count (bits 0-4)
+# followed by additional params
+
+lz_lit = 0
+# print literal for [count] bytes
+
+lz_iter = 1
+# print one byte [count] times
+
+lz_alt = 2
+# print alternating bytes (2 params) for [count] bytes
+
+lz_zeros = 3
+# print 00 for [count] bytes
+
+# repeater control commands have a signed parameter used to determine the start point
+# wraparound is simulated
+# positive values are added to the start address of the decompressed data
+# and negative values are subtracted from the current position
+
+lz_repeat = 4
+# print [count] bytes from decompressed data
+
+lz_flip = 5
+# print [count] bytes from decompressed data in bit order 01234567
+
+lz_reverse = 6
+# print [count] bytes from decompressed data backwards
+
+lz_hi = 7
+# -used when the count exceeds 5 bits. uses a 10-bit count instead
+# -bits 2-4 now contain the control code, bits 0-1 are bits 8-9 of the count
+# -the following byte contains bits 0-7 of the count
+
+lz_end = 0xff
+# if 0xff is encountered the decompression ends
+
+# since frontpics have animation tiles lumped onto them,
+# sizes must be grabbed from base stats to know when to stop reading them
+
+max_length = 1 << 10 # can't go higher than 10 bits
+lowmax = 1 << 5 # standard 5-bit param
+
+
+class Compressed:
+
+ """
+ Compress 2bpp data.
+ """
+
+ def __init__(self, image=None, mode='horiz', size=None):
+ assert image, 'need something to compress!'
+ image = list(image)
+ self.image = image
+ self.pic = []
+ self.animtiles = []
+
+ # only transpose pic (animtiles were never transposed in decompression)
+ if size != None:
+ for byte in range((size*size)*16):
+ self.pic += image[byte]
+ for byte in range(((size*size)*16),len(image)):
+ self.animtiles += image[byte]
+ else:
+ self.pic = image
+
+ if mode == 'vert':
+ self.tiles = get_tiles(self.pic)
+ self.tiles = transpose(self.tiles)
+ self.pic = connect(self.tiles)
+
+ self.image = self.pic + self.animtiles
+
+ self.end = len(self.image)
+
+ self.byte = None
+ self.address = 0
+
+ self.stream = []
+
+ self.zeros = []
+ self.alts = []
+ self.iters = []
+ self.repeats = []
+ self.flips = []
+ self.reverses = []
+ self.literals = []
+
+ self.output = []
+
+ self.compress()
+
+
+ def compress(self):
+ """
+ Incomplete, but outputs working compressed data.
+ """
+
+ self.address = 0
+
+ # todo
+ #self.scanRepeats()
+
+ while ( self.address < self.end ):
+
+ #if (self.repeats):
+ # self.doRepeats()
+
+ #if (self.flips):
+ # self.doFlips()
+
+ #if (self.reverses):
+ # self.doReverses
+
+ if (self.checkWhitespace()):
+ self.doLiterals()
+ self.doWhitespace()
+
+ elif (self.checkIter()):
+ self.doLiterals()
+ self.doIter()
+
+ elif (self.checkAlts()):
+ self.doLiterals()
+ self.doAlts()
+
+ else: # doesn't fit any pattern -> literal
+ self.addLiteral()
+ self.next()
+
+ self.doStream()
+
+ # add any literals we've been sitting on
+ self.doLiterals()
+
+ # done
+ self.output.append(lz_end)
+
+
+ def getCurByte(self):
+ if self.address < self.end:
+ self.byte = ord(self.image[self.address])
+ else: self.byte = None
+
+ def next(self):
+ self.address += 1
+ self.getCurByte()
+
+ def addLiteral(self):
+ self.getCurByte()
+ self.literals.append(self.byte)
+ if len(self.literals) > max_length:
+ raise Exception, "literals exceeded max length and the compressor didn't catch it"
+ elif len(self.literals) == max_length:
+ self.doLiterals()
+
+ def doLiterals(self):
+ if len(self.literals) > lowmax:
+ self.output.append( (lz_hi << 5) | (lz_lit << 2) | ((len(self.literals) - 1) >> 8) )
+ self.output.append( (len(self.literals) - 1) & 0xff )
+ elif len(self.literals) > 0:
+ self.output.append( (lz_lit << 5) | (len(self.literals) - 1) )
+ for byte in self.literals:
+ self.output.append(byte)
+ self.literals = []
+
+ def doStream(self):
+ for byte in self.stream:
+ self.output.append(byte)
+ self.stream = []
+
+
+ def scanRepeats(self):
+ """
+ Works, but doesn't do flipped/reversed streams yet.
+
+ This takes up most of the compress time and only saves a few bytes
+ it might be more feasible to exclude it entirely.
+ """
+
+ self.repeats = []
+ self.flips = []
+ self.reverses = []
+
+ # make a 5-letter word list of the sequence
+ letters = 5 # how many bytes it costs to use a repeat over a literal
+ # any shorter and it's not worth the trouble
+ num_words = len(self.image) - letters
+ words = []
+ for i in range(self.address,num_words):
+ word = []
+ for j in range(letters):
+ word.append( ord(self.image[i+j]) )
+ words.append((word, i))
+
+ zeros = []
+ for zero in range(letters):
+ zeros.append( 0 )
+
+ # check for matches
+ def get_matches():
+ # TODO:
+ # append to 3 different match lists instead of yielding to one
+ #
+ #flipped = []
+ #for byte in enumerate(this[0]):
+ # flipped.append( sum(1<<(7-i) for i in range(8) if (this[0][byte])>>i&1) )
+ #reversed = this[0][::-1]
+ #
+ for whereabout, this in enumerate(words):
+ for that in range(whereabout+1,len(words)):
+ if words[that][0] == this[0]:
+ if words[that][1] - this[1] >= letters:
+ # remove zeros
+ if this[0] != zeros:
+ yield [this[0], this[1], words[that][1]]
+
+ matches = list(get_matches())
+
+ # remove more zeros
+ buffer = []
+ for match in matches:
+ # count consecutive zeros in a word
+ num_zeros = 0
+ highest = 0
+ for j in range(letters):
+ if match[0][j] == 0:
+ num_zeros += 1
+ else:
+ if highest < num_zeros: highest = num_zeros
+ num_zeros = 0
+ if highest < 4:
+ # any more than 3 zeros in a row isn't worth it
+ # (and likely to already be accounted for)
+ buffer.append(match)
+ matches = buffer
+
+ # combine overlapping matches
+ buffer = []
+ for this, match in enumerate(matches):
+ if this < len(matches) - 1: # special case for the last match
+ if matches[this+1][1] <= (match[1] + len(match[0])): # check overlap
+ if match[1] + len(match[0]) < match[2]:
+ # next match now contains this match's bytes too
+ # this only appends the last byte (assumes overlaps are +1
+ match[0].append(matches[this+1][0][-1])
+ matches[this+1] = match
+ elif match[1] + len(match[0]) == match[2]:
+ # we've run into the thing we matched
+ buffer.append(match)
+ # else we've gone past it and we can ignore it
+ else: # no more overlaps
+ buffer.append(match)
+ else: # last match, so there's nothing to check
+ buffer.append(match)
+ matches = buffer
+
+ # remove alternating sequences
+ buffer = []
+ for match in matches:
+ for i in range(6 if letters > 6 else letters):
+ if match[0][i] != match[0][i&1]:
+ buffer.append(match)
+ break
+ matches = buffer
+
+ self.repeats = matches
+
+
+ def doRepeats(self):
+ """doesn't output the right values yet"""
+
+ unusedrepeats = []
+ for repeat in self.repeats:
+ if self.address >= repeat[2]:
+
+ # how far in we are
+ length = (len(repeat[0]) - (self.address - repeat[2]))
+
+ # decide which side we're copying from
+ if (self.address - repeat[1]) <= 0x80:
+ self.doLiterals()
+ self.stream.append( (lz_repeat << 5) | length - 1 )
+
+ # wrong?
+ self.stream.append( (((self.address - repeat[1])^0xff)+1)&0xff )
+
+ else:
+ self.doLiterals()
+ self.stream.append( (lz_repeat << 5) | length - 1 )
+
+ # wrong?
+ self.stream.append(repeat[1]>>8)
+ self.stream.append(repeat[1]&0xff)
+
+ #print hex(self.address) + ': ' + hex(len(self.output)) + ' ' + hex(length)
+ self.address += length
+
+ else: unusedrepeats.append(repeat)
+
+ self.repeats = unusedrepeats
+
+
+ def checkWhitespace(self):
+ self.zeros = []
+ self.getCurByte()
+ original_address = self.address
+
+ if ( self.byte == 0 ):
+ while ( self.byte == 0 ) & ( len(self.zeros) <= max_length ):
+ self.zeros.append(self.byte)
+ self.next()
+ if len(self.zeros) > 1:
+ return True
+ self.address = original_address
+ return False
+
+ def doWhitespace(self):
+ if (len(self.zeros) + 1) >= lowmax:
+ self.stream.append( (lz_hi << 5) | (lz_zeros << 2) | ((len(self.zeros) - 1) >> 8) )
+ self.stream.append( (len(self.zeros) - 1) & 0xff )
+ elif len(self.zeros) > 1:
+ self.stream.append( lz_zeros << 5 | (len(self.zeros) - 1) )
+ else:
+ raise Exception, "checkWhitespace() should prevent this from happening"
+
+
+ def checkAlts(self):
+ self.alts = []
+ self.getCurByte()
+ original_address = self.address
+ num_alts = 0
+
+ # make sure we don't check for alts at the end of the file
+ if self.address+3 >= self.end: return False
+
+ self.alts.append(self.byte)
+ self.alts.append(ord(self.image[self.address+1]))
+
+ # are we onto smething?
+ if ( ord(self.image[self.address+2]) == self.alts[0] ):
+ cur_alt = 0
+ while (ord(self.image[(self.address)+1]) == self.alts[num_alts&1]) & (num_alts <= max_length):
+ num_alts += 1
+ self.next()
+ # include the last alternated byte
+ num_alts += 1
+ self.address = original_address
+ if num_alts > lowmax:
+ return True
+ elif num_alts > 2:
+ return True
+ return False
+
+ def doAlts(self):
+ original_address = self.address
+ self.getCurByte()
+
+ #self.alts = []
+ #num_alts = 0
+
+ #self.alts.append(self.byte)
+ #self.alts.append(ord(self.image[self.address+1]))
+
+ #i = 0
+ #while (ord(self.image[self.address+1]) == self.alts[i^1]) & (num_alts <= max_length):
+ # num_alts += 1
+ # i ^=1
+ # self.next()
+ ## include the last alternated byte
+ #num_alts += 1
+
+ num_alts = len(self.iters) + 1
+
+ if num_alts > lowmax:
+ self.stream.append( (lz_hi << 5) | (lz_alt << 2) | ((num_alts - 1) >> 8) )
+ self.stream.append( num_alts & 0xff )
+ self.stream.append( self.alts[0] )
+ self.stream.append( self.alts[1] )
+ elif num_alts > 2:
+ self.stream.append( (lz_alt << 5) | (num_alts - 1) )
+ self.stream.append( self.alts[0] )
+ self.stream.append( self.alts[1] )
+ else:
+ raise Exception, "checkAlts() should prevent this from happening"
+
+ self.address = original_address
+ self.address += num_alts
+
+
+ def checkIter(self):
+ self.iters = []
+ self.getCurByte()
+ iter = self.byte
+ original_address = self.address
+ while (self.byte == iter) & (len(self.iters) < max_length):
+ self.iters.append(self.byte)
+ self.next()
+ self.address = original_address
+ if len(self.iters) > 3:
+ # 3 or fewer isn't worth the trouble and actually longer
+ # if part of a larger literal set
+ return True
+
+ return False
+
+ def doIter(self):
+ self.getCurByte()
+ iter = self.byte
+ original_address = self.address
+
+ self.iters = []
+ while (self.byte == iter) & (len(self.iters) < max_length):
+ self.iters.append(self.byte)
+ self.next()
+
+ if (len(self.iters) - 1) >= lowmax:
+ self.stream.append( (lz_hi << 5) | (lz_iter << 2) | ((len(self.iters)-1) >> 8) )
+ self.stream.append( (len(self.iters) - 1) & 0xff )
+ self.stream.append( iter )
+ elif len(self.iters) > 3:
+ # 3 or fewer isn't worth the trouble and actually longer
+ # if part of a larger literal set
+ self.stream.append( (lz_iter << 5) | (len(self.iters) - 1) )
+ self.stream.append( iter )
+ else:
+ self.address = original_address
+ raise Exception, "checkIter() should prevent this from happening"
+
+
+
+
+
+class Decompressed:
+ """
+ Parse compressed 2bpp data.
+
+ parameters:
+ [compressed 2bpp data]
+ [tile arrangement] default: 'vert'
+ [size of pic] default: None
+ [start] (optional)
+
+ splits output into pic [size] and animation tiles if applicable
+ data can be fed in from rom if [start] is specified
+ """
+
+ def __init__(self, lz=None, mode=None, size=None, start=0):
+ # todo: play nice with Compressed
+
+ assert lz, 'need something to compress!'
+ self.lz = lz
+
+ self.byte = None
+ self.address = 0
+ self.start = start
+
+ self.output = []
+
+ self.decompress()
+
+ debug = False
+ # print tuple containing start and end address
+ if debug: print '(' + hex(self.start) + ', ' + hex(self.start + self.address+1) + '),'
+
+ # only transpose pic
+ self.pic = []
+ self.animtiles = []
+
+ if size != None:
+ self.tiles = get_tiles(self.output)
+ self.pic = connect(self.tiles[:(size*size)])
+ self.animtiles = connect(self.tiles[(size*size):])
+ else: self.pic = self.output
+
+ if mode == 'vert':
+ self.tiles = get_tiles(self.pic)
+ self.tiles = transpose(self.tiles)
+ self.pic = connect(self.tiles)
+
+ self.output = self.pic + self.animtiles
+
+
+ def decompress(self):
+ """
+ Replica of crystal's decompression.
+ """
+
+ self.output = []
+
+ while True:
+ self.getCurByte()
+
+ if (self.byte == lz_end):
+ break
+
+ self.cmd = (self.byte & 0b11100000) >> 5
+
+ if self.cmd == lz_hi: # 10-bit param
+ self.cmd = (self.byte & 0b00011100) >> 2
+ self.length = (self.byte & 0b00000011) << 8
+ self.next()
+ self.length += self.byte + 1
+ else: # 5-bit param
+ self.length = (self.byte & 0b00011111) + 1
+
+ # literals
+ if self.cmd == lz_lit:
+ self.doLiteral()
+ elif self.cmd == lz_iter:
+ self.doIter()
+ elif self.cmd == lz_alt:
+ self.doAlt()
+ elif self.cmd == lz_zeros:
+ self.doZeros()
+
+ else: # repeaters
+ self.next()
+ if self.byte > 0x7f: # negative
+ self.displacement = self.byte & 0x7f
+ self.displacement = len(self.output) - self.displacement - 1
+ else: # positive
+ self.displacement = self.byte * 0x100
+ self.next()
+ self.displacement += self.byte
+
+ if self.cmd == lz_flip:
+ self.doFlip()
+ elif self.cmd == lz_reverse:
+ self.doReverse()
+ else: # lz_repeat
+ self.doRepeat()
+
+ self.address += 1
+ #self.next() # somewhat of a hack
+
+
+ def getCurByte(self):
+ self.byte = ord(self.lz[self.start+self.address])
+
+ def next(self):
+ self.address += 1
+ self.getCurByte()
+
+ def doLiteral(self):
+ """
+ Copy 2bpp data directly.
+ """
+ for byte in range(self.length):
+ self.next()
+ self.output.append(self.byte)
+
+ def doIter(self):
+ """
+ Write one byte repeatedly.
+ """
+ self.next()
+ for byte in range(self.length):
+ self.output.append(self.byte)
+
+ def doAlt(self):
+ """
+ Write alternating bytes.
+ """
+ self.alts = []
+ self.next()
+ self.alts.append(self.byte)
+ self.next()
+ self.alts.append(self.byte)
+
+ for byte in range(self.length):
+ self.output.append(self.alts[byte&1])
+
+ def doZeros(self):
+ """
+ Write zeros.
+ """
+ for byte in range(self.length):
+ self.output.append(0x00)
+
+ def doFlip(self):
+ """
+ Repeat flipped bytes from 2bpp output.
+
+ eg 11100100 -> 00100111
+ quat 3 2 1 0 -> 0 2 1 3
+ """
+ for byte in range(self.length):
+ flipped = sum(1<<(7-i) for i in range(8) if self.output[self.displacement+byte]>>i&1)
+ self.output.append(flipped)
+
+ def doReverse(self):
+ """
+ Repeat reversed bytes from 2bpp output.
+ """
+ for byte in range(self.length):
+ self.output.append(self.output[self.displacement-byte])
+
+ def doRepeat(self):
+ """
+ Repeat bytes from 2bpp output.
+ """
+ for byte in range(self.length):
+ self.output.append(self.output[self.displacement+byte])
+
+
+
+sizes = [
+ 5, 6, 7, 5, 6, 7, 5, 6, 7, 5, 5, 7, 5, 5, 7, 5,
+ 6, 7, 5, 6, 5, 7, 5, 7, 5, 7, 5, 6, 5, 6, 7, 5,
+ 6, 7, 5, 6, 6, 7, 5, 6, 5, 7, 5, 6, 7, 5, 7, 5,
+ 7, 5, 7, 5, 7, 5, 7, 5, 7, 5, 7, 5, 6, 7, 5, 6,
+ 7, 5, 7, 7, 5, 6, 7, 5, 6, 5, 6, 6, 6, 7, 5, 7,
+ 5, 6, 6, 5, 7, 6, 7, 5, 7, 5, 7, 7, 6, 6, 7, 6,
+ 7, 5, 7, 5, 5, 7, 7, 5, 6, 7, 6, 7, 6, 7, 7, 7,
+ 6, 6, 7, 5, 6, 6, 7, 6, 6, 6, 7, 6, 6, 6, 7, 7,
+ 6, 7, 7, 5, 5, 6, 6, 6, 6, 5, 6, 5, 6, 7, 7, 7,
+ 7, 7, 5, 6, 7, 7, 5, 5, 6, 7, 5, 6, 7, 5, 6, 7,
+ 6, 6, 5, 7, 6, 6, 5, 7, 7, 6, 6, 5, 5, 5, 5, 7,
+ 5, 6, 5, 6, 7, 7, 5, 7, 6, 7, 5, 6, 7, 5, 5, 6,
+ 6, 5, 6, 6, 6, 6, 7, 6, 5, 6, 7, 5, 7, 6, 6, 7,
+ 6, 6, 5, 7, 5, 6, 6, 5, 7, 5, 6, 5, 6, 6, 5, 6,
+ 6, 7, 7, 6, 7, 7, 5, 7, 6, 7, 7, 5, 7, 5, 6, 6,
+ 6, 7, 7, 7, 7, 5, 6, 7, 7, 7, 5,
+]
+
+def make_sizes():
+ """
+ Front pics have specified sizes.
+ """
+ top = 251
+ base_stats = 0x51424
+ # print monster sizes
+ address = base_stats + 0x11
+
+ output = ''
+
+ for id in range(top):
+ size = (ord(rom[address])) & 0x0f
+ if id % 16 == 0: output += '\n\t'
+ output += str(size) + ', '
+ address += 0x20
+
+ print output
+
+
+
+fxs = 0xcfcf6
+num_fx = 40
+
+def decompress_fx_by_id(id):
+ address = fxs + id*4 # len_fxptr
+ # get size
+ num_tiles = ord(rom[address]) # # tiles
+ # get pointer
+ bank = ord(rom[address+1])
+ address = (ord(rom[address+3]) << 8) + ord(rom[address+2])
+ address = (bank * 0x4000) + (address & 0x3fff)
+ # decompress
+ fx = Decompressed(rom, 'horiz', num_tiles, address)
+ return fx
+
+def decompress_fx():
+ for id in range(num_fx):
+ fx = decompress_fx_by_id(id)
+ filename = '../gfx/fx/' + str(id).zfill(3) + '.2bpp' # ../gfx/fx/039.2bpp
+ to_file(filename, fx.pic)
+
+
+num_pics = 2
+front = 0
+back = 1
+
+monsters = 0x120000
+num_monsters = 251
+
+unowns = 0x124000
+num_unowns = 26
+unown_dex = 201
+
+def decompress_monster_by_id(id=0, type=front):
+ # no unowns here
+ if id + 1 == unown_dex: return None
+ # get size
+ if type == front:
+ size = sizes[id]
+ else: size = None
+ # get pointer
+ address = monsters + (id*2 + type)*3 # bank, address
+ bank = ord(rom[address]) + 0x36 # crystal
+ address = (ord(rom[address+2]) << 8) + ord(rom[address+1])
+ address = (bank * 0x4000) + (address & 0x3fff)
+ # decompress
+ monster = Decompressed(rom, 'vert', size, address)
+ return monster
+
+def decompress_monsters(type=front):
+ for id in range(num_monsters):
+ # decompress
+ monster = decompress_monster_by_id(id, type)
+ if monster != None: # no unowns here
+ if not type: # front
+ filename = 'front.2bpp'
+ folder = '../gfx/pics/' + str(id+1).zfill(3) + '/'
+ to_file(folder+filename, monster.pic)
+ filename = 'tiles.2bpp'
+ folder = '../gfx/pics/' + str(id+1).zfill(3) + '/'
+ to_file(folder+filename, monster.animtiles)
+ else: # back
+ filename = 'back.2bpp'
+ folder = '../gfx/pics/' + str(id+1).zfill(3) + '/'
+ to_file(folder+filename, monster.pic)
+
+
+def decompress_unown_by_id(letter, type=front):
+ # get size
+ if type == front:
+ size = sizes[unown_dex-1]
+ else: size = None
+ # get pointer
+ address = unowns + (letter*2 + type)*3 # bank, address
+ bank = ord(rom[address]) + 0x36 # crystal
+ address = (ord(rom[address+2]) << 8) + ord(rom[address+1])
+ address = (bank * 0x4000) + (address & 0x3fff)
+ # decompress
+ unown = Decompressed(rom, 'vert', size, address)
+ return unown
+
+def decompress_unowns(type=front):
+ for letter in range(num_unowns):
+ # decompress
+ unown = decompress_unown_by_id(letter, type)
+
+ if not type: # front
+ filename = 'front.2bpp'
+ folder = '../gfx/pics/' + str(unown_dex).zfill(3) + chr(ord('a') + letter) + '/'
+ to_file(folder+filename, unown.pic)
+ filename = 'tiles.2bpp'
+ folder = '../gfx/anim/'
+ to_file(folder+filename, unown.animtiles)
+ else: # back
+ filename = 'back.2bpp'
+ folder = '../gfx/pics/' + str(unown_dex).zfill(3) + chr(ord('a') + letter) + '/'
+ to_file(folder+filename, unown.pic)
+
+
+trainers = 0x128000
+num_trainers = 67
+
+def decompress_trainer_by_id(id):
+ # get pointer
+ address = trainers + id*3 # bank, address
+ bank = ord(rom[address]) + 0x36 # crystal
+ address = (ord(rom[address+2]) << 8) + ord(rom[address+1])
+ address = (bank * 0x4000) + (address & 0x3fff)
+ # decompress
+ trainer = Decompressed(rom, 'vert', None, address)
+ return trainer
+
+def decompress_trainers():
+ for id in range(num_trainers):
+ # decompress
+ trainer = decompress_trainer_by_id(id)
+ filename = '../gfx/trainers/' + str(id).zfill(3) + '.2bpp' # ../gfx/trainers/066.2bpp
+ to_file(filename, trainer.pic)
+
+
+# in order of use (sans repeats)
+intro_gfx = [
+ ('logo', 0x109407),
+ ('001', 0xE641D), # tilemap
+ ('unowns', 0xE5F5D),
+ ('pulse', 0xE634D),
+ ('002', 0xE63DD), # tilemap
+ ('003', 0xE5ECD), # tilemap
+ ('background', 0xE5C7D),
+ ('004', 0xE5E6D), # tilemap
+ ('005', 0xE647D), # tilemap
+ ('006', 0xE642D), # tilemap
+ ('pichu_wooper', 0xE592D),
+ ('suicune_run', 0xE555D),
+ ('007', 0xE655D), # tilemap
+ ('008', 0xE649D), # tilemap
+ ('009', 0xE76AD), # tilemap
+ ('suicune_jump', 0xE6DED),
+ ('unown_back', 0xE785D),
+ ('010', 0xE764D), # tilemap
+ ('011', 0xE6D0D), # tilemap
+ ('suicune_close', 0xE681D),
+ ('012', 0xE6C3D), # tilemap
+ ('013', 0xE778D), # tilemap
+ ('suicune_back', 0xE72AD),
+ ('014', 0xE76BD), # tilemap
+ ('015', 0xE676D), # tilemap
+ ('crystal_unowns', 0xE662D),
+ ('017', 0xE672D), # tilemap
+]
+
+def decompress_intro():
+ for name, address in intro_gfx:
+ filename = '../gfx/intro/' + name + '.2bpp'
+ gfx = Decompressed( rom, 'horiz', None, address )
+ to_file(filename, gfx.output)
+
+
+title_gfx = [
+ ('suicune', 0x10EF46),
+ ('logo', 0x10F326),
+ ('crystal', 0x10FCEE),
+]
+
+def decompress_title():
+ for name, address in title_gfx:
+ filename = '../gfx/title/' + name + '.2bpp'
+ gfx = Decompressed( rom, 'horiz', None, address )
+ to_file(filename, gfx.output)
+
+def decompress_tilesets():
+ tileset_headers = 0x4d596
+ len_tileset = 15
+ num_tilesets = 0x25
+ for tileset in range(num_tilesets):
+ ptr = tileset*len_tileset + tileset_headers
+ address = (ord(rom[ptr])*0x4000) + (((ord(rom[ptr+1]))+ord(rom[ptr+2])*0x100)&0x3fff)
+ tiles = Decompressed( rom, 'horiz', None, address )
+ filename = '../gfx/tilesets/'+str(tileset).zfill(2)+'.2bpp'
+ to_file( filename, tiles.output )
+ #print '(' + hex(address) + ', '+ hex(address+tiles.address+1) + '),'
+
+misc = [
+ ('player', 0x2BA1A, 'vert'),
+ ('dude', 0x2BBAA, 'vert'),
+ ('town_map', 0xF8BA0, 'horiz'),
+ ('pokegear', 0x1DE2E4, 'horiz'),
+ ('pokegear_sprites', 0x914DD, 'horiz'),
+]
+def decompress_misc():
+ for name, address, mode in misc:
+ filename = '../gfx/misc/' + name + '.2bpp'
+ gfx = Decompressed( rom, mode, None, address )
+ to_file(filename, gfx.output)
+
+def decompress_all(debug=False):
+ """
+ Decompress all known compressed data in baserom.
+ """
+
+ if debug: print 'fronts'
+ decompress_monsters(front)
+ if debug: print 'backs'
+ decompress_monsters(back)
+ if debug: print 'unown fronts'
+ decompress_unowns(front)
+ if debug: print 'unown backs'
+ decompress_unowns(back)
+
+ if debug: print 'trainers'
+ decompress_trainers()
+
+ if debug: print 'fx'
+ decompress_fx()
+
+ if debug: print 'intro'
+ decompress_intro()
+
+ if debug: print 'title'
+ decompress_title()
+
+ if debug: print 'tilesets'
+ decompress_tilesets()
+
+ if debug: print 'misc'
+ decompress_misc()
+
+ return
+
+
+def decompress_from_address(address, mode='horiz', filename='de.2bpp', size=None):
+ """
+ Write decompressed data from an address to a 2bpp file.
+ """
+ image = Decompressed(rom, mode, size, address)
+ to_file(filename, image.pic)
+
+
+def decompress_file(filein, fileout, mode='horiz', size=None):
+ f = open(filein, 'rb')
+ image = f.read()
+ f.close()
+
+ de = Decompressed(image, mode, size)
+
+ to_file(fileout, de.pic)
+
+
+def compress_file(filein, fileout, mode='horiz'):
+ f = open(filein, 'rb')
+ image = f.read()
+ f.close()
+
+ lz = Compressed(image, mode)
+
+ to_file(fileout, lz.output)
+
+
+
+
+def compress_monster_frontpic(id, fileout):
+ mode = 'vert'
+
+ fpic = '../gfx/pics/' + str(id).zfill(3) + '/front.2bpp'
+ fanim = '../gfx/pics/' + str(id).zfill(3) + '/tiles.2bpp'
+
+ pic = open(fpic, 'rb').read()
+ anim = open(fanim, 'rb').read()
+ image = pic + anim
+
+ lz = Compressed(image, mode, sizes[id-1])
+
+ out = '../gfx/pics/' + str(id).zfill(3) + '/front.lz'
+
+ to_file(out, lz.output)
+
+
+
+def get_uncompressed_gfx(start, num_tiles, filename):
+ """
+ Grab tiles directly from rom and write to file.
+ """
+ bytes_per_tile = 0x10
+ length = num_tiles*bytes_per_tile
+ end = start + length
+ rom = load_rom()
+ image = []
+ for address in range(start,end):
+ image.append(ord(rom[address]))
+ to_file(filename, image)
+
+
+
+def hex_to_rgb(word):
+ red = word & 0b11111
+ word >>= 5
+ green = word & 0b11111
+ word >>= 5
+ blue = word & 0b11111
+ return (red, green, blue)
+
+def grab_palettes(address, length=0x80):
+ output = ''
+ for word in range(length/2):
+ color = ord(rom[address+1])*0x100 + ord(rom[address])
+ address += 2
+ color = hex_to_rgb(color)
+ red = str(color[0]).zfill(2)
+ green = str(color[1]).zfill(2)
+ blue = str(color[2]).zfill(2)
+ output += '\tRGB '+red+', '+green+', '+blue
+ output += '\n'
+ return output
+
+
+
+
+
+
+
+def dump_monster_pals():
+ rom = load_rom()
+
+ pals = 0xa8d6
+ pal_length = 0x4
+ for mon in range(251):
+
+ name = pokemon_constants[mon+1].title().replace('_','')
+ num = str(mon+1).zfill(3)
+ dir = 'gfx/pics/'+num+'/'
+
+ address = pals + mon*pal_length*2
+
+
+ pal_data = []
+ for byte in range(pal_length):
+ pal_data.append(ord(rom[address]))
+ address += 1
+
+ filename = 'normal.pal'
+ to_file('../'+dir+filename, pal_data)
+
+ spacing = ' ' * (15 - len(name))
+ #print name+'Palette:'+spacing+' INCBIN "'+dir+filename+'"'
+
+
+ pal_data = []
+ for byte in range(pal_length):
+ pal_data.append(ord(rom[address]))
+ address += 1
+
+ filename = 'shiny.pal'
+ to_file('../'+dir+filename, pal_data)
+
+ spacing = ' ' * (10 - len(name))
+ #print name+'ShinyPalette:'+spacing+' INCBIN "'+dir+filename+'"'
+
+
+def dump_trainer_pals():
+ rom = load_rom()
+
+ pals = 0xb0d2
+ pal_length = 0x4
+ for trainer in range(67):
+
+ name = trainer_group_names[trainer+1]['constant'].title().replace('_','')
+ num = str(trainer).zfill(3)
+ dir = 'gfx/trainers/'
+
+ address = pals + trainer*pal_length
+
+ pal_data = []
+ for byte in range(pal_length):
+ pal_data.append(ord(rom[address]))
+ address += 1
+
+ filename = num+'.pal'
+ to_file('../'+dir+filename, pal_data)
+
+ spacing = ' ' * (12 - len(name))
+ print name+'Palette:'+spacing+' INCBIN"'+dir+filename+'"'
+
+
+
+def flatten(planar):
+ """
+ Flatten planar 2bpp image data into a quaternary pixel map.
+ """
+ strips = []
+ for pair in range(len(planar)/2):
+ bottom = ord(planar[(pair*2) ])
+ top = ord(planar[(pair*2)+1])
+ strip = []
+ for i in range(7,-1,-1):
+ color = ((bottom >> i) & 1) + (((top >> i-1) if i > 0 else (top << 1-i)) & 2)
+ strip.append(color)
+ strips += strip
+ return strips
+
+
+def to_lines(image, width):
+ """
+ Convert a tiled quaternary pixel map to lines of quaternary pixels.
+ """
+
+ tile = 8 * 8
+
+ # so we know how many strips of 8px we're putting into a line
+ num_columns = width / 8
+ # number of lines
+ height = len(image) / width
+
+ lines = []
+ for cur_line in range(height):
+ tile_row = int(cur_line / 8)
+ line = []
+ for column in range(num_columns):
+ anchor = num_columns*tile_row*tile + column*tile + (cur_line%8)*8
+ line += image[anchor:anchor+8]
+ lines.append(line)
+ return lines
+
+def dmg2rgb(word):
+ red = word & 0b11111
+ word >>= 5
+ green = word & 0b11111
+ word >>= 5
+ blue = word & 0b11111
+ alpha = 255
+ return ((red<<3)+0b100, (green<<3)+0b100, (blue<<3)+0b100, alpha)
+
+def rgb_to_dmg(color):
+ word = (color['r'] / 8)
+ word += (color['g'] / 8) << 5
+ word += (color['b'] / 8) << 10
+ return word
+
+
+def png_pal(filename):
+ palette = []
+ with open(filename, 'rb') as pal_data:
+ words = pal_data.read()
+ dmg_pals = []
+ for word in range(len(words)/2):
+ dmg_pals.append(ord(words[word*2]) + ord(words[word*2+1])*0x100)
+ white = (255,255,255,255)
+ black = (000,000,000,255)
+ for word in dmg_pals: palette += [dmg2rgb(word)]
+ if white not in dmg_pals and len(palette) < 4: palette = [white] + palette
+ if black not in dmg_pals and len(palette) < 4: palette += [black]
+ return palette
+
+
+def to_png(filein, fileout=None, pal_file=None, height=None, width=None):
+ """
+ Take a planar 2bpp graphics file and converts it to png.
+ """
+
+ if fileout == None: fileout = '.'.join(filein.split('.')[:-1]) + '.png'
+
+ image = open(filein, 'rb').read()
+
+ num_pixels = len(image) * 4
+
+ if num_pixels == 0: return 'empty image!'
+
+
+ # unless the pic is square, at least one dimension should be given
+
+ if width == None and height == None:
+ width = int(sqrt(num_pixels))
+ height = width
+
+ elif height == None:
+ height = num_pixels / width
+
+ elif width == None:
+ width = num_pixels / height
+
+
+ # but try to see if it can be made rectangular
+
+ if width * height != num_pixels:
+
+ # look for possible combos of width/height that would form a rectangle
+ matches = []
+
+ # this is pretty inefficient, and there is probably a simpler way
+ for width in range(8,256+1,8): # we only want dimensions that fit in tiles
+ height = num_pixels / width
+ if height % 8 == 0:
+ matches.append((width, height))
+
+ # go for the most square image
+ width, height = sorted(matches, key=lambda (x,y): x+y)[0] # favors height
+
+
+ # if it can't, the only option is a width of 1 tile
+
+ if width * height != num_pixels:
+ width = 8
+ height = num_pixels / width
+
+
+ # if this still isn't rectangular, then the image isn't made of tiles
+
+ # for now we'll just spit out a warning
+ if width * height != num_pixels:
+ print 'Warning! ' + fileout + ' is ' + width + 'x' + height + '(' + width*height + ' pixels),\n' +\
+ 'but ' + filein + ' is ' + num_pixels + ' pixels!'
+
+
+ # map it out
+
+ lines = to_lines(flatten(image), width)
+
+ if pal_file == None:
+ if os.path.exists(os.path.splitext(fileout)[0]+'.pal'):
+ pal_file = os.path.splitext(fileout)[0]+'.pal'
+
+ if pal_file == None:
+ palette = None
+ greyscale = True
+ bitdepth = 2
+ inverse = { 0:3, 1:2, 2:1, 3:0 }
+ map = [[inverse[pixel] for pixel in line] for line in lines]
+
+ else: # gbc color
+ palette = png_pal(pal_file)
+ greyscale = False
+ bitdepth = 8
+ map = [[pixel for pixel in line] for line in lines]
+
+
+ w = png.Writer(width, height, palette=palette, compression = 9, greyscale = greyscale, bitdepth = bitdepth)
+ with open(fileout, 'wb') as file:
+ w.write(file, map)
+
+
+
+
+def to_2bpp(filein, fileout=None, palout=None):
+ """
+ Take a png and converts it to planar 2bpp.
+ """
+
+ if fileout == None: fileout = '.'.join(filein.split('.')[:-1]) + '.2bpp'
+
+ with open(filein, 'rb') as file:
+
+ r = png.Reader(file)
+ info = r.asRGBA8()
+
+ width = info[0]
+ height = info[1]
+
+ rgba = list(info[2])
+ greyscale = info[3]['greyscale']
+
+
+ padding = { 'left': 0,
+ 'right': 0,
+ 'top': 0,
+ 'bottom': 0, }
+ #if width % 8 != 0:
+ # padding['left'] = int(ceil((width / 8 + 8 - width) / 2))
+ # padding['right'] = int(floor((width / 8 + 8 - width) / 2))
+ #if height % 8 != 0:
+ # padding['top'] = int(ceil((height / 8 + 8 - height) / 2))
+ # padding['bottom'] = int(floor((height / 8 + 8 - height) / 2))
+
+
+ # turn the flat values into something more workable
+
+ pixel_length = 4 # rgba
+ image = []
+
+ # while we're at it, let's size up the palette
+
+ palette = []
+
+ for line in rgba:
+ newline = []
+ for pixel in range(len(line)/pixel_length):
+ i = pixel * pixel_length
+ color = { 'r': line[i ],
+ 'g': line[i+1],
+ 'b': line[i+2],
+ 'a': line[i+3], }
+ newline += [color]
+ if color not in palette: palette += [color]
+ image.append(newline)
+
+ # pad out any small palettes
+ hues = {
+ 'white': { 'r': 0xff, 'g': 0xff, 'b': 0xff, 'a': 0xff },
+ '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 },
+ }
+ while len(palette) < 4:
+ for hue in hues.values():
+ if not any(color is hue for color in palette):
+ palette += [hue]
+ if len(palette) >= 4: break
+
+ assert len(palette) <= 4, 'Palette should be 4 colors, is really ' + str(len(palette))
+
+ # sort by luminance
+ def luminance(color):
+ # this is actually in reverse, thanks to dmg/cgb palette ordering
+ rough = { 'r': 4.7,
+ 'g': 1.4,
+ 'b': 13.8, }
+ return sum(color[key] * -rough[key] for key in rough.keys())
+ palette = sorted(palette, key=luminance)
+
+ # spit out a new .pal file
+ # disable this if it causes problems with paletteless images
+ if palout == None:
+ if os.path.exists(os.path.splitext(fileout)[0]+'.pal'):
+ palout = os.path.splitext(fileout)[0]+'.pal'
+ if palout != None:
+ output = []
+ for color in palette:
+ word = rgb_to_dmg(color)
+ output += [word & 0xff]
+ output += [word >> 8]
+ to_file(palout, output)
+
+ # create a new map of quaternary color ids
+ map = []
+ if padding['top']: map += [0] * (width + padding['left'] + padding['right']) * padding['top']
+ for line in image:
+ if padding['left']: map += [0] * padding['left']
+ for color in line:
+ map.append(palette.index(color))
+ if padding['right']: map += [0] * padding['right']
+ if padding['bottom']: map += [0] * (width + padding['left'] + padding['right']) * padding['bottom']
+
+ # split it into strips of 8, and make them planar
+ num_columns = width / 8
+ num_rows = height / 8
+ tile = 8 * 8
+ image = []
+ for row in range(num_rows):
+ for column in range(num_columns):
+ for strip in range(tile / 8):
+ anchor = row*num_columns*tile + column*tile/8 + strip*width
+ line = map[anchor:anchor+8]
+ bottom = 0
+ top = 0
+ for bit, quad in enumerate(line):
+ bottom += (quad & 1) << (7-bit)
+ top += ((quad & 2) >> 1) << (7-bit)
+ image.append(bottom)
+ image.append(top)
+
+ to_file(fileout, image)
+
+
+def png_to_lz(filein):
+
+ name = os.path.splitext(filein)[0]
+
+ to_2bpp(filein)
+ image = open(name+'.2bpp', 'rb').read()
+ to_file(name+'.lz', Compressed(image).output)
+
+
+
+
+def mass_to_png(debug=False):
+ # greyscale
+ for root, dirs, files in os.walk('../gfx/'):
+ for name in files:
+ if debug: print os.path.splitext(name), os.path.join(root, name)
+ if os.path.splitext(name)[1] == '.2bpp':
+ to_png(os.path.join(root, name))
+
+def mass_to_colored_png(debug=False):
+ # greyscale, unless a palette is detected
+ for root, dirs, files in os.walk('../gfx/'):
+ if 'pics' not in root and 'trainers' not in root:
+ for name in files:
+ if debug: print os.path.splitext(name), os.path.join(root, name)
+ if os.path.splitext(name)[1] == '.2bpp':
+ to_png(os.path.join(root, name))
+ os.utime(os.path.join(root, name), None)
+
+ # only monster and trainer pics for now
+ for root, dirs, files in os.walk('../gfx/pics/'):
+ for name in files:
+ if debug: print os.path.splitext(name), os.path.join(root, name)
+ if os.path.splitext(name)[1] == '.2bpp':
+ if 'normal.pal' in files:
+ to_png(os.path.join(root, name), None, os.path.join(root, 'normal.pal'))
+ else:
+ to_png(os.path.join(root, name))
+ os.utime(os.path.join(root, name), None)
+
+ for root, dirs, files in os.walk('../gfx/trainers/'):
+ for name in files:
+ if debug: print os.path.splitext(name), os.path.join(root, name)
+ if os.path.splitext(name)[1] == '.2bpp':
+ to_png(os.path.join(root, name))
+ os.utime(os.path.join(root, name), None)
+
+
+def mass_decompress(debug=False):
+ for root, dirs, files in os.walk('../gfx/'):
+ for name in files:
+ if 'lz' in name:
+ if '/pics' in root:
+ if 'front' in name:
+ id = root.split('pics/')[1][:3]
+ if id != 'egg':
+ with open(os.path.join(root, name), 'rb') as lz: de = Decompressed(lz.read(), 'vert', sizes[int(id)-1])
+ else:
+ with open(os.path.join(root, name), 'rb') as lz: de = Decompressed(lz.read(), 'vert', 4)
+ to_file(os.path.join(root, 'front.2bpp'), de.pic)
+ to_file(os.path.join(root, 'tiles.2bpp'), de.animtiles)
+ elif 'back' in name:
+ with open(os.path.join(root, name), 'rb') as lz: de = Decompressed(lz.read(), 'vert')
+ to_file(os.path.join(root, 'back.2bpp'), de.output)
+ elif '/trainers' in root or '/fx' in root:
+ with open(os.path.join(root, name), 'rb') as lz: de = Decompressed(lz.read(), 'vert')
+ to_file(os.path.join(root, os.path.splitext(name)[0]+'.2bpp'), de.output)
+ else:
+ with open(os.path.join(root, name), 'rb') as lz: de = Decompressed(lz.read())
+ to_file(os.path.join(root, os.path.splitext(name)[0]+'.2bpp'), de.output)
+ os.utime(os.path.join(root, name), None)
+
+def append_terminator_to_lzs(directory):
+ # fix lzs that were extracted with a missing terminator
+ for root, dirs, files in os.walk(directory):
+ for file in files:
+ if '.lz' in file:
+ data = open(root+file,'rb').read()
+ if data[-1] != chr(0xff):
+ data += chr(0xff)
+ new = open(root+file,'wb')
+ new.write(data)
+ new.close()
+
+def lz_to_png_by_file(filename):
+ """
+ Convert a lz file to png. Dump a 2bpp file too.
+ """
+ assert filename[-3:] == ".lz"
+ lz_data = open(filename, "rb").read()
+ bpp = Decompressed(lz_data).output
+ bpp_filename = filename.replace(".lz", ".2bpp")
+ to_file(bpp_filename, bpp)
+ to_png(bpp_filename)
+
+def dump_tileset_pngs():
+ """
+ Convert .lz format tilesets into .png format tilesets.
+
+ Also, leaves a bunch of wonderful .2bpp files everywhere for your amusement.
+ """
+ for tileset_id in range(37):
+ tileset_filename = "../gfx/tilesets/" + str(tileset_id).zfill(2) + ".lz"
+ lz_to_png_by_file(tileset_filename)
+
+def decompress_frontpic(lz_file):
+ """
+ Convert the pic portion of front.lz to front.2bpp
+ """
+ lz = open(lz_file, 'rb').read()
+ to_file(Decompressed(lz).pic, os.path.splitext(filein)[0] + '.2bpp')
+
+def decompress_frontpic_anim(lz_file):
+ """
+ Convert the animation tile portion of front.lz to tiles.2bpp
+ """
+ lz = open(lz_file, 'rb').read()
+ to_file(Decompressed(lz).animtiles, 'tiles.2bpp')
+
+def expand_pic_palettes():
+ """
+ Add white and black to palette files with fewer than 4 colors.
+
+ Pokemon Crystal only defines two colors for a pic palette to
+ save space, filling in black/white at runtime.
+ Instead of managing palette files of varying length, black
+ and white are added to pic palettes and excluded from incbins.
+ """
+ for root, dirs, files in os.walk('../gfx/'):
+ if 'gfx/pics' in root or 'gfx/trainers' in root:
+ for name in files:
+ if os.path.splitext(name)[1] == '.pal':
+ filename = os.path.join(root, name)
+ palette = bytearray(open(filename, 'rb').read())
+ w = bytearray([0xff, 0x7f])
+ b = bytearray([0x00, 0x00])
+ if len(palette) == 4:
+ with open(filename, 'wb') as out:
+ out.write(w + palette + b)
+
+if __name__ == "__main__":
+ debug = False
+
+ argv = [None] * 5
+ for i, arg in enumerate(sys.argv):
+ argv[i] = arg
+
+ if argv[1] == 'dump-pngs':
+ mass_to_colored_png()
+
+ elif argv[1] == 'mass-decompress':
+ mass_decompress()
+
+ elif argv[1] == 'front-to-2bpp':
+ decompress_frontpic(argv[2])
+
+ elif argv[1] == 'anim-from-front':
+ decompress_frontpic_anim(argv[2])
+
+ elif argv[1] == 'lz-to-2bpp':
+ name = os.path.splitext(argv[3])[0]
+ lz = open(name+'.lz', 'rb').read()
+ if argv[2] == '--vert':
+ to_file(name+'.2bpp', Decompressed(lz, 'vert').output)
+ else:
+ to_file(name+'.2bpp', Decompressed(lz).output)
+
+ elif argv[1] == 'lz-to-png':
+ if argv[2] == '--vert':
+ name = os.path.splitext(argv[3])[0]
+ lz = open(name+'.lz', 'rb').read()
+ to_file(name+'.2bpp', Decompressed(lz, 'vert').output)
+ pic = open(name+'.2bpp', 'rb').read()
+ to_file(name+'.png', to_png(pic))
+ else:
+ lz_to_png_by_file(argv[2])
+
+ elif argv[1] == 'png-to-lz':
+ # python gfx.py png-to-lz [--front anim(2bpp) | --vert] [png]
+ if argv[2] == '--front':
+ # front.2bpp and tiles.2bpp are combined before compression,
+ # so we have to pass in the anim file and pic size
+ name = os.path.splitext(argv[4])[0]
+ to_2bpp(name+'.png', name+'.2bpp')
+ pic = open(name+'.2bpp', 'rb').read()
+ anim = open(argv[3], 'rb').read()
+ size = int(sqrt(len(pic)/16)) # assume square pic
+ to_file(name+'.lz', Compressed(pic + anim, 'vert', size).output)
+ elif argv[2] == '--vert':
+ name = os.path.splitext(argv[3])[0]
+ to_2bpp(name+'.png', name+'.2bpp')
+ pic = open(name+'.2bpp', 'rb').read()
+ to_file(name+'.lz', Compressed(pic, 'vert').output)
+ else:
+ png_to_lz(argv[2])
+
+ elif argv[1] == 'png-to-2bpp':
+ to_2bpp(argv[2])
+
+ elif argv[1] == '2bpp-to-lz':
+ if argv[2] == '--vert':
+ filein = argv[3]
+ fileout = argv[4]
+ compress_file(filein, fileout, 'vert')
+ else:
+ filein = argv[2]
+ fileout = argv[3]
+ compress_file(filein, fileout)
+
+ elif argv[1] == '2bpp-to-png':
+ to_png(argv[2])
diff --git a/pokemontools/graph.py b/pokemontools/graph.py
new file mode 100644
index 0000000..47087e5
--- /dev/null
+++ b/pokemontools/graph.py
@@ -0,0 +1,169 @@
+# -*- coding: utf-8 -*-
+
+import networkx as nx
+
+from romstr import (
+ RomStr,
+ relative_jumps,
+ call_commands,
+ relative_unconditional_jumps,
+)
+
+class RomGraph(nx.DiGraph):
+ """
+ Graphs various functions pointing to each other.
+
+ TODO: Bank switches are nasty. They should be detected. Otherwise,
+ functions will point to non-functions within the same bank. Another way
+ to detect bankswitches is retroactively. By disassembling one function
+ after another within the function banks, it can be roughly assumed that
+ anything pointing to something else (within the same bank) is really
+ actually a bankswitch. An even better method to handle bankswitches
+ would be to just detect those situations in the asm (but I presently
+ forget how bankswitches are performed in pokecrystal).
+ """
+
+ # some areas shouldn't be parsed as asm
+ exclusions = []
+
+ # where is the first function located?
+ start_address = 0x150
+
+ # and where is a good place to stop?
+ end_address = 0x4000 * 0x03 # only do the first bank? sure..
+
+ # where is the rom stored?
+ rompath = "../baserom.gbc"
+
+ def __init__(self, rom=None, **kwargs):
+ """
+ Loads and parses the ROM into a function graph.
+ """
+ # continue the initialization
+ nx.DiGraph.__init__(self, **kwargs)
+
+ # load the graph
+ if rom == None:
+ self.load_rom()
+ else:
+ self.rom = rom
+
+ # start parsing the ROM
+ self.parse()
+
+ def load_rom(self):
+ """
+ Creates a RomStr from rompath.
+ """
+ file_handler = open(self.rompath, "r")
+ self.rom = RomStr(file_handler.read())
+ file_handler.close()
+
+ def parse(self):
+ """
+ Parses the ROM starting with the first function address. Each
+ function is disassembled and parsed to find where else it leads to.
+ """
+ functions = {}
+
+ address = self.start_address
+
+ other_addresses = set()
+
+ count = 0
+
+ while True:
+ if count > 3000:
+ break
+
+ if address < self.end_address and (address not in functions.keys()) and address >= 0x150:
+ # address is okay to parse at, keep going
+ pass
+ elif len(other_addresses) > 0:
+ # parse some other address possibly in a remote bank
+ address = other_addresses.pop()
+ else:
+ # no more addresses detected- exit loop
+ break
+
+ # parse the asm
+ func = self.rom.to_asm(address)
+
+ # check if there are any nops (probably not a function)
+ nops = 0
+ for (id, command) in func.asm_commands.items():
+ if command.has_key("id") and command["id"] == 0x0:
+ nops += 1
+
+ # skip this function
+ if nops > 1:
+ address = 0
+ continue
+
+ # store this parsed function
+ functions[address] = func
+
+ # where does this function jump to?
+ used_addresses = set(func.used_addresses())
+
+ # add this information to the graph
+ for used_address in used_addresses:
+ # only add this remote address if it's not yet parsed
+ if used_address not in functions.keys():
+ other_addresses.update([used_address])
+
+ # add this other address to the graph
+ if used_address > 100:
+ self.add_node(used_address)
+
+ # add this as an edge between the two nodes
+ self.add_edge(address, used_address)
+
+ # setup the next function to be parsed
+ address = func.last_address
+
+ count += 1
+
+ self.functions = functions
+
+ def pretty_printer(self):
+ """
+ Shows some text output describing which nodes point to which other
+ nodes.
+ """
+ print self.edges()
+
+ def to_d3(self):
+ """
+ Exports to d3.js because we're gangster like that.
+ """
+ import networkx.readwrite.json_graph as json_graph
+ content = json_graph.dumps(self)
+ fh = open("crystal/crystal.json", "w")
+ fh.write(content)
+ fh.close()
+
+ def to_gephi(self):
+ """
+ Generates a gexf file.
+ """
+ nx.write_gexf(self, "graph.gexf")
+
+class RedGraph(RomGraph):
+ """
+ Not implemented. Go away.
+ """
+
+ rompath = "../pokered-baserom.gbc"
+
+class CryGraph(RomGraph):
+ exclusions = [
+ [0x000, 0x149],
+ ]
+
+ rompath = "../baserom.gbc"
+
+if __name__ == "__main__":
+ crygraph = CryGraph()
+ crygraph.pretty_printer()
+ crygraph.to_gephi()
diff --git a/pokemontools/interval_map.py b/pokemontools/interval_map.py
new file mode 100644
index 0000000..daf22cd
--- /dev/null
+++ b/pokemontools/interval_map.py
@@ -0,0 +1,103 @@
+# -*- coding: utf-8 -*-
+
+from bisect import bisect_left, bisect_right
+from itertools import izip
+
+class IntervalMap(object):
+ """
+ This class maps a set of intervals to a set of values.
+
+ >>> i = IntervalMap()
+ >>> i[0:5] = "hello world"
+ >>> i[6:10] = "hello cruel world"
+ >>> print i[4]
+ "hello world"
+ """
+
+ def __init__(self):
+ """initializes an empty IntervalMap"""
+ self._bounds = []
+ self._items = []
+ self._upperitem = None
+
+ def __setitem__(self, _slice, _value):
+ """sets an interval mapping"""
+ assert isinstance(_slice, slice), 'The key must be a slice object'
+
+ if _slice.start is None:
+ start_point = -1
+ else:
+ start_point = bisect_left(self._bounds, _slice.start)
+
+ if _slice.stop is None:
+ end_point = -1
+ else:
+ end_point = bisect_left(self._bounds, _slice.stop)
+
+ if start_point>=0:
+ if start_point < len(self._bounds) and self._bounds[start_point]<_slice.start:
+ start_point += 1
+
+ if end_point>=0:
+ self._bounds[start_point:end_point] = [_slice.start, _slice.stop]
+ if start_point < len(self._items):
+ self._items[start_point:end_point] = [self._items[start_point], _value]
+ else:
+ self._items[start_point:end_point] = [self._upperitem, _value]
+ else:
+ self._bounds[start_point:] = [_slice.start]
+ if start_point < len(self._items):
+ self._items[start_point:] = [self._items[start_point], _value]
+ else:
+ self._items[start_point:] = [self._upperitem]
+ self._upperitem = _value
+ else:
+ if end_point>=0:
+ self._bounds[:end_point] = [_slice.stop]
+ self._items[:end_point] = [_value]
+ else:
+ self._bounds[:] = []
+ self._items[:] = []
+ self._upperitem = _value
+
+ def __getitem__(self,_point):
+ """gets a value from the mapping"""
+ assert not isinstance(_point, slice), 'The key cannot be a slice object'
+
+ index = bisect_right(self._bounds, _point)
+ if index < len(self._bounds):
+ return self._items[index]
+ else:
+ return self._upperitem
+
+ def items(self):
+ """returns an iterator with each item being
+ ((low_bound, high_bound), value)
+ these items are returned in order"""
+ previous_bound = None
+ for (b, v) in izip(self._bounds, self._items):
+ if v is not None:
+ yield (previous_bound, b), v
+ previous_bound = b
+ if self._upperitem is not None:
+ yield (previous_bound, None), self._upperitem
+
+ def values(self):
+ """returns an iterator with each item being a stored value
+ the items are returned in order"""
+ for v in self._items:
+ if v is not None:
+ yield v
+ if self._upperitem is not None:
+ yield self._upperitem
+
+ def __repr__(self):
+ s = []
+ for b,v in self.items():
+ if v is not None:
+ s.append('[%r, %r] => %r'%(
+ b[0],
+ b[1],
+ v
+ ))
+ return '{'+', '.join(s)+'}'
diff --git a/pokemontools/item_constants.py b/pokemontools/item_constants.py
new file mode 100644
index 0000000..3692c92
--- /dev/null
+++ b/pokemontools/item_constants.py
@@ -0,0 +1,240 @@
+# -*- coding: utf-8 -*-
+
+item_constants = {
+1: 'MASTER_BALL',
+2: 'ULTRA_BALL',
+3: 'BRIGHTPOWDER',
+4: 'GREAT_BALL',
+5: 'POKE_BALL',
+7: 'BICYCLE',
+8: 'MOON_STONE',
+9: 'ANTIDOTE',
+10: 'BURN_HEAL',
+11: 'ICE_HEAL',
+12: 'AWAKENING',
+13: 'PARLYZ_HEAL',
+14: 'FULL_RESTORE',
+15: 'MAX_POTION',
+16: 'HYPER_POTION',
+17: 'SUPER_POTION',
+18: 'POTION',
+19: 'ESCAPE_ROPE',
+20: 'REPEL',
+21: 'MAX_ELIXER',
+22: 'FIRE_STONE',
+23: 'THUNDERSTONE',
+24: 'WATER_STONE',
+26: 'HP_UP',
+27: 'PROTEIN',
+28: 'IRON',
+29: 'CARBOS',
+30: 'LUCKY_PUNCH',
+31: 'CALCIUM',
+32: 'RARE_CANDY',
+33: 'X_ACCURACY',
+34: 'LEAF_STONE',
+35: 'METAL_POWDER',
+36: 'NUGGET',
+37: 'POKE_DOLL',
+38: 'FULL_HEAL',
+39: 'REVIVE',
+40: 'MAX_REVIVE',
+41: 'GUARD_SPEC',
+42: 'SUPER_REPEL',
+43: 'MAX_REPEL',
+44: 'DIRE_HIT',
+46: 'FRESH_WATER',
+47: 'SODA_POP',
+48: 'LEMONADE',
+49: 'X_ATTACK',
+51: 'X_DEFEND',
+52: 'X_SPEED',
+53: 'X_SPECIAL',
+54: 'COIN_CASE',
+55: 'ITEMFINDER',
+57: 'EXP_SHARE',
+58: 'OLD_ROD',
+59: 'GOOD_ROD',
+60: 'SILVER_LEAF',
+61: 'SUPER_ROD',
+62: 'PP_UP',
+63: 'ETHER',
+64: 'MAX_ETHER',
+65: 'ELIXER',
+66: 'RED_SCALE',
+67: 'SECRETPOTION',
+68: 'S_S_TICKET',
+69: 'MYSTERY_EGG',
+70: 'CLEAR_BELL',
+71: 'SILVER_WING',
+72: 'MOOMOO_MILK',
+73: 'QUICK_CLAW',
+74: 'PSNCUREBERRY',
+75: 'GOLD_LEAF',
+76: 'SOFT_SAND',
+77: 'SHARP_BEAK',
+78: 'PRZCUREBERRY',
+79: 'BURNT_BERRY',
+80: 'ICE_BERRY',
+81: 'POISON_BARB',
+82: "KINGS_ROCK",
+83: 'BITTER_BERRY',
+84: 'MINT_BERRY',
+85: 'RED_APRICORN',
+86: 'TINYMUSHROOM',
+87: 'BIG_MUSHROOM',
+88: 'SILVERPOWDER',
+89: 'BLU_APRICORN',
+91: 'AMULET_COIN',
+92: 'YLW_APRICORN',
+93: 'GRN_APRICORN',
+94: 'CLEANSE_TAG',
+95: 'MYSTIC_WATER',
+96: 'TWISTEDSPOON',
+97: 'WHT_APRICORN',
+98: 'BLACKBELT',
+99: 'BLK_APRICORN',
+101: 'PNK_APRICORN',
+102: 'BLACKGLASSES',
+103: 'SLOWPOKETAIL',
+104: 'PINK_BOW',
+105: 'STICK',
+106: 'SMOKE_BALL',
+107: 'NEVERMELTICE',
+108: 'MAGNET',
+109: 'MIRACLEBERRY',
+110: 'PEARL',
+111: 'BIG_PEARL',
+112: 'EVERSTONE',
+113: 'SPELL_TAG',
+114: 'RAGECANDYBAR',
+115: 'GS_BALL',
+116: 'BLUE_CARD',
+117: 'MIRACLE_SEED',
+118: 'THICK_CLUB',
+119: 'FOCUS_BAND',
+121: 'ENERGYPOWDER',
+122: 'ENERGY_ROOT',
+123: 'HEAL_POWDER',
+124: 'REVIVAL_HERB',
+125: 'HARD_STONE',
+126: 'LUCKY_EGG',
+127: 'CARD_KEY',
+128: 'MACHINE_PART',
+129: 'EGG_TICKET',
+130: 'LOST_ITEM',
+131: 'STARDUST',
+132: 'STAR_PIECE',
+133: 'BASEMENT_KEY',
+134: 'PASS',
+138: 'CHARCOAL',
+139: 'BERRY_JUICE',
+140: 'SCOPE_LENS',
+143: 'METAL_COAT',
+144: 'DRAGON_FANG',
+146: 'LEFTOVERS',
+150: 'MYSTERYBERRY',
+151: 'DRAGON_SCALE',
+152: 'BERSERK_GENE',
+156: 'SACRED_ASH',
+157: 'HEAVY_BALL',
+158: 'FLOWER_MAIL',
+159: 'LEVEL_BALL',
+160: 'LURE_BALL',
+161: 'FAST_BALL',
+163: 'LIGHT_BALL',
+164: 'FRIEND_BALL',
+165: 'MOON_BALL',
+166: 'LOVE_BALL',
+167: 'NORMAL_BOX',
+168: 'GORGEOUS_BOX',
+169: 'SUN_STONE',
+170: 'POLKADOT_BOW',
+172: 'UP_GRADE',
+173: 'BERRY',
+174: 'GOLD_BERRY',
+175: 'SQUIRTBOTTLE',
+177: 'PARK_BALL',
+178: 'RAINBOW_WING',
+180: 'BRICK_PIECE',
+181: 'SURF_MAIL',
+182: 'LITEBLUEMAIL',
+183: 'PORTRAITMAIL',
+184: 'LOVELY_MAIL',
+185: 'EON_MAIL',
+186: 'MORPH_MAIL',
+187: 'BLUESKY_MAIL',
+188: 'MUSIC_MAIL',
+189: 'MIRAGE_MAIL',
+191: 'TM_01',
+192: 'TM_02',
+193: 'TM_03',
+194: 'TM_04',
+196: 'TM_05',
+197: 'TM_06',
+198: 'TM_07',
+199: 'TM_08',
+200: 'TM_09',
+201: 'TM_10',
+202: 'TM_11',
+203: 'TM_12',
+204: 'TM_13',
+205: 'TM_14',
+206: 'TM_15',
+207: 'TM_16',
+208: 'TM_17',
+209: 'TM_18',
+210: 'TM_19',
+211: 'TM_20',
+212: 'TM_21',
+213: 'TM_22',
+214: 'TM_23',
+215: 'TM_24',
+216: 'TM_25',
+217: 'TM_26',
+218: 'TM_27',
+219: 'TM_28',
+221: 'TM_29',
+222: 'TM_30',
+223: 'TM_31',
+224: 'TM_32',
+225: 'TM_33',
+226: 'TM_34',
+227: 'TM_35',
+228: 'TM_36',
+229: 'TM_37',
+230: 'TM_38',
+231: 'TM_39',
+232: 'TM_40',
+233: 'TM_41',
+234: 'TM_42',
+235: 'TM_43',
+236: 'TM_44',
+237: 'TM_45',
+238: 'TM_46',
+239: 'TM_47',
+240: 'TM_48',
+241: 'TM_49',
+242: 'TM_50',
+243: 'HM_01',
+244: 'HM_02',
+245: 'HM_03',
+246: 'HM_04',
+247: 'HM_05',
+248: 'HM_06',
+249: 'HM_07',
+}
+
+def find_item_label_by_id(id):
+ if id in item_constants.keys():
+ return item_constants[id]
+ else: return None
+
+def generate_item_constants():
+ """make a list of items to put in constants.asm"""
+ output = ""
+ for (id, item) in item_constants.items():
+ val = ("$%.2x"%id).upper()
+ while len(item)<13: item+= " "
+ output += item + " EQU " + val + "\n"
+ return output
diff --git a/pokemontools/labels.py b/pokemontools/labels.py
new file mode 100644
index 0000000..d553bdc
--- /dev/null
+++ b/pokemontools/labels.py
@@ -0,0 +1,171 @@
+# -*- coding: utf-8 -*-
+"""
+Various label/line-related functions.
+"""
+
+from pointers import (
+ calculate_pointer,
+ calculate_bank,
+)
+
+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 = calculate_bank(offset)
+ returnable["bank"] = bank
+ returnable["offset"] = offset
+ returnable["address"] = 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
+ if "::" in line:
+ 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
diff --git a/pokemontools/map_names.py b/pokemontools/map_names.py
new file mode 100644
index 0000000..00cf452
--- /dev/null
+++ b/pokemontools/map_names.py
@@ -0,0 +1,484 @@
+# -*- coding: utf-8 -*-
+
+# this is modified in crystal.py during run-time
+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 44"},
+ 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": "Burned 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 North"},
+ 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 South"},
+ 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"},
+ },
+}
diff --git a/pokemontools/move_constants.py b/pokemontools/move_constants.py
new file mode 100644
index 0000000..a20af85
--- /dev/null
+++ b/pokemontools/move_constants.py
@@ -0,0 +1,255 @@
+# -*- coding: utf-8 -*-
+
+moves = {
+0x01: "POUND",
+0x02: "KARATE_CHOP",
+0x03: "DOUBLESLAP",
+0x04: "COMET_PUNCH",
+0x05: "MEGA_PUNCH",
+0x06: "PAY_DAY",
+0x07: "FIRE_PUNCH",
+0x08: "ICE_PUNCH",
+0x09: "THUNDERPUNCH",
+0x0A: "SCRATCH",
+0x0B: "VICEGRIP",
+0x0C: "GUILLOTINE",
+0x0D: "RAZOR_WIND",
+0x0E: "SWORDS_DANCE",
+0x0F: "CUT",
+0x10: "GUST",
+0x11: "WING_ATTACK",
+0x12: "WHIRLWIND",
+0x13: "FLY",
+0x14: "BIND",
+0x15: "SLAM",
+0x16: "VINE_WHIP",
+0x17: "STOMP",
+0x18: "DOUBLE_KICK",
+0x19: "MEGA_KICK",
+0x1A: "JUMP_KICK",
+0x1B: "ROLLING_KICK",
+0x1C: "SAND_ATTACK",
+0x1D: "HEADBUTT",
+0x1E: "HORN_ATTACK",
+0x1F: "FURY_ATTACK",
+0x20: "HORN_DRILL",
+0x21: "TACKLE",
+0x22: "BODY_SLAM",
+0x23: "WRAP",
+0x24: "TAKE_DOWN",
+0x25: "THRASH",
+0x26: "DOUBLE_EDGE",
+0x27: "TAIL_WHIP",
+0x28: "POISON_STING",
+0x29: "TWINEEDLE",
+0x2A: "PIN_MISSILE",
+0x2B: "LEER",
+0x2C: "BITE",
+0x2D: "GROWL",
+0x2E: "ROAR",
+0x2F: "SING",
+0x30: "SUPERSONIC",
+0x31: "SONICBOOM",
+0x32: "DISABLE",
+0x33: "ACID",
+0x34: "EMBER",
+0x35: "FLAMETHROWER",
+0x36: "MIST",
+0x37: "WATER_GUN",
+0x38: "HYDRO_PUMP",
+0x39: "SURF",
+0x3A: "ICE_BEAM",
+0x3B: "BLIZZARD",
+0x3C: "PSYBEAM",
+0x3D: "BUBBLEBEAM",
+0x3E: "AURORA_BEAM",
+0x3F: "HYPER_BEAM",
+0x40: "PECK",
+0x41: "DRILL_PECK",
+0x42: "SUBMISSION",
+0x43: "LOW_KICK",
+0x44: "COUNTER",
+0x45: "SEISMIC_TOSS",
+0x46: "STRENGTH",
+0x47: "ABSORB",
+0x48: "MEGA_DRAIN",
+0x49: "LEECH_SEED",
+0x4A: "GROWTH",
+0x4B: "RAZOR_LEAF",
+0x4C: "SOLARBEAM",
+0x4D: "POISONPOWDER",
+0x4E: "STUN_SPORE",
+0x4F: "SLEEP_POWDER",
+0x50: "PETAL_DANCE",
+0x51: "STRING_SHOT",
+0x52: "DRAGON_RAGE",
+0x53: "FIRE_SPIN",
+0x54: "THUNDERSHOCK",
+0x55: "THUNDERBOLT",
+0x56: "THUNDER_WAVE",
+0x57: "THUNDER",
+0x58: "ROCK_THROW",
+0x59: "EARTHQUAKE",
+0x5A: "FISSURE",
+0x5B: "DIG",
+0x5C: "TOXIC",
+0x5D: "CONFUSION",
+0x5E: "PSYCHIC_M",
+0x5F: "HYPNOSIS",
+0x60: "MEDITATE",
+0x61: "AGILITY",
+0x62: "QUICK_ATTACK",
+0x63: "RAGE",
+0x64: "TELEPORT",
+0x65: "NIGHT_SHADE",
+0x66: "MIMIC",
+0x67: "SCREECH",
+0x68: "DOUBLE_TEAM",
+0x69: "RECOVER",
+0x6A: "HARDEN",
+0x6B: "MINIMIZE",
+0x6C: "SMOKESCREEN",
+0x6D: "CONFUSE_RAY",
+0x6E: "WITHDRAW",
+0x6F: "DEFENSE_CURL",
+0x70: "BARRIER",
+0x71: "LIGHT_SCREEN",
+0x72: "HAZE",
+0x73: "REFLECT",
+0x74: "FOCUS_ENERGY",
+0x75: "BIDE",
+0x76: "METRONOME",
+0x77: "MIRROR_MOVE",
+0x78: "SELFDESTRUCT",
+0x79: "EGG_BOMB",
+0x7A: "LICK",
+0x7B: "SMOG",
+0x7C: "SLUDGE",
+0x7D: "BONE_CLUB",
+0x7E: "FIRE_BLAST",
+0x7F: "WATERFALL",
+0x80: "CLAMP",
+0x81: "SWIFT",
+0x82: "SKULL_BASH",
+0x83: "SPIKE_CANNON",
+0x84: "CONSTRICT",
+0x85: "AMNESIA",
+0x86: "KINESIS",
+0x87: "SOFTBOILED",
+0x88: "HI_JUMP_KICK",
+0x89: "GLARE",
+0x8A: "DREAM_EATER",
+0x8B: "POISON_GAS",
+0x8C: "BARRAGE",
+0x8D: "LEECH_LIFE",
+0x8E: "LOVELY_KISS",
+0x8F: "SKY_ATTACK",
+0x90: "TRANSFORM",
+0x91: "BUBBLE",
+0x92: "DIZZY_PUNCH",
+0x93: "SPORE",
+0x94: "FLASH",
+0x95: "PSYWAVE",
+0x96: "SPLASH",
+0x97: "ACID_ARMOR",
+0x98: "CRABHAMMER",
+0x99: "EXPLOSION",
+0x9A: "FURY_SWIPES",
+0x9B: "BONEMERANG",
+0x9C: "REST",
+0x9D: "ROCK_SLIDE",
+0x9E: "HYPER_FANG",
+0x9F: "SHARPEN",
+0xA0: "CONVERSION",
+0xA1: "TRI_ATTACK",
+0xA2: "SUPER_FANG",
+0xA3: "SLASH",
+0xA4: "SUBSTITUTE",
+0xA5: "STRUGGLE",
+0xA6: "SKETCH",
+0xA7: "TRIPLE_KICK",
+0xA8: "THIEF",
+0xA9: "SPIDER_WEB",
+0xAA: "MIND_READER",
+0xAB: "NIGHTMARE",
+0xAC: "FLAME_WHEEL",
+0xAD: "SNORE",
+0xAE: "CURSE",
+0xAF: "FLAIL",
+0xB0: "CONVERSION2",
+0xB1: "AEROBLAST",
+0xB2: "COTTON_SPORE",
+0xB3: "REVERSAL",
+0xB4: "SPITE",
+0xB5: "POWDER_SNOW",
+0xB6: "PROTECT",
+0xB7: "MACH_PUNCH",
+0xB8: "SCARY_FACE",
+0xB9: "FAINT_ATTACK",
+0xBA: "SWEET_KISS",
+0xBB: "BELLY_DRUM",
+0xBC: "SLUDGE_BOMB",
+0xBD: "MUD_SLAP",
+0xBE: "OCTAZOOKA",
+0xBF: "SPIKES",
+0xC0: "ZAP_CANNON",
+0xC1: "FORESIGHT",
+0xC2: "DESTINY_BOND",
+0xC3: "PERISH_SONG",
+0xC4: "ICY_WIND",
+0xC5: "DETECT",
+0xC6: "BONE_RUSH",
+0xC7: "LOCK_ON",
+0xC8: "OUTRAGE",
+0xC9: "SANDSTORM",
+0xCA: "GIGA_DRAIN",
+0xCB: "ENDURE",
+0xCC: "CHARM",
+0xCD: "ROLLOUT",
+0xCE: "FALSE_SWIPE",
+0xCF: "SWAGGER",
+0xD0: "MILK_DRINK",
+0xD1: "SPARK",
+0xD2: "FURY_CUTTER",
+0xD3: "STEEL_WING",
+0xD4: "MEAN_LOOK",
+0xD5: "ATTRACT",
+0xD6: "SLEEP_TALK",
+0xD7: "HEAL_BELL",
+0xD8: "RETURN",
+0xD9: "PRESENT",
+0xDA: "FRUSTRATION",
+0xDB: "SAFEGUARD",
+0xDC: "PAIN_SPLIT",
+0xDD: "SACRED_FIRE",
+0xDE: "MAGNITUDE",
+0xDF: "DYNAMICPUNCH",
+0xE0: "MEGAHORN",
+0xE1: "DRAGONBREATH",
+0xE2: "BATON_PASS",
+0xE3: "ENCORE",
+0xE4: "PURSUIT",
+0xE5: "RAPID_SPIN",
+0xE6: "SWEET_SCENT",
+0xE7: "IRON_TAIL",
+0xE8: "METAL_CLAW",
+0xE9: "VITAL_THROW",
+0xEA: "MORNING_SUN",
+0xEB: "SYNTHESIS",
+0xEC: "MOONLIGHT",
+0xED: "HIDDEN_POWER",
+0xEE: "CROSS_CHOP",
+0xEF: "TWISTER",
+0xF0: "RAIN_DANCE",
+0xF1: "SUNNY_DAY",
+0xF2: "CRUNCH",
+0xF3: "MIRROR_COAT",
+0xF4: "PSYCH_UP",
+0xF5: "EXTREMESPEED",
+0xF6: "ANCIENTPOWER",
+0xF7: "SHADOW_BALL",
+0xF8: "FUTURE_SIGHT",
+0xF9: "ROCK_SMASH",
+0xFA: "WHIRLPOOL",
+0xFB: "BEAT_UP",
+}
diff --git a/pokemontools/old_parse_scripts.py b/pokemontools/old_parse_scripts.py
new file mode 100644
index 0000000..8ed10ef
--- /dev/null
+++ b/pokemontools/old_parse_scripts.py
@@ -0,0 +1,1885 @@
+# -*- coding: utf-8 -*-
+"""
+This is in a separate file because it screws up highlighting. Doesn't
+do anything on its own.
+
+old_parse is a part of the Script class
+"""
+
+def old_parse(self, *args, **kwargs):
+ """parses a script-engine script; force=True if you want to re-parse
+ and get the debug information"""
+ print "Script.old_parse address="+hex(self.address)
+ #can't handle more than one argument
+ if len(args) > 1:
+ raise Exception, "Script.parse_script doesn't know how to handle positional arguments"
+ #use the first positional argument as the address
+ elif len(args) == 1:
+ self.address = args[0]
+ if type(self.address) == str:
+ self.address = int(self.address, 16)
+ elif type(self.address) != int:
+ raise Exception, "address param is the wrong type"
+ #parse any keyword arguments, first make up the defaults
+ kwargsorig = {"map_group": None, "map_id": None, "force": False, "debug": True, "origin": False}
+ #let the caller override any defaults
+ kwargsorig.update(kwargs)
+ #copy these into kwargs
+ kwargs = kwargsorig
+ #set these defaults
+ map_group = kwargs["map_group"]
+ map_id = kwargs["map_id"]
+ force = kwargs["force"]
+ debug = kwargs["debug"]
+ origin = kwargs["origin"]
+ self.map_group = map_group
+ self.map_id = map_id
+
+ global rom
+ if rom == None:
+ direct_load_rom()
+
+ #max number of commands in a 'recursive' script
+ max_cmds = 150
+
+ #set the address to be parsed
+ address = self.address
+ original_start_address = address
+
+ #don't parse these crazy things (battle tower things, some rival things, etc.)
+ if address in stop_points:
+ print "got " + hex(address) + ".. map_group=" + str(map_group) + " map_id=" + str(map_id)
+ return None
+ #don't parse anything that looks crazy
+ if address < 0x4000 and address not in [0x26ef, 0x114, 0x1108]:
+ print "address is less than 0x4000.. address is: " + hex(address)
+ sys.exit()
+
+ #check if work is being repeated
+ if is_script_already_parsed_at(address) and not force:
+ raise Exception, "this script has already been parsed before, please use that instance"
+ #use the commands from a previously-parsed Script object
+ #self.commands = script_parse_table[address].commands
+ #return True
+
+ #return a previously-created Script object
+ #return script_parse_table[address]
+
+ #this next line stops the same script from being re-parsed multiple times
+ #for instance.. maybe there's a script jump, then a jump back
+ #the original script should only be parsed once
+ script_parse_table[original_start_address:original_start_address+1] = "incomplete Script"
+
+ #set up some variables
+ self.commands = {}
+ commands = self.commands
+ offset = address
+ end = False
+
+ #main loop.. parse each command byte
+ while not end:
+ #reset variables so we don't contaminate this command
+ info, long_info, size = None, None, 0
+ #read the current command byte
+ command_byte = ord(rom[offset])
+ #setup the current command representation
+ command = {"type": command_byte, "start_address": offset}
+
+ #size is the total size including the command byte
+ #last_byte_address is offset+size-1
+ start_address = offset
+
+ if (len(commands.keys()) > max_cmds) and origin != False:
+ print "too many commands in this script? might not be a script (starting at: " +\
+ hex(original_start_address) + ").. called from a script at: " + hex(origin)
+ sys.exit()
+
+ #start checking against possible command bytes
+ if command_byte == 0x00: #Pointer code [2b+ret]
+ pksv_name = "2call"
+ info = "pointer code"
+ long_info = """
+ 2byte pointer points to script; when pointed script ends --> return to old script
+ [code][2 byte pointer]
+ """
+ size = 3
+ start_address = offset
+ last_byte_address = offset + size - 1
+ pointer = calculate_pointer_from_bytes_at(start_address+1)
+ if pointer == None:
+ raise Exception, "pointer is None (shouldn't be None pointer on 0x0 script command"
+ command["pointer"] = pointer
+ if debug:
+ print "in script starting at "+hex(original_start_address)+\
+ " about to parse script at "+hex(pointer)+\
+ " called by "+info+" byte="+hex(command_byte)
+ script = rec_parse_script_engine_script_at(pointer, original_start_address, debug=debug)
+ command["script"] = script
+ elif command_byte == 0x01: #Pointer code [3b+ret]
+ pksv_name = "3call"
+ info = "pointer code"
+ long_info = """
+ 3byte pointer points to script; when pointed script ends --> return to old script
+ [Code][resp. pointer(2byte or 3byte)]
+ """
+ size = 4
+ info = "pointer code"
+ pointer = calculate_pointer_from_bytes_at(start_address+1, bank=True)
+ if debug:
+ print "in script starting at "+hex(original_start_address)+\
+ " about to parse script at "+hex(pointer)+\
+ " called by "+info+" byte="+hex(command_byte)
+ script = rec_parse_script_engine_script_at(pointer, original_start_address, debug)
+ command["pointer"] = pointer
+ command["script"] = script
+ elif command_byte == 0x02: #Pointer code [2b+3b+ret]
+ info = "pointer code"
+ long_info = """
+ 2byte pointer points to 3byte pointer; when pointed script --> return to old script
+ [Code][resp. pointer(2byte or 3byte)]
+ """
+ size = 3
+ pointer = calculate_pointer_from_bytes_at(start_address+1)
+ if debug:
+ print "in script starting at "+hex(original_start_address)+\
+ " about to parse script at "+hex(pointer)+\
+ " called by "+info+" byte="+hex(command_byte)
+ script = rec_parse_script_engine_script_at(pointer, original_start_address, debug=debug)
+ command["pointer"] = pointer
+ command["script"] = script
+ elif command_byte == 0x03: #Pointer code [2b]
+ #XXX what does "new script is part of main script" mean?
+ info = "pointer code"
+ long_info = """
+ 2byte pointer points to script; new script is part of main script
+ [Code][resp. pointer(2byte or 3byte)]
+ """
+ size = 3
+ pointer = calculate_pointer_from_bytes_at(start_address+1)
+ if debug:
+ print "in script starting at "+hex(original_start_address)+\
+ " about to parse script at "+hex(pointer)+\
+ " called by "+info+" byte="+hex(command_byte)
+ script = rec_parse_script_engine_script_at(pointer, original_start_address, debug=debug)
+ command["pointer"] = pointer
+ command["script"] = script
+ end = True #according to pksv
+ elif command_byte == 0x04: #Pointer code [3b]
+ info = "pointer code"
+ long_info = """
+ 3byte pointer points to script; new script is part of main script
+ [Code][resp. pointer(2byte or 3byte)]
+ """
+ size = 4
+ pointer = calculate_pointer_from_bytes_at(start_address+1, bank=True)
+ if debug:
+ print "in script starting at "+hex(original_start_address)+\
+ " about to parse script at "+hex(pointer)+\
+ " called by "+info+" byte="+hex(command_byte)
+ script = rec_parse_script_engine_script_at(pointer, original_start_address, debug=debug)
+ command["pointer"] = pointer
+ command["script"] = script
+ end = True #according to pksv
+ elif command_byte == 0x05: #Pointer code [2b+3b]
+ info = "pointer code"
+ long_info = """
+ 2byte pointer points to 3byte pointer; new script is part of main script
+ [Code][resp. pointer(2byte or 3byte)]
+ """
+ size = 3
+ command["pointer"] = calculate_pointer_from_bytes_at(start_address+1)
+ command["target_pointer"] = calculate_pointer_from_bytes_at(command["pointer"], bank=True)
+
+ if debug:
+ print "in script starting at "+hex(original_start_address)+\
+ " about to parse script at "+hex(command["target_pointer"])+\
+ " called by "+info+" byte="+hex(command_byte)
+ script = rec_parse_script_engine_script_at(command["target_pointer"], original_start_address, debug=debug)
+ command["script"] = script
+ end = True #according to pksv
+ elif command_byte == 0x06: #RAM check [=byte]
+ info = "RAM check [=byte]"
+ long_info = """
+ When the conditional is true...
+ .. then go to pointed script, else resume interpreting after the pointer
+ """
+ size = 4
+ command["byte"] = ord(rom[start_address+1])
+ pointer = calculate_pointer_from_bytes_at(start_address+2)
+ if debug:
+ print "in script starting at "+hex(original_start_address)+\
+ " about to parse script at "+hex(pointer)+\
+ " called by "+info+" byte="+hex(command_byte)
+ script = rec_parse_script_engine_script_at(pointer, original_start_address, debug=debug)
+ command["pointer"] = pointer
+ command["script"] = script
+ elif command_byte == 0x07: #RAM check [<>byte]
+ info = "RAM check [<>byte]"
+ long_info = """
+ When the conditional is true...
+ .. then go to pointed script, else resume interpreting after the pointer
+ """
+ size = 4
+ command["byte"] = ord(rom[start_address+1])
+ pointer = calculate_pointer_from_bytes_at(start_address+2)
+ if debug:
+ print "in script starting at "+hex(original_start_address)+\
+ " about to parse script at "+hex(pointer)+\
+ " called by "+info+" byte="+hex(command_byte)
+ script = rec_parse_script_engine_script_at(pointer, original_start_address, debug=debug)
+ command["pointer"] = pointer
+ command["script"] = script
+ elif command_byte == 0x08: #RAM check [=0]
+ info = "RAM check [=0]"
+ long_info = """
+ When the conditional is true...
+ .. then go to pointed script, else resume interpreting after the pointer
+ """
+ size = 3
+ pointer = calculate_pointer_from_bytes_at(start_address+1)
+ if debug:
+ print "in script starting at "+hex(original_start_address)+\
+ " about to parse script at "+hex(pointer)+\
+ " called by "+info+" byte="+hex(command_byte)
+ script = rec_parse_script_engine_script_at(pointer, original_start_address, debug=debug)
+ command["pointer"] = pointer
+ command["script"] = script
+ elif command_byte == 0x09: #RAM check [<>0]
+ info = "RAM check [<>0]"
+ long_info = """
+ When the conditional is true...
+ .. then go to pointed script, else resume interpreting after the pointer
+ """
+ size = 3
+ pointer = calculate_pointer_from_bytes_at(start_address+1)
+ if debug:
+ print "in script starting at "+hex(original_start_address)+\
+ " about to parse script at "+hex(pointer)+\
+ " called by "+info+" byte="+hex(command_byte)
+ script = rec_parse_script_engine_script_at(pointer, original_start_address, debug=debug)
+ command["pointer"] = pointer
+ command["script"] = script
+ elif command_byte == 0x0A: #RAM check [<byte]
+ info = "RAM check [<byte]"
+ long_info = """
+ When the conditional is true...
+ .. then go to pointed script, else resume interpreting after the pointer
+ """
+ size = 4
+ command["byte"] = ord(rom[start_address+1])
+ pointer = calculate_pointer_from_bytes_at(start_address+2)
+ if debug:
+ print "in script starting at "+hex(original_start_address)+\
+ " about to parse script at "+hex(pointer)+\
+ " called by "+info+" byte="+hex(command_byte)
+ script = rec_parse_script_engine_script_at(pointer, original_start_address, debug=debug)
+ command["pointer"] = pointer
+ command["script"] = script
+ elif command_byte == 0x0B: #RAM check [>byte]
+ info = "RAM check [>byte]"
+ long_info = """
+ When the conditional is true...
+ .. then go to pointed script, else resume interpreting after the pointer
+ """
+ size = 4
+ command["byte"] = ord(rom[start_address+1])
+ pointer = calculate_pointer_from_bytes_at(start_address+2)
+ if debug:
+ print "in script starting at "+hex(original_start_address)+\
+ " about to parse script at "+hex(pointer)+\
+ " called by "+info+" byte="+hex(command_byte)
+ script = rec_parse_script_engine_script_at(pointer, original_start_address, debug=debug)
+ command["pointer"] = pointer
+ command["script"] = script
+ elif command_byte == 0x0C: #0C codes [xxyy]
+ info = "call predefined script then end"
+ long_info = """
+ Calls predefined scripts. After this code the script ends.
+ [0C][xxyy]
+ """
+ size = 3
+ end = True
+ byte1 = ord(rom[offset+1])
+ byte2 = ord(rom[offset+2])
+ number = byte1 + (byte2 << 8)
+ #0000 to 000AD ... XXX how should these be handled?
+ command["predefined_script_number"] = number
+ elif command_byte == 0x0D: #0D codes [xxyy]
+ info = "call some predefined script"
+ long_info = """
+ Calls predefined scripts. Exactly like $0C except the script does not end.
+ [0D][xxyy]
+ """
+ size = 3
+ byte1 = ord(rom[offset+1])
+ byte2 = ord(rom[offset+2])
+ number = byte1 + (byte2 << 8)
+ #0000 to 000AD ... XXX how should these be handled?
+ command["predefined_script_number"] = number
+ elif command_byte == 0x0E: #ASM code1 [3b]
+ info = "ASM code1 [3b]"
+ long_info = """
+ Calls a predefined routine by interpreting the ASM the pointer points to.
+ [0E][3byte pointer]
+ """
+ size = 4
+ command["pointer"] = calculate_pointer_from_bytes_at(start_address+1, bank=True)
+ #XXX should we dissassemble the asm at the target location?
+ elif command_byte == 0x0F: #0F codes [xxyy]
+ info = "call some predefined script [0F][yyxx]"
+ long_info = """
+ Calls predefined scripts.
+ [0F][yyxx]
+ NOTE: For (some) dialogues the font needs to be loaded with the Text box&font code.
+ """
+ size = 3
+ byte1 = ord(rom[offset+1])
+ byte2 = ord(rom[offset+2])
+ number = byte1 + (byte2 << 8)
+ command["predefined_script_number"] = number
+ elif command_byte == 0x10: #ASM code2 [2b]
+ info = "ASM code2 [2b to 3b to asm]"
+ long_info = """
+ Call an ASM script via a 2byte pointer pointing to a 3byte pointer.
+ [10][2byte pointer pointing to 3byte pointer pointing to ASM script]
+ """
+ size = 3
+ command["pointer"] = calculate_pointer_from_bytes_at(start_address+1, bank=False)
+ #XXX should i include the 3-byte pointer at the target location?
+ #XXX should we dissassemble the asm at the target location?
+ elif command_byte == 0x11: #Trigger event check1 [xxyy]
+ info = "Trigger event check1 [xx][yy]"
+ long_info = """
+ Check the current number of the trigger event on map (map group/map id).
+ [11][map group][map number]
+ """
+ size = 3
+ command["map_group"] = ord(rom[start_address+1])
+ command["map_id"] = ord(rom[start_address+2])
+ elif command_byte == 0x12: #Activate trigger event from afar [xxyyzz]
+ info = "Activate trigger event from afar [xx][yy][zz]"
+ long_info = """
+ Changes trigger event number on map (map bank/map no) to xx.
+ xx = trigger event number that should be activated
+ [12][MapBank][MapNo][xx]
+ """
+ size = 4
+ command["map_group"] = ord(rom[start_address+1])
+ command["map_id"] = ord(rom[start_address+2])
+ command["trigger_number"] = ord(rom[start_address+3])
+ elif command_byte == 0x13: #Trigger event check
+ info = "Trigger event check"
+ long_info = """
+ Checks the number of the trigger events on the current map.
+ [13]
+ """
+ size = 1
+ elif command_byte == 0x14: #De-/activate trigger event [xx]
+ info = "De-/activate trigger event [xx]"
+ long_info = """
+ Changes trigger event number on current map to xx.
+ xx = trigger event number that should be activated
+ [14][xx]
+ deactivate? Just activate a different trigger event number. There's a limit of 1 active trigger.
+ """
+ size = 2
+ command["trigger_number"] = ord(rom[start_address+1])
+ elif command_byte == 0x15: #Load variable into RAM [xx]
+ info = "Load variable into RAM [xx]"
+ long_info = "[15][xx]"
+ size = 2
+ command["variable"] = ord(rom[start_address+1])
+ elif command_byte == 0x16: #Add variables [xx]
+ info = "Add variables [xx]"
+ long_info = """
+ Adds xx and the variable in RAM.
+ #[16][xx]
+ """
+ size = 2
+ command["variable"] = ord(rom[start_address+1])
+ elif command_byte == 0x17: #Random number [xx]
+ info = "Random number [xx]"
+ long_info = """
+ #Reads xx and creates a random number between 00 and xx -1.
+ #According to this xx can be all but 00. Random number = [00; xx)
+ #The nearer the random number is to xx, the rarer it occurs.
+ #Random number gets written to RAM.
+ """
+ size = 2
+ command["rarest"] = ord(rom[start_address+1])
+ elif command_byte == 0x18: #Version check
+ info = "G/S version check"
+ long_info = """
+ #Check if version is gold or silver. Gives feedback.
+ #00 = Gold
+ #01 = Silver
+ #[18]
+ """
+ size = 1
+ elif command_byte == 0x19: #Copy variable code1 [xxyy]
+ info = "Copy from RAM address to script RAM variable [xxyy]"
+ long_info = """
+ #Writes variable from ram address to RAM.
+ #[19][2-byte RAM address]
+ """
+ size = 3
+ #XXX maybe a pointer is a bad idea?
+ command["pointer"] = calculate_pointer_from_bytes_at(start_address+1, bank=False)
+ elif command_byte == 0x1A: #Copy variable code2 [xxyy]
+ info = "Write variable from script RAM variable to actual RAM address [xxyy]"
+ long_info = """
+ #Writes variable from RAM to actual RAM address.
+ #[1A][2-byte RAM address]
+ """
+ size = 3
+ command["pointer"] = calculate_pointer_from_bytes_at(start_address+1, bank=False)
+ elif command_byte == 0x1B: #Load variable [xxyyzz]
+ info = "Load variable [xxyy][zz]"
+ long_info = """
+ #Writes zz to ram address.
+ #[1B][2-byte RAM address][zz]
+ """
+ size = 4
+ command["pointer"] = calculate_pointer_from_bytes_at(start_address+1, bank=False)
+ command["value"] = ord(rom[start_address+3])
+ elif command_byte == 0x1C: #Check codes [xx]
+ #XXX no idea what's going on in this one :(
+ info = "Check pre-ID-mapped RAM location [xx]"
+ long_info = """
+ #Checks special game-technical values and writes then into RAM.
+ #[1C][following part][Ram check (when <> 08/09 see „numbers“ in list of following parts)]
+ #following part (and then hex values)
+ #01 = PKMN count in party
+ # 00 - 06
+ #02 = ???
+ #03 = Battle type of wild PKMN
+ #04 = ???
+ #05 = PokéDex caught
+ # 00 - FA
+ #06 = PokéDex seen
+ # 00 - FA
+ #07 = Badge count
+ # 00 - 10
+ #08 = Movement
+ # 00 = walk
+ # 01 = bike
+ # 02 = slipping
+ # 04 = surfer
+ # 08 = surfing pikachu
+ #09 = HIRO direction
+ # 00 (d)
+ # 01 (u)
+ # 02 (l)
+ # 03 (r)
+ #0A = Time in hours
+ # 00 - 18
+ #0B = Day
+ # 00 (Mo) - 06 (Su)
+ #0C = Map bank of current map
+ #0D = Map no of current map
+ #0E = Num. of diff. unowns seen
+ # 00 - 1A
+ #0F = Action byte of map
+ #10 = Amount of free spaces in pkmn box
+ # 00 - 14
+ #11 = Minutes until end bug contest
+ # 00 - 14
+ #12 = X position of HIRO
+ #13 = Y position of HIRO
+ #14 = phone call number
+ """
+ size = 2 #i think?
+ command["following_part"] = ord(rom[start_address+1])
+ elif command_byte == 0x1D: #Input code1 [xx]
+ info = "Write to pre-ID-mapped RAM location [xx]"
+ long_info = """
+ #Writes variable from RAM to special game-technical value offsets.
+ #[1D][following part]
+ #where [following part] is the same as 0x1C
+ """
+ size = 2
+ command["following_part"] = ord(rom[start_address+1])
+ elif command_byte == 0x1E: #Input code2 [xxyy]
+ info = "Write byte value to pre-ID-mapped RAM location [aa][xx]"
+ long_info = """
+ #Writes variable xx to special game-technical value offsets.
+ #[1E][following part][xx]
+ #where [following part] is the same as 0x1C
+ """
+ size = 3
+ command["following_part"] = ord(rom[start_address+1])
+ command["value"] = ord(rom[start_address+2])
+ elif command_byte == 0x1F: #Give item code [xxyy]
+ info = "Give item by id and quantity [xx][yy]"
+ long_info = """
+ #Gives item (item no) amount times.
+ #feedback:
+ # 00 = bag full
+ # 01 = OK
+ #[1F][item no][amount]
+ """
+ size = 3
+ command["item_id"] = ord(rom[start_address+1])
+ command["quantity"] = ord(rom[start_address+2])
+ elif command_byte == 0x20: #Take item code [xxyy]
+ info = "Take item by id and quantity [xx][yy]"
+ long_info = """
+ #Gives item (item no) amount times
+ #feedback:
+ # 00 = not enough items
+ #[20][item no][amount]
+ """
+ size = 3
+ command["item_id"] = ord(rom[start_address+1])
+ command["quantity"] = ord(rom[start_address+2])
+ elif command_byte == 0x21: #Check for item code [xx]
+ info = "Check if player has item [xx]"
+ long_info = """
+ #Checks if item is possessed.
+ #feedback:
+ # 00 = does not have item
+ # 01 = has item
+ #[21][item no]
+ """
+ size = 2
+ command["item_id"] = ord(rom[start_address+1])
+ elif command_byte == 0x22: #Give money code [xxyyzzaa]
+ info = "Give money to HIRO/account [xxyyzzaa]"
+ long_info = """
+ #Gives zzyyxx money to HIRO/account.
+ #zzyyxx = amount of money (000000 - 0F423F)
+ #[22][00-HIRO/01-account][xxyyzz]
+ """
+ size = 5
+ bytes = rom_interval(start_address, size, strings=False)
+ command["account"] = bytes[1]
+ command["amount"] = bytes[2:]
+ #raise NotImplementedError, "don't know if zzyyxx is a decimal or hex value"
+ elif command_byte == 0x23: #Take money code [xxyyzzaa]
+ info = "Take money from HIRO/account [xxyyzzaa]"
+ long_info = """
+ #Takes zzyyxx money from HIRO/account.
+ #zzyyxx = amount of money (000000 - 0F423F)
+ #[23][00-HIRO/01-account][xxyyzz]
+ """
+ size = 5
+ bytes = rom_interval(start_address, size, strings=False)
+ command["account"] = bytes[1]
+ command["amount"] = bytes[2:]
+ #raise NotImplementedError, "don't know if zzyyxx is a decimal or hex value"
+ elif command_byte == 0x24: #Check for money code [xxyyzzaa]
+ info = "Check if HIRO/account has enough money [xxyyzzaa]"
+ long_info = """
+ #Checks if HIRO/account has got zzyyxx money.
+ #feedback:
+ # 00 = enough money
+ # 01 = exact amount
+ # 02 = less money
+ #zzyyxx = amount of money (000000 - 0F423F)
+ #[24][00-HIRO/01-account][xxyyzz]
+ """
+ size = 5
+ bytes = rom_interval(start_address, size, strings=False)
+ command["account"] = bytes[1]
+ command["quantity"] = bytes[2:]
+ #XXX how is quantity formatted?
+ #raise NotImplementedError, "don't know if zzyyxx is a decimal or hex value"
+ elif command_byte == 0x25: #Give coins code [xxyy]
+ info = "Give coins to HIRO [xxyy]"
+ long_info = """
+ #Gives coins to HIRO.
+ #yyxx = amount of coins (0000 - 270F)
+ #[25][xxyy]
+ """
+ size = 3
+ bytes = rom_interval(start_address, size, strings=False)
+ command["quantity"] = bytes[1] + (bytes[2] << 8)
+ elif command_byte == 0x26: #Take coins code [xxyy]
+ info = "Take coins from HIRO [xxyy]"
+ long_info = """
+ #Takes coins away from HIRO.
+ #yyxx = amount of coins (0000 - 270F)
+ #[26][xxyy]
+ """
+ size = 3
+ bytes = rom_interval(start_address, size, strings=False)
+ command["quantity"] = bytes[1] + (bytes[2] << 8)
+ elif command_byte == 0x27: #Check for coins code [xxyy]
+ info = "Check if HIRO has enough coins [xxyy]"
+ long_info = """
+ #Checks if HIRO has enough coins.
+ #feedback:
+ # 00 = has enough coins
+ # 01 = has exact amount
+ # 02 = does not have enough
+ #yyxx = amount of coins necessary (0000 - 270F)
+ #[27][xxyy]
+ """
+ size = 3
+ bytes = rom_interval(start_address, size, strings=False)
+ command["quantity"] = bytes[1] + (bytes[2] << 8)
+ elif command_byte == 0x28: #Give cell phone number [xx]
+ info = "Give cell phone number [xx]"
+ long_info = """
+ #Gives number to HIRO.
+ #feedback:
+ # 00 = number was added successfully
+ # 01 = Number already added, or no memory
+ #xx = number of person
+ #[28][xx]
+ #01 = mother
+ #02 = bike store
+ #03 = bll
+ #04 = elm
+ """
+ size = 2
+ command["number"] = ord(rom[start_address+1])
+ elif command_byte == 0x29: #Take cell phone number [xx]
+ info = "Delete cell phone number [xx]"
+ long_info = """
+ #Deletes a number from the list.
+ #feedback:
+ # 00 = number deleted successfully
+ # 01 = number wasn't in list
+ #xx = number of person
+ #[29][xx]
+ """
+ size = 2
+ command["number"] = ord(rom[start_address+1])
+ elif command_byte == 0x2A: #Check for cell phone number [xx]
+ info = "Check for cell phone number [xx]"
+ long_info = """
+ #Checks if a number is in the list.
+ #feedback:
+ # 00 = number is in list
+ # 01 = number not in list
+ #xx = number to look for
+ #[2A][xx]
+ """
+ size = 2
+ command["number"] = ord(rom[start_address+1])
+ elif command_byte == 0x2B: #Check time of day [xx]
+ info = "Check time of day [xx]"
+ long_info = """
+ #Checks the time of day.
+ #feedback:
+ # 00 = time of day is the same
+ # 01 = time of day is not the same
+ #[2B][time of day (01morn-04night)]
+ """
+ size = 2
+ command["time_of_day"] = ord(rom[start_address+1])
+ elif command_byte == 0x2C: #Check for PKMN [xx]
+ info = "Check for pokemon [xx]"
+ long_info = """
+ #Checks if there is a certain PKMN in team.
+ #feedback:
+ # 00 = in team
+ # 01 = not in team
+ #xx = pkmn id
+ #[2C][xx]
+ """
+ size = 2
+ command["pokemon_id"] = ord(rom[start_address+1])
+ elif command_byte == 0x2D: #Give PKMN [xxyyzzaa(+2b +2b)]
+ info = "Give pokemon [pokemon][level][item][trainer2b][...]"
+ long_info = """
+ #Gives a PKMN if there's space
+ #feedback:
+ # trainer id
+ #[2D][PKMN][PKMNlvl][PKMNitem][TRAINER]
+ #trainer:
+ # 00 = HIRO
+ # 01 = after the main code there are 4 bytes added
+ # [2byte pointer to trainer's name (max.0x0A figures + 0x50)][2byte pointer to nickname (max.0x0A figures + 0x50)]
+ """
+ size = 5
+ bytes = rom_interval(start_address, size, strings=False)
+ command["pokemon_id"] = bytes[1]
+ command["pokemon_level"] = bytes[2]
+ command["held_item_id"] = bytes[3]
+ command["trainer"] = bytes[4]
+ if command["trainer"] == 0x01:
+ size += 4
+ bytes = rom_interval(start_address, size, strings=False)
+ command["trainer_name_pointer"] = calculate_pointer_from_bytes_at(start_address+5, bank=False)
+ command["pokemon_nickname_pointer"] = calculate_pointer_from_bytes_at(start_address+7, bank=False)
+ elif command_byte == 0x2E: #Give EGG [xxyy]
+ info = "Give egg [xx][yy]"
+ long_info = """
+ #Gives egg if there's space.
+ #feedback:
+ # 00 = OK
+ # 02 = transaction not complete
+ #[2E][PKMN][PKMNlvl]
+ """
+ size = 3
+ command["pokemon_id"] = ord(rom[start_address+1])
+ command["pokemon_level"] = ord(rom[start_address+2])
+ elif command_byte == 0x2F: #Attach item code [2B]
+ info = "Attach item to last pokemon in list [xxyy]"
+ long_info = """
+ #Gives last PKMN in list an item and letter text if applicable. Replaces existing items.
+ #[2F][2byte pointer to item no + 0x20 bytes letter text]
+ """
+ size = 3
+ command["item_pointer"] = calculate_pointer_from_bytes_at(start_address+1, bank=False)
+ #XXX are those 20 bytes supposed to happen here? or at the pointer's destination?
+ elif command_byte == 0x30: #Check letter code [2b]
+ info = "Check letter against known letter [xxyy]"
+ long_info = """
+ #Opens a PKMN list. Selected PKMN must have the right letter + the right contents. If OK, then PKMN is taken away
+ #feedback:
+ # 00 = wrong letter
+ # 01 = OK
+ # 02 = Cancelled
+ # 03 = Chosen PKMN has no letter
+ # 04 = Chosen PKMN is the only one in the list.
+ #[30][2byte pointer to letter item no + 0x20 bytes letter text]
+ """
+ size = 3
+ command["item_pointer"] = calculate_pointer_from_bytes_at(start_address+1, bank=False)
+ elif command_byte == 0x31: #BitTable1 check [xxyy]
+ info = "Check some bit on bit table 1 [xxyy]"
+ long_info = """
+ #Checks whether a bit of BitTable1 has the value 0 or 1.
+ #feedback:
+ # 00 = value 0 (off)
+ # 01 = value 1 (on)
+ #[31][2-byte bit number]
+ """
+ #XXX what format is the 2-byte number in?
+ size = 3
+ bytes = rom_interval(start_address+1, size-1, strings=False)
+ command["bit_number_bytes"] = bytes
+ #raise NotImplementedError, "what format is the 2-byte number in?"
+ elif command_byte == 0x32: #BitTable1 reset [xxyy]
+ info = "Reset (to 0) a bit on bit table 1 [xxyy]"
+ long_info = """
+ #Sets a bit of BitTable1 to value 0.
+ #[32][Bit no (2byte)]
+ """
+ size = 3
+ bytes = rom_interval(start_address+1, size-1, strings=False)
+ command["bit_number_bytes"] = bytes
+ elif command_byte == 0x33: #BitTable1 set [xxyy]
+ info = "Set (to 1) a bit on bit table 1 [xxyy]"
+ long_info = """
+ #Sets a bit of BitTable1 to value 1.
+ #[33][Bit-No (2byte)]
+ """
+ size = 3
+ bytes = rom_interval(start_address+1, size-1, strings=False)
+ command["bit_number_bytes"] = bytes
+ elif command_byte == 0x34: #BitTable2 check [xxyy]
+ info = "Check some bit on bit table 2 [xxyy]"
+ long_info = """
+ #Checks whether a bit of BitTable2 has the value 0 or 1.
+ #feedback:
+ # 00 = value 0 (off)
+ # 01 = value 1 (on)
+ #[34][Bit no (2byte)]
+ """
+ size = 3
+ bytes = rom_interval(start_address+1, size-1, strings=False)
+ command["bit_number_bytes"] = bytes
+ elif command_byte == 0x35: #BitTable2 reset [xxyy]
+ info = "Reset (to 0) a bit on bit table 2 [xxyy]"
+ long_info = """
+ #Sets a bit of BitTable2 to value 0.
+ #[35][Bit no (2byte)]
+ """
+ size = 3
+ bytes = rom_interval(start_address+1, size-1, strings=False)
+ command["bit_number_bytes"] = bytes
+ elif command_byte == 0x36: #BitTable2 set [xxyy]
+ info = "Set (to 1) a bit on bit table 2 [xxyy]"
+ long_info = """
+ #Sets a bit of BitTable2 to value 1.
+ #[36][Bit no (2byte)]
+ """
+ size = 3
+ bytes = rom_interval(start_address+1, size-1, strings=False)
+ command["bit_number_bytes"] = bytes
+ elif command_byte == 0x37: #Deactivate PKMN battles
+ info = "Turn off wild pokemon battles"
+ long_info = """
+ #This code turns all wild PKMN battles off.
+ #[37]
+ """
+ size = 1
+ elif command_byte == 0x38: #Activate PKMN battles
+ info = "Turn no wild pokemon battles"
+ long_info = "This code turns all wild PKMN battles on."
+ size = 1
+ elif command_byte == 0x39: #X/Y comparison [xxyy]
+ info = "BUGGY x/y comparison [xxyy]"
+ long_info = """
+ #This code is buggy (Bug fix: 0x3021 --> C6) and can't used as
+ #described without fix. This code compares the X and Y coordinates of
+ #HIRO with the ones in a table (max. 20h XY pairs) on the current map.
+ #It sets or resets the 4 bytes D17C to D17F accordingly to this table,
+ #1 bit for every table entry. To be useful, this code can only be used
+ #in a command queue, because with every regular move of HIRO the bits
+ #are reset again. This code is an alternative to the trigger events and
+ #can be used via the command queue code.
+ #See Write command queue, Additional documentation: 3:4661 with c= index
+ #in table (start=00), hl=D171, b=01, d=00.
+ """
+ size = 3
+ command["table_pointer"] = rom_interval(start_address+1, size-1, strings=False)
+ elif command_byte == 0x3A: #Warp modifier [xxyyzz]
+ info = "Set target for 0xFF warps [warp id][map group][map id]"
+ long_info = """
+ #Changes warp data for all warps of the current map that have a 0xFF for warp-to data.
+ #[3A][Nee warp-to][New map bank][New map no]
+ """
+ size = 4
+ bytes = rom_interval(start_address+1, size-1, strings=False)
+ command["nee_warp_to"] = bytes[0]
+ command["map_group"] = bytes[1]
+ command["map_id"] = bytes[2]
+ elif command_byte == 0x3B: #Blackout modifier [xxyy]
+ info = "Blackout warp modifier [map group][map id]"
+ long_info = """
+ #Changes the map HIRO arrives at, after having a blackout.
+ #There needs to be flying data for that map.
+ #[3B][Map bank][Map no]
+ """
+ size = 3
+ command["map_group"] = ord(rom[start_address+1])
+ command["map_id"] = ord(rom[start_address+2])
+ elif command_byte == 0x3C: #Warp code [xxyyzzaa]
+ info = "Warp to [map group][map id][x][y]"
+ long_info = """
+ #Warps to another map.
+ #If all data is 00s, then the current map is reloaded with
+ #the current X and Y coordinates. Old script is not finished
+ #without a [90].
+ #[3C][Map bank][Map no][X][Y]
+ """
+ size = 5
+ command["map_group"] = ord(rom[start_address+1])
+ command["map_id"] = ord(rom[start_address+2])
+ command["x"] = ord(rom[start_address+3])
+ command["y"] = ord(rom[start_address+4])
+ elif command_byte == 0x3D: #Account code [xxyy]
+ info = "Read money amount [xx][yy]"
+ long_info = """
+ #Reads amount of money in accounts of HIRO and mother and writes
+ #it to MEMORY1, 2 or 3 for later use in text.
+ #[3D][00 = HIRO| <> 00 = Mother][00-02 MEMORY]
+ #see http://hax.iimarck.us/files/scriptingcodes_eng.htm#InText01
+ """
+ size = 3
+ command["account_id"] = ord(rom[start_address+1])
+ command["memory_id"] = ord(rom[start_address+2])
+ elif command_byte == 0x3E: #Coin case code [xx]
+ info = "Read coins amount [xx]"
+ long_info = """
+ #Reads amount of coins in coin case and writes it to MEMORY 1, 2,
+ #or 3 for later use in text.
+ #[3E][00-02 MEMORY]
+ """
+ size = 2
+ command["memory_id"] = ord(rom[start_address+1])
+ elif command_byte == 0x3F: #Display RAM [xx]
+ info = "Copy script RAM value into memX [xx]"
+ long_info = """
+ #Reads RAM value and writes it to MEMORY1, 2 or 3 for later use in text.
+ #[3F][00-02 MEMORY]
+ """
+ size = 2
+ command["memory_id"] = ord(rom[start_address+1])
+ elif command_byte == 0x40: #Display pokémon name [xxyy]
+ info = "Copy pokemon name (by id) to memX [id][xx]"
+ long_info = """
+ #Writes pokémon name to MEMORY1, 2 or 3 for later use in text.
+ #[40][PKMN no][00-02 MEMORY]
+ """
+ size = 3
+ command["map_id"] = ord(rom[start_address+1])
+ command["memory_id"] = ord(rom[start_address+2])
+ elif command_byte == 0x41: #Display item name [xxyy]
+ info = "Copy item name (by id) to memX [id][xx]"
+ long_info = """
+ #Writes item name to MEMORY1, 2 or 3 for later use in text.
+ #[41][Item no][00-02 MEMORY]
+ """
+ size = 3
+ command["item_id"] = ord(rom[start_address+1])
+ command["memory_id"] = ord(rom[start_address+2])
+ elif command_byte == 0x42: #Display location name [xx]
+ info = "Copy map name to memX [xx]"
+ long_info = """
+ #Writes current location's name to MEMORY1, 2 or 3 for later use in text.
+ #[42][00-02 MEMORY]
+ """
+ size = 2
+ command["memory_id"] = ord(rom[start_address+1])
+ elif command_byte == 0x43: #Display trainer name [xxyyzz]
+ info = "Copy trainer name (by id&group) to memZ [xx][yy][zz]"
+ long_info = """
+ #Writes trainer name to MEMORY1, 2 or 3 for later use in text.
+ #[43][Trainer number][Trainer group][00-02 MEMORY]
+ """
+ size = 4
+ command["trainer_id"] = ord(rom[start_address+1])
+ command["trainer_group"] = ord(rom[start_address+2])
+ command["memory_id"] = ord(rom[start_address+3])
+ elif command_byte == 0x44: #Display strings [2b + xx]
+ info = "Copy text (by pointer) to memX [aabb][xx]"
+ long_info = """
+ #Writes string to MEMORY1, 2 or 3 for later use in text.
+ #[44][2byte pointer to string (max. 0x0C figures + 0x50)][00-02 MEMORY]
+ #See 0C codes: 0C2900, 0C2A00, 0C1B00, 0C2200, Usage of variable strings in text.
+ """
+ size = 4
+ command["pointer"] = calculate_pointer_from_bytes_at(start_address+1, bank=False)
+ command["memory_id"] = ord(rom[start_address+3])
+ command["text"] = parse_text_engine_script_at(command["pointer"], map_group=map_group, map_id=map_id, debug=debug)
+ elif command_byte == 0x45: #Stow away item code
+ info = "Show HIRO put the ITEMNAME in the ITEMPOCKET text box"
+ long_info = """
+ #Text box: "HIRO put the ITEMNAME in the ITEMPOCKET."
+ #The item number has to have been loaded beforehand
+ #(e.g. by Give item code).
+ """
+ size = 1
+ elif command_byte == 0x46: #Full item pocket code
+ info = "Show ITEMPOCKET is full textbox"
+ long_info = """
+ #Text box: "ITEMPOCKET is full..." The item number has to have
+ #been loaded beforehand (e.g. by Give item code).
+ """
+ size = 1
+ elif command_byte == 0x47: #Text box&font code
+ info = "Loads the font into the ram and opens a text box."
+ size = 1
+ elif command_byte == 0x48: #Refresh code [xx]
+ info = "Screen refresh [xx]"
+ long_info = """
+ #Executes a complete screen refresh.
+ #[48][xx]
+ #xx is a dummy byte
+ """
+ size = 2
+ command["dummy"] = ord(rom[start_address+1])
+ elif command_byte == 0x49: #Load moving sprites
+ info = "Load moving sprites into memory"
+ long_info = "Loads moving sprites for person events into ram."
+ size = 1
+ elif command_byte == 0x4A: #Load byte to C1CE [xx]
+ info = "Load specific byte to $C1CE [xx]"
+ long_info = """
+ #Loads a byte to C1CE. Seems to have no function in the game.
+ #[4A][Byte]
+ """
+ size = 2
+ command["byte"] = ord(rom[start_address+1])
+ elif command_byte == 0x4B: #Display text [3b]
+ info = "Display text by pointer [bb][xxyy]"
+ long_info = """
+ #Opens a text box and writes text. Doesn't load font.
+ #[4B][Text bank][2byte text pointer]
+ """
+ size = 4
+ command["text_group"] = ord(rom[start_address+1])
+ command["pointer"] = calculate_pointer_from_bytes_at(start_address+1, bank=True)
+ command["text"] = parse_text_engine_script_at(command["pointer"], map_group=map_group, map_id=map_id, debug=debug)
+ elif command_byte == 0x4C: #Display text [2b]
+ info = "Display text by pointer [xxyy]"
+ long_info = """
+ #Opens a text box and writes text. Doesn't load font.
+ #[4C][2byte text pointer]
+ """
+ size = 3
+ command["pointer"] = calculate_pointer_from_bytes_at(start_address+1, bank=False)
+ command["text"] = parse_text_engine_script_at(command["pointer"], map_group=map_group, map_id=map_id, debug=debug)
+ elif command_byte == 0x4D: #Repeat text [xxyy]
+ info = "Repeat text [FF][FF]"
+ long_info = """
+ #Opens a text box and writes the text written latest resp. whose address was put statically to D175-D177.
+ #Doesn't load font.
+ #[4D][FF][FF]
+ #Without FF for both bytes, no operation occurs
+ """
+ size = 3
+ command["bytes"] = rom_interval(start_address+1, 2, strings=False)
+ elif command_byte == 0x4E: #YES/No box
+ info = "YES/No box"
+ long_info = """
+ #Displays a YES/NO box at X0F/Y07
+ #feedback:
+ # 00 = no
+ # 01 = yes
+ """
+ size = 1
+ elif command_byte == 0x4F: #Menu data code [2b]
+ info = "Load menu data by pointer [xxyy]"
+ long_info = """
+ #Loads data for menus
+ #[4F][2byte pointer to menu data]
+ #see http://hax.iimarck.us/files/scriptingcodes_eng.htm#ZusatzDatA4F
+ """
+ size = 3
+ command["menu_pointer"] = calculate_pointer_from_bytes_at(start_address+1, bank=False)
+ elif command_byte == 0x50: #Write backup code
+ info = "Write screen backup"
+ long_info = "Writes backup of parts of the screen the box was overlapping."
+ size = 1
+ elif command_byte == 0x51: #Text1 code [2b]
+ info = "Display text (by pointer), turn to HIRO, end [xxyy]"
+ long_info = """
+ #Displays a text and lets person turn to HIRO.
+ #Afterwards there is no other script interpreted.
+ #Corresponds to 6A + 47 + 4C + 53 + 49 + 90
+ #[51][2byte textpointer]
+ """
+ size = 3
+ end = True
+ command["pointer"] = calculate_pointer_from_bytes_at(start_address+1, bank=False)
+ command["text"] = parse_text_engine_script_at(command["pointer"], map_group=map_id, map_id=map_id, debug=debug)
+ elif command_byte == 0x53: #Text2 code [2b]
+ info = "Display text (by pointer) and end [xxyy]"
+ long_info = """
+ #Displays a text. Afterwards there is no other script interpreted.
+ #Corresponds to 47 + 4C + 53 + 49 + 90
+ #[52][2byte textpointer]
+ """
+ size = 3
+ end = True
+ command["pointer"] = calculate_pointer_from_bytes_at(start_address+1, bank=False)
+ command["text"] = parse_text_engine_script_at(command["pointer"], map_group=map_id, map_id=map_id, debug=debug)
+ elif command_byte == 0x54: #Close text box code
+ info = "Close text box"
+ long_info = "Closes a text box which was opened by 47 resp. 4B/4C/4D."
+ size = 1
+ elif command_byte == 0x55: #Keep text box open code
+ info = "Keep text box open"
+ long_info = "Keeps a text box open which was opened by 47 resp. 4B/4C/4D."
+ size = 1
+ elif command_byte == 0x56: #Pokémon picture code [xx]
+ info = "Display a pokemon picture in a box by pokemon id [xx]"
+ long_info = """
+ #Opens a box and puts a Pokémon picture into it.
+ #[55][xx]
+ #xx:
+ # <>00 : Pokémon no
+ # =00 : Pokémon no gets read from RAM
+ """
+ size = 2
+ command["byte"] = ord(rom[start_address+1])
+ elif command_byte == 0x57: #Pokémon picture YES/NO code
+ info = "?? Display a pokemon picture and a yes/no box"
+ long_info = """
+ #Displays a YES/NO box at X08/Y05.
+ #feedback:
+ # 00 = no chosen
+ # 01 = yes chosen
+ """
+ size = 1
+ elif command_byte == 0x58: #Menu interpreter 1
+ info = "Menu interpreter 1 (see menu loader)"
+ long_info = """
+ #Interprets menu data loaded by 4F.
+ #see also http://hax.iimarck.us/files/scriptingcodes_eng.htm#ZusatzDatA57
+ """
+ size = 1
+ elif command_byte == 0x59: #Menu interpreter 2
+ info = "Menu interpreter 2 (see menu loader)"
+ long_info = """
+ #Interprets menu data loaded by 4F.
+ #see also http://hax.iimarck.us/files/scriptingcodes_eng.htm#Marke57
+ #see also http://hax.iimarck.us/files/scriptingcodes_eng.htm#ZusatzDatA58
+ """
+ size = 1
+ elif command_byte == 0x5A: #Load Pikachu data
+ info = "Load pikachu data"
+ long_info = "Loads 0x19 (Pikachu) to PokéRAM and level 5 to LevelRAM."
+ size = 1
+ elif command_byte == 0x5B: #Delete FightRAM/reset person check
+ info = "? Disable fleeing from battle"
+ long_info = """
+ #Deletes the value in BattleRAM.
+ #Turns off the check if the battle was started by entering
+ #a trainer's area of view.
+ """
+ size = 1
+ elif command_byte == 0x5C: #Load trainer data1
+ info = "Load trainer from RAM"
+ long_info = """
+ #Loads trainer data when HIRO is in a trainer's range of sight.
+ #Trainer group is read from CF2E and written to
+ #TrRAM1, the trainer number is read from CF2F and written to
+ #TrRAM2. 81 is written to BattleRAM.
+ """
+ size = 1
+ elif command_byte == 0x5D: #Load Pokémon data [xxyy]
+ info = "Loads pokemon by id and level for BattleRAM [xx][yy]"
+ long_info = """
+ #Loads Pokémon data. Writes 80 to BattleRAM.
+ #[5C][Poke no][Level]
+ """
+ size = 3
+ command["pokemon_id"] = ord(rom[start_address+1])
+ command["pokemon_level"] = ord(rom[start_address+2])
+ elif command_byte == 0x5E: #Load trainer data2 [xxyy]
+ info = "Load trainer by group/id for BattleRAM [xx][yy]"
+ long_info = """
+ #Loads trainer data. Trainer group --> TrRAM1,
+ #trainer number --> TrRAM2. Writes 81 to BattleRAM.
+ #[5D][Trainer group][Trainer no]
+ """
+ size = 3
+ command["trainer_group"] = ord(rom[start_address+1])
+ command["trainer_id"] = ord(rom[start_address+2])
+ elif command_byte == 0x5F: #Start battle
+ info = "Start pre-configured battle"
+ long_info = """
+ #Starts trainer or Pokémon battle. BattleRAM: 80 = Poké battle; 81 = Trainer battle.
+ #feedback:
+ # 00 = win
+ # 01 = lose
+ """
+ size = 1
+ elif command_byte == 0x60: #Return to In game engine after battle
+ info = "Return to in-game engine after battle"
+ long_info = "Returns to ingame engine and evaluates battle. When lost then return to last Pokémon Center etc."
+ size = 1
+ elif command_byte == 0x61: #Learn how to catch PKMN [xx]
+ info = "Pokemon catching tutorial [xx]"
+ long_info = """
+ #Starts a learn-how-to-catch battle with a Pokémon, whose data needs to be loaded beforehand
+ #see http://hax.iimarck.us/files/scriptingcodes_eng.htm#Marke5C
+ #Player has to have at least 1 Pokémon for it to work.
+ #Items that are statically used: 1xPotion, 5xPoké ball.
+ #[60][xx]
+ #xx: Between 01 and 03. If <> 03 then HIRO sprite instead of dude sprite and kills
+ #itself when using the item system.
+ """
+ size = 2
+ command["byte"] = ord(rom[start_address+1])
+ elif command_byte == 0x62: #Trainer text code
+ info = "Set trainer text by id [xx]"
+ long_info = """
+ #Interprets the data of a in the event structure defined trainer.
+ #[61][xx]
+ #Xx decides which text to use.
+ #xx: Between 00 and 03.
+ #see http://hax.iimarck.us/files/scriptingcodes_eng.htm#Eventaufbau
+ """
+ size = 2
+ command["byte"] = ord(rom[start_address+1])
+ elif command_byte == 0x63: #Trainer status code [xx]
+ info = "? Check trainer status [xx]"
+ long_info = """
+ #Checks/changes the status of a in the event structure defined trainer.
+ #see http://hax.iimarck.us/files/scriptingcodes_eng.htm#Eventaufbau
+ #[62][xx]
+ #xx is:
+ # 00 = deactivate
+ # 01 = activate
+ # 02 = check
+ """
+ size = 2
+ command["byte"] = ord(rom[start_address+1])
+ elif command_byte == 0x64: #Pointer Win/Lose [2b + 2b]
+ info = "Set win/lose pointers for battle [xxyy][xxyy]"
+ long_info = """
+ #Writes the win/lose pointer of a battle into the ram.
+ #[63][2byte pointer to text Win][2byte pointer to text Loss*]
+ #* When pointer = 0000 then "Blackout" instead of return to gameplay.
+ """
+ size = 5
+ #sometimes win/lost can be a pointer to 0000 or None?
+ command["won_pointer"] = calculate_pointer_from_bytes_at(start_address+1, bank=False)
+ command["lost_pointer"] = calculate_pointer_from_bytes_at(start_address+3, bank=False)
+ if command["won_pointer"] == None:
+ command["won_pointer"] = 0
+ else:
+ command["text_won"] = parse_text_engine_script_at(command["won_pointer"], map_group=map_id, map_id=map_id, debug=debug)
+ if command["lost_pointer"] == None:
+ command["lost_pointer"] = 0
+ else:
+ command["text_lost"] = parse_text_engine_script_at(command["lost_pointer"], map_group=map_id, map_id=map_id, debug=debug)
+ elif command_byte == 0x65: #Script talk-after
+ #XXX this is a really poor description of whatever this is
+ info = "? Load the trainer talk-after script"
+ long_info = """
+ #Interprets which script is going to be run, when a in the event-structure-defined
+ #trainer is talked to again.
+ #see http://hax.iimarck.us/files/scriptingcodes_eng.htm#Eventaufbau
+ #[64]
+ """
+ size = 1
+ elif command_byte == 0x66: #Script talk-after-cancel
+ info = "Disable/cancel trainer after-battle text"
+ long_info = """
+ #Cancels the talk-after script of the in the event-structure-defined
+ #trainer when talk-after script is executed just after the battle.
+ #[65]
+ """
+ size = 1
+ elif command_byte == 0x67: #Script talk-after-check
+ #XXX also bad explanation/name...
+ info = "? Check if trainer talk-after script is executed just after battle or not"
+ long_info = """
+ #Checks if the talk-after script of the event structure defined trainer
+ #is executed just after the battle or at a later point in time.
+ #feedback:
+ # 00 = no
+ # 01 = yes
+ #[66]
+ """
+ size = 1
+ elif command_byte == 0x68: #Set talked-to person [xx]
+ info = "Set last talked-to person [xx]"
+ long_info = """
+ #Sets the number of the last person talked to.
+ #[67][person]
+ """
+ size = 2
+ command["person_id"] = ord(rom[start_address+1])
+ elif command_byte == 0x69: #Moving code [xx + 2b]
+ info = "Move person (by id) with moving data (by pointer) [id][xxyy]"
+ long_info = """
+ #Moves the person using moving data.
+ #[68][Person][2byte pointer to moving data]
+ #see also http://hax.iimarck.us/files/scriptingcodes_eng.htm#ZusatzB68bis69
+ """
+ size = 4
+ command["person_id"] = ord(rom[start_address+1])
+ command["moving_data_pointer"] = calculate_pointer_from_bytes_at(start_address+2, bank=False)
+ elif command_byte == 0x6A: #Moving code for talked-to person [2b]
+ info = "Move talked-to person with moving data (by pointer) [xxyy]"
+ long_info = """
+ #Moves talked-to person using moving data.
+ #[69][2byte pointer to moving data]
+ """
+ size = 3
+ command["moving_data_pointer"] = calculate_pointer_from_bytes_at(start_address+1, bank=False)
+ elif command_byte == 0x6B: #Talk-to facing code
+ info = "Move talked-to person's facing direction to HIRO"
+ long_info = """
+ #Turns the heads of the talked-to persons to HIRO.
+ #[6A]
+ """
+ size = 1
+ elif command_byte == 0x6C: #Facing of people code [xxyy]
+ info = "Move facing direction of person1 to look at person2 [2][1]"
+ long_info = """
+ #Turns the head of person1 to another person2.
+ #[6B][Person2][Person1]
+ #Person2 = If number is greater than 0xFD, then use number of talked-to person.
+ #Person1 = If number equals 0xFE, then take number of talked-to person.
+ """
+ size = 3
+ command["person2_id"] = ord(rom[start_address+1])
+ command["person1_id"] = ord(rom[start_address+2])
+ elif command_byte == 0x6D: #Variable sprites [xxyy]
+ info = "Store value in variable sprite RAM location x by id Y [xx][yy]"
+ long_info = """
+ #Writes a number to the variable sprite RAM from D555 to D564 (see Compendium on the sprite system).
+ #[6C][xx][Sprite no]
+ #xx: Number between 0x00 and 0x0F
+ """
+ size = 3
+ command["number"] = ord(rom[start_address+1])
+ command["sprite_id"] = ord(rom[start_address+2])
+ elif command_byte == 0x6E: #Hide person [xx]
+ info = "Hide person by id [xx]"
+ long_info = """
+ #Hides a person.
+ #[6D][person id]
+ """
+ size = 2
+ command["person_id"] = ord(rom[start_address+1])
+ elif command_byte == 0x6F: #Show person [xx]
+ info = "Show person by id [xx]"
+ long_info = """
+ #Shows a hidden person again.
+ #[6E][person id]
+ """
+ size = 2
+ command["person_id"] = ord(rom[start_address+1])
+ elif command_byte == 0x70: #Following code1 [xxyy]
+ info = "Following code1 [leader id][follower id]"
+ long_info = """
+ #A person1 follows another person2. The person1 that follows
+ #just repeats the movement of person2, even if the persons are
+ #not directly next to each other.
+ #[6F][Leader Person2][Follower Person1]
+ """
+ size = 3
+ command["leader_person_id"] = ord(rom[start_address+1])
+ command["follower_person_id"] = ord(rom[start_address+2])
+ elif command_byte == 0x71: #Stop following code
+ info = "Stop all follow code"
+ long_info = "Ends all current follow codes."
+ size = 1
+ elif command_byte == 0x72: #Move person [xxyyzz]
+ info = "Move person by id to xy [id][xx][yy]"
+ long_info = """
+ #Sets the X/Y values of a person anew.
+ #The person doesn't get shown immediately. Use hide&show.
+ #[71][Person][X][Y]
+ """
+ size = 4
+ command["person_id"] = ord(rom[start_address+1])
+ command["x"] = ord(rom[start_address+2])
+ command["y"] = ord(rom[start_address+3])
+ elif command_byte == 0x73: #Write person location [xx] (lock person location?)
+ info = "Lock person's location by id [id]"
+ long_info = """
+ #Writes the current X/Y values of a person into the ram.
+ #The person is going to stand at its current location even when
+ #it's out of HIRO's sight and is not going to return to its old
+ #location until the next map load.
+ #[72][person]
+ """
+ size = 2
+ command["person_id"] = ord(rom[start_address+1])
+ elif command_byte == 0x74: #Load emoticons [xx]
+ info = "Load emoticon bubble [xx]"
+ long_info = """
+ #Loads the emoticon bubble depending on the given bubble number.
+ #[73][bubble number]
+ #xx: If xx = FF then take number from RAM.
+ # 00 = Exclamation mark
+ # 01 = Question mark
+ # 02 = Happy
+ # 03 = Sad
+ # 04 = Heart
+ # 05 = Flash
+ # 06 = Snoring
+ # 07 = Fish
+ """
+ size = 2
+ command["bubble_number"] = ord(rom[start_address+1])
+ elif command_byte == 0x75: #Display emoticon [xxyyzz]
+ info = "Display emoticon by bubble id and person id and time [xx][yy][zz]"
+ long_info = """
+ #Displays the bubble above a persons head for the given time period.
+ #Attention: Bubbles get loaded into ram!
+ #[74][Bubble][Person][Time]
+ #for bubble ids see 0x73
+ """
+ size = 4
+ command["bubble_number"] = ord(rom[start_address+1])
+ command["person_id"] = ord(rom[start_address+2])
+ command["time"] = ord(rom[start_address+3])
+ elif command_byte == 0x76: #Change facing [xxyy]
+ info = "Set facing direction of person [person][facing]"
+ long_info = """
+ #Changes the facing direction of a person.
+ #[75][person][facing]
+ """
+ size = 3
+ command["person_id"] = ord(rom[start_address+1])
+ command["facing"] = ord(rom[start_address+2])
+ elif command_byte == 0x77: #Following code2 [xxyy]
+ info = "Following code2 [leader id][follower id]"
+ long_info = """
+ #A person1 follows a person2. The following person1 automatically clings to person2.
+ #Person1 just follows person2, but doesn't make the exact same movements at person2.
+ #[76][Leader Person2][Follower Person1]
+ """
+ size = 3
+ command["leader_person_id"] = ord(rom[start_address+1])
+ command["follower_person_id"] = ord(rom[start_address+2])
+ elif command_byte == 0x78: #Earth quake [xx]
+ info = "Earthquake [xx]"
+ long_info = """
+ #The screen shakes. xx gives time as well as displacement of the screen.
+ #[77][xx]
+ """
+ size = 2
+ command["shake_byte"] = ord(rom[start_address+1])
+ elif command_byte == 0x79: #Exchange map [3b]
+ info = "Draw map data over current map [bank][pointer]"
+ long_info = """
+ #This code draws another whole map as wide and high as the
+ #current map over the current map.
+ #The 3byte pointer points to the new map.
+ #[78][3byte pointer to new map data]
+ """
+ size = 4
+ command["map_data_pointer"] = calculate_pointer_from_bytes_at(start_address+1, bank=True)
+ elif command_byte == 0x7A: #Change block code [xxyyzz]
+ info = "Change block to block id on map [xx][yy][id]"
+ long_info = """
+ #Changes a block on the current map by giving the new block
+ #number and its X/Y values measured in half-blocks.
+ #[79][X][Y][Block]
+ """
+ size = 4
+ command["x"] = ord(rom[start_address+1])
+ command["y"] = ord(rom[start_address+2])
+ command["block"] = ord(rom[start_address+3])
+ elif command_byte == 0x7B: #Reload map code
+ info = "Reload/redisplay map"
+ long_info = """
+ #Reloads and re-displays the map completely.
+ #Loads tileset and all map data anew. Screen gets light.
+ #[7A]
+ """
+ size = 1
+ elif command_byte == 0x7C: #Reload map part code
+ info = "Reload/redisplay map portion occupied by HIRO"
+ long_info = """
+ #Reloads and re-displays the part of the map HIRO is on,
+ #without reloading any other map data or the tileset.
+ #[7B]
+ """
+ size = 1
+ elif command_byte == 0x7D: #Write command queue
+ info = "Write command queue [xxyy]"
+ long_info = """
+ #Writes a command queue to the next free slot in ram.
+ #Max 4 command queues à 5 bytes. This code is buggy (bug fix: 25:7C74 --> 12).
+ #[7C][2byte pointer to 5byte command queue]
+ #see http://hax.iimarck.us/files/scriptingcodes_eng.htm#ZusatzDok25_7CC9
+ """
+ size = 3
+ command["command_queue_pointer"] = calculate_pointer_from_bytes_at(start_address+1, bank=False)
+ elif command_byte == 0x7E: #Delete command queue
+ info = "Delete command queue"
+ long_info = """
+ #Deletes a command queue and frees a slot in ram.
+ #[7D][First command of the resp. queue]
+ """
+ #XXX wtf?
+ size = 2
+ command["first_command"] = ord(rom[start_address+1])
+ elif command_byte == 0x7F: #Song code1 [xxyy]
+ info = "Play music by number [xxyy]"
+ long_info = """
+ #Immediately plays the music.
+ #[7E][Music no (2byte)]
+ #Music no: See the music archive that should accompany
+ #this document Thanks to Filb. He dumped all the songs
+ #via gameboy player and gave them to me.
+ """
+ size = 3
+ #XXX what is the format of this music data?
+ command["music_number"] = rom_interval(start_address+1, size-1, strings=False)
+ elif command_byte == 0x80: #Song code2
+ info = "Song code2"
+ long_info = """
+ #Plays the music of the trainer group in TrRAM1.
+ #Takes music numbers from list at 3A:5027.
+ #[7F]
+ """
+ size = 1
+ elif command_byte == 0x81: #Music fade-out code [xxyy][zz]
+ info = "Music fade-out then play next [xxyy][time]"
+ long_info = """
+ #The current music is faded out and the new music is played afterwards.
+ #[80][Music no (2byte)][Time to fade out (00-7F)]
+ """
+ size = 4
+ command["music_number"] = rom_interval(start_address+1, 2, strings=False)
+ command["fade_time"] = ord(rom[start_address+3])
+ elif command_byte == 0x82: #Play map music code
+ info = "Play map's music"
+ long_info = """
+ #Starts playing the original map music.
+ #Includes special check for surfer and bug contest song.
+ #[81]
+ """
+ size = 1
+ elif command_byte == 0x83: #Map reload music code
+ info = "Reload map music"
+ long_info = """
+ #After a map reload no music is played.
+ #[82]
+ """
+ size = 1
+ elif command_byte == 0x84: #Cry code [xx00]
+ info = "Play cry by id or RAM [cry][00]"
+ long_info = """
+ #Plays the Pokémon's cry.
+ #[83][Cry no][00]
+ #If the cry no = 00 then the number is taken from RAM.
+ """
+ size = 3
+ command["cry_number"] = ord(rom[start_address+1])
+ command["other_byte"] = ord(rom[start_address+2])
+ elif command_byte == 0x85: #Sound code [xxyy]
+ info = "Play sound by sound number [xxyy]"
+ long_info = """
+ #Plays the sound.
+ #[84][Sound no (2byte)]
+ #Sound no: See the music archive that should accompany this document
+ #Thanks to philb for this matter. He helped me to record a big
+ #part of these sounds.
+ """
+ size = 3
+ command["sound_number"] = rom_interval(start_address+1, 2, strings=False)
+ elif command_byte == 0x86: #Key-down code
+ info = "Wait for key-down"
+ long_info = """
+ #Waits for the Player to press a button.
+ #[85]
+ """
+ size = 1
+ elif command_byte == 0x87: #Warp sound
+ info = "Warp sound"
+ long_info = """
+ #Evaluates which sound is played when HIRO enters a Warp field.
+ #Usage via script ingame is rather not useful.
+ #[86]
+ """
+ size = 1
+ elif command_byte == 0x88: #Special sound
+ info = "Special sound if TM was last checked"
+ long_info = """
+ #When last given/checked Item was a TM then it plays sound 0x9B. If not, then 0x01.
+ #[87]
+ """
+ size = 1
+ elif command_byte == 0x89: #Engine remote control [2b]
+ info = "Engine remote control [bb][xxyy]"
+ long_info = """
+ #This code controls the engine via "data stream".
+ #[88][3byte pointer to control structure]
+ #see http://hax.iimarck.us/files/scriptingcodes_eng.htm#ZusatzDatA88
+ """
+ size = 4
+ command["data_stream_pointer"] = calculate_pointer_from_bytes_at(start_address+1, bank=True)
+ elif command_byte == 0x8A: #Load map anew [xx]
+ info = "Load map with specific loading process [xx]"
+ long_info = """
+ #The number decides which map loading process is used.
+ #The number must be 0xF0 + process number to work correctly.
+ #[89][Number]
+ #see map loading process:
+ # http://hax.iimarck.us/files/scriptingcodes_eng.htm#ZusatzDok5_5550
+ """
+ size = 2
+ command["number"] = ord(rom[start_address+1])
+ elif command_byte == 0x8B: #Waiting code [xx]
+ info = "Wait code"
+ long_info = """
+ #This code lets the game wait for 2 * xx time intervals.
+ #[8A][xx]
+ #xx: Numbers from 0x01 to 0xFF.
+ #If 0x00 is chosen then the time can be manipulated by previously loading a number to RAM2.
+ """
+ size = 2
+ command["time"] = ord(rom[start_address+1])
+ elif command_byte == 0x8C: #Deactivate static facing [xx]
+ info = "Deactive static facing after time [xx]"
+ long_info = """
+ #Deactivates static facings on all persons on the screen after a time xx.
+ #[8B][xx]
+ """
+ size = 2
+ command["time"] = ord(rom[start_address+1])
+ elif command_byte == 0x8D: #Priority jump1 [2b]
+ info = "Priority jump to script by pointer [xxyy]"
+ long_info = """
+ #The pointer acts like code 00, but with this higher
+ #functions like the bike etc. are not paid attention to,
+ #while the script is running.
+ #[8C][2byte pointer to script]
+ """
+ size = 3
+ script_pointer = calculate_pointer_from_bytes_at(start_address+1, bank=False)
+ if debug:
+ print "in script starting at "+hex(original_start_address)+\
+ " about to parse script at "+hex(script_pointer)+\
+ " called by "+info+" byte="+hex(command_byte)
+ script = rec_parse_script_engine_script_at(script_pointer, original_start_address, debug=debug)
+ command["script_pointer"] = script_pointer
+ command["script"] = script
+ end = True #according to pksv
+ elif command_byte == 0x8E: #Warp check
+ info = "Reactive all engine checks if player is warping"
+ long_info = """
+ #If HIRO is entering or leaving a warp then this code reactivates all the engine-checks.
+ #[8D]
+ """
+ size = 1
+ elif command_byte == 0x8F: #Priority jump2 [2b]
+ info = "Priority jump to script by pointer (after 1st cycle) [xxyy]"
+ long_info = """
+ #The pointer acts like code 03, but with this code all
+ #higher functions wait for a cycle before the script gets interpreted.
+ #[8E][2byte pointer to script]
+ """
+ size = 3
+ script_pointer = calculate_pointer_from_bytes_at(start_address+1, bank=False)
+ if debug:
+ print "in script starting at "+hex(original_start_address)+\
+ " about to parse script at "+hex(script_pointer)+\
+ " called by "+info+" byte="+hex(command_byte)
+ script = rec_parse_script_engine_script_at(script_pointer, original_start_address, debug=debug)
+ command["script_pointer"] = script_pointer
+ command["script"] = script
+ end = True #according to pksv
+ elif command_byte == 0x90: #Return code1
+ info = "Return code 1"
+ long_info = """
+ #Ends the current script and loads the backup offset for "linked"
+ #scripts if applicable. The sophisticated functions are not affected
+ #and run like before the code. This code is mostly used for scripts
+ #called by the 2nd part of the script header, because else malfunctions
+ #occur.
+ #[8F]
+ """
+ size = 1
+ end = True
+ elif command_byte == 0x91: #Return code2
+ info = "Return code 2"
+ long_info = """
+ #Ends the current script and loads the backup offset for "linked"
+ #scripts if applicable. The sophisticated functions get reset if
+ #no backup offset was loaded. This code is used to end most scripts.
+ #[90]
+ """
+ size = 1
+ end = True
+ elif command_byte == 0x92: #Return code3
+ info = "Return code 3"
+ long_info = """
+ #Reloads the map completely like the code 0x7A
+ #and else acts completely like Return code2
+ #[91]
+ #see reload map code
+ # http://hax.iimarck.us/files/scriptingcodes_eng.htm#Marke7A
+ #see 0x90
+ """
+ size = 1
+ #XXX does this end the script?? "else acts like 0x90"
+ # else? what's the "if"?
+ end = True
+ elif command_byte == 0x93: #Reset sophisticated functions
+ info = "Reset sophisticated functions"
+ long_info = """
+ #Resets all sophisticated functions to 0.
+ #[92]
+ """
+ size = 1
+ elif command_byte == 0x94: #Mart menu [xxyyzz]
+ info = "Mart menu [dialog no][mart no 2b]"
+ long_info = """
+ #Displays a whole mart menu, however, doesn't load font to ram.
+ #[93][Dialog no][Mart no (2byte)]
+ #see http://hax.iimarck.us/files/scriptingcodes_eng.htm#AwBsp93
+ #see http://hax.iimarck.us/files/scriptingcodes_eng.htm#ZusatzB93
+ """
+ size = 4
+ command["dialog_number"] = ord(rom[start_address+1])
+ command["mart_number"] = rom_interval(start_address+2, 2, strings=False)
+ elif command_byte == 0x95: #Elevator menu [2b]
+ info = "Display elevator menu by pointer [xxyy]"
+ long_info = """
+ #Displays a whole elevator menu, but it doesn't load font to ram.
+ #Only works with warps with warp-to = 0xFF.
+ #[94][2byte pointer to floor list]
+ """
+ size = 3
+ command["floor_list_pointer"] = calculate_pointer_from_bytes_at(start_address+1, bank=False)
+ elif command_byte == 0x96: #Trade menu [xx]
+ info = "Display trade menu by trade id [xx]"
+ long_info = """
+ #Displays a whole trade menu, but it doesn't load font to ram.
+ #[95][trade no]
+ #see http://hax.iimarck.us/files/scriptingcodes_eng.htm#ZusatzDokTausch
+ """
+ size = 2
+ command["trade_number"] = ord(rom[start_address+1])
+ elif command_byte == 0x97: #Give cell phone number with YES/NO [xx]
+ info = "Give cell phone number by id with YES/NO [id]"
+ long_info = """
+ #Gives a telephone number but asks for decision beforehand.
+ #feedback:
+ # 00 = ok chosen
+ # 01 = Cell phone number already registered/Memory full
+ # 02 = no chosen
+ #[96][Cell phone number]
+ """
+ size = 2
+ #maybe this next param should be called "phone_number"
+ command["number"] = ord(rom[start_address+1])
+ elif command_byte == 0x98: #Call code [2b]
+ info = "Call code pointing to name of caller [xxyy]"
+ long_info = """
+ #Displays the upper cell phone box and displays a freely selectable name.
+ #[97][2byte pointer to name of caller]
+ """
+ size = 3
+ command["caller_name_pointer"] = calculate_pointer_from_bytes_at(start_address+1, bank=False)
+ elif command_byte == 0x99: #Hang-up code
+ info = "Hang-up phone"
+ long_info = """
+ #Simulates the hanging-up.
+ #[98]
+ """
+ size = 1
+ elif command_byte == 0x9A: #Decoration code [xx]
+ info = "Set monologue decoration [xx]"
+ long_info = """
+ #Displays monologues according to the selected ornament.
+ #[99][xx]
+ #xx values:
+ # 00 = Map/Poster
+ # 01 = Ornament left
+ # 02 = Ornament right
+ # 03 = Huge doll
+ # 04 = Console
+ """
+ size = 2
+ command["ornament"] = ord(rom[start_address+1])
+ elif command_byte == 0x9B: #Berry tree code [xx]
+ info = "Berry tree by tree id [xx]"
+ long_info = """
+ #Creates a typical berry tree monologue.
+ #There is a maximum of 32 berry trees in the game.
+ #After this code the script ends.
+ #[9A][Fruit tree number]
+ #Fruit tree number + 11:4091 is the offset where the item no of the berry is defined.
+ """
+ size = 2
+ end = True
+ command["tree_id"] = ord(rom[start_address+1])
+ elif command_byte == 0x9C: #Cell phone call code [xx00]
+ #XXX confirm this?
+ info = "Cell phone call [2-byte call id]" #was originally: [call id][00]
+ long_info = """
+ #Initiates with the next step on a outer world map (permission byte) a phone call.
+ #[9B][Call no] and maybe [00] ???
+ #call no:
+ # 01 = PokéRus
+ # 02 = Pokémon stolen
+ # 03 = Egg examined/ Assistant in Viola City
+ # 04 = Team Rocket on the radio
+ # 05 = PROF. ELM has got something for HIRO
+ # 06 = Bike shop gives bike away
+ # 07 = Mother is unhappy that HIRO didn't talk to her before leaving
+ # 08 = PROF. ELM has got something for HIRO a second time
+ """
+ size = 3
+ command["call_id"] = ord(rom[start_address+1])
+ command["id"] = rom_interval(start_address+2, 2, strings=False)
+ elif command_byte == 0x9D: #Check cell phone call code
+ info = "Check if/which a phone call is active"
+ long_info = """
+ #Checks if a phone call is "in the line".
+ #feedback:
+ # 00 = no
+ # <>00 = call number
+ #[9C]
+ """
+ size = 1
+ elif command_byte == 0x9E: #Commented give item code [xxyy]
+ info = "Give item by id and quantity with 'put in pocket' text [id][qty]"
+ long_info = """
+ #The same as 0x1F but this code comments where
+ #HIRO puts what item in a short monologue.
+ #[9D][Item][Amount]
+ """
+ size = 3
+ command["item_id"] = ord(rom[start_address+1])
+ command["quantity"] = ord(rom[start_address+2])
+ elif command_byte == 0x9F: #Commented ive item code?
+ info = "Give item by id and quantity with 'put in pocket' text [id][qty]"
+ long_info = """
+ #The same as 0x1F but this code comments where
+ #HIRO puts what item in a short monologue.
+ #[9D][Item][Amount]
+ """
+ size = 3
+ command["item_id"] = ord(rom[start_address+1])
+ command["quantity"] = ord(rom[start_address+2])
+ elif command_byte == 0xA0: #Load special wild PKMN data [xxyy]
+ info = "Load wild pokemon data for a remote map [map group][map id]"
+ long_info = """
+ #Activates the checks in the special tables for the wild pokémon data.
+ #[9E][map group][map id]
+ #see also http://hax.iimarck.us/files/scriptingcodes_eng.htm#ZusatzDok3E_66ED
+ """
+ size = 3
+ command["map_group"] = ord(rom[start_address+1])
+ command["map_id"] = ord(rom[start_address+2])
+ elif command_byte == 0xA1: #Hall of Fame code
+ info = "Hall of Fame"
+ long_info = """
+ #Saves and enters HIRO's complete Team in the Hall of Fame.
+ #Shows the credits and restarts the game with HIRO located in New Bark Town.
+ #[9F]
+ """
+ size = 1
+ elif command_byte == 0xA2: #Credits code
+ info = "Credits"
+ long_info = """
+ #Shows the credits and HIRO is located on the Silver mountain plateau.
+ #[A0]
+ """
+ size = 1
+ elif command_byte == 0xA3: #Facing warp
+ info = "Warp-to and set facing direction [Facing (00-03)][Map bank][Map no][X][Y]"
+ long_info = """
+ #Acts like code 0x3C but defines the desired facing of HIRO.
+ #[A1][Facing (00-03)][Map bank][Map no][X][Y]
+ """
+ size = 6
+ command["facing"] = ord(rom[start_address+1])
+ command["map_group"] = ord(rom[start_address+2])
+ command["map_id"] = ord(rom[start_address+3])
+ command["x"] = ord(rom[start_address+4])
+ command["y"] = ord(rom[start_address+5])
+ elif command_byte == 0xA4: #MEMORY code [2b + Bank + xx]
+ info = "Set memX to a string by a pointer [aabb][bank][xx]"
+ long_info = """
+ #MEMORY1, 2 or 3 can directly be filled with a string from
+ #a different rom bank.
+ #[A2][2byte pointer][Bank][00-02 MEMORY]
+ """
+ size = 5
+ command["string_pointer"] = calculate_pointer_from_bytes_at(start_address+1, bank="reversed")
+ command["string_pointer_bank"] = ord(rom[start_address+3])
+ command["memory_id"] = ord(rom[start_address+4])
+ elif command_byte == 0xA5: #Display any location name [xx]
+ info = "Copy the name of a location (by id) to TEMPMEMORY1"
+ long_info = """
+ #By the location number the name of that location is written to TEMPMEMORY1.
+ #[A3][Location no]
+ """
+ size = 2
+ command["location_number"] = ord(rom[start_address+1])
+ else:
+ size = 1
+ #end = True
+ #raise NotImplementedError, "command byte is " + hex(command_byte) + " at " + hex(offset) + " on map " + str(map_group) + "." + str(map_id)
+ print "dunno what this command is: " + hex(command_byte)
+ long_info = clean_up_long_info(long_info)
+
+ if command_byte in pksv_crystal.keys():
+ pksv_name = pksv_crystal[command_byte]
+ else:
+ pksv_name = None
+ if command_byte in pksv_no_names.keys():
+ pksv_no_names[command_byte].append(address)
+ else:
+ pksv_no_names[command_byte] = [address]
+
+ if debug:
+ print command_debug_information(command_byte=command_byte, map_group=map_group, map_id=map_id, address=offset, info=info, long_info=long_info, pksv_name=pksv_name)
+
+ #store the size of the command
+ command["size"] = size
+ #the end address is just offset + size - 1 (because size includes command byte)
+ offset += size - 1
+ #the end address is the last byte belonging to this command
+ command["last_byte_address"] = offset
+ #we also add the size of the command byte to get to the next command
+ offset += 1
+ #add the command into the command list please
+ commands[len(commands.keys())] = command
+
+ self.commands = commands
+ script_parse_table[original_start_address : offset] = self
+ return True
diff --git a/pokemontools/overworldripper.py b/pokemontools/overworldripper.py
new file mode 100644
index 0000000..654f287
--- /dev/null
+++ b/pokemontools/overworldripper.py
@@ -0,0 +1,18 @@
+import gfx
+
+def rip_sprites_from_bank(bank, offset=0):
+ """
+ Rips sprites from specified bank.
+
+ Sprites are 4x4.
+ """
+ file_handler = open("../gfx/overworld/bank" + str(hex(bank))[2:] + ".asm", "w")
+ for sprite in range(0 + offset, 256 + offset):
+ filename = "../gfx/overworld/" + str(sprite).zfill(3) + ".2bpp"
+ gfx.get_uncompressed_gfx((bank * 0x4000 + ((sprite - offset) * 4 * 0x10)), 4, filename)
+ gfx.to_png(filename)
+ file_handler.write("INCBIN \"" + filename[3:] + "\"\n")
+ file_handler.close()
+
+rip_sprites_from_bank(0x30)
+rip_sprites_from_bank(0x31, offset=256)
diff --git a/pokemontools/parse_consecutive_strings.py b/pokemontools/parse_consecutive_strings.py
new file mode 100644
index 0000000..4c864ba
--- /dev/null
+++ b/pokemontools/parse_consecutive_strings.py
@@ -0,0 +1,25 @@
+import sys
+
+import crystal
+
+rom = crystal.load_rom()
+
+addr = int(sys.argv[1], 16)
+count = int(sys.argv[2]) if len(sys.argv) > 2 else 256
+label_prefix = sys.argv[3] if len(sys.argv) > 3 else "UnknownString"
+
+ex = None
+
+for i in range(count):
+ try:
+ string = crystal.PokedexText(addr)
+ asm = string.to_asm()
+ except Exception as ex:
+ break
+ print label_prefix+str(i)+": ; "+hex(addr)
+ print "\t"+asm
+ print
+ addr = string.last_address
+
+print "; "+hex(addr)
+if ex: raise ex
diff --git a/pokemontools/pksv.py b/pokemontools/pksv.py
new file mode 100644
index 0000000..2f02ec0
--- /dev/null
+++ b/pokemontools/pksv.py
@@ -0,0 +1,314 @@
+# -*- coding: utf-8 -*-
+
+pksv_gs = {
+ 0x00: "2call",
+ 0x01: "3call",
+ 0x02: "2ptcall",
+ 0x03: "2jump",
+ 0x04: "3jump",
+ 0x05: "2ptjump",
+ 0x06: "if equal",
+ 0x07: "if not equal",
+ 0x08: "if false",
+ 0x09: "if true",
+ 0x0A: "if less than",
+ 0x0B: "if greater than",
+ 0x0C: "jumpstd",
+ 0x0D: "callstd",
+ 0x0E: "3callasm",
+ 0x0F: "special",
+ 0x10: "2ptcallasm",
+ 0x11: "checkmaptriggers",
+ 0x12: "domaptrigger",
+ 0x13: "checktriggers",
+ 0x14: "dotrigger",
+ 0x15: "writebyte",
+ 0x16: "addvar",
+ 0x17: "random",
+ 0x19: "copybytetovar",
+ 0x1A: "copyvartobyte",
+ 0x1B: "loadvar",
+ 0x1C: "checkcode",
+ 0x1E: "writecode",
+ 0x1F: "giveitem",
+ 0x20: "takeitem",
+ 0x21: "checkitem",
+ 0x22: "givemoney",
+ 0x23: "takemoney",
+ 0x24: "checkmoney",
+ 0x25: "givecoins",
+ 0x26: "takecoins",
+ 0x27: "checkcoins",
+ 0x2B: "checktime",
+ 0x2C: "checkpoke",
+ 0x2D: "givepoke",
+ 0x2E: "giveegg",
+ 0x2F: "givepokeitem",
+ 0x31: "checkbit1",
+ 0x32: "clearbit1",
+ 0x33: "setbit1",
+ 0x34: "checkbit2",
+ 0x35: "clearbit2",
+ 0x36: "setbit2",
+ 0x37: "wildoff",
+ 0x38: "wildon",
+ 0x39: "xycompare",
+ 0x3A: "warpmod",
+ 0x3B: "blackoutmod",
+ 0x3C: "warp",
+ 0x41: "itemtotext",
+ 0x43: "trainertotext",
+ 0x44: "stringtotext",
+ 0x45: "itemnotify",
+ 0x46: "pocketisfull",
+ 0x47: "loadfont",
+ 0x48: "refreshscreen",
+ 0x49: "loadmovesprites",
+ 0x4B: "3writetext",
+ 0x4C: "2writetext",
+ 0x4E: "yesorno",
+ 0x4F: "loadmenudata",
+ 0x50: "writebackup",
+ 0x51: "jumptextfaceplayer",
+ 0x52: "jumptext",
+ 0x53: "closetext",
+ 0x54: "keeptextopen",
+ 0x55: "pokepic",
+ 0x56: "pokepicyesorno",
+ 0x57: "interpretmenu",
+ 0x58: "interpretmenu2",
+ 0x5C: "loadpokedata",
+ 0x5D: "loadtrainer",
+ 0x5E: "startbattle",
+ 0x5F: "returnafterbattle",
+ 0x60: "catchtutorial",
+ 0x63: "winlosstext",
+ 0x65: "talkaftercancel",
+ 0x67: "setlasttalked",
+ 0x68: "applymovement",
+ 0x6A: "faceplayer",
+ 0x6B: "faceperson",
+ 0x6C: "variablesprite",
+ 0x6D: "disappear",
+ 0x6E: "appear",
+ 0x6F: "follow",
+ 0x70: "stopfollow",
+ 0x71: "moveperson",
+ 0x74: "showemote",
+ 0x75: "spriteface",
+ 0x76: "follownotexact",
+ 0x77: "earthquake",
+ 0x79: "changeblock",
+ 0x7A: "reloadmap",
+ 0x7B: "reloadmappart",
+ 0x7C: "writecmdqueue",
+ 0x7D: "delcmdqueue",
+ 0x7E: "playmusic",
+ 0x7F: "playrammusic",
+ 0x80: "musicfadeout",
+ 0x81: "playmapmusic",
+ 0x82: "reloadmapmusic",
+ 0x83: "cry",
+ 0x84: "playsound",
+ 0x85: "waitbutton",
+ 0x86: "warpsound",
+ 0x87: "specialsound",
+ 0x88: "passtoengine",
+ 0x89: "newloadmap",
+ 0x8A: "pause",
+ 0x8B: "deactivatefacing",
+ 0x8C: "priorityjump",
+ 0x8D: "warpcheck",
+ 0x8E: "ptpriorityjump",
+ 0x8F: "return",
+ 0x90: "end",
+ 0x91: "reloadandreturn",
+ 0x92: "resetfuncs",
+ 0x93: "pokemart",
+ 0x94: "elevator",
+ 0x95: "trade",
+ 0x96: "askforphonenumber",
+ 0x97: "phonecall",
+ 0x98: "hangup",
+ 0x99: "describedecoration",
+ 0x9A: "fruittree",
+ 0x9C: "checkphonecall",
+ 0x9D: "verbosegiveitem",
+ 0x9E: "loadwilddata",
+ 0x9F: "halloffame",
+ 0xA0: "credits",
+ 0xA1: "warpfacing",
+ 0xA2: "storetext",
+ 0xA3: "displaylocation",
+}
+
+# see http://www.pokecommunity.com/showpost.php?p=4347261
+# NOTE: this has some updates that need to be back-ported to gold
+pksv_crystal = {
+ 0x00: "2call",
+ 0x01: "3call",
+ 0x02: "2ptcall",
+ 0x03: "2jump",
+ 0x04: "3jump",
+ 0x05: "2ptjump",
+ 0x06: "if equal",
+ 0x07: "if not equal",
+ 0x08: "if false",
+ 0x09: "if true",
+ 0x0A: "if less than",
+ 0x0B: "if greater than",
+ 0x0C: "jumpstd",
+ 0x0D: "callstd",
+ 0x0E: "3callasm",
+ 0x0F: "special",
+ 0x10: "2ptcallasm",
+ 0x11: "checkmaptriggers",
+ 0x12: "domaptrigger",
+ 0x13: "checktriggers",
+ 0x14: "dotrigger",
+ 0x15: "writebyte",
+ 0x16: "addvar",
+ 0x17: "random",
+ 0x19: "copybytetovar",
+ 0x1A: "copyvartobyte",
+ 0x1B: "loadvar",
+ 0x1C: "checkcode",
+ 0x1D: "writevarcode",
+ 0x1E: "writecode",
+ 0x1F: "giveitem",
+ 0x20: "takeitem",
+ 0x21: "checkitem",
+ 0x22: "givemoney",
+ 0x23: "takemoney",
+ 0x24: "checkmoney",
+ 0x25: "givecoins",
+ 0x26: "takecoins",
+ 0x27: "checkcoins",
+ 0x28: "addcellnum",
+ 0x29: "delcellnum",
+ 0x2B: "checktime",
+ 0x2C: "checkpoke",
+ 0x2D: "givepoke",
+ 0x2E: "giveegg",
+ 0x2F: "givepokeitem",
+ 0x31: "checkbit1",
+ 0x32: "clearbit1",
+ 0x33: "setbit1",
+ 0x34: "checkbit2",
+ 0x35: "clearbit2",
+ 0x36: "setbit2",
+ 0x37: "wildoff",
+ 0x38: "wildon",
+ 0x39: "xycompare",
+ 0x3A: "warpmod",
+ 0x3B: "blackoutmod",
+ 0x3C: "warp",
+ 0x41: "itemtotext",
+ 0x43: "trainertotext",
+ 0x44: "stringtotext",
+ 0x45: "itemnotify",
+ 0x46: "pocketisfull",
+ 0x47: "loadfont",
+ 0x48: "refreshscreen",
+ 0x49: "loadmovesprites",
+ 0x4B: "3writetext",
+ 0x4C: "2writetext",
+ 0x4E: "yesorno",
+ 0x4F: "loadmenudata",
+ 0x50: "writebackup",
+ 0x51: "jumptextfaceplayer",
+ 0x53: "jumptext",
+ 0x54: "closetext",
+ 0x55: "keeptextopen",
+ 0x56: "pokepic",
+ 0x57: "pokepicyesorno",
+ 0x58: "interpretmenu",
+ 0x59: "interpretmenu2",
+ 0x5D: "loadpokedata",
+ 0x5E: "loadtrainer",
+ 0x5F: "startbattle",
+ 0x60: "returnafterbattle",
+ 0x61: "catchtutorial",
+ 0x64: "winlosstext",
+ 0x66: "talkaftercancel",
+ 0x68: "setlasttalked",
+ 0x69: "applymovement",
+ 0x6B: "faceplayer",
+ 0x6C: "faceperson",
+ 0x6D: "variablesprite",
+ 0x6E: "disappear",
+ 0x6F: "appear",
+ 0x70: "follow",
+ 0x71: "stopfollow",
+ 0x72: "moveperson",
+ 0x75: "showemote",
+ 0x76: "spriteface",
+ 0x77: "follownotexact",
+ 0x78: "earthquake",
+ 0x7A: "changeblock",
+ 0x7B: "reloadmap",
+ 0x7C: "reloadmappart",
+ 0x7D: "writecmdqueue",
+ 0x7E: "delcmdqueue",
+ 0x7F: "playmusic",
+ 0x80: "playrammusic",
+ 0x81: "musicfadeout",
+ 0x82: "playmapmusic",
+ 0x83: "reloadmapmusic",
+ 0x84: "cry",
+ 0x85: "playsound",
+ 0x86: "waitbutton",
+ 0x87: "warpsound",
+ 0x88: "specialsound",
+ 0x89: "passtoengine",
+ 0x8A: "newloadmap",
+ 0x8B: "pause",
+ 0x8C: "deactivatefacing",
+ 0x8D: "priorityjump",
+ 0x8E: "warpcheck",
+ 0x8F: "ptpriorityjump",
+ 0x90: "return",
+ 0x91: "end",
+ 0x92: "reloadandreturn",
+ 0x93: "resetfuncs",
+ 0x94: "pokemart",
+ 0x95: "elevator",
+ 0x96: "trade",
+ 0x97: "askforphonenumber",
+ 0x98: "phonecall",
+ 0x99: "hangup",
+ 0x9A: "describedecoration",
+ 0x9B: "fruittree",
+ 0x9C: "specialphonecall",
+ 0x9D: "checkphonecall",
+ 0x9E: "verbosegiveitem",
+ 0x9F: "verbosegiveitem2",
+ 0xA0: "loadwilddata",
+ 0xA1: "halloffame",
+ 0xA2: "credits",
+ 0xA3: "warpfacing",
+ 0xA4: "storetext",
+ 0xA5: "displaylocation",
+ 0xB2: "unknown0xb2",
+}
+
+#these cause the script to end; used in create_command_classes
+pksv_crystal_more_enders = [0x03, 0x04, 0x05, 0x0C, 0x51, 0x52,
+ 0x53, 0x8D, 0x8F, 0x90, 0x91, 0x92,
+ 0x9B,
+ 0xB2, #maybe?
+ 0xCC, #maybe?
+ 0x9A, # describedecoration
+ ]
+
+# these have no pksv names as of pksv 2.1.1
+pksv_crystal_unknowns = [
+ 0x9F,
+ 0xA6, 0xA7, 0xA8, 0xA9, 0xAA, 0xAB, 0xAC, 0xAD, 0xAE, 0xAF,
+ 0xB0, 0xB1, 0xB2, 0xB3, 0xB4, 0xB5, 0xB6, 0xB7, 0xB8,
+ 0xCC, 0xCD,
+ 0xD1, 0xD2, 0xD3, 0xD4, 0xD5, 0xD6,
+ 0xE4, 0xE5, 0xE6, 0xE7, 0xE8, 0xE9, 0xEA, 0xEB, 0xEC, 0xED, 0xEE, 0xEF,
+ 0xF0, 0xF1, 0xF2, 0xF3, 0xF4, 0xF5, 0xF6, 0xF7, 0xF8,
+ 0xFA, 0xFB, 0xFC, 0xFD, 0xFE, 0xFF,
+]
diff --git a/pokemontools/pointers.py b/pokemontools/pointers.py
new file mode 100644
index 0000000..161a53e
--- /dev/null
+++ b/pokemontools/pointers.py
@@ -0,0 +1,24 @@
+# -*- coding: utf-8 -*-
+"""
+Various functions related to pointer and address math. Mostly to avoid
+depedency loops.
+"""
+
+def calculate_bank(address):
+ """you are too lazy to divide on your own?"""
+ if type(address) == str:
+ address = int(address, 16)
+ #if 0x4000 <= address <= 0x7FFF:
+ # raise Exception, "bank 1 does not exist"
+ return int(address) / 0x4000
+
+def calculate_pointer(short_pointer, bank=None):
+ """calculates the full address given a 4-byte pointer and bank byte"""
+ short_pointer = int(short_pointer)
+ if 0x4000 <= short_pointer <= 0x7fff:
+ short_pointer -= 0x4000
+ bank = int(bank)
+ else:
+ bank = 0
+ pointer = short_pointer + (bank * 0x4000)
+ return pointer
diff --git a/pokemontools/pokemon_constants.py b/pokemontools/pokemon_constants.py
new file mode 100644
index 0000000..221a31c
--- /dev/null
+++ b/pokemontools/pokemon_constants.py
@@ -0,0 +1,255 @@
+# -*- coding: utf-8 -*-
+
+pokemon_constants = {
+1: "BULBASAUR",
+2: "IVYSAUR",
+3: "VENUSAUR",
+4: "CHARMANDER",
+5: "CHARMELEON",
+6: "CHARIZARD",
+7: "SQUIRTLE",
+8: "WARTORTLE",
+9: "BLASTOISE",
+10: "CATERPIE",
+11: "METAPOD",
+12: "BUTTERFREE",
+13: "WEEDLE",
+14: "KAKUNA",
+15: "BEEDRILL",
+16: "PIDGEY",
+17: "PIDGEOTTO",
+18: "PIDGEOT",
+19: "RATTATA",
+20: "RATICATE",
+21: "SPEAROW",
+22: "FEAROW",
+23: "EKANS",
+24: "ARBOK",
+25: "PIKACHU",
+26: "RAICHU",
+27: "SANDSHREW",
+28: "SANDSLASH",
+29: "NIDORAN_F",
+30: "NIDORINA",
+31: "NIDOQUEEN",
+32: "NIDORAN_M",
+33: "NIDORINO",
+34: "NIDOKING",
+35: "CLEFAIRY",
+36: "CLEFABLE",
+37: "VULPIX",
+38: "NINETALES",
+39: "JIGGLYPUFF",
+40: "WIGGLYTUFF",
+41: "ZUBAT",
+42: "GOLBAT",
+43: "ODDISH",
+44: "GLOOM",
+45: "VILEPLUME",
+46: "PARAS",
+47: "PARASECT",
+48: "VENONAT",
+49: "VENOMOTH",
+50: "DIGLETT",
+51: "DUGTRIO",
+52: "MEOWTH",
+53: "PERSIAN",
+54: "PSYDUCK",
+55: "GOLDUCK",
+56: "MANKEY",
+57: "PRIMEAPE",
+58: "GROWLITHE",
+59: "ARCANINE",
+60: "POLIWAG",
+61: "POLIWHIRL",
+62: "POLIWRATH",
+63: "ABRA",
+64: "KADABRA",
+65: "ALAKAZAM",
+66: "MACHOP",
+67: "MACHOKE",
+68: "MACHAMP",
+69: "BELLSPROUT",
+70: "WEEPINBELL",
+71: "VICTREEBEL",
+72: "TENTACOOL",
+73: "TENTACRUEL",
+74: "GEODUDE",
+75: "GRAVELER",
+76: "GOLEM",
+77: "PONYTA",
+78: "RAPIDASH",
+79: "SLOWPOKE",
+80: "SLOWBRO",
+81: "MAGNEMITE",
+82: "MAGNETON",
+83: "FARFETCH_D",
+84: "DODUO",
+85: "DODRIO",
+86: "SEEL",
+87: "DEWGONG",
+88: "GRIMER",
+89: "MUK",
+90: "SHELLDER",
+91: "CLOYSTER",
+92: "GASTLY",
+93: "HAUNTER",
+94: "GENGAR",
+95: "ONIX",
+96: "DROWZEE",
+97: "HYPNO",
+98: "KRABBY",
+99: "KINGLER",
+100: "VOLTORB",
+101: "ELECTRODE",
+102: "EXEGGCUTE",
+103: "EXEGGUTOR",
+104: "CUBONE",
+105: "MAROWAK",
+106: "HITMONLEE",
+107: "HITMONCHAN",
+108: "LICKITUNG",
+109: "KOFFING",
+110: "WEEZING",
+111: "RHYHORN",
+112: "RHYDON",
+113: "CHANSEY",
+114: "TANGELA",
+115: "KANGASKHAN",
+116: "HORSEA",
+117: "SEADRA",
+118: "GOLDEEN",
+119: "SEAKING",
+120: "STARYU",
+121: "STARMIE",
+122: "MR__MIME",
+123: "SCYTHER",
+124: "JYNX",
+125: "ELECTABUZZ",
+126: "MAGMAR",
+127: "PINSIR",
+128: "TAUROS",
+129: "MAGIKARP",
+130: "GYARADOS",
+131: "LAPRAS",
+132: "DITTO",
+133: "EEVEE",
+134: "VAPOREON",
+135: "JOLTEON",
+136: "FLAREON",
+137: "PORYGON",
+138: "OMANYTE",
+139: "OMASTAR",
+140: "KABUTO",
+141: "KABUTOPS",
+142: "AERODACTYL",
+143: "SNORLAX",
+144: "ARTICUNO",
+145: "ZAPDOS",
+146: "MOLTRES",
+147: "DRATINI",
+148: "DRAGONAIR",
+149: "DRAGONITE",
+150: "MEWTWO",
+151: "MEW",
+152: "CHIKORITA",
+153: "BAYLEEF",
+154: "MEGANIUM",
+155: "CYNDAQUIL",
+156: "QUILAVA",
+157: "TYPHLOSION",
+158: "TOTODILE",
+159: "CROCONAW",
+160: "FERALIGATR",
+161: "SENTRET",
+162: "FURRET",
+163: "HOOTHOOT",
+164: "NOCTOWL",
+165: "LEDYBA",
+166: "LEDIAN",
+167: "SPINARAK",
+168: "ARIADOS",
+169: "CROBAT",
+170: "CHINCHOU",
+171: "LANTURN",
+172: "PICHU",
+173: "CLEFFA",
+174: "IGGLYBUFF",
+175: "TOGEPI",
+176: "TOGETIC",
+177: "NATU",
+178: "XATU",
+179: "MAREEP",
+180: "FLAAFFY",
+181: "AMPHAROS",
+182: "BELLOSSOM",
+183: "MARILL",
+184: "AZUMARILL",
+185: "SUDOWOODO",
+186: "POLITOED",
+187: "HOPPIP",
+188: "SKIPLOOM",
+189: "JUMPLUFF",
+190: "AIPOM",
+191: "SUNKERN",
+192: "SUNFLORA",
+193: "YANMA",
+194: "WOOPER",
+195: "QUAGSIRE",
+196: "ESPEON",
+197: "UMBREON",
+198: "MURKROW",
+199: "SLOWKING",
+200: "MISDREAVUS",
+201: "UNOWN",
+202: "WOBBUFFET",
+203: "GIRAFARIG",
+204: "PINECO",
+205: "FORRETRESS",
+206: "DUNSPARCE",
+207: "GLIGAR",
+208: "STEELIX",
+209: "SNUBBULL",
+210: "GRANBULL",
+211: "QWILFISH",
+212: "SCIZOR",
+213: "SHUCKLE",
+214: "HERACROSS",
+215: "SNEASEL",
+216: "TEDDIURSA",
+217: "URSARING",
+218: "SLUGMA",
+219: "MAGCARGO",
+220: "SWINUB",
+221: "PILOSWINE",
+222: "CORSOLA",
+223: "REMORAID",
+224: "OCTILLERY",
+225: "DELIBIRD",
+226: "MANTINE",
+227: "SKARMORY",
+228: "HOUNDOUR",
+229: "HOUNDOOM",
+230: "KINGDRA",
+231: "PHANPY",
+232: "DONPHAN",
+233: "PORYGON2",
+234: "STANTLER",
+235: "SMEARGLE",
+236: "TYROGUE",
+237: "HITMONTOP",
+238: "SMOOCHUM",
+239: "ELEKID",
+240: "MAGBY",
+241: "MILTANK",
+242: "BLISSEY",
+243: "RAIKOU",
+244: "ENTEI",
+245: "SUICUNE",
+246: "LARVITAR",
+247: "PUPITAR",
+248: "TYRANITAR",
+249: "LUGIA",
+250: "HO_OH",
+251: "CELEBI",
+}
diff --git a/pokemontools/romstr.py b/pokemontools/romstr.py
new file mode 100644
index 0000000..69a4f2a
--- /dev/null
+++ b/pokemontools/romstr.py
@@ -0,0 +1,219 @@
+# -*- coding: utf-8 -*-
+
+import sys
+import os
+import time
+import datetime
+from ctypes import c_int8
+from copy import copy
+import json
+
+# New versions of json don't have read anymore.
+if not hasattr(json, "read"):
+ json.read = json.loads
+
+from labels import (
+ get_label_from_line,
+ get_address_from_line_comment,
+)
+
+relative_jumps = [0x38, 0x30, 0x20, 0x28, 0x18, 0xc3, 0xda, 0xc2, 0x32]
+relative_unconditional_jumps = [0xc3, 0x18]
+call_commands = [0xdc, 0xd4, 0xc4, 0xcc, 0xcd]
+end_08_scripts_with = [
+ 0xe9, # jp hl
+ 0xc9, # ret
+] # possibly also:
+ # 0xc3, # jp
+ # 0xc18, # jr
+ # 0xda, 0xe9, 0xd2, 0xc2, 0xca, 0x38, 0x30, 0x20, 0x28, 0x18, 0xd8,
+ # 0xd0, 0xc0, 0xc8, 0xc9
+
+spacing = "\t"
+
+class RomStr(str):
+ """
+ Simple wrapper to prevent a giant rom from being shown on screen.
+ """
+
+ def __init__(self, *args, **kwargs):
+ if "labels" in kwargs.keys() and kwargs["labels"] == True:
+ self.load_labels()
+ str.__init__(self)
+
+ def __repr__(self):
+ """
+ Simplifies this object so that the output doesn't overflow stdout.
+ """
+ return "RomStr(too long)"
+
+ @classmethod
+ def load(cls, filename=None, crystal=True, red=False):
+ """
+ Load a ROM into a RomStr.
+ """
+ if crystal and not red and not filename:
+ file_handler = open("../baserom.gbc", "r")
+ elif red and not crystal and not filename:
+ file_handler = open("../pokered-baserom.gbc", "r")
+ elif filename not in ["", None]:
+ file_handler = open(filename, "rb")
+ else:
+ raise Exception("not sure which rom to load?")
+ bytes = file_handler.read()
+ file_handler.close()
+ return RomStr(bytes)
+
+ def load_labels(self, filename="labels.json"):
+ """
+ Loads labels from labels.json.
+
+ (Or parses the source code file and
+ generates new labels.)
+ """
+ filename = os.path.join(os.path.dirname(__file__), filename)
+
+ # blank out the hash
+ self.labels = {}
+
+ # check if the labels file exists
+ file_existence = os.path.exists(filename)
+
+ generate_labels = False
+
+ # determine if the labels file needs to be regenerated
+ if file_existence:
+ modified = os.path.getmtime(filename)
+ modified = datetime.datetime.fromtimestamp(modified)
+ current = datetime.datetime.fromtimestamp(time.time())
+
+ is_old = (current - modified) > datetime.timedelta(days=3)
+
+ if is_old:
+ generate_labels = True
+ else:
+ generate_labels = True
+
+ # scan the asm source code for labels
+ if generate_labels:
+ asm = open(os.path.join(os.path.dirname(__file__), "../main.asm"), "r").read().split("\n")
+
+ for line in asm:
+ label = get_label_from_line(line)
+
+ if label:
+ address = get_address_from_line_comment(line)
+
+ self.labels[address] = label
+
+ content = json.dumps(self.labels)
+ file_handler = open(filename, "w")
+ file_handler.write(content)
+ file_handler.close()
+
+ # load the labels from the file
+ self.labels = json.read(open(filename, "r").read())
+
+ def get_address_for(self, label):
+ """
+ Return the address of a label.
+
+ This is slow and could be improved dramatically.
+ """
+ label = str(label)
+ for address in self.labels.keys():
+ if self.labels[address] == label:
+ return address
+ return None
+
+ def length(self):
+ """
+ len(self)
+ """
+ return len(self)
+
+ def len(self):
+ """
+ len(self)
+ """
+ return self.length()
+
+ def interval(self, offset, length, strings=True, debug=True):
+ """
+ Return hex values for the rom starting at offset until offset+length.
+ """
+ returnable = []
+ for byte in self[offset:offset+length]:
+ if strings:
+ returnable.append(hex(ord(byte)))
+ else:
+ returnable.append(ord(byte))
+ return returnable
+
+ def until(self, offset, byte, strings=True, debug=False):
+ """
+ Return hex values from rom starting at offset until the given byte.
+ """
+ return self.interval(offset, self.find(chr(byte), offset) - offset, strings=strings)
+
+ def to_asm(self, address, end_address=None, size=None, max_size=0x4000, debug=None):
+ """
+ Disassemble ASM at some address.
+
+ This will stop disassembling when either the end_address or size is
+ met. Also, there's a maximum size that will be parsed, so that large
+ patches of data aren't parsed as code.
+ """
+ if type(address) in [str, unicode] and "0x" in address:
+ address = int(address, 16)
+
+ start_address = address
+
+ if start_address == None:
+ raise Exception, "address must be given"
+
+ if debug == None:
+ if not hasattr(self, "debug"):
+ debug = False
+ else:
+ debug = self.debug
+
+ # this is probably a terrible idea.. why am i doing this?
+ if size != None and max_size < size:
+ raise Exception, "max_size must be greater than or equal to size"
+ elif end_address != None and (end_address - start_address) > max_size:
+ raise Exception, "end_address is out of bounds"
+ elif end_address != None and size != None:
+ if (end_address - start_address) >= size:
+ size = end_address - start_address
+ else:
+ end_address = start_address + size
+ elif end_address == None and size != None:
+ end_address = start_address + size
+ elif end_address != None and size == None:
+ size = end_address - start_address
+
+ raise NotImplementedError("DisAsm was removed and never worked; hook up another disassembler please.")
+ #return DisAsm(start_address=start_address, end_address=end_address, size=size, max_size=max_size, debug=debug, rom=self)
+
+class AsmList(list):
+ """
+ Simple wrapper to prevent all asm lines from being shown on screen.
+ """
+
+ def length(self):
+ """
+ len(self)
+ """
+ return len(self)
+
+ def __repr__(self):
+ """
+ Simplifies this object so that the output doesn't overflow stdout.
+ """
+ return "AsmList(too long)"
+
+if __name__ == "__main__":
+ cryrom = RomStr(open("../pokecrystal.gbc", "r").read());
+ asm = cryrom.to_asm(sys.argv[1])
+ print asm
diff --git a/pokemontools/sym.py b/pokemontools/sym.py
new file mode 100644
index 0000000..f78b9af
--- /dev/null
+++ b/pokemontools/sym.py
@@ -0,0 +1,54 @@
+# -*- coding: utf-8 -*-
+
+import json
+
+# from crystal import load_rom
+# from gbz80disasm import load_labels
+
+
+def make_sym_from_json(filename = '../pokecrystal.sym', j = 'labels.json'):
+ # todo: delete and remake labels.json at runtime
+ with open(filename, 'w') as sym:
+ for label in json.load(open(j)):
+ sym.write('{0:x}:{1:x} {2}\n'.format(label['bank'], label['address']%0x4000 + (0x4000 if label['bank'] else 0), label['label']))
+
+
+def make_sym_from_mapfile(filename = '../pokecrystal.sym', mapfile = '../mapfile.txt'):
+ # todo: sort label definitions by address
+
+ output = ''
+ # get label definitions
+ with open(mapfile,'r') as map:
+ lines = map.readlines()
+ for line in lines:
+ # bank #
+ if 'Bank #' in line:
+ cur_bank = int(line.lstrip('Bank #').strip(':\n').strip(' (HOME)'))
+
+ # label definition
+ elif '=' in line:
+ thing = line.split('=')
+ spacing = ' ' * 11 # arbitrary
+ addr = int(thing[0].lstrip(spacing)[1:5],16)
+
+ # rgbds doesn't support wram banks yet,
+ # so this hack is applied instead
+ if addr > 0xbfff: # 0xc000+ (wram only)
+ cur_bank = 0
+ if addr > 0xcfff: # 0xd000+ (wram only)
+ cur_bank = 1
+
+ # convert to sym format (bank:addr label)
+ label = thing[1].strip('\n')
+ output += hex(cur_bank)[2:] + ':' + hex(addr)[2:] + ' ' + label + '\n'
+
+ # dump contents to symfile
+ with open(filename, 'w') as sym:
+ sym.write(output)
+
+
+if __name__ == "__main__":
+ # default behavior: generate sym file from rgbds mapfile
+ try: make_sym_from_mapfile()
+ # if no mapfile exists, generate from labels.json
+ except: make_sym_from_json()
diff --git a/pokemontools/trainers.py b/pokemontools/trainers.py
new file mode 100644
index 0000000..cf17b98
--- /dev/null
+++ b/pokemontools/trainers.py
@@ -0,0 +1,108 @@
+# -*- coding: utf-8 -*-
+# url: http://hax.iimarck.us/topic/8/
+
+# for fixing trainer_group_names
+import re
+
+trainer_group_pointer_table_address = 0x39999
+trainer_group_pointer_table_address_gs = 0x3993E
+
+# Any trainer that appears more than once should have an id after each
+# trainer name.
+
+# "uses_numeric_trainer_ids" means never use a name for the trainer_id
+trainer_group_names = {
+0x01: {"name": "Falkner", "uses_numeric_trainer_ids": True, "constant": "FALKNER"},
+0x02: {"name": "Whitney", "uses_numeric_trainer_ids": True, "constant": "WHITNEY"},
+0x03: {"name": "Bugsy", "uses_numeric_trainer_ids": True, "constant": "BUGSY"},
+0x04: {"name": "Morty", "uses_numeric_trainer_ids": True, "constant": "MORTY"},
+0x05: {"name": "Pryce", "uses_numeric_trainer_ids": True, "constant": "PRYCE"},
+0x06: {"name": "Jasmine", "uses_numeric_trainer_ids": True, "constant": "JASMINE"},
+0x07: {"name": "Chuck", "uses_numeric_trainer_ids": True, "constant": "CHUCK"},
+0x08: {"name": "Clair", "uses_numeric_trainer_ids": True, "constant": "CLAIR"},
+0x09: {"name": "Rival1", "constant": "RIVAL1"},
+#PokemonProf group is empty :/
+0x0A: {"name": "Pokémon Prof.", "constant": "POKEMON_PROF"},
+0x0B: {"name": "Elite Four Will", "uses_numeric_trainer_ids": True, "constant": "WILL"},
+0x0C: {"name": "PKMN [Cal]", "constant": "CAL"},
+0x0D: {"name": "Elite Four Bruno", "uses_numeric_trainer_ids": True, "constant": "BRUNO"},
+0x0E: {"name": "Elite Four Karen", "uses_numeric_trainer_ids": True, "constant": "KAREN"},
+0x0F: {"name": "Elite Four Koga", "uses_numeric_trainer_ids": True, "constant": "KOGA"},
+0x10: {"name": "Champion", "constant": "CHAMPION"},
+0x11: {"name": "Brock", "uses_numeric_trainer_ids": True, "constant": "BROCK"},
+0x12: {"name": "Misty", "uses_numeric_trainer_ids": True, "constant": "MISTY"},
+0x13: {"name": "Lt.Surge", "uses_numeric_trainer_ids": True, "constant": "LT_SURGE"},
+0x14: {"name": "Scientist", "constant": "SCIENTIST"},
+0x15: {"name": "Erika", "uses_numeric_trainer_ids": True, "constant": "ERIKA"},
+0x16: {"name": "Youngster", "constant": "YOUNGSTER"},
+0x17: {"name": "Schoolboy", "constant": "SCHOOLBOY"},
+0x18: {"name": "Bird Keeper", "constant": "BIRD_KEEPER"},
+0x19: {"name": "Lass", "constant": "LASS"},
+0x1A: {"name": "Janine", "uses_numeric_trainer_ids": True, "constant": "JANINE"},
+0x1B: {"name": "CooltrainerM", "constant": "COOLTRAINERM"},
+0x1C: {"name": "CooltrainerF", "constant": "COOLTRAINERF"},
+0x1D: {"name": "Beauty", "constant": "BEAUTY"},
+0x1E: {"name": "Pokémaniac", "constant": "POKEMANIAC"},
+0x1F: {"name": "GruntM", "uses_numeric_trainer_ids": True, "constant": "GRUNTM"},
+0x20: {"name": "Gentleman", "constant": "GENTLEMAN"},
+0x21: {"name": "Skier", "constant": "SKIER"},
+0x22: {"name": "Teacher", "constant": "TEACHER"},
+0x23: {"name": "Sabrina", "uses_numeric_trainer_ids": True, "constant": "SABRINA"},
+0x24: {"name": "Bug Catcher", "constant": "BUG_CATCHER"},
+0x25: {"name": "Fisher", "constant": "FISHER"},
+0x26: {"name": "SwimmerM", "constant": "SWIMMERM"},
+0x27: {"name": "SwimmerF", "constant": "SWIMMERF"},
+0x28: {"name": "Sailor", "constant": "SAILOR"},
+0x29: {"name": "Super Nerd", "constant": "SUPER_NERD"},
+0x2A: {"name": "Rival2", "uses_numeric_trainer_ids": True, "constant": "RIVAL2"},
+0x2B: {"name": "Guitarist", "constant": "GUITARIST"},
+0x2C: {"name": "Hiker", "constant": "HIKER"},
+0x2D: {"name": "Biker", "constant": "BIKER"},
+0x2E: {"name": "Blaine", "uses_numeric_trainer_ids": True, "constant": "BLAINE"},
+0x2F: {"name": "Burglar", "constant": "BURGLAR"},
+0x30: {"name": "Firebreather", "constant": "FIREBREATHER"},
+0x31: {"name": "Juggler", "constant": "JUGGLER"},
+0x32: {"name": "Blackbelt_T", "constant": "BLACKBELT_T"},
+0x33: {"name": "ExecutiveM", "uses_numeric_trainer_ids": True, "constant": "EXECUTIVEM"},
+0x34: {"name": "Psychic_T", "constant": "PSYCHIC_T"},
+0x35: {"name": "Picnicker", "constant": "PICNICKER"},
+0x36: {"name": "Camper", "constant": "CAMPER"},
+0x37: {"name": "ExecutiveF", "uses_numeric_trainer_ids": True, "constant": "EXECUTIVEF"},
+0x38: {"name": "Sage", "constant": "SAGE"},
+0x39: {"name": "Medium", "constant": "MEDIUM"},
+0x3A: {"name": "Boarder", "constant": "BOARDER"},
+0x3B: {"name": "PokéfanM", "constant": "POKEFANM"},
+0x3C: {"name": "Kimono Girl", "constant": "KIMONO_GIRL"},
+0x3D: {"name": "Twins", "constant": "TWINS"},
+0x3E: {"name": "PokéfanF", "constant": "POKEFANF"},
+0x3F: {"name": "Red", "uses_numeric_trainer_ids": True, "constant": "RED"},
+0x40: {"name": "Blue", "uses_numeric_trainer_ids": True, "constant": "BLUE"},
+0x41: {"name": "Officer", "constant": "OFFICER"},
+0x42: {"name": "GruntF", "uses_numeric_trainer_ids": True, "constant": "GRUNTF"},
+0x43: {"name": "Mysticalman [Eusine]", "constant": "MYSTICALMAN"}, # crystal only
+}
+
+def remove_parentheticals_from_trainer_group_names():
+ """
+ Clean up the trainer group names.
+ """
+ i = 0
+ for (key, value) in trainer_group_names.items():
+ # remove the brackets and inner contents from each name
+ newvalue = re.sub(r'\[[^\)]*\]', '', value["name"]).strip()
+
+ # clean up some characters
+ newvalue = newvalue.replace("♀", "F")\
+ .replace("♂", "M")\
+ .replace(".", "")\
+ .replace(" ", "")\
+ .replace("é", "e")
+
+ # and calculate the address of the first byte of this pointer
+ trainer_group_names[key]["name"] = newvalue
+ trainer_group_names[key]["pointer_address"] = trainer_group_pointer_table_address + (i * 2)
+ i += 1
+ return trainer_group_names
+
+# remove [Blue] from each trainer group name
+remove_parentheticals_from_trainer_group_names()
diff --git a/pokemontools/type_constants.py b/pokemontools/type_constants.py
new file mode 100644
index 0000000..da89b0b
--- /dev/null
+++ b/pokemontools/type_constants.py
@@ -0,0 +1,21 @@
+# -*- coding: utf-8 -*-
+type_constants = {
+ "NORMAL": 0x00,
+ "FIGHTING": 0x01,
+ "FLYING": 0x02,
+ "POISON": 0x03,
+ "GROUND": 0x04,
+ "ROCK": 0x05,
+ "BUG": 0x07,
+ "GHOST": 0x08,
+ "STEEL": 0x09,
+ "CURSE_T": 0x13,
+ "FIRE": 0x14,
+ "WATER": 0x15,
+ "GRASS": 0x16,
+ "ELECTRIC": 0x17,
+ "PSYCHIC": 0x18,
+ "ICE": 0x19,
+ "DRAGON": 0x1A,
+ "DARK": 0x1B,
+}
diff --git a/pokemontools/wram.py b/pokemontools/wram.py
new file mode 100644
index 0000000..5352fb4
--- /dev/null
+++ b/pokemontools/wram.py
@@ -0,0 +1,125 @@
+# coding: utf-8
+"""
+RGBDS BSS section and constant parsing.
+"""
+
+import os
+path = os.path.dirname(os.path.abspath(__file__))
+
+def read_bss_sections(bss):
+ sections = []
+ section = {
+ "labels": [],
+ }
+ address = None
+ if type(bss) is not list: bss = bss.split('\n')
+ for line in bss:
+ line = line.lstrip()
+ if 'SECTION' in line:
+ if section: sections.append(section) # last section
+
+ address = eval(line[line.find('[')+1:line.find(']')].replace('$','0x'))
+ section = {
+ 'name': line.split('"')[1],
+ #'type': line.split(',')[1].split('[')[0].strip(),
+ 'start': address,
+ 'labels': [],
+ }
+
+ elif ':' in line:
+ # rgbds allows labels without :, but prefer convention
+ label = line[:line.find(':')]
+ if ';' not in label:
+ section['labels'] += [{
+ 'label': label,
+ 'address': address,
+ 'length': 0,
+ }]
+
+ elif line[:3] == 'ds ':
+ length = eval(line[3:line.find(';')].replace('$','0x'))
+ address += length
+ # adjacent labels use the same space
+ for label in section['labels'][::-1]:
+ if label['length'] == 0:
+ label['length'] = length
+ else:
+ break
+
+ elif 'EQU' in line:
+ # some space is defined using constants
+ name, value = line.split('EQU')
+ name, value = name.strip(), value.strip().replace('$','0x').replace('%','0b')
+ globals()[name] = eval(value)
+
+ sections.append(section)
+ return sections
+
+def read_wram_sections():
+ """
+ Opens the wram file and calls read_bss_sections.
+ """
+ wram_content = None
+ wram_file_path = os.path.join(os.path.dirname(path), 'wram.asm')
+ try:
+ wram_file_handler = open(wram_file_path, 'r')
+ except IOError as exception:
+ wram_content = [""]
+ else:
+ wram_content = wram_file_handler.readlines()
+ wram_sections = read_bss_sections(wram_content)
+ return wram_sections
+
+wram_sections = read_wram_sections()
+
+def make_wram_labels():
+ 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
+
+wram_labels = make_wram_labels()
+
+def constants_to_dict(constants):
+ return dict((eval(constant[constant.find('EQU')+3:constant.find(';')].replace('$','0x')), constant[:constant.find('EQU')].strip()) for constant in constants)
+
+def scrape_constants(text):
+ if type(text) is not list:
+ text = text.split('\n')
+ return constants_to_dict([line for line in text if 'EQU' in line[:line.find(';')]])
+
+def read_constants(filepath):
+ """
+ Load lines from a file and call scrape_constants.
+ """
+ try:
+ file_handler = open(filepath, "r")
+ except IOError as exception:
+ lines = [""]
+ else:
+ lines = file_handler.readlines()
+ constants = scrape_constants(lines)
+ return constants
+
+def read_hram_constants():
+ """
+ Load constants from hram.asm.
+ """
+ hram_path = os.path.join(os.path.dirname(path), 'hram.asm')
+ return read_constants(hram_path)
+
+# TODO: get rid of this global
+hram_constants = read_hram_constants()
+
+def read_gbhw_constants():
+ """
+ Load constants from gbhw.asm.
+ """
+ gbhw_path = os.path.join(os.path.dirname(path), 'gbhw.asm')
+ return read_constants(gbhw_path)
+
+# TODO: get rid of this global
+gbhw_constants = read_gbhw_constants()