Merge pull request #10 from kanzure/config

Configuration for paths
This commit is contained in:
Bryan Bishop 2013-09-04 20:38:11 -07:00
commit ee05e2fe1d
7 changed files with 732 additions and 589 deletions

View File

@ -1,2 +1,3 @@
import config
import crystal
import preprocessor

46
pokemontools/config.py Normal file
View File

@ -0,0 +1,46 @@
"""
Configuration
"""
import os
import exceptions
class Config(object):
"""
The Config class handles all configuration for pokemontools. Other classes
and functions use a Config object to determine where expected files can be
located.
"""
def __init__(self, **kwargs):
"""
Store all parameters.
"""
self._config = {}
for (key, value) in kwargs.items():
if key not in self.__dict__:
self._config[key] = value
else:
raise exceptions.ConfigException(
"Can't store \"{0}\" in configuration because the key conflicts with an existing property."
.format(key)
)
if "path" not in self._config:
self._config["path"] = os.getcwd()
def __getattr__(self, key):
"""
Grab the value from the class properties, then check the configuration,
and raise an exception if nothing works.
"""
if key in self.__dict__:
return self.__dict__[key]
elif key in self._config:
return self._config[key]
else:
raise exceptions.ConfigException(
"no config found for \"{0}\"".format(key)
)

View File

@ -11,3 +11,18 @@ class TextScriptException(Exception):
"""
TextScript encountered an inconsistency or problem.
"""
class ConfigException(Exception):
"""
Configuration error. Maybe a missing config variable.
"""
class PreprocessorException(Exception):
"""
There was a problem in the preprocessor.
"""
class MacroException(PreprocessorException):
"""
There was a problem with a macro.
"""

View File

