#!/usr/bin/python # -*- coding: utf-8 -*- import sys from extras.pokemontools.crystal import ( command_classes, Warp, XYTrigger, Signpost, PeopleEvent, DataByteWordMacro, text_command_classes, movement_command_classes, music_classes, effect_classes, ) def load_pokecrystal_macros(): """ Construct a list of macros that are needed for pokecrystal preprocessing. """ ourmacros = [] even_more_macros = [ Warp, XYTrigger, Signpost, PeopleEvent, DataByteWordMacro, ] ourmacros += command_classes ourmacros += even_more_macros ourmacros += [each[1] for each in text_command_classes] ourmacros += movement_command_classes ourmacros += music_classes ourmacros += effect_classes return ourmacros chars = { "ガ": 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, "ァ": 0xE9, "@": 0x50, "#": 0x54, "…": 0x75, "┌": 0x79, "─": 0x7A, "┐": 0x7B, "│": 0x7C, "└": 0x7D, "┘": 0x7E, "№": 0x74, " ": 0x7F, "A": 0x80, "B": 0x81, "C": 0x82, "D": 0x83, "E": 0x84, "F": 0x85, "G": 0x86, "H": 0x87, "I": 0x88, "J": 0x89, "K": 0x8A, "L": 0x8B, "M": 0x8C, "N": 0x8D, "O": 0x8E, "P": 0x8F, "Q": 0x90, "R": 0x91, "S": 0x92, "T": 0x93, "U": 0x94, "V": 0x95, "W": 0x96, "X": 0x97, "Y": 0x98, "Z": 0x99, "(": 0x9A, ")": 0x9B, ":": 0x9C, ";": 0x9D, "[": 0x9E, "]": 0x9F, "a": 0xA0, "b": 0xA1, "c": 0xA2, "d": 0xA3, "e": 0xA4, "f": 0xA5, "g": 0xA6, "h": 0xA7, "i": 0xA8, "j": 0xA9, "k": 0xAA, "l": 0xAB, "m": 0xAC, "n": 0xAD, "o": 0xAE, "p": 0xAF, "q": 0xB0, "r": 0xB1, "s": 0xB2, "t": 0xB3, "u": 0xB4, "v": 0xB5, "w": 0xB6, "x": 0xB7, "y": 0xB8, "z": 0xB9, "Ä": 0xC0, "Ö": 0xC1, "Ü": 0xC2, "ä": 0xC3, "ö": 0xC4, "ü": 0xC5, "'d": 0xD0, "'l": 0xD1, "'m": 0xD2, "'r": 0xD3, "'s": 0xD4, "'t": 0xD5, "'v": 0xD6, "'": 0xE0, "-": 0xE3, "?": 0xE6, "!": 0xE7, ".": 0xE8, "&": 0xE9, "é": 0xEA, "→": 0xEB, "▷": 0xEC, "▶": 0xED, "▼": 0xEE, "♂": 0xEF, "¥": 0xF0, "×": 0xF1, "/": 0xF3, ",": 0xF4, "♀": 0xF5, "0": 0xF6, "1": 0xF7, "2": 0xF8, "3": 0xF9, "4": 0xFA, "5": 0xFB, "6": 0xFC, "7": 0xFD, "8": 0xFE, "9": 0xFF } class PreprocessorException(Exception): """ There was a problem in the preprocessor. """ class MacroException(PreprocessorException): """ There was a problem with a macro. """ def separate_comment(l): """ Separates asm and comments on a single line. """ in_quotes = False for i in xrange(len(l)): if not in_quotes: if l[i] == ";": break if l[i] == "\"": in_quotes = not in_quotes return l[:i], l[i:] or None def quote_translator(asm): """ Writes asm with quoted text translated into bytes. """ # split by quotes asms = asm.split('"') # skip asm that actually does use ASCII in quotes if "SECTION" in asms[0]\ or "INCBIN" in asms[0]\ or "INCLUDE" in asms[0]: return asm print_macro = False if asms[0].strip() == 'print': asms[0] = asms[0].replace('print','db 0,') print_macro = True output = '' even = False for token in asms: if even: characters = [] # token is a string to convert to byte values while len(token): # read a single UTF-8 codepoint char = token[0] if ord(char) < 0xc0: token = token[1:] # certain apostrophe-letter pairs are considered a single character if char == "'" and token: if token[0] in 'dlmrstv': char += token[0] token = token[1:] elif ord(char) < 0xe0: char = char + token[1:2] token = token[2:] elif ord(char) < 0xf0: char = char + token[1:3] token = token[3:] elif ord(char) < 0xf8: char = char + token[1:4] token = token[4:] elif ord(char) < 0xfc: char = char + token[1:5] token = token[5:] else: char = char + token[1:6] token = token[6:] characters += [char] if print_macro: line = 0 while len(characters): last_char = 1 if len(characters) > 18 and characters[-1] != '@': for i, char in enumerate(characters): last_char = i + 1 if ' ' not in characters[i+1:18]: break output += ", ".join("${0:02X}".format(chars[char]) for char in characters[:last_char-1]) if characters[last_char-1] != " ": output += ", ${0:02X}".format(characters[last_char-1]) if not line & 1: line_ending = 0x4f else: line_ending = 0x51 output += ", ${0:02X}".format(line_ending) line += 1 else: output += ", ".join(["${0:02X}".format(chars[char]) for char in characters[:last_char]]) characters = characters[last_char:] if len(characters): output += ", " # end text line_ending = 0x57 output += ", ${0:02X}".format(line_ending) output += ", ".join(["${0:02X}".format(chars[char]) for char in characters]) else: output += token even = not even return output def extract_token(asm): return asm.split(" ")[0].strip() def make_macro_table(macros): return dict(((macro.macro_name, macro) for macro in macros)) def macro_test(asm, macro_table): """ Returns a matching macro, or None/False. """ # macros are determined by the first symbol on the line token = extract_token(asm) # skip db and dw since rgbasm handles those and they aren't macros if token is not None and token not in ["db", "dw"] and token in macro_table: return (macro_table[token], token) else: return (None, None) def is_based_on(something, base): """ Checks whether or not 'something' is a class that is a subclass of a class by name. This is a terrible hack but it removes a direct dependency on existing macros. Used by macro_translator. """ options = [str(klass.__name__) for klass in something.__bases__] options += [something.__name__] return (base in options) def check_macro_sanity(params, macro, original_line): """ Checks whether or not the correct number of arguments are being passed to a certain macro. There are a number of possibilities based on the types of parameters that define the macro. @param params: a list of parameters given to the macro @param macro: macro klass @param original_line: the line being preprocessed """ allowed_length = 0 for (index, param_type) in macro.param_types.items(): param_klass = param_type["class"] if param_klass.byte_type == "db": allowed_length += 1 # just one value elif param_klass.byte_type == "dw": if param_klass.size == 2: allowed_length += 1 # just label elif param_klass.size == 3: allowed_length += 2 # bank and label else: raise MacroException( "dunno what to do with a macro param with a size > 3 (size={size})" .format(size=param_klass.size) ) else: raise MacroException( "dunno what to do with this non db/dw macro param: {klass} in line {line}" .format(klass=param_klass, line=original_line) ) # sometimes the allowed length can vary if hasattr(macro, "allowed_lengths"): allowed_lengths = macro.allowed_lengths + [allowed_length] else: allowed_lengths = [allowed_length] # used twice, so precompute once params_len = len(params) if params_len not in allowed_lengths: raise PreprocessorException( "mismatched number of parameters ({count}, instead of any of {allowed}) on this line: {line}" .format( count=params_len, allowed=allowed_lengths, line=original_line, ) ) return True def macro_translator(macro, token, line, show_original_lines=False, do_macro_sanity_check=False): """ Converts a line with a macro into a rgbasm-compatible line. @param show_original_lines: show lines before preprocessing in stdout @param do_macro_sanity_check: helpful for debugging macros """ if macro.macro_name != token: raise MacroException("macro/token mismatch") original_line = line # remove trailing newline if line[-1] == "\n": line = line[:-1] else: original_line += "\n" # remove first tab has_tab = False if line[0] == "\t": has_tab = True line = line[1:] # remove duplicate whitespace (also trailing) line = " ".join(line.split()) params = [] # check if the line has params if " " in line: # split the line into separate parameters params = line.replace(token, "").split(",") # check if there are no params (redundant) if len(params) == 1 and params[0] == "": raise MacroException("macro has no params?") # write out a comment showing the original line if show_original_lines: sys.stdout.write("; original_line: " + original_line) # rgbasm can handle "db" so no preprocessing is required, plus this wont be # reached because of earlier checks in macro_test. if macro.macro_name in ["db", "dw"]: sys.stdout.write(original_line) return # certain macros don't need an initial byte written # do: all scripting macros # don't: signpost, warp_def, person_event, xy_trigger if not macro.override_byte_check: sys.stdout.write("db ${0:02X}\n".format(macro.id)) # Does the number of parameters on this line match any allowed number of # parameters that the macro expects? if do_macro_sanity_check: check_macro_sanity(params, macro, original_line) # used for storetext correction = 0 output = "" index = 0 while index < len(params): param_type = macro.param_types[index - correction] description = param_type["name"] param_klass = param_type["class"] byte_type = param_klass.byte_type # db or dw size = param_klass.size param = params[index].strip() # param_klass.to_asm() won't work here because it doesn't # include db/dw. # some parameters are really multiple types of bytes if (byte_type == "dw" and size != 2) or \ (byte_type == "db" and size != 1): output += ("; " + description + "\n") if size == 3 and is_based_on(param_klass, "PointerLabelBeforeBank"): # write the bank first output += ("db " + param + "\n") # write the pointer second output += ("dw " + params[index+1].strip() + "\n") index += 2 correction += 1 elif size == 3 and is_based_on(param_klass, "PointerLabelAfterBank"): # write the pointer first output += ("dw " + param + "\n") # write the bank second output += ("db " + params[index+1].strip() + "\n") index += 2 correction += 1 elif size == 3 and "from_asm" in dir(param_klass): output += ("db " + param_klass.from_asm(param) + "\n") index += 1 else: raise MacroException( "dunno what to do with this macro param ({klass}) in line: {line}" .format( klass=param_klass, line=original_line, ) ) # or just print out the byte else: output += (byte_type + " " + param + " ; " + description + "\n") index += 1 sys.stdout.write(output) def read_line(l, macro_table): """Preprocesses a given line of asm.""" if l in ["\n", ""] or l[0] == ";": sys.stdout.write(l) return # jump out early # strip comments from asm asm, comment = separate_comment(l) # export all labels if ':' in asm[:asm.find('"')]: sys.stdout.write('GLOBAL ' + asm.split(':')[0] + '\n') # expect preprocessed .asm files if "INCLUDE" in asm: asm = asm.replace('.asm','.tx') sys.stdout.write(asm) # ascii string macro preserves the bytes as ascii (skip the translator) elif len(asm) > 6 and ("ascii " == asm[:6] or "\tascii " == asm[:7]): asm = asm.replace("ascii", "db", 1) sys.stdout.write(asm) # convert text to bytes when a quote appears (not in a comment) elif "\"" in asm: sys.stdout.write(quote_translator(asm)) # check against other preprocessor features else: macro, token = macro_test(asm, macro_table) if macro: macro_translator(macro, token, asm) else: sys.stdout.write(asm) if comment: sys.stdout.write(comment) def preprocess(macro_table, lines=None): """Main entry point for the preprocessor.""" if not lines: # read each line from stdin lines = (sys.stdin.readlines()) elif not isinstance(lines, list): # split up the input into individual lines lines = lines.split("\n") for l in lines: read_line(l, macro_table) def main(): macros = load_pokecrystal_macros() macro_table = make_macro_table(macros) preprocess(macro_table) # only run against stdin when not included as a module if __name__ == "__main__": main()