Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
tree: bbd77f0693
Fetching contributors…

Cannot retrieve contributors at this time

8811 lines (7689 sloc) 365.519 kB
# -*- coding: utf-8 -*-
#utilities to help disassemble pokémon crystal
import sys, os, inspect, md5, json
from copy import copy, deepcopy
import subprocess
from new import classobj
import random
#for IntervalMap
from bisect import bisect_left, bisect_right
from itertools import izip
#for testing all this crap
try:
import unittest2 as unittest
except ImportError:
import unittest
# for capwords
import string
# Check for things we need in unittest.
if not hasattr(unittest.TestCase, 'setUpClass'):
sys.stderr.write("The unittest2 module or Python 2.7 is required to run this script.")
sys.exit(1)
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
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 *
from move_constants import moves
# for fixing trainer_group_names
import re
trainer_group_pointer_table_address = 0x39999
trainer_group_pointer_table_address_gs = 0x3993E
class Size():
"""a simple way to track whether or not a size
includes the first value or not, like for
whether or not the size of a command in a script
also includes the command byte or not"""
def __init__(self, size, inclusive=False):
self.inclusive = inclusive
if inclusive: size = size-1
self.size = size
def inclusive(self):
return self.size + 1
def exclusive(self):
return self.size
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)+'}'
# ---- 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 load_asm(filename="../main.asm"):
"""loads the asm source code into memory"""
global asm
asm = open(filename, "r").read().split("\n")
asm = AsmList(asm)
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
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)
from pokemon_constants import pokemon_constants
def get_pokemon_constant_by_id(id):
if id == 0: return None
return pokemon_constants[id]
def parse_script_asm_at(*args, **kwargs):
#XXX TODO
return None
from item_constants import item_constants
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
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"
from pksv import pksv_gs, pksv_crystal, pksv_crystal_unknowns,\
pksv_crystal_more_enders
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):
PointerLabelParam.parse(self)
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:
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.address, bank=self.bank)
address2 = calculate_pointer_from_bytes_at(address, bank="reverse") #maybe not "reverse"?
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",
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",
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", PointerLabelToScriptPointer]],
0x03: ["2jump", ["pointer", ScriptPointerLabelParam]],
0x04: ["3jump", ["pointer", ScriptPointerLabelBeforeBank]],
0x05: ["2ptjump", ["pointer", PointerLabelToScriptPointer]],
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", PointerToAsmPointerParam]],
#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]],
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", SingleByteParam], ["wtf", SingleByteParam]],
0x9D: ["checkphonecall"],
0x9E: ["verbosegiveitem", ["item", ItemLabelByte], ["quantity", DecimalParam]],
0x9F: ["verbosegiveitem2", ["item", ItemLabelByte]],
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]],
0xA8: ["unknown0xa8", ["unknown", SingleByteParam]],
0xB2: ["unknown0xb2", ["unknown", SingleByteParam]],
0xCC: ["unknown0xcc"],
}
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()
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
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:
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]:
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:
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])
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):
</