@ -1,4 +1,7 @@
# -*- coding: utf-8 -*-
"""
GBC disassembler
"""
import os
import sys
@ -7,22 +10,15 @@ from ctypes import c_int8
import random
import json
from wram import *
import config
import crystal
import labels
import wram
# 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 = [
@ -563,49 +559,6 @@ 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.
@ -627,290 +580,6 @@ def get_global_address(address, bank):
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.
@ -929,18 +598,351 @@ def all_outstanding_labels_are_reverse(byte_labels, offset):
if not label_id < offset: return False
return True
class Disassembler(object):
"""
GBC disassembler
"""
def __init__(self, config):
"""
Setup the class instance.
"""
self.config = config
self.wram = wram.WRAMProcessor(self.config)
self.labels = labels.Labels(self.config)
def initialize(self):
"""
Setup the disassembler.
"""
self.wram.initialize()
self.labels.initialize()
# TODO: fix how ROM is handled throughout the project.
rom_path = os.path.join(self.config.path, "baserom.gbc")
self.rom = bytearray(open(rom_path, "rb").read())
def find_label(self, local_address, bank_id=0):
# keep an integer
if type(local_address) == str:
local_address = int(local_address.replace("$", "0x"), 16)
if local_address < 0x8000:
for label_entry in self.labels.labels:
if get_local_address(label_entry["address"]) == local_address:
if label_entry["bank"] == bank_id or label_entry["bank"] == 0:
return label_entry["label"]
if local_address in self.wram.wram_labels.keys():
return self.wram.wram_labels[local_address][-1]
for constants in [self.wram.gbhw_constants, self.wram.hram_constants]:
if local_address in constants.keys() and local_address >= 0xff00:
return constants[local_address]
return None
def find_address_from_label(self, label):
for label_entry in self.labels.labels:
if label == label_entry["label"]:
return label_entry["address"]
return None
def output_bank_opcodes(self, original_offset, max_byte_count=0x4000, include_last_address=True, stop_at=[], debug=False):
"""
Output bank opcodes.
fs = current_address
b = bank_byte
in = input_data -- rom
bank_size = byte_count
i = offset
ad = end_address
a, oa = current_byte_number
stop_at can be used to supply a list of addresses to not disassemble
over. This is useful if you know in advance that there are a lot of
fall-throughs.
"""
bank_id = original_offset / 0x4000
if debug: print "bank id is: " + str(bank_id)
last_hl_address = None #for when we're scanning the main map script
last_a_address = None
used_3d97 = False
rom = self.rom
offset = original_offset
current_byte_number = 0 #start from the beginning
#we don't actually have an end address, but we'll just say $4000
end_address = original_offset + max_byte_count
byte_labels = {}
data_tables = {}
first_loop = True
output = ""
keep_reading = True
is_data = False
while offset <= end_address and keep_reading:
current_byte = rom[offset]
maybe_byte = current_byte
# stop at any address
if not first_loop and offset in stop_at:
keep_reading = False
break
#first check if this byte already has a label
#if it does, use the label
#if not, generate a new label
if offset in byte_labels.keys():
line_label = byte_labels[offset]["name"]
byte_labels[offset]["usage"] += 1
output += "\n"
else:
line_label = asm_label(offset)
byte_labels[offset] = {}
byte_labels[offset]["name"] = line_label
byte_labels[offset]["usage"] = 0
byte_labels[offset]["definition"] = True
output += line_label + "\n" #" ; " + hex(offset) + "\n"
#find out if there's a two byte key like this
temp_maybe = maybe_byte
temp_maybe += ( rom[offset+1] << 8)
if not is_data and temp_maybe in opt_table.keys() and rom[offset+1]!=0:
opstr = opt_table[temp_maybe][0].lower()
if "x" in opstr:
for x in range(0, opstr.count("x")):
insertion = rom[offset + 1]
insertion = "$" + hex(insertion)[2:]
opstr = opstr[:opstr.find("x")].lower() + insertion + opstr[opstr.find("x")+1:].lower()
current_byte += 1
offset += 1
if "?" in opstr:
for y in range(0, opstr.count("?")):
byte1 = rom[offset + 1]
byte2 = rom[offset + 2]
number = byte1
number += byte2 << 8;
insertion = "$%.4x" % (number)
opstr = opstr[:opstr.find("?")].lower() + insertion + opstr[opstr.find("?")+1:].lower()
current_byte_number += 2
offset += 2
output += spacing + opstr #+ " ; " + hex(offset)
output += "\n"
current_byte_number += 2
offset += 2
elif not is_data and maybe_byte in opt_table.keys():
op_code = opt_table[maybe_byte]
op_code_type = op_code[1]
op_code_byte = maybe_byte
#type = -1 when it's the E op
#if op_code_type != -1:
if op_code_type == 0 and rom[offset] == op_code_byte:
op_str = op_code[0].lower()
output += spacing + op_code[0].lower() #+ " ; " + hex(offset)
output += "\n"
offset += 1
current_byte_number += 1
elif op_code_type == 1 and rom[offset] == op_code_byte:
oplen = len(op_code[0])
opstr = copy(op_code[0])
xes = op_code[0].count("x")
include_comment = False
for x in range(0, xes):
insertion = rom[offset + 1]
insertion = "$" + hex(insertion)[2:]
if current_byte == 0x18 or current_byte==0x20 or current_byte in relative_jumps: #jr or jr nz
#generate a label for the byte we're jumping to
target_address = offset + 2 + c_int8(rom[offset + 1]).value
if target_address in byte_labels.keys():
byte_labels[target_address]["usage"] = 1 + byte_labels[target_address]["usage"]
line_label2 = byte_labels[target_address]["name"]
else:
line_label2 = asm_label(target_address)
byte_labels[target_address] = {}
byte_labels[target_address]["name"] = line_label2
byte_labels[target_address]["usage"] = 1
byte_labels[target_address]["definition"] = False
insertion = line_label2
if has_outstanding_labels(byte_labels) and all_outstanding_labels_are_reverse(byte_labels, offset):
include_comment = True
elif current_byte == 0x3e:
last_a_address = rom[offset + 1]
opstr = opstr[:opstr.find("x")].lower() + insertion + opstr[opstr.find("x")+1:].lower()
# because the $ff00+$ff syntax is silly
if opstr.count("$") > 1 and "+" in opstr:
first_orig = opstr[opstr.find("$"):opstr.find("+")]
first_val = eval(first_orig.replace("$","0x"))
second_orig = opstr[opstr.find("+$")+1:opstr.find("]")]
second_val = eval(second_orig.replace("$","0x"))
combined_val = "$%.4x" % (first_val + second_val)
result = self.find_label(combined_val, bank_id)
if result != None:
combined_val = result
replacetron = "[%s+%s]" % (first_orig, second_orig)
opstr = opstr.replace(replacetron, "[%s]" % combined_val)
output += spacing + opstr
if include_comment:
output += " ; " + hex(offset)
if current_byte in relative_jumps:
output += " $" + hex(rom[offset + 1])[2:]
output += "\n"
current_byte_number += 1
offset += 1
insertion = ""
current_byte_number += 1
offset += 1
include_comment = False
elif op_code_type == 2 and rom[offset] == op_code_byte:
oplen = len(op_code[0])
opstr = copy(op_code[0])
qes = op_code[0].count("?")
for x in range(0, qes):
byte1 = rom[offset + 1]
byte2 = rom[offset + 2]
number = byte1
number += byte2 << 8
if current_byte not in call_commands + discrete_jumps + relative_jumps:
pointer = get_global_address(number, bank_id)
if pointer not in data_tables.keys():
data_tables[pointer] = {}
data_tables[pointer]['usage'] = 0
else:
data_tables[pointer]['usage'] += 1
insertion = "$%.4x" % (number)
result = self.find_label(insertion, bank_id)
if result != None:
insertion = result
opstr = opstr[:opstr.find("?")].lower() + insertion + opstr[opstr.find("?")+1:].lower()
output += spacing + opstr #+ " ; " + hex(offset)
output += "\n"
current_byte_number += 2
offset += 2
current_byte_number += 1
offset += 1
if current_byte == 0x21:
last_hl_address = byte1 + (byte2 << 8)
if current_byte == 0xcd:
if number == 0x3d97: used_3d97 = True
#duck out if this is jp $24d7
if current_byte == 0xc3 or current_byte in relative_unconditional_jumps:
if current_byte == 0xc3:
if number == 0x3d97: used_3d97 = True
#if number == 0x24d7: #jp
if not has_outstanding_labels(byte_labels) or all_outstanding_labels_are_reverse(byte_labels, offset):
keep_reading = False
is_data = False
break
else:
is_data = True
else:
#if is_data and keep_reading:
output += spacing + "db $" + hex(rom[offset])[2:] #+ " ; " + hex(offset)
output += "\n"
offset += 1
current_byte_number += 1
if offset in byte_labels.keys():
is_data = False
keep_reading = True
#else the while loop would have spit out the opcode
#these two are done prior
#offset += 1
#current_byte_number += 1
if not is_data and current_byte in relative_unconditional_jumps + end_08_scripts_with:
#stop reading at a jump, relative jump or return
if not has_outstanding_labels(byte_labels) or all_outstanding_labels_are_reverse(byte_labels, offset):
keep_reading = False
is_data = False #cleanup
break
elif offset not in byte_labels.keys() and offset in data_tables.keys():
is_data = True
keep_reading = True
else:
is_data = False
keep_reading = True
output += "\n"
elif is_data and offset not in byte_labels.keys():
is_data = True
keep_reading = True
else:
is_data = False
keep_reading = True
if offset in data_tables.keys():
output = output.replace('$%x' % (get_local_address(offset)), data_label(offset))
output += data_label(offset) + '\n'
is_data = True
keep_reading = True
first_loop = False
#clean up unused labels
for label_line in byte_labels.keys():
address = label_line
label_line = byte_labels[label_line]
if label_line["usage"] == 0:
output = output.replace((label_line["name"] + "\n"), "")
#tone down excessive spacing
output = output.replace("\n\n\n","\n\n")
#add the offset of the final location
if include_last_address:
output += "; " + hex(offset)
return (output, offset, last_hl_address, last_a_address, used_3d97)
if __name__ == "__main__":
load_labels()
conf = config.Config()
disasm = Disassembler(conf)
disasm.initialize()
addr = sys.argv[1]
if ":" in addr:
addr = addr.split(":")
addr = int(addr[0], 16)*0x4000+(int(addr[1], 16)%0x4000)
else:
label_addr = find_address_from_label(addr)
label_addr = disasm.find_address_from_label(addr)
if label_addr:
addr = label_addr
else:
addr = int(addr, 16)
print output_bank_opcodes(addr)[0]
output = disasm.output_bank_opcodes(addr)[0]
print output

View File

@ -3,7 +3,37 @@
Various label/line-related functions.
"""
import os
import json
import pointers
import crystal
class Labels(object):
"""
Store all labels.
"""
filename = "labels.json"
def __init__(self, config):
"""
Setup the instance.
"""
self.config = config
self.path = os.path.join(self.config.path, Labels.filename)
def initialize(self):
"""
Handle anything requiring file-loading and such.
"""
if not os.path.exists(self.path):
logging.info(
"Running crystal.scan_for_predefined_labels to create \"{0}\". Trying.."
.format(Labels.filename)
)
crystal.scan_for_predefined_labels()
self.labels = json.read(open(self.path, "r").read())
def remove_quoted_text(line):
"""get rid of content inside quotes

View File

@ -5,13 +5,8 @@ Basic preprocessor for both pokecrystal and pokered.
import sys
from crystal import (
DataByteWordMacro,
)
default_macros = [
DataByteWordMacro,
]
import exceptions
import crystal
chars = {
"": 0x05,
@ -278,16 +273,6 @@ chars = {
"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.
@ -299,7 +284,10 @@ def separate_comment(l):
break
if l[i] == "\"":
in_quotes = not in_quotes
return l[:i], l[i:] or None
return (l[:i], l[i:]) or None
def make_macro_table(macros):
return dict(((macro.macro_name, macro) for macro in macros))
def quote_translator(asm):
"""
@ -387,38 +375,7 @@ def quote_translator(asm):
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):
def check_macro_sanity(self, 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
@ -441,12 +398,12 @@ def check_macro_sanity(params, macro, original_line):
elif param_klass.size == 3:
allowed_length += 2 # bank and label
else:
raise MacroException(
raise exceptions.MacroException(
"dunno what to do with a macro param with a size > 3 (size={size})"
.format(size=param_klass.size)
)
else:
raise MacroException(
raise exceptions.MacroException(
"dunno what to do with this non db/dw macro param: {klass} in line {line}"
.format(klass=param_klass, line=original_line)
)
@ -461,7 +418,7 @@ def check_macro_sanity(params, macro, original_line):
params_len = len(params)
if params_len not in allowed_lengths:
raise PreprocessorException(
raise exceptions.PreprocessorException(
"mismatched number of parameters ({count}, instead of any of {allowed}) on this line: {line}"
.format(
count=params_len,
@ -472,170 +429,223 @@ def check_macro_sanity(params, macro, original_line):
return True
def macro_translator(macro, token, line, show_original_lines=False, do_macro_sanity_check=False):
def extract_token(asm):
return asm.split(" ")[0].strip()
def is_based_on(something, base):
"""
Converts a line with a macro into a rgbasm-compatible line.
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.
@param show_original_lines: show lines before preprocessing in stdout
@param do_macro_sanity_check: helpful for debugging macros
Used by macro_translator.
"""
if macro.macro_name != token:
raise MacroException("macro/token mismatch")
options = [str(klass.__name__) for klass in something.__bases__]
options += [something.__name__]
return (base in options)
original_line = line
class Preprocessor(object):
"""
A wrapper around the actual preprocessing step. Because rgbasm can't handle
many of these macros.
"""
# remove trailing newline
if line[-1] == "\n":
line = line[:-1]
else:
original_line += "\n"
default_macros = [
crystal.DataByteWordMacro,
]
# remove first tab
has_tab = False
if line[0] == "\t":
has_tab = True
line = line[1:]
def __init__(self, config, macros=None):
"""
Setup the preprocessor.
"""
self.config = config
# remove duplicate whitespace (also trailing)
line = " ".join(line.split())
if macros == None:
macros = Preprocessor.default_macros
params = []
self.macros = macros
self.macro_table = make_macro_table(self.macros)
# check if the line has params
if " " in line:
# split the line into separate parameters
params = line.replace(token, "").split(",")
def preprocess(self, lines=None):
"""
Run the preprocessor against stdin.
"""
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")
# check if there are no params (redundant)
if len(params) == 1 and params[0] == "":
raise MacroException("macro has no params?")
for l in lines:
self.read_line(l)
# write out a comment showing the original line
if show_original_lines:
sys.stdout.write("; original_line: " + original_line)
def read_line(self, l):
"""
Preprocesses a given line of asm.
"""
# 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
if l in ["\n", ""] or l[0] == ";":
sys.stdout.write(l)
return # jump out early
# 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))
# strip comments from asm
asm, comment = separate_comment(l)
# 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)
# export all labels
if ':' in asm[:asm.find('"')] and "macro" not in asm.lower():
sys.stdout.write('GLOBAL ' + asm.split(':')[0] + '\n')
# 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('"')] and "macro" not in asm.lower():
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:
# expect preprocessed .asm files
if "INCLUDE" in asm:
asm = asm.replace('.asm','.tx')
sys.stdout.write(asm)
if comment:
sys.stdout.write(comment)
# 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)
def preprocess(macro_table, lines=None):
"""Main entry point for the preprocessor."""
# convert text to bytes when a quote appears (not in a comment)
elif "\"" in asm:
sys.stdout.write(quote_translator(asm))
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")
# check against other preprocessor features
else:
macro, token = self.macro_test(asm)
if macro:
self.macro_translator(macro, token, asm)
else:
sys.stdout.write(asm)
for l in lines:
read_line(l, macro_table)
if comment:
sys.stdout.write(comment)
def macro_translator(self, 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 exceptions.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 exceptions.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:
self.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 exceptions.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 macro_test(self, asm):
"""
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 self.macro_table:
return (self.macro_table[token], token)
else:
return (None, None)

View File

@ -4,7 +4,15 @@ RGBDS BSS section and constant parsing.
"""
import os
path = os.path.dirname(os.path.abspath(__file__))
def make_wram_labels(wram_sections):
wram_labels = {}
for section in wram_sections:
for label in section['labels']:
if label['address'] not in wram_labels.keys():
wram_labels[label['address']] = []
wram_labels[label['address']] += [label['label']]
return wram_labels
def read_bss_sections(bss):
sections = []
@ -55,34 +63,6 @@ def read_bss_sections(bss):
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_sections):
wram_labels = {}
for section in wram_sections:
for label in section['labels']:
if label['address'] not in wram_labels.keys():
wram_labels[label['address']] = []
wram_labels[label['address']] += [label['label']]
return wram_labels
wram_labels = make_wram_labels(wram_sections)
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)
@ -95,31 +75,90 @@ 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 = None
with open(filepath, "r") as file_handler:
lines = file_handler.readlines()
constants = scrape_constants(lines)
return constants
def read_hram_constants():
class WRAMProcessor(object):
"""
Load constants from hram.asm.
RGBDS BSS section and constant parsing.
"""
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 __init__(self, config):
"""
Setup for WRAM parsing.
"""
self.config = config
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)
self.paths = {}
self.paths["wram"] = os.path.join(self.config.path, "wram.asm")
self.paths["hram"] = os.path.join(self.config.path, "hram.asm")
self.paths["gbhw"] = os.path.join(self.config.path, "gbhw.asm")
# TODO: get rid of this global
gbhw_constants = read_gbhw_constants()
def initialize(self):
"""
Read constants.
"""
self.setup_wram_sections()
self.setup_wram_labels()
self.setup_hram_constants()
self.setup_gbhw_constants()
def read_wram_sections(self):
"""
Opens the wram file and calls read_bss_sections.
"""
wram_content = None
wram_file_path = self.paths["wram"]
with open(wram_file_path, "r") as wram:
wram_content = wram.readlines()
wram_sections = read_bss_sections(wram_content)
return wram_sections
def setup_wram_sections(self):
"""
Call read_wram_sections and set a variable.
"""
self.wram_sections = self.read_wram_sections()
return self.wram_sections
def setup_wram_labels(self):
"""
Make wram labels based on self.wram_sections as input.
"""
self.wram_labels = make_wram_labels(self.wram_sections)
return self.wram_labels
def read_hram_constants(self):
"""
Read constants from hram.asm using read_constants.
"""
hram_constants = read_constants(self.paths["hram"])
return hram_constants
def setup_hram_constants(self):
"""
Call read_hram_constants and set a variable.
"""
self.hram_constants = self.read_hram_constants()
return self.hram_constants
def read_gbhw_constants(self):
"""
Read constants from gbhw.asm using read_constants.
"""
gbhw_constants = read_constants(self.paths["gbhw"])
return gbhw_constants
def setup_gbhw_constants(self):
"""
Call read_gbhw_constants and set a variable.
"""
self.gbhw_constants = self.read_gbhw_constants()
return self.gbhw_constants