summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--pokemontools/audio.py83
-rw-r--r--pokemontools/battle_animations.py324
-rw-r--r--pokemontools/crystal.py95
-rw-r--r--pokemontools/data/pokecrystal/gbhw.asm102
-rw-r--r--pokemontools/data/pokecrystal/hram.asm71
-rw-r--r--pokemontools/gfx.py116
-rw-r--r--pokemontools/labels.py26
-rw-r--r--pokemontools/map_editor.py36
-rw-r--r--pokemontools/map_gfx.py380
-rw-r--r--pokemontools/preprocessor.py96
-rw-r--r--pokemontools/scan_includes.py36
-rw-r--r--pokemontools/sym.py33
-rw-r--r--pokemontools/vba/path.py664
-rw-r--r--pokemontools/wram.py74
14 files changed, 1916 insertions, 220 deletions
diff --git a/pokemontools/audio.py b/pokemontools/audio.py
index 38fd65f..0cdfcbc 100644
--- a/pokemontools/audio.py
+++ b/pokemontools/audio.py
@@ -5,14 +5,13 @@ import os
from math import ceil
from gbz80disasm import get_global_address, get_local_address
-
-import crystal
+from labels import line_has_label
from crystal import music_classes as sound_classes
-
from crystal import (
Command,
SingleByteParam,
MultiByteParam,
+ PointerLabelParam,
load_rom,
)
@@ -23,10 +22,34 @@ import configuration
conf = configuration.Config()
+def is_comment(asm):
+ return asm.startswith(';')
+
+def asm_sort(asm_def):
+ """
+ Sort key for asm lists.
+
+ Usage:
+ list.sort(key=asm_sort)
+ sorted(list, key=asm_sort)
+ """
+ address, asm, last_address = asm_def
+ return (
+ address,
+ last_address,
+ not is_comment(asm),
+ not line_has_label(asm),
+ asm
+ )
+
def sort_asms(asms):
- """sort and remove duplicates from a list of tuples
- format (address, asm, last_address)"""
- return sorted(set(asms), key=lambda (x,y,z):(x,z,not y.startswith(';'), ':' not in y))
+ """
+ Sort and remove duplicates from an asm list.
+
+ Format: [(address, asm, last_address), ...]
+ """
+ return sorted(set(asms), key=asm_sort)
+
class NybbleParam:
size = 0.5
@@ -43,15 +66,18 @@ class NybbleParam:
def parse(self):
self.nybble = (rom[self.address] >> {'lo': 0, 'hi': 4}[self.which]) & 0xf
-class HiNybbleParam(NybbleParam):
- which = 'hi'
def to_asm(self):
return '%d' % self.nybble
+ @staticmethod
+ def from_asm(value):
+ return value
+
+class HiNybbleParam(NybbleParam):
+ which = 'hi'
+
class LoNybbleParam(NybbleParam):
which = 'lo'
- def to_asm(self):
- return '%d' % self.nybble
class PitchParam(HiNybbleParam):
def to_asm(self):
@@ -66,14 +92,23 @@ class PitchParam(HiNybbleParam):
pitch += '_'
return pitch
+class NoteDurationParam(LoNybbleParam):
+ def to_asm(self):
+ self.nybble += 1
+ return LoNybbleParam.to_asm(self)
+
+ @staticmethod
+ def from_asm(value):
+ value = str(int(value) - 1)
+ return LoNybbleParam.from_asm(value)
class Note(Command):
macro_name = "note"
- size = 1
+ size = 0
end = False
param_types = {
0: {"name": "pitch", "class": PitchParam},
- 1: {"name": "duration", "class": LoNybbleParam},
+ 1: {"name": "duration", "class": NoteDurationParam},
}
allowed_lengths = [2]
override_byte_check = True
@@ -83,6 +118,7 @@ class Note(Command):
self.params = []
byte = rom[self.address]
current_address = self.address
+ size = 0
for (key, param_type) in self.param_types.items():
name = param_type["name"]
class_ = param_type["class"]
@@ -92,12 +128,20 @@ class Note(Command):
self.params += [obj]
current_address += obj.size
+ size += obj.size
+
+ # can't fit bytes into nybbles
+ if obj.size > 0.5:
+ if current_address % 1:
+ current_address = int(ceil(current_address))
+ if size % 1:
+ size = int(ceil(size))
self.params = dict(enumerate(self.params))
- # obj sizes were 0.5, but were working with ints
+ # obj sizes were 0.5, but we're working with ints
current_address = int(ceil(current_address))
- self.size = int(ceil(self.size))
+ self.size += int(ceil(size))
self.last_address = current_address
return True
@@ -105,7 +149,6 @@ class Note(Command):
class Noise(Note):
macro_name = "noise"
- size = 0
end = False
param_types = {
0: {"name": "duration", "class": LoNybbleParam},
@@ -155,7 +198,7 @@ class Channel:
# label any jumps or calls
for key, param in class_.param_types.items():
- if param['class'] == crystal.PointerLabelParam:
+ if param['class'] == PointerLabelParam:
label_address = class_.params[key].parsed_address
label = '%s_branch_%x' % (
self.base_label,
@@ -199,10 +242,10 @@ class Channel:
output = sort_asms(self.output + self.labels)
text = ''
for i, (address, asm, last_address) in enumerate(output):
- if ':' in asm:
+ if line_has_label(asm):
# dont print labels for empty chunks
for (address_, asm_, last_address_) in output[i:]:
- if ':' not in asm_:
+ if not line_has_label(asm_):
text += '\n' + asm + '\n'
break
else:
@@ -214,7 +257,7 @@ class Channel:
for class_ in sound_classes:
if class_.id == i:
return class_
- if self.channel in [4, 8]: return Noise
+ if self.channel == 8: return Noise
return Note
@@ -320,7 +363,6 @@ def dump_sounds(origin, names, base_label='Sound_'):
sound_at = read_bank_address_pointer(origin + i * 3)
sound = Sound(sound_at, base_label + name)
output = sound.to_asm(labels) + '\n'
-
# incbin trailing commands that didnt get picked up
index = addresses.index((sound.start_address, sound.last_address))
if index + 1 < len(addresses):
@@ -386,5 +428,4 @@ def generate_crystal_cry_pointers():
if __name__ == '__main__':
dump_crystal_music()
- dump_crystal_sfx()
diff --git a/pokemontools/battle_animations.py b/pokemontools/battle_animations.py
new file mode 100644
index 0000000..ffc89e4
--- /dev/null
+++ b/pokemontools/battle_animations.py
@@ -0,0 +1,324 @@
+# coding: utf-8
+
+import os
+from new import classobj
+
+import configuration
+conf = configuration.Config()
+
+from crystal import (
+ SingleByteParam,
+ PointerLabelParam,
+ DecimalParam,
+ BigEndianParam,
+ Command,
+ load_rom
+)
+
+from gbz80disasm import get_local_address, get_global_address
+from audio import sort_asms
+
+
+from wram import read_constants
+
+rom = bytearray(load_rom())
+
+sfx_constants = read_constants(os.path.join(conf.path, 'constants/sfx_constants.asm'))
+class SoundEffectParam(SingleByteParam):
+ def to_asm(self):
+ if self.byte in sfx_constants.keys():
+ sfx_constant = sfx_constants[self.byte]
+ return sfx_constant
+ return SingleByteParam.to_asm(self)
+
+anim_gfx_constants = read_constants(os.path.join(conf.path, 'constants/gfx_constants.asm'))
+class AnimGFXParam(SingleByteParam):
+ def to_asm(self):
+ if self.byte in anim_gfx_constants.keys():
+ return anim_gfx_constants[self.byte]
+ return SingleByteParam.to_asm(self)
+
+anims = read_constants(os.path.join(conf.path, 'constants/animation_constants.asm'))
+objs = { k: v for k, v in anims.items() if 'ANIM_OBJ' in v }
+bgs = { k: v for k, v in anims.items() if 'ANIM_BG' in v }
+anims = { k: v.replace('ANIM_','') for k, v in anims.items() }
+from move_constants import moves
+anims.update(moves)
+
+class AnimObjParam(SingleByteParam):
+ def to_asm(self):
+ if self.byte in objs.keys():
+ return objs[self.byte]
+ return SingleByteParam.to_asm(self)
+
+class BGEffectParam(SingleByteParam):
+ def to_asm(self):
+ if self.byte in bgs.keys():
+ return bgs[self.byte]
+ return SingleByteParam.to_asm(self)
+
+
+battle_animation_commands = {
+ 0xd0: ['anim_obj', ['obj', AnimObjParam], ['x', DecimalParam], ['y', DecimalParam], ['param', SingleByteParam]],
+ 0xd1: ['anim_1gfx', ['gfx1', AnimGFXParam]],
+ 0xd2: ['anim_2gfx', ['gfx1', AnimGFXParam], ['gfx2', AnimGFXParam]],
+ 0xd3: ['anim_3gfx', ['gfx1', AnimGFXParam], ['gfx2', AnimGFXParam], ['gfx3', AnimGFXParam]],
+ 0xd4: ['anim_4gfx', ['gfx1', AnimGFXParam], ['gfx2', AnimGFXParam], ['gfx3', AnimGFXParam], ['gfx4', AnimGFXParam]],
+ 0xd5: ['anim_5gfx', ['gfx1', AnimGFXParam], ['gfx2', AnimGFXParam], ['gfx3', AnimGFXParam], ['gfx4', AnimGFXParam], ['gfx5', AnimGFXParam]],
+ 0xd6: ['anim_incobj', ['id', SingleByteParam]],
+ 0xd7: ['anim_setobj', ['id', SingleByteParam], ['obj', AnimObjParam]], # bug: second param is interpreted as a command if not found in the object array
+ 0xd8: ['anim_incbgeffect', ['effect', BGEffectParam]],
+ 0xd9: ['anim_enemyfeetobj'],
+ 0xda: ['anim_playerheadobj'],
+ 0xdb: ['anim_checkpokeball'],
+ 0xdc: ['anim_transform'],
+ 0xdd: ['anim_raisesub'],
+ 0xde: ['anim_dropsub'],
+ 0xdf: ['anim_resetobp0'],
+ 0xe0: ['anim_sound', ['tracks', SingleByteParam], ['id', SoundEffectParam]],
+ 0xe1: ['anim_cry', ['pitch', SingleByteParam]],
+ 0xe2: ['anim_minimizeopp'], # unused
+ 0xe3: ['anim_oamon'],
+ 0xe4: ['anim_oamoff'],
+ 0xe5: ['anim_clearobjs'],
+ 0xe6: ['anim_beatup'],
+ 0xe7: ['anim_0xe7'], # nothing
+ 0xe8: ['anim_updateactorpic'],
+ 0xe9: ['anim_minimize'],
+ 0xea: ['anim_0xea'], # nothing
+ 0xeb: ['anim_0xeb'], # nothing
+ 0xec: ['anim_0xec'], # nothing
+ 0xed: ['anim_0xed'], # nothing
+ 0xee: ['anim_jumpand', ['value', SingleByteParam], ['address', PointerLabelParam]],
+ 0xef: ['anim_jumpuntil', ['address', PointerLabelParam]],
+ 0xf0: ['anim_bgeffect', ['effect', BGEffectParam], ['unknown', SingleByteParam], ['unknown', SingleByteParam], ['unknown', SingleByteParam]],
+ 0xf1: ['anim_bgp', ['colors', SingleByteParam]],
+ 0xf2: ['anim_obp0', ['colors', SingleByteParam]],
+ 0xf3: ['anim_obp1', ['colors', SingleByteParam]],
+ 0xf4: ['anim_clearsprites'],
+ 0xf5: ['anim_0xf5'], # nothing
+ 0xf6: ['anim_0xf6'], # nothing
+ 0xf7: ['anim_0xf7'], # nothing
+ 0xf8: ['anim_jumpif', ['value', SingleByteParam], ['address', PointerLabelParam]],
+ 0xf9: ['anim_setvar', ['value', SingleByteParam]],
+ 0xfa: ['anim_incvar'],
+ 0xfb: ['anim_jumpvar', ['value', SingleByteParam], ['address', PointerLabelParam]],
+ 0xfc: ['anim_jump', ['address', PointerLabelParam]],
+ 0xfd: ['anim_loop', ['count', SingleByteParam], ['address', PointerLabelParam]],
+ 0xfe: ['anim_call', ['address', PointerLabelParam]],
+ 0xff: ['anim_ret'],
+}
+
+battle_animation_enders = [
+ 'anim_jump',
+ 'anim_ret',
+]
+
+def create_battle_animation_classes():
+ classes = []
+ for cmd, command in battle_animation_commands.items():
+ cmd_name = command[0]
+ params = {
+ 'id': cmd,
+ 'size': 1,
+ 'end': cmd_name in battle_animation_enders,
+ 'macro_name': cmd_name,
+ 'param_types': {},
+ }
+ for i, (name, class_) in enumerate(command[1:]):
+ params['param_types'][i] = {'name': name, 'class': class_}
+ params['size'] += class_.size
+ class_name = cmd_name + 'Command'
+ class_ = classobj(class_name, (Command,), params)
+ globals()[class_name] = class_
+ classes += [class_]
+ return classes
+
+battle_animation_classes = create_battle_animation_classes()
+
+
+class BattleAnimWait(Command):
+ macro_name = 'anim_wait'
+ size = 1
+ end = macro_name in battle_animation_enders
+ param_types = {
+ 0: {'name': 'duration', 'class': DecimalParam},
+ }
+ override_byte_check = True
+
+
+class BattleAnim:
+ """
+ A list of battle animation commands read from a given address.
+
+ Results in a list of commands (self.output) and a list of labels (self.labels).
+ Format is (address, asm, last_address). Includes any subroutines and their output.
+
+ To convert to text, use self.to_asm().
+
+ For combining multiple BattleAnims, take self.output + self.labels from each
+ and sort with sort_asms.
+ """
+
+ def __init__(self, address, base_label=None, label=None, used_labels=[], macros=[]):
+ self.start_address = address
+ self.address = address
+
+ self.base_label = base_label
+ if self.base_label == None:
+ self.base_label = 'BattleAnim_' + hex(self.start_address)
+
+ self.label = label
+ if self.label == None:
+ self.label = self.base_label
+
+ self.used_labels = used_labels
+
+ self.output = []
+ self.labels = []
+ self.label_asm = (
+ self.start_address,
+ '%s: ; %x' % (self.label, self.start_address),
+ self.start_address
+ )
+ self.labels += [self.label_asm]
+ self.used_labels += [self.label_asm]
+
+ self.macros = macros
+
+ self.parse()
+
+ def parse(self):
+
+ done = False
+ while not done:
+ cmd = rom[self.address]
+ class_ = self.get_command_class(cmd)(address=self.address)
+ asm = class_.to_asm()
+
+ # label jumps/calls
+ for key, param in class_.param_types.items():
+ if param['class'] == PointerLabelParam:
+ label_address = class_.params[key].parsed_address
+ label = '%s_branch_%x' % (self.base_label, label_address)
+ label_def = '%s: ; %x' % (label, label_address)
+ label_asm = (label_address, label_def, label_address)
+ if label_asm not in self.used_labels:
+ self.labels += [label_asm]
+ asm = asm.replace('$%x' % get_local_address(label_address), label)
+
+ self.output += [(self.address, '\t' + asm, self.address + class_.size)]
+ self.address += class_.size
+
+ done = class_.end
+ # infinite loops are enders
+ if class_.macro_name == 'anim_loop':
+ if class_.params[0].byte == 0:
+ done = True
+
+ # last_address comment
+ self.output += [(self.address, '; %x\n' % self.address, self.address)]
+
+ # parse any other branches too
+ self.labels = list(set(self.labels))
+ for address, asm, last_address in self.labels:
+ if not (self.start_address <= address < self.address) and (address, asm, last_address) not in self.used_labels:
+ self.used_labels += [(address, asm, last_address)]
+ sub = BattleAnim(
+ address=address,
+ base_label=self.base_label,
+ label=asm.split(':')[0],
+ used_labels=self.used_labels,
+ macros=self.macros
+ )
+ self.output += sub.output
+ self.labels += sub.labels
+
+ self.output = list(set(self.output))
+ self.labels = list(set(self.labels))
+
+ def to_asm(self):
+ output = sort_asms(self.output + self.labels)
+ text = ''
+ for (address, asm, last_address) in output:
+ text += asm + '\n'
+ return text
+
+ def get_command_class(self, cmd):
+ if cmd < 0xd0:
+ return BattleAnimWait
+ for class_ in self.macros:
+ if class_.id == cmd:
+ return class_
+ return None
+
+
+def battle_anim_label(i):
+ """
+ Return a label matching the name of a battle animation by id.
+ """
+ if i in anims.keys():
+ base_label = 'BattleAnim_%s' % anims[i].title().replace('_','')
+ else:
+ base_label = 'BattleAnim_%d' % i
+ return base_label
+
+def dump_battle_anims(table_address=0xc906f, num_anims=278, macros=battle_animation_classes):
+ """
+ Dump each battle animation from a pointer table.
+ """
+
+ asms = []
+
+ asms += [(table_address, 'BattleAnimations: ; %x' % table_address, table_address)]
+
+ address = table_address
+ bank = address / 0x4000
+
+ for i in xrange(num_anims):
+ pointer_address = address
+ anim_address = rom[pointer_address] + rom[pointer_address + 1] * 0x100
+ anim_address = get_global_address(anim_address, bank)
+ base_label = battle_anim_label(i)
+ address += 2
+
+ # anim pointer
+ asms += [(pointer_address, '\tdw %s' % base_label, address)]
+
+ # anim script
+ anim = BattleAnim(
+ address=anim_address,
+ base_label=base_label,
+ macros=macros
+ )
+ asms += anim.output + anim.labels
+
+ asms += [(address, '; %x\n' % address, address)]
+
+ # jp sonicboom
+ anim = BattleAnim(
+ address=0xc9c00,
+ base_label='BattleAnim_Sonicboom_JP',
+ macros=macros
+ )
+ asms += anim.output + anim.labels
+
+ asms = sort_asms(asms)
+ return asms
+
+def asm_list_to_text(asms):
+ output = ''
+ last = asms[0][0]
+ for addr, asm, last_addr in asms:
+ if addr > last:
+ # incbin any unknown areas
+ output += '\nINCBIN "baserom.gbc", $%x, $%x - $%x\n\n\n' % (last, addr, last)
+ if addr >= last:
+ output += asm + '\n'
+ last = last_addr
+ return output
+
+if __name__ == '__main__':
+ asms = dump_battle_anims()
+ print asm_list_to_text(asms)
+
diff --git a/pokemontools/crystal.py b/pokemontools/crystal.py
index 4e556d8..f663a87 100644
--- a/pokemontools/crystal.py
+++ b/pokemontools/crystal.py
@@ -70,6 +70,11 @@ OldTextScript = old_text_script
import configuration
conf = configuration.Config()
+data_path = os.path.join(os.path.abspath(os.path.dirname(__file__)), "data/pokecrystal/")
+conf.wram = os.path.join(data_path, "wram.asm")
+conf.gbhw = os.path.join(data_path, "gbhw.asm")
+conf.hram = os.path.join(data_path, "hram.asm")
+
from map_names import map_names
from song_names import song_names
@@ -175,7 +180,7 @@ def how_many_until(byte, starting, rom):
def load_map_group_offsets(map_group_pointer_table, map_group_count, rom=None):
"""reads the map group table for the list of pointers"""
map_group_offsets = [] # otherwise this method can only be used once
- data = rom.interval(map_group_pointer_table, map_group_count*2, strings=False, rom=rom)
+ data = rom.interval(map_group_pointer_table, map_group_count*2, strings=False)
data = helpers.grouper(data)
for pointer_parts in data:
pointer = pointer_parts[0] + (pointer_parts[1] << 8)
@@ -250,7 +255,10 @@ class TextScript:
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):
+ def __init__(self, address, map_group=None, map_id=None, debug=False, label=None, force=False, show=None, script_parse_table=None, text_command_classes=None):
+ self.text_command_classes = text_command_classes
+ self.script_parse_table = script_parse_table
+
self.address = address
# $91, $84, $82, $54, $8c
# 0x19768c is a a weird problem?
@@ -426,7 +434,7 @@ def parse_text_engine_script_at(address, map_group=None, map_id=None, debug=True
"""
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)
+ return TextScript(address, map_group=map_group, map_id=map_id, debug=debug, show=show, force=force, script_parse_table=script_parse_table, text_command_classes=text_command_classes)
def find_text_addresses():
"""returns a list of text pointers
@@ -561,7 +569,7 @@ def parse_text_at3(address, map_group=None, map_id=None, debug=False):
if deh:
return deh
else:
- text = TextScript(address, map_group=map_group, map_id=map_id, debug=debug)
+ text = TextScript(address, map_group=map_group, map_id=map_id, debug=debug, script_parse_table=script_parse_table, text_command_classes=text_command_classes)
if text.is_valid():
return text
else:
@@ -776,7 +784,7 @@ HexByte=DollarSignByte
class ItemLabelByte(DollarSignByte):
def to_asm(self):
- label = item_constants.item_constants.find_item_label_by_id(self.byte)
+ label = item_constants.find_item_label_by_id(self.byte)
if label:
return label
elif not label:
@@ -846,8 +854,6 @@ class PointerLabelParam(MultiByteParam):
# 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"]))
@@ -920,8 +926,11 @@ class PointerLabelParam(MultiByteParam):
bank_part = "$%.2x" % (pointers.calculate_bank(caddress))
else:
bank_part = "BANK("+label+")"
+ # for labels, expand bank_part at build time
+ if bank in ["reverse", True] and label:
+ return pointer_part
# return the asm based on the order the bytes were specified to be in
- if bank == "reverse": # pointer, bank
+ elif bank == "reverse": # pointer, bank
return pointer_part+", "+bank_part
elif bank == True: # bank, pointer
return bank_part+", "+pointer_part
@@ -937,13 +946,22 @@ class PointerLabelParam(MultiByteParam):
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"
+ bank = True # bank appears first, see calculate_pointer_from_bytes_at
+ byte_type = 'db'
+
+ @staticmethod
+ def from_asm(value):
+ return 'BANK({0})\n\tdw {0}'.format(value)
class PointerLabelAfterBank(PointerLabelParam):
- bank = "reverse" # bank appears last, see calculate_pointer_from_bytes_at
size = 3
+ bank = "reverse" # bank appears last, see calculate_pointer_from_bytes_at
+ byte_type = 'dw'
+
+ @staticmethod
+ def from_asm(value):
+ return '{0}\n\tdb BANK({0})'.format(value)
class ScriptPointerLabelParam(PointerLabelParam): pass
@@ -2363,7 +2381,7 @@ pksv_crystal_more = {
0xA1: ["halloffame"],
0xA2: ["credits"],
0xA3: ["warpfacing", ["facing", SingleByteParam], ["map_group", MapGroupParam], ["map_id", MapIdParam], ["x", SingleByteParam], ["y", SingleByteParam]],
- 0xA4: ["storetext", ["pointer", PointerLabelBeforeBank], ["memory", SingleByteParam]],
+ 0xA4: ["storetext", ["memory", SingleByteParam]],
0xA5: ["displaylocation", ["id", SingleByteParam], ["memory", SingleByteParam]],
0xA6: ["trainerclassname", ["id", SingleByteParam]],
0xA7: ["name", ["type", SingleByteParam], ["id", SingleByteParam]],
@@ -2938,7 +2956,7 @@ class Script:
if start_address in stop_points and force == False:
if debug:
logging.debug(
- "script parsing is stopping at stop_point={address} at map_group={map_group} map_id={map_id}"
+ "script parsing is stopping at stop_point={stop_point} at map_group={map_group} map_id={map_id}"
.format(
stop_point=hex(start_address),
map_group=str(map_group),
@@ -6910,7 +6928,7 @@ def list_texts_in_bank(bank):
Narrows down the list of objects that you will be inserting into Asm.
"""
if len(all_texts) == 0:
- raise Exception("all_texts is blank.. main() will populate it")
+ raise Exception("all_texts is blank.. parse_rom() will populate it")
assert bank != None, "list_texts_in_banks must be given a particular bank"
@@ -6928,7 +6946,7 @@ def list_movements_in_bank(bank, all_movements):
Narrows down the list of objects to speed up Asm insertion.
"""
if len(all_movements) == 0:
- raise Exception("all_movements is blank.. main() will populate it")
+ raise Exception("all_movements is blank.. parse_rom() will populate it")
assert bank != None, "list_movements_in_bank must be given a particular bank"
assert 0 <= bank < 0x80, "bank doesn't exist in the ROM (out of bounds)"
@@ -6947,7 +6965,7 @@ def dump_asm_for_texts_in_bank(bank, start=50, end=100, rom=None):
# load and parse the ROM if necessary
if rom == None or len(rom) <= 4:
rom = load_rom()
- main()
+ parse_rom()
# get all texts
# first 100 look okay?
@@ -6967,7 +6985,7 @@ def dump_asm_for_texts_in_bank(bank, start=50, end=100, rom=None):
def dump_asm_for_movements_in_bank(bank, start=0, end=100, all_movements=None):
if rom == None or len(rom) <= 4:
rom = load_rom()
- main()
+ parse_rom()
movements = list_movements_in_bank(bank, all_movements)[start:end]
@@ -6983,7 +7001,7 @@ def dump_things_in_bank(bank, start=50, end=100):
# load and parse the ROM if necessary
if rom == None or len(rom) <= 4:
rom = load_rom()
- main()
+ parse_rom()
things = list_things_in_bank(bank)[start:end]
@@ -7020,6 +7038,14 @@ def write_all_labels(all_labels, filename="labels.json"):
fh.close()
return True
+def setup_wram_labels(config=conf):
+ """
+ Get all wram labels and store it on the module.
+ """
+ wramproc = wram.WRAMProcessor(config=config)
+ wramproc.initialize()
+ wram.wram_labels = wramproc.wram_labels
+
def get_ram_label(address):
"""
returns a label assigned to a particular ram address
@@ -7267,16 +7293,28 @@ Command.trainer_group_maximums = trainer_group_maximums
SingleByteParam.map_internal_ids = map_internal_ids
MultiByteParam.map_internal_ids = map_internal_ids
-def main(rom=None):
+def add_map_offsets_into_map_names(map_group_offsets, map_names=None):
+ """
+ Add the offsets for each map into the map_names variable.
+ """
+ # add the offsets into our map structure, why not (johto maps only)
+ return [map_names[map_group_id+1].update({"offset": offset}) for map_group_id, offset in enumerate(map_group_offsets)]
+
+rom_parsed = False
+
+def parse_rom(rom=None):
if not rom:
# read the rom and figure out the offsets for maps
rom = direct_load_rom()
+ # make wram.wram_labels available
+ setup_wram_labels()
+
# figure out the map offsets
map_group_offsets = load_map_group_offsets(map_group_pointer_table=map_group_pointer_table, map_group_count=map_group_count, rom=rom)
- # add the offsets into our map structure, why not (johto maps only)
- [map_names[map_group_id+1].update({"offset": offset}) for map_group_id, offset in enumerate(map_group_offsets)]
+ # populate the map_names structure with the offsets
+ add_map_offsets_into_map_names(map_group_offsets, map_names=map_names)
# parse map header bytes for each map
parse_all_map_headers(map_names, all_map_headers=all_map_headers)
@@ -7292,5 +7330,20 @@ def main(rom=None):
# improve duplicate trainer names
make_trainer_group_name_trainer_ids(trainer_group_table)
+ global rom_parsed
+ rom_parsed = True
+
+ return map_names
+
+def cachably_parse_rom(rom=None):
+ """
+ Calls parse_rom if it hasn't been called and completed yet.
+ """
+ global rom_parsed
+ if not rom_parsed:
+ return parse_rom(rom=rom)
+ else:
+ return map_names
+
if __name__ == "crystal":
pass
diff --git a/pokemontools/data/pokecrystal/gbhw.asm b/pokemontools/data/pokecrystal/gbhw.asm
new file mode 100644
index 0000000..0f12e48
--- /dev/null
+++ b/pokemontools/data/pokecrystal/gbhw.asm
@@ -0,0 +1,102 @@
+; Graciously aped from http://nocash.emubase.de/pandocs.htm .
+
+; MBC3
+MBC3SRamEnable EQU $0000
+MBC3RomBank EQU $2000
+MBC3SRamBank EQU $4000
+MBC3LatchClock EQU $6000
+MBC3RTC EQU $a000
+
+SRAM_DISABLE EQU $00
+SRAM_ENABLE EQU $0a
+
+NUM_SRAM_BANKS EQU 4
+
+RTC_S EQU $08 ; Seconds 0-59 (0-3Bh)
+RTC_M EQU $09 ; Minutes 0-59 (0-3Bh)
+RTC_H EQU $0a ; Hours 0-23 (0-17h)
+RTC_DL EQU $0b ; Lower 8 bits of Day Counter (0-FFh)
+RTC_DH EQU $0c ; Upper 1 bit of Day Counter, Carry Bit, Halt Flag
+ ; Bit 0 Most significant bit of Day Counter (Bit 8)
+ ; Bit 6 Halt (0=Active, 1=Stop Timer)
+ ; Bit 7 Day Counter Carry Bit (1=Counter Overflow)
+
+; interrupt flags
+VBLANK EQU 0
+LCD_STAT EQU 1
+TIMER EQU 2
+SERIAL EQU 3
+JOYPAD EQU 4
+
+; Hardware registers
+rJOYP EQU $ff00 ; Joypad (R/W)
+rSB EQU $ff01 ; Serial transfer data (R/W)
+rSC EQU $ff02 ; Serial Transfer Control (R/W)
+rSC_ON EQU 7
+rSC_CGB EQU 1
+rSC_CLOCK EQU 0
+rDIV EQU $ff04 ; Divider Register (R/W)
+rTIMA EQU $ff05 ; Timer counter (R/W)
+rTMA EQU $ff06 ; Timer Modulo (R/W)
+rTAC EQU $ff07 ; Timer Control (R/W)
+rTAC_ON EQU 2
+rTAC_4096_HZ EQU 0
+rTAC_262144_HZ EQU 1
+rTAC_65536_HZ EQU 2
+rTAC_16384_HZ EQU 3
+rIF EQU $ff0f ; Interrupt Flag (R/W)
+rNR10 EQU $ff10 ; Channel 1 Sweep register (R/W)
+rNR11 EQU $ff11 ; Channel 1 Sound length/Wave pattern duty (R/W)
+rNR12 EQU $ff12 ; Channel 1 Volume Envelope (R/W)
+rNR13 EQU $ff13 ; Channel 1 Frequency lo (Write Only)
+rNR14 EQU $ff14 ; Channel 1 Frequency hi (R/W)
+rNR21 EQU $ff16 ; Channel 2 Sound Length/Wave Pattern Duty (R/W)
+rNR22 EQU $ff17 ; Channel 2 Volume Envelope (R/W)
+rNR23 EQU $ff18 ; Channel 2 Frequency lo data (W)
+rNR24 EQU $ff19 ; Channel 2 Frequency hi data (R/W)
+rNR30 EQU $ff1a ; Channel 3 Sound on/off (R/W)
+rNR31 EQU $ff1b ; Channel 3 Sound Length
+rNR32 EQU $ff1c ; Channel 3 Select output level (R/W)
+rNR33 EQU $ff1d ; Channel 3 Frequency's lower data (W)
+rNR34 EQU $ff1e ; Channel 3 Frequency's higher data (R/W)
+rNR41 EQU $ff20 ; Channel 4 Sound Length (R/W)
+rNR42 EQU $ff21 ; Channel 4 Volume Envelope (R/W)
+rNR43 EQU $ff22 ; Channel 4 Polynomial Counter (R/W)
+rNR44 EQU $ff23 ; Channel 4 Counter/consecutive; Inital (R/W)
+rNR50 EQU $ff24 ; Channel control / ON-OFF / Volume (R/W)
+rNR51 EQU $ff25 ; Selection of Sound output terminal (R/W)
+rNR52 EQU $ff26 ; Sound on/off
+rLCDC EQU $ff40 ; LCD Control (R/W)
+rSTAT EQU $ff41 ; LCDC Status (R/W)
+rSCY EQU $ff42 ; Scroll Y (R/W)
+rSCX EQU $ff43 ; Scroll X (R/W)
+rLY EQU $ff44 ; LCDC Y-Coordinate (R)
+rLYC EQU $ff45 ; LY Compare (R/W)
+rDMA EQU $ff46 ; DMA Transfer and Start Address (W)
+rBGP EQU $ff47 ; BG Palette Data (R/W) - Non CGB Mode Only
+rOBP0 EQU $ff48 ; Object Palette 0 Data (R/W) - Non CGB Mode Only
+rOBP1 EQU $ff49 ; Object Palette 1 Data (R/W) - Non CGB Mode Only
+rWY EQU $ff4a ; Window Y Position (R/W)
+rWX EQU $ff4b ; Window X Position minus 7 (R/W)
+rKEY1 EQU $ff4d ; CGB Mode Only - Prepare Speed Switch
+rVBK EQU $ff4f ; CGB Mode Only - VRAM Bank
+rHDMA1 EQU $ff51 ; CGB Mode Only - New DMA Source, High
+rHDMA2 EQU $ff52 ; CGB Mode Only - New DMA Source, Low
+rHDMA3 EQU $ff53 ; CGB Mode Only - New DMA Destination, High
+rHDMA4 EQU $ff54 ; CGB Mode Only - New DMA Destination, Low
+rHDMA5 EQU $ff55 ; CGB Mode Only - New DMA Length/Mode/Start
+rRP EQU $ff56 ; CGB Mode Only - Infrared Communications Port
+rBGPI EQU $ff68 ; CGB Mode Only - Background Palette Index
+rBGPD EQU $ff69 ; CGB Mode Only - Background Palette Data
+rOBPI EQU $ff6a ; CGB Mode Only - Sprite Palette Index
+rOBPD EQU $ff6b ; CGB Mode Only - Sprite Palette Data
+rUNKNOWN1 EQU $ff6c ; (FEh) Bit 0 (Read/Write) - CGB Mode Only
+rSVBK EQU $ff70 ; CGB Mode Only - WRAM Bank
+rUNKNOWN2 EQU $ff72 ; (00h) - Bit 0-7 (Read/Write)
+rUNKNOWN3 EQU $ff73 ; (00h) - Bit 0-7 (Read/Write)
+rUNKNOWN4 EQU $ff74 ; (00h) - Bit 0-7 (Read/Write) - CGB Mode Only
+rUNKNOWN5 EQU $ff75 ; (8Fh) - Bit 4-6 (Read/Write)
+rUNKNOWN6 EQU $ff76 ; (00h) - Always 00h (Read Only)
+rUNKNOWN7 EQU $ff77 ; (00h) - Always 00h (Read Only)
+rIE EQU $ffff ; Interrupt Enable (R/W)
+
diff --git a/pokemontools/data/pokecrystal/hram.asm b/pokemontools/data/pokecrystal/hram.asm
new file mode 100644
index 0000000..051d418
--- /dev/null
+++ b/pokemontools/data/pokecrystal/hram.asm
@@ -0,0 +1,71 @@
+hPushOAM EQU $ff80
+
+hBuffer EQU $ff8b
+
+hRTCDayHi EQU $ff8d
+hRTCDayLo EQU $ff8e
+hRTCHours EQU $ff8f
+hRTCMinutes EQU $ff90
+hRTCSeconds EQU $ff91
+
+hHours EQU $ff94
+
+hMinutes EQU $ff96
+
+hSeconds EQU $ff98
+
+hROMBank EQU $ff9d
+
+hJoypadReleased EQU $ffa2
+hJoypadPressed EQU $ffa3
+hJoypadDown EQU $ffa4
+hJoypadSum EQU $ffa5
+hJoyReleased EQU $ffa6
+hJoyPressed EQU $ffa7
+hJoyDown EQU $ffa8
+
+hConnectionStripLength EQU $ffaf
+hConnectedMapWidth EQU $ffb0
+
+hPastLeadingZeroes EQU $ffb3
+
+hDividend EQU $ffb3
+hDivisor EQU $ffb7
+hQuotient EQU $ffb4
+
+hMultiplicand EQU $ffb4
+hMultiplier EQU $ffb7
+hProduct EQU $ffb3
+
+hMathBuffer EQU $ffb8
+
+hLCDStatCustom EQU $ffc6
+
+hSerialSend EQU $ffcd
+hSerialReceive EQU $ffce
+
+hSCX EQU $ffcf
+hSCY EQU $ffd0
+hWX EQU $ffd1
+hWY EQU $ffd2
+
+hBGMapMode EQU $ffd4
+hBGMapThird EQU $ffd5
+hBGMapAddress EQU $ffd6
+
+hOAMUpdate EQU $ffd8
+hSPBuffer EQU $ffd9
+
+hBGMapUpdate EQU $ffdb
+
+hTileAnimFrame EQU $ffdf
+
+hRandomAdd EQU $ffe1
+hRandomSub EQU $ffe2
+
+hBattleTurn EQU $ffe4
+hCGBPalUpdate EQU $ffe5
+hCGB EQU $ffe6
+hSGB EQU $ffe7
+hDMATransfer EQU $ffe8
+
diff --git a/pokemontools/gfx.py b/pokemontools/gfx.py
index 8397337..04ccac5 100644
--- a/pokemontools/gfx.py
+++ b/pokemontools/gfx.py
@@ -1012,7 +1012,7 @@ def get_uncompressed_gfx(start, num_tiles, filename):
-def hex_to_rgb(word):
+def bin_to_rgb(word):
red = word & 0b11111
word >>= 5
green = word & 0b11111
@@ -1020,23 +1020,39 @@ def hex_to_rgb(word):
blue = word & 0b11111
return (red, green, blue)
-def grab_palettes(address, length=0x80):
+def rgb_from_rom(address, length=0x80):
+ return convert_binary_pal_to_text(rom[address:address+length])
+
+def convert_binary_pal_to_text_by_filename(filename):
+ with open(filename) as f:
+ pal = bytearray(f.read())
+ return convert_binary_pal_to_text(pal)
+
+def convert_binary_pal_to_text(pal):
output = ''
- for word in range(length/2):
- color = ord(rom[address+1])*0x100 + ord(rom[address])
- address += 2
- color = hex_to_rgb(color)
- red = str(color[0]).zfill(2)
- green = str(color[1]).zfill(2)
- blue = str(color[2]).zfill(2)
- output += '\tRGB '+red+', '+green+', '+blue
+ words = [hi * 0x100 + lo for lo, hi in zip(pal[::2], pal[1::2])]
+ for word in words:
+ red, green, blue = ['%.2d' % c for c in bin_to_rgb(word)]
+ output += '\tRGB ' + ', '.join((red, green, blue))
output += '\n'
return output
+def read_rgb_macros(lines):
+ colors = []
+ for line in lines:
+ macro = line.split(" ")[0].strip()
+ if macro == 'RGB':
+ params = ' '.join(line.split(" ")[1:]).split(',')
+ red, green, blue = [int(v) for v in params]
+ colors += [[red, green, blue]]
+ return colors
-
-
+def rewrite_binary_pals_to_text(filenames):
+ for filename in filenames:
+ pal_text = convert_binary_pal_to_text_by_filename(filename)
+ with open(filename, 'w') as out:
+ out.write(pal_text)
def dump_monster_pals():
@@ -1147,6 +1163,9 @@ def to_lines(image, width):
def dmg2rgb(word):
+ """
+ For PNGs.
+ """
def shift(value):
while True:
yield value & (2**5 - 1)
@@ -1159,27 +1178,49 @@ def dmg2rgb(word):
def rgb_to_dmg(color):
+ """
+ For PNGs.
+ """
word = (color['r'] / 8)
word += (color['g'] / 8) << 5
word += (color['b'] / 8) << 10
return word
-def png_pal(filename):
- with open(filename, 'rb') as pal_data:
- words = pal_data.read()
- dmg_pals = []
- for word in range(len(words)/2):
- dmg_pals.append(ord(words[word*2]) + ord(words[word*2+1])*0x100)
+def pal_to_png(filename):
+ """
+ Interpret a .pal file as a png palette.
+ """
+ with open(filename) as rgbs:
+ colors = read_rgb_macros(rgbs.readlines())
+ a = 255
palette = []
+ for color in colors:
+ # even distribution over 000-255
+ r, g, b = [int(hue * 8.25) for hue in color]
+ palette += [(r, g, b, a)]
white = (255,255,255,255)
black = (000,000,000,255)
- for word in dmg_pals: palette += [dmg2rgb(word)]
- if white not in dmg_pals and len(palette) < 4: palette = [white] + palette
- if black not in dmg_pals and len(palette) < 4: palette += [black]
+ if white not in palette and len(palette) < 4:
+ palette = [white] + palette
+ if black not in palette and len(palette) < 4:
+ palette = palette + [black]
return palette
+def png_to_rgb(palette):
+ """
+ Convert a png palette to rgb macros.
+ """
+ output = ''
+ for color in palette:
+ r, g, b = [color[c] / 8 for c in 'rgb']
+ output += '\tRGB ' + ', '.join(['%.2d' % hue for hue in (r, g, b)])
+ output += '\n'
+ return output
+
+
+
def export_2bpp_to_png(filein, fileout=None, pal_file=None, height=0, width=0):
if fileout == None:
fileout = os.path.splitext(filein)[0] + '.png'
@@ -1234,7 +1275,7 @@ def convert_2bpp_to_png(image, width=0, height=0, pal_file=None):
px_map = [[3 - pixel for pixel in line] for line in lines]
else: # gbc color
- palette = png_pal(pal_file)
+ palette = pal_to_png(pal_file)
greyscale = False
bitdepth = 8
px_map = [[pixel for pixel in line] for line in lines]
@@ -1371,13 +1412,22 @@ def png_to_2bpp(filein):
def export_palette(palette, filename):
+ """
+ Export a palette from png to rgb macros in a .pal file.
+ """
+
if os.path.exists(filename):
- output = []
- for color in palette:
- word = rgb_to_dmg(color)
- output += [word & 0xff]
- output += [word >> 8]
- to_file(filename, output)
+
+ # Pic palettes are 2 colors (black/white are added later).
+ with open(filename) as rgbs:
+ colors = read_rgb_macros(rgbs.readlines())
+
+ if len(colors) == 2:
+ palette = palette[1:3]
+
+ text = png_to_rgb(palette)
+ with open(filename, 'w') as out:
+ out.write(text)
def png_to_lz(filein):
@@ -1516,11 +1566,16 @@ def export_lz_to_png(filename):
"""
assert filename[-3:] == ".lz"
lz_data = open(filename, "rb").read()
+
bpp = Decompressed(lz_data).output
bpp_filename = filename.replace(".lz", ".2bpp")
to_file(bpp_filename, bpp)
+
export_2bpp_to_png(bpp_filename)
+ # touch the lz file so it doesn't get remade
+ os.utime(filename, None)
+
def dump_tileset_pngs():
"""
Convert .lz format tilesets into .png format tilesets.
@@ -1566,6 +1621,8 @@ def expand_pic_palettes():
with open(filename, 'wb') as out:
out.write(w + palette + b)
+
+
if __name__ == "__main__":
debug = False
@@ -1627,6 +1684,9 @@ if __name__ == "__main__":
elif argv[1] == 'png-to-1bpp':
export_png_to_1bpp(argv[2])
+ elif argv[1] == '1bpp-to-png':
+ export_1bpp_to_png(argv[2])
+
elif argv[1] == '2bpp-to-lz':
if argv[2] == '--vert':
filein = argv[3]
diff --git a/pokemontools/labels.py b/pokemontools/labels.py
index 96e34b9..87e9990 100644
--- a/pokemontools/labels.py
+++ b/pokemontools/labels.py
@@ -8,33 +8,39 @@ import json
import logging
import pointers
+import sym
class Labels(object):
"""
Store all labels.
"""
- filename = "labels.json"
- def __init__(self, config):
+ def __init__(self, config, filename="pokecrystal.map"):
"""
Setup the instance.
"""
self.config = config
- self.path = os.path.join(self.config.path, Labels.filename)
+ self.filename = filename
+ self.path = os.path.join(self.config.path, self.filename)
def initialize(self):
"""
Handle anything requiring file-loading and such.
"""
+ # Look for a mapfile if it's not given
if not os.path.exists(self.path):
- logging.info(
- "Running crystal.scan_for_predefined_labels to create \"{0}\". Trying.."
- .format(Labels.filename)
- )
- import crystal
- crystal.scan_for_predefined_labels()
+ self.filename = find_mapfile_in_dir(self.config.path)
+ if self.filename == None:
+ raise Exception, "Couldn't find any mapfiles. Run rgblink -m to create a mapfile."
+ self.path = os.path.join(self.config.path, self.filename)
- self.labels = json.read(open(self.path, "r").read())
+ self.labels = sym.read_mapfile(self.path)
+
+def find_mapfile_in_dir(path):
+ for filename in os.listdir(path):
+ if os.path.splitext(filename)[1] == '.map':
+ return filename
+ return None
def remove_quoted_text(line):
"""get rid of content inside quotes
diff --git a/pokemontools/map_editor.py b/pokemontools/map_editor.py
index 192d06c..47b7d95 100644
--- a/pokemontools/map_editor.py
+++ b/pokemontools/map_editor.py
@@ -15,6 +15,7 @@ from Tkinter import (
X,
TclError,
)
+import tkFileDialog
from ttk import (
Frame,
@@ -178,7 +179,8 @@ class Application(Frame):
def new_map(self):
self.map_name = None
self.init_map()
- self.map.blockdata = [self.paint_tile] * 20 * 20
+ self.map.blockdata_filename = os.path.join(self.config.map_dir, 'newmap.blk')
+ self.map.blockdata = bytearray([self.paint_tile] * 20 * 20)
self.map.width = 20
self.map.height = 20
self.draw_map()
@@ -193,7 +195,8 @@ class Application(Frame):
def save_map(self):
if hasattr(self, 'map'):
if self.map.blockdata_filename:
- with open(self.map.blockdata_filename, 'wb') as save:
+ filename = tkFileDialog.asksaveasfilename(initialfile=self.map.blockdata_filename)
+ with open(filename, 'wb') as save:
save.write(self.map.blockdata)
self.log.info('blockdata saved as {}'.format(self.map.blockdata_filename))
else:
@@ -445,7 +448,7 @@ class Tileset:
def get_tiles(self):
filename = self.get_tileset_gfx_filename()
if not os.path.exists(filename):
- gfx.export_2bpp_to_png(filename.replace('.png','.2bpp'), filename)
+ gfx.export_2bpp_to_png(filename.replace('.png','.2bpp'))
self.img = Image.open(filename)
self.img.width, self.img.height = self.img.size
self.tiles = []
@@ -505,30 +508,9 @@ class Tileset:
self.palettes = get_palettes(filename)
def get_palettes(filename):
- pals = bytearray(open(filename, 'rb').read())
-
- num_colors = 4
- color_length = 2
-
- palette_length = num_colors * color_length
-
- num_pals = len(pals) / palette_length
-
- palettes = []
- for pal in xrange(num_pals):
- palettes += [[]]
-
- for color in xrange(num_colors):
- i = pal * palette_length
- i += color * color_length
- word = pals[i] + pals[i+1] * 0x100
- palettes[pal] += [[
- c & 0x1f for c in [
- word >> 0,
- word >> 5,
- word >> 10,
- ]
- ]]
+ lines = open(filename, 'r').readlines()
+ colors = gfx.read_rgb_macros(lines)
+ palettes = [colors[i:i+4] for i in xrange(0, len(colors), 4)]
return palettes
def get_available_maps(config=config):
diff --git a/pokemontools/map_gfx.py b/pokemontools/map_gfx.py
new file mode 100644
index 0000000..b06f0df
--- /dev/null
+++ b/pokemontools/map_gfx.py
@@ -0,0 +1,380 @@
+"""
+Map-related graphic functions.
+"""
+
+import os
+import png
+from io import BytesIO
+
+from PIL import (
+ Image,
+ ImageDraw,
+)
+
+import crystal
+import gfx
+
+tile_width = 8
+tile_height = 8
+block_width = 4
+block_height = 4
+
+WALKING_SPRITE = 1
+STANDING_SPRITE = 2
+STILL_SPRITE = 3
+
+# use the same configuration
+gfx.config = crystal.conf
+config = gfx.config
+
+def add_pokecrystal_paths_to_configuration(config=config):
+ """
+ Assumes that the current working directory is the pokecrystal project path.
+ """
+ config.gfx_dir = os.path.join(os.path.abspath("."), "gfx/tilesets/")
+ config.block_dir = os.path.join(os.path.abspath("."), "tilesets/")
+ config.palmap_dir = config.block_dir
+ config.palette_dir = config.block_dir
+ config.sprites_dir = os.path.join(os.path.abspath("."), "gfx/overworld/")
+
+add_pokecrystal_paths_to_configuration(config=config)
+
+def read_map_blockdata(map_header):
+ """
+ Reads out the list of bytes representing the blockdata for the current map.
+ """
+ width = map_header.second_map_header.blockdata.width.byte
+ height = map_header.second_map_header.blockdata.height.byte
+
+ start_address = map_header.second_map_header.blockdata.address
+ end_address = start_address + (width * height)
+
+ blockdata = crystal.rom[start_address : end_address]
+
+ return [ord(x) for x in blockdata]
+
+def load_png(filepath):
+ """
+ Makes an image object from file.
+ """
+ return Image.open(filepath)
+
+all_blocks = {}
+def read_blocks(tileset_id, config=config):
+ """
+ Makes a list of blocks, such that each block is a list of tiles by id, for
+ the given tileset.
+ """
+ if tileset_id in all_blocks.keys():
+ return all_blocks[tileset_id]
+
+ blocks = []
+
+ block_width = 4
+ block_height = 4
+ block_length = block_width * block_height
+
+ filename = "{id}{ext}".format(id=str(tileset_id).zfill(2), ext="_metatiles.bin")
+ filepath = os.path.join(config.block_dir, filename)
+
+ blocksetdata = bytearray(open(filepath, "rb").read())
+
+ for blockbyte in xrange(len(blocksetdata) / block_length):
+ block_num = blockbyte * block_length
+ block = blocksetdata[block_num : block_num + block_length]
+ blocks += [block]
+
+ all_blocks[tileset_id] = blocks
+
+ return blocks
+
+def colorize_tile(tile, palette):
+ """
+ Make the tile have colors.
+ """
+ (width, height) = tile.size
+ tile = tile.convert("RGB")
+ px = tile.load()
+
+ for y in xrange(height):
+ for x in xrange(width):
+ # assume greyscale
+ which_color = 3 - (px[x, y][0] / 0x55)
+ (r, g, b) = [v * 8 for v in palette[which_color]]
+ px[x, y] = (r, g, b)
+
+ return tile
+
+pre_cropped = {}
+def read_tiles(tileset_id, palette_map, palettes, config=config):
+ """
+ Opens the tileset png file and reads bytes for each tile in the tileset.
+ """
+
+ if tileset_id not in pre_cropped.keys():
+ pre_cropped[tileset_id] = {}
+
+ tile_width = 8
+ tile_height = 8
+
+ tiles = []
+
+ filename = "{id}.{ext}".format(id=str(tileset_id).zfill(2), ext="png")
+ filepath = os.path.join(config.gfx_dir, filename)
+
+ image = load_png(filepath)
+ (image.width, image.height) = image.size
+
+ cur_tile = 0
+
+ for y in xrange(0, image.height, tile_height):
+ for x in xrange(0, image.width, tile_width):
+ if (x, y) in pre_cropped[tileset_id].keys():
+ tile = pre_cropped[tileset_id][(x, y)]
+ else:
+ tile = image.crop((x, y, x + tile_width, y + tile_height))
+ pre_cropped[tileset_id][(x, y)] = tile
+
+ # palette maps are padded to make vram mapping easier
+ pal = palette_map[cur_tile + 0x20 if cur_tile > 0x60 else cur_tile] & 0x7
+ tile = colorize_tile(tile, palettes[pal])
+
+ tiles.append(tile)
+
+ cur_tile += 1
+
+ return tiles
+
+all_palette_maps = {}
+def read_palette_map(tileset_id, config=config):
+ """
+ Loads a palette map.
+ """
+ if tileset_id in all_palette_maps.keys():
+ return all_palette_maps[tileset_id]
+
+ filename = "{id}{ext}".format(id=str(tileset_id).zfill(2), ext="_palette_map.bin")
+ filepath = os.path.join(config.palmap_dir, filename)
+
+ palette_map = []
+
+ palmap = bytearray(open(filepath, "rb").read())
+
+ for i in xrange(len(palmap)):
+ palette_map += [palmap[i] & 0xf]
+ palette_map += [(palmap[i] >> 4) & 0xf]
+
+ all_palette_maps[tileset_id] = palette_map
+
+ return palette_map
+
+def read_palettes(time_of_day=1, config=config):
+ """
+ Loads up the .pal file?
+ """
+ palettes = []
+
+ actual_time_of_day = ["morn", "day", "nite"][time_of_day]
+ filename = "{}.pal".format(actual_time_of_day)
+ filepath = os.path.join(config.palette_dir, filename)
+
+ lines = open(filepath, "r").readlines()
+ colors = gfx.read_rgb_macros(lines)
+ palettes = [colors[i:i+4] for i in xrange(0, len(colors), 4)]
+ return palettes
+
+def load_sprite_image(address, config=config):
+ """
+ Make standard file path.
+ """
+ pal_file = os.path.join(config.block_dir, "day.pal")
+
+ length = 0x40
+
+ image = crystal.rom[address:address + length]
+ width, height, palette, greyscale, bitdepth, px_map = gfx.convert_2bpp_to_png(image, width=16, height=16, pal_file=pal_file)
+ w = png.Writer(16, 16, palette=palette, compression=9, greyscale=greyscale, bitdepth=bitdepth)
+ some_buffer = BytesIO()
+ w.write(some_buffer, px_map)
+ some_buffer.seek(0)
+
+ sprite_image = Image.open(some_buffer)
+
+ return sprite_image
+
+sprites = {}
+def load_all_sprite_images(config=config):
+ """
+ Loads all images for each sprite in each direction.
+ """
+ crystal.direct_load_rom()
+
+ sprite_headers_address = 0x14736
+ sprite_header_size = 6
+ sprite_count = 102
+ frame_size = 0x40
+
+ current_address = sprite_headers_address
+
+ current_image_id = 0
+
+ for sprite_id in xrange(1, sprite_count):
+ rom_bytes = crystal.rom[current_address : current_address + sprite_header_size]
+ header = [ord(x) for x in rom_bytes]
+
+ bank = header[3]
+
+ lo = header[0]
+ hi = header[1]
+ sprite_address = (hi * 0x100) + lo - 0x4000
+ sprite_address += 0x4000 * bank
+
+ sprite_size = header[2]
+ sprite_type = header[4]
+ sprite_palette = header[5]
+ image_count = sprite_size / frame_size
+
+ sprite = {
+ "size": sprite_size,
+ "image_count": image_count,
+ "type": sprite_type,
+ "palette": sprite_palette,
+ "images": {},
+ }
+
+ if sprite_type in [WALKING_SPRITE, STANDING_SPRITE]:
+ # down, up, left, move down, move up, move left
+ sprite["images"]["down"] = load_sprite_image(sprite_address, config=config)
+ sprite["images"]["up"] = load_sprite_image(sprite_address + 0x40, config=config)
+ sprite["images"]["left"] = load_sprite_image(sprite_address + (0x40 * 2), config=config)
+
+ if sprite_type == WALKING_SPRITE:
+ current_image_id += image_count * 2
+ elif sprite_type == STANDING_SPRITE:
+ current_image_id += image_count * 1
+ elif sprite_type == STILL_SPRITE:
+ # just one image
+ sprite["images"]["still"] = load_sprite_image(sprite_address, config=config)
+
+ current_image_id += image_count * 1
+
+ # store the actual metadata
+ sprites[sprite_id] = sprite
+
+ current_address += sprite_header_size
+
+ return sprites
+
+def draw_map_sprites(map_header, map_image, config=config):
+ """
+ Show NPCs and items on the map.
+ """
+
+ events = map_header.second_map_header.event_header.people_events
+
+ for event in events:
+ sprite_image_id = event.params[0].byte
+ y = (event.params[1].byte - 4) * 4
+ x = (event.params[2].byte - 4) * 4
+ facing = event.params[3].byte
+ movement = event.params[4].byte
+ sight_range = event.params[8].byte
+ some_pointer = event.params[9]
+ bit_table_bit_number = event.params[10]
+
+ other_args = {}
+
+ if sprite_image_id not in sprites.keys() or sprite_image_id > 0x66:
+ print "sprite_image_id {} is not in sprites".format(sprite_image_id)
+
+ sprite_image = Image.new("RGBA", (16, 16))
+
+ draw = ImageDraw.Draw(sprite_image, "RGBA")
+ draw.rectangle([(0, 0), (16, 16)], fill=(0, 0, 0, 127))
+
+ other_args["mask"] = sprite_image
+ else:
+ sprite = sprites[sprite_image_id]
+
+ # TODO: pick the correct direction based on "facing"
+ sprite_image = sprite["images"].values()[0]
+
+ # TODO: figure out how to calculate the correct position
+ map_image.paste(sprite_image, (x * 4, y * 4), **other_args)
+
+def draw_map(map_group_id, map_id, palettes, show_sprites=True, config=config):
+ """
+ Makes a picture of a map.
+ """
+ # extract data from the ROM
+ crystal.cachably_parse_rom()
+
+ map_header = crystal.map_names[map_group_id][map_id]["header_new"]
+ second_map_header = map_header.second_map_header
+
+ width = second_map_header.blockdata.width.byte
+ height = second_map_header.blockdata.height.byte
+
+ tileset_id = map_header.tileset.byte
+ blockdata = read_map_blockdata(map_header)
+
+ palette_map = read_palette_map(tileset_id, config=config)
+
+ tileset_blocks = read_blocks(tileset_id, config=config)
+ tileset_images = read_tiles(tileset_id, palette_map, palettes, config=config)
+
+ map_image = Image.new("RGB", (width * tile_width * block_width, height * tile_height * block_height))
+
+ # draw each block on the map
+ for block_num in xrange(len(blockdata)):
+ block_x = block_num % width
+ block_y = block_num / width
+
+ block = blockdata[block_y * width + block_x]
+
+ for (tile_num, tile) in enumerate(tileset_blocks[block]):
+ # tile gfx are split in half to make vram mapping easier
+ if tile >= 0x80:
+ tile -= 0x20
+
+ tile_x = block_x * 32 + (tile_num % 4) * 8
+ tile_y = block_y * 32 + (tile_num / 4) * 8
+
+ tile_image = tileset_images[tile]
+
+ map_image.paste(tile_image, (tile_x, tile_y))
+
+ # draw each sprite on the map
+ draw_map_sprites(map_header, map_image, config=config)
+
+ return map_image
+
+def save_map(map_group_id, map_id, savedir, show_sprites=True, config=config):
+ """
+ Makes a map and saves it to a file in savedir.
+ """
+ # this could be moved into a decorator
+ crystal.cachably_parse_rom()
+
+ map_name = crystal.map_names[map_group_id][map_id]["label"]
+ filename = "{name}.{ext}".format(name=map_name, ext="png")
+ filepath = os.path.join(savedir, filename)
+
+ palettes = read_palettes(config=config)
+
+ print "Drawing {}".format(map_name)
+ map_image = draw_map(map_group_id, map_id, palettes, show_sprites=show_sprites, config=config)
+ map_image.save(filepath)
+
+ return map_image
+
+def save_maps(savedir, show_sprites=True, config=config):
+ """
+ Draw as many maps as possible.
+ """
+ crystal.cachably_parse_rom()
+
+ for map_group_id in crystal.map_names.keys():
+ for map_id in crystal.map_names[map_group_id].keys():
+ if isinstance(map_id, int):
+ image = save_map(map_group_id, map_id, savedir, show_sprites=show_sprites, config=config)
diff --git a/pokemontools/preprocessor.py b/pokemontools/preprocessor.py
index f4e92b6..954263a 100644
--- a/pokemontools/preprocessor.py
+++ b/pokemontools/preprocessor.py
@@ -483,22 +483,18 @@ class Preprocessor(object):
for l in lines:
self.read_line(l)
- self.update_globals()
-
def update_globals(self):
"""
Add any labels not already in globals.asm.
"""
- # TODO: pokered needs to be fixed
- try:
- globes = open(os.path.join(self.config.path, 'globals.asm'), 'r+')
+ path = os.path.join(self.config.path, 'globals.asm')
+ if os.path.exists(path):
+ globes = open(path, 'r+')
lines = globes.readlines()
for globe in self.globes:
line = 'GLOBAL ' + globe + '\n'
if line not in lines:
globes.write(line)
- except Exception as exception:
- pass # don't care if it's not there...
def read_line(self, l):
"""
@@ -527,7 +523,7 @@ class Preprocessor(object):
sys.stdout.write(asm)
# convert text to bytes when a quote appears (not in a comment)
- elif "\"" in asm:
+ elif "\"" in asm and "EQUS" not in asm:
sys.stdout.write(quote_translator(asm))
# check against other preprocessor features
@@ -553,40 +549,22 @@ class Preprocessor(object):
original_line = line
- # remove trailing newline
- if line[-1] == "\n":
- line = line[:-1]
-
- # remove first tab
- has_tab = False
- if line[0] == "\t":
- has_tab = True
- line = line[1:]
+ has_tab = line[0] == "\t"
- # remove duplicate whitespace (also trailing)
+ # remove whitespace
line = " ".join(line.split())
- params = []
-
# check if the line has params
if " " in line:
# split the line into separate parameters
params = line.replace(token, "").split(",")
-
- # check if there are no params (redundant)
- if len(params) == 1 and params[0] == "":
- raise exceptions.MacroException("macro has no params?")
+ else:
+ params = []
# write out a comment showing the original line
if show_original_lines:
sys.stdout.write("; original_line: " + original_line)
- # rgbasm can handle "db" so no preprocessing is required, plus this wont be
- # reached because of earlier checks in macro_test.
- if macro.macro_name in ["db", "dw"]:
- sys.stdout.write(original_line)
- return
-
# rgbasm can handle other macros too
if "is_rgbasm_macro" in dir(macro):
if macro.is_rgbasm_macro:
@@ -604,64 +582,18 @@ class Preprocessor(object):
if do_macro_sanity_check:
self.check_macro_sanity(params, macro, original_line)
- # used for storetext
- correction = 0
-
output = ""
-
- index = 0
- while index < len(params):
- param_type = macro.param_types[index - correction]
+ for index in xrange(len(params)):
+ param_type = macro.param_types[index]
description = param_type["name"].strip()
param_klass = param_type["class"]
- byte_type = param_klass.byte_type # db or dw
- size = param_klass.size
+ byte_type = param_klass.byte_type
param = params[index].strip()
- # param_klass.to_asm() won't work here because it doesn't
- # include db/dw.
-
- # some parameters are really multiple types of bytes
- if (byte_type == "dw" and size != 2) or \
- (byte_type == "db" and size != 1):
-
- output += ("; " + description + "\n")
-
- if size == 3 and is_based_on(param_klass, "PointerLabelBeforeBank"):
- # write the bank first
- output += ("db " + param + "\n")
- # write the pointer second
- output += ("dw " + params[index+1].strip() + "\n")
- index += 2
- correction += 1
- elif size == 3 and is_based_on(param_klass, "PointerLabelAfterBank"):
- # write the pointer first
- output += ("dw " + param + "\n")
- # write the bank second
- output += ("db " + params[index+1].strip() + "\n")
- index += 2
- correction += 1
- elif size == 3 and "from_asm" in dir(param_klass):
- output += ("\t" + byte_type + " " + param_klass.from_asm(param) + "\n")
- index += 1
- else:
- raise exceptions.MacroException(
- "dunno what to do with this macro param ({klass}) in line: {line}"
- .format(
- klass=param_klass,
- line=original_line,
- )
- )
-
- elif "from_asm" in dir(param_klass):
- output += ("\t" + byte_type + " " + param_klass.from_asm(param) + " ; " + description + "\n")
- index += 1
-
- # or just print out the byte
- else:
- output += ("\t" + byte_type + " " + param + " ; " + description + "\n")
+ if "from_asm" in dir(param_klass):
+ param = param_klass.from_asm(param)
- index += 1
+ output += ("\t" + byte_type + " " + param + " ; " + description + "\n")
sys.stdout.write(output)
diff --git a/pokemontools/scan_includes.py b/pokemontools/scan_includes.py
new file mode 100644
index 0000000..7f34e92
--- /dev/null
+++ b/pokemontools/scan_includes.py
@@ -0,0 +1,36 @@
+# coding: utf-8
+
+"""
+Recursively scan an asm file for rgbasm INCLUDEs and INCBINs.
+Used to generate dependencies for each rgbasm object.
+"""
+
+import os
+import sys
+
+import configuration
+conf = configuration.Config()
+
+def recursive_scan(filename, includes = []):
+ if (filename[-4:] == '.asm' or filename[-3] == '.tx') and os.path.exists(filename):
+ lines = open(filename).readlines()
+ for line in lines:
+ for directive in ('INCLUDE', 'INCBIN'):
+ if directive in line:
+ line = line[:line.find(';')]
+ if directive in line:
+ include = line.split('"')[1]
+ if include not in includes:
+ includes += [include]
+ includes = recursive_scan(os.path.join(conf.path, include), includes)
+ break
+ return includes
+
+if __name__ == '__main__':
+ filenames = sys.argv[1:]
+ dependencies = []
+ for filename in filenames:
+ dependencies += recursive_scan(os.path.join(conf.path, filename))
+ dependencies = list(set(dependencies))
+ sys.stdout.write(' '.join(dependencies))
+
diff --git a/pokemontools/sym.py b/pokemontools/sym.py
index ebd8532..b1e755f 100644
--- a/pokemontools/sym.py
+++ b/pokemontools/sym.py
@@ -4,7 +4,7 @@ import os
import sys
import json
-def make_sym_from_json(filename = '../pokecrystal.sym', j = 'labels.json'):
+def make_sym_from_json(filename = 'pokecrystal.sym', j = 'labels.json'):
output = ''
labels = json.load(open(j))
for label in labels:
@@ -12,13 +12,13 @@ def make_sym_from_json(filename = '../pokecrystal.sym', j = 'labels.json'):
with open(filename, 'w') as sym:
sym.write(output)
-def make_json_from_mapfile(filename='labels.json', mapfile='../pokecrystal.map'):
+def make_json_from_mapfile(filename='labels.json', mapfile='pokecrystal.map'):
output = []
labels = filter_wram_addresses(read_mapfile(mapfile))
with open(filename, 'w') as out:
out.write(json.dumps(labels))
-def read_mapfile(filename='../pokecrystal.map'):
+def read_mapfile(filename='pokecrystal.map'):
"""
Scrape label addresses from an rgbds mapfile.
"""
@@ -29,9 +29,15 @@ def read_mapfile(filename='../pokecrystal.map'):
lines = mapfile.readlines()
for line in lines:
- # bank #
- if 'Bank #' in line:
- cur_bank = int(line.lstrip('Bank #').strip(';\n').strip(' (HOME)'))
+ if line[0].strip(): # section type def
+ section_type = line.split(' ')[0]
+ if section_type == 'Bank': # ROM
+ cur_bank = int(line.split(' ')[1].split(':')[0][1:])
+ elif section_type in ['WRAM0', 'HRAM']:
+ cur_bank = 0
+ elif section_type in ['WRAM, VRAM']:
+ cur_bank = int(line.split(' ')[2].split(':')[0][1:])
+ cur_bank = int(line.split(' ')[2].split(':')[0][1:])
# label definition
elif '=' in line:
@@ -39,21 +45,10 @@ def read_mapfile(filename='../pokecrystal.map'):
address = int(address.lstrip().replace('$', '0x'), 16)
label = label.strip()
- # rgbds doesn't support ram banks yet
bank = cur_bank
offset = address
-
- ranges = [
- 0x8000 <= address < 0xa000,
- 0xa000 <= address < 0xc000,
- 0xc000 <= address < 0xd000,
- 0xd000 <= address < 0xe000,
- ]
-
- if any(ranges):
- bank = 0
- else:
- offset += (bank * 0x4000 - 0x4000) if bank > 0 else 0
+ if address < 0x8000 and bank: # ROM
+ offset += (bank - 1) * 0x4000
labels += [{
'label': label,
diff --git a/pokemontools/vba/path.py b/pokemontools/vba/path.py
new file mode 100644
index 0000000..2e50d1b
--- /dev/null
+++ b/pokemontools/vba/path.py
@@ -0,0 +1,664 @@
+"""
+path finding implementation
+
+1) For each position on the map, create a node representing the position.
+2) For each NPC/item, mark nearby nodes as members of that NPC's threat zone
+ (note that they can be members of multiple zones simultaneously).
+"""
+
+import pokemontools.configuration
+config = pokemontools.configuration.Config()
+
+import pokemontools.crystal
+import pokemontools.map_gfx
+
+from PIL import (
+ Image,
+ ImageDraw,
+)
+
+PENALTIES = {
+ # The minimum cost for a step must be greater than zero or else the path
+ # finding implementation might take the player through elaborate routes
+ # through nowhere.
+ "NONE": 1,
+
+ # for any area that might be near a trainer or moving object
+ "THREAT_ZONE": 50,
+
+ # for any nodes that might be under active observation (sight) by a trainer
+ "SIGHT_RANGE": 80,
+
+ # active sight range is where the trainer will definitely see the player
+ "ACTIVE_SIGHT_RANGE": 100,
+
+ # This is impossible, but the pathfinder might have a bug, and it would be
+ # nice to know about such a bug very soon.
+ "COLLISION": -999999,
+}
+
+DIRECTIONS = {
+ "UP": "UP",
+ "DOWN": "DOWN",
+ "LEFT": "LEFT",
+ "RIGHT": "RIGHT",
+}
+
+class Node(object):
+ """
+ A ``Node`` represents a position on the map.
+ """
+
+ def __init__(self, position, threat_zones=None, contents=None):
+ self.position = position
+ self.y = position[0]
+ self.x = position[1]
+
+ # by default a node is not a member of any threat zones
+ self.threat_zones = threat_zones or set()
+
+ # by default a node does not have any objects at this location
+ self.contents = contents or set()
+
+ self.cost = self.calculate_cost()
+
+ def calculate_cost(self, PENALTIES=PENALTIES):
+ """
+ Calculates a cost associated with passing through this node.
+ """
+ penalty = PENALTIES["NONE"]
+
+ # 1) assign a penalty based on whether or not this object is passable,
+ # if it's a collision then return a priority immediately
+ if self.is_collision_by_map_data() or self.is_collision_by_map_obstacle():
+ penalty += PENALTIES["COLLISION"]
+ return penalty
+
+ # 2) assign a penalty based on whether or not this object is grass/water
+
+ # 3) assign a penalty based on whether or not there is a map_obstacle here,
+ # check each of the contents to see if there are any objects that exist
+ # at this location, if anything exists here then return a priority immediately
+
+ # 4) consider any additional penalties due to the presence of a threat
+ # zone. Only calculate detailed penalties about the threat zone if the
+ # player is within range.
+ for threat_zone in self.threat_zones:
+ # the player might be inside the threat zone or the player might be
+ # just on the boundary
+ player_y = get_player_y()
+ player_x = get_player_x()
+ if threat_zone.is_player_near(player_y, player_x):
+ consider_sight_range = True
+ else:
+ consider_sight_range = False
+
+ penalty += threat_zone.calculate_node_cost(self.y, self.x, consider_sight_range=consider_sight_range, PENALTIES=PENALTIES)
+
+ return penalty
+
+ def is_collision_by_map_data(self):
+ """
+ Checks if the player can walk on this location.
+ """
+ raise NotImplementedError
+
+ def is_collision_by_map_obstacle(self):
+ """
+ Checks if there is a map_obstacle on the current position that prevents
+ the player walking here.
+ """
+ for content in self.contents:
+ if self.content.y == self.y and self.content.x == self.x:
+ return True
+ else:
+ return False
+
+class MapObstacle(object):
+ """
+ A ``MapObstacle`` represents an item, npc or trainer on the map.
+ """
+
+ def __init__(self, some_map, identifier, sight_range=None, movement=None, turn=None, simulation=False, facing_direction=DIRECTIONS["DOWN"]):
+ """
+ :param some_map: a reference to the map that this object belongs to
+ :param identifier: which object on the map does this correspond to?
+ :param simulation: set to False to not read from RAM
+ """
+ self.simulation = simulation
+
+ self.some_map = some_map
+ self.identifier = identifier
+
+ self._sight_range = sight_range
+ if self._sight_range is None:
+ self._sight_range = self._get_sight_range()
+
+ self._movement = movement
+ if self._movement is None:
+ self._movement = self._get_movement()
+
+ self._turn = turn
+ if self._turn is None:
+ self._turn = self._get_turn()
+
+ self.facing_direction = facing_direction
+ if not self.facing_direction:
+ self.facing_direction = self.get_current_facing_direction()
+
+ self.update_location()
+
+ def update_location(self):
+ """
+ Determines the (y, x) location of the given map_obstacle object, which
+ can be a reference to an item, npc or trainer npc.
+ """
+ if self.simulation:
+ return (self.y, self.x)
+ else:
+ raise NotImplementedError
+
+ self.y = new_y
+ self.x = new_x
+
+ return (new_y, new_x)
+
+ def _get_current_facing_direction(self, DIRECTIONS=DIRECTIONS):
+ """
+ Get the current facing direction of the map_obstacle.
+ """
+ raise NotImplementedError
+
+ def get_current_facing_direction(self, DIRECTIONS=DIRECTIONS):
+ """
+ Get the current facing direction of the map_obstacle.
+ """
+ if not self.simulation:
+ self.facing_direction = self._get_current_facing_direction(DIRECTIONS=DIRECTIONS)
+ return self.facing_direction
+
+ def _get_movement(self):
+ """
+ Figures out the "movement" variable. Also, this converts from the
+ internal game's format into True or False for whether or not the object
+ is capable of moving.
+ """
+ raise NotImplementedError
+
+ @property
+ def movement(self):
+ if self._movement is None:
+ self._movement = self._get_movement()
+ return self._movement
+
+ def can_move(self):
+ """
+ Checks if this map_obstacle is capable of movement.
+ """
+ return self.movement
+
+ def _get_turn(self):
+ """
+ Checks whether or not the map_obstacle can turn. This only matters for
+ trainers.
+ """
+ raise NotImplementedError
+
+ @property
+ def turn(self):
+ if self._turn is None:
+ self._turn = self._get_turn()
+ return self._turn
+
+ def can_turn_without_moving(self):
+ """
+ Checks whether or not the map_obstacle can turn. This only matters for
+ trainers.
+ """
+ return self.turn
+
+ def _get_sight_range(self):
+ """
+ Figure out the sight range of this map_obstacle.
+ """
+ raise NotImplementedError
+
+ @property
+ def sight_range(self):
+ if self._sight_range is None:
+ self._sight_range = self._get_sight_range()
+ return self._sight_range
+
+class ThreatZone(object):
+ """
+ A ``ThreatZone`` represents the area surrounding a moving or turning object
+ that the player can try to avoid.
+ """
+
+ def __init__(self, map_obstacle, main_graph):
+ """
+ Constructs a ``ThreatZone`` based on a graph of a map and a particular
+ object on that map.
+
+ :param map_obstacle: the subject based on which to build a threat zone
+ :param main_graph: a reference to the map's nodes
+ """
+
+ self.map_obstacle = map_obstacle
+ self.main_graph = main_graph
+
+ self.sight_range = self.calculate_sight_range()
+
+ self.top_left_y = None
+ self.top_left_x = None
+ self.bottom_right_y = None
+ self.bottom_right_x = None
+ self.height = None
+ self.width = None
+ self.size = self.calculate_size()
+
+ # nodes specific to this threat zone
+ self.nodes = []
+
+ def calculate_size(self):
+ """
+ Calculate the bounds of the threat zone based on the map obstacle.
+ Returns the top left corner (y, x) and the bottom right corner (y, x)
+ in the form of ((y, x), (y, x), height, width).
+ """
+ top_left_y = 0
+ top_left_x = 0
+
+ bottom_right_y = 1
+ bottom_right_x = 1
+
+ # TODO: calculate the correct bounds of the threat zone.
+
+ raise NotImplementedError
+
+ # if there is a sight_range for this map_obstacle then increase the size of the zone.
+ if self.sight_range > 0:
+ top_left_y += self.sight_range
+ top_left_x += self.sight_range
+ bottom_right_y += self.sight_range
+ bottom_right_x += self.sight_range
+
+ top_left = (top_left_y, top_left_x)
+ bottom_right = (bottom_right_y, bottom_right_x)
+
+ height = bottom_right_y - top_left_y
+ width = bottom_right_x - top_left_x
+
+ self.top_left_y = top_left_y
+ self.top_left_x = top_left_x
+ self.bottom_right_y = bottom_right_y
+ self.bottom_right_x = bottom_right_x
+ self.height = height
+ self.width = width
+
+ return (top_left, bottom_right, height, width)
+
+ def is_player_near(self, y, x):
+ """
+ Applies a boundary of one around the threat zone, then checks if the
+ player is inside. This is how the threatzone activates to calculate an
+ updated graph or set of penalties for each step.
+ """
+ y_condition = (self.top_left_y - 1) <= y < (self.bottom_right_y + 1)
+ x_condition = (self.top_left_x - 1) <= x < (self.bottom_right_x + 1)
+ return y_condition and x_condition
+
+ def check_map_obstacle_has_sight(self):
+ """
+ Determines if the map object has the sight feature.
+ """
+ return self.map_obstacle.sight_range > 0
+
+ def calculate_sight_range(self):
+ """
+ Calculates the range that the object is able to see.
+ """
+ if not self.check_map_obstacle_has_sight():
+ return 0
+ else:
+ return self.map_obstacle.sight_range
+
+ def get_current_facing_direction(self, DIRECTIONS=DIRECTIONS):
+ """
+ Get the current facing direction of the map_obstacle.
+ """
+ return self.map_obstacle.get_current_facing_direction(DIRECTIONS=DIRECTIONS)
+
+ # this isn't used anywhere yet
+ def is_map_obstacle_in_screen_range(self):
+ """
+ Determines if the map_obstacle is within the bounds of whatever is on
+ screen at the moment. If the object is of a type that is capable of
+ moving, and it is not on screen, then it is not moving.
+ """
+ raise NotImplementedError
+
+ def mark_nodes_as_members_of_threat_zone(self):
+ """
+ Based on the nodes in this threat zone, mark each main graph's nodes as
+ members of this threat zone.
+ """
+
+ for y in range(self.top_left_y, self.top_left_y + self.height):
+ for x in range(self.top_left_x, self.top_left_x + self.width):
+ main_node = self.main_graph[y][x]
+ main_node.threat_zones.add(self)
+
+ self.nodes.append(main_node)
+
+ def update_obstacle_location(self):
+ """
+ Updates which node has the obstacle. This does not recompute the graph
+ based on this new information.
+
+ Each threat zone is responsible for updating its own map objects. So
+ there will never be a time when the current x value attached to the
+ map_obstacle does not represent the actual previous location.
+ """
+
+ # find the previous location of the obstacle
+ old_y = self.map_obstacle.y
+ old_x = self.map_obstacle.x
+
+ # remove it from the main graph
+ self.main_graph[old_y][old_x].contents.remove(self.map_obstacle)
+
+ # get the latest location
+ self.map_obstacle.update_location()
+ (new_y, new_x) = (self.map_obstacle.y, self.map_obstacle.x)
+
+ # add it back into the main graph
+ self.main_graph[new_y][new_x].contents.add(self.map_obstacle)
+
+ # update the map obstacle (not necessary, but it doesn't hurt)
+ self.map_obstacle.y = new_y
+ self.map_obstacle.x = new_x
+
+ def is_node_in_threat_zone(self, y, x):
+ """
+ Checks if the node is in the range of the threat zone.
+ """
+ y_condition = self.top_left_y <= y < self.top_left_y + self.height
+ x_condition = self.top_left_x <= x < self.top_left_x + self.width
+ return y_condition and x_condition
+
+ def is_node_in_sight_range(self, y, x, skip_range_check=False):
+ """
+ Checks if the node is in the sight range of the threat.
+ """
+ if not skip_range_check:
+ if not self.is_node_in_threat_zone(y, x):
+ return False
+
+ if self.sight_range == 0:
+ return False
+
+ # TODO: sight range can be blocked by collidable map objects. But this
+ # node wouldn't be in the threat zone anyway.
+ y_condition = self.map_obstacle.y == y
+ x_condition = self.map_obstacle.x == x
+
+ # this probably only happens if the player warps to the exact spot
+ if y_condition and x_condition:
+ raise Exception(
+ "Don't know the meaning of being on top of the map_obstacle."
+ )
+
+ # check if y or x matches the map object
+ return y_condition or x_condition
+
+ def is_node_in_active_sight_range(self,
+ y,
+ x,
+ skip_sight_range_check=False,
+ skip_range_check=False,
+ DIRECTIONS=DIRECTIONS):
+ """
+ Checks if the node has active sight range lock.
+ """
+
+ if not skip_sight_range_check:
+ # can't be in active sight range if not in sight range
+ if not self.is_in_sight_range(y, x, skip_range_check=skip_range_check):
+ return False
+
+ y_condition = self.map_obstacle.y == y
+ x_condition = self.map_obstacle.x == x
+
+ # this probably only happens if the player warps to the exact spot
+ if y_condition and x_condition:
+ raise Exception(
+ "Don't know the meaning of being on top of the map_obstacle."
+ )
+
+ current_facing_direction = self.get_current_facing_direction(DIRECTIONS=DIRECTIONS)
+
+ if current_facing_direction not in DIRECTIONS.keys():
+ raise Exception(
+ "Invalid direction."
+ )
+
+ if current_facing_direction in [DIRECTIONS["UP"], DIRECTIONS["DOWN"]]:
+ # map_obstacle is looking up/down but player doesn't match y
+ if not y_condition:
+ return False
+
+ if current_facing_direction == DIRECTIONS["UP"]:
+ return y < self.map_obstacle.y
+ elif current_facing_direction == DIRECTIONS["DOWN"]:
+ return y > self.map_obstacle.y
+ else:
+ # map_obstacle is looking left/right but player doesn't match x
+ if not x_condition:
+ return False
+
+ if current_facing_direction == DIRECTIONS["LEFT"]:
+ return x < self.map_obstacle.x
+ elif current_facing_direction == DIRECTIONS["RIGHT"]:
+ return x > self.map_obstacle.x
+
+ def calculate_node_cost(self, y, x, consider_sight_range=True, PENALTIES=PENALTIES):
+ """
+ Calculates the cost of the node w.r.t this threat zone. Turn off
+ consider_sight_range when not in the threat zone.
+ """
+ penalty = 0
+
+ # The node is probably in the threat zone because otherwise why would
+ # this cost function be called? Only the nodes that are members of the
+ # current threat zone would have a reference to this threat zone and
+ # this function.
+ if not self.is_node_in_threat_zone(y, x):
+ penalty += PENALTIES["NONE"]
+
+ # Additionally, if htis codepath is ever hit, the other node cost
+ # function will have already used the "NONE" penalty, so this would
+ # really be doubling the penalty of the node..
+ raise Exception(
+ "Didn't expect to calculate a non-threat-zone node's cost, "
+ "since this is a threat zone function."
+ )
+ else:
+ penalty += PENALTIES["THREAT_ZONE"]
+
+ if consider_sight_range:
+ if self.is_node_in_sight_range(y, x, skip_range_check=True):
+ penalty += PENALTIES["SIGHT_RANGE"]
+
+ params = {
+ "skip_sight_range_check": True,
+ "skip_range_check": True,
+ }
+
+ active_sight_range = self.is_node_in_active_sight_range(y, x, **params)
+
+ if active_sight_range:
+ penalty += PENALTIES["ACTIVE_SIGHT_RANGE"]
+
+ return penalty
+
+def create_graph(some_map):
+ """
+ Creates the array of nodes representing the in-game map.
+ """
+
+ map_height = some_map.height
+ map_width = some_map.width
+ map_obstacles = some_map.obstacles
+
+ nodes = [[None] * map_width] * map_height
+
+ # create a node representing each position on the map
+ for y in range(0, map_height):
+ for x in range(0, map_width):
+ position = (y, x)
+
+ # create a node describing this position
+ node = Node(position=position)
+
+ # store it on the graph
+ nodes[y][x] = node
+
+ # look through all moving characters, non-moving characters, and items
+ for map_obstacle in map_obstacles:
+ # all characters must start somewhere
+ node = nodes[map_obstacle.y][map_obstacle.x]
+
+ # store the map_obstacle on this node.
+ node.contents.add(map_obstacle)
+
+ # only create threat zones for moving/turning entities
+ if map_obstacle.can_move() or map_obstacle.can_turn_without_moving():
+ threat_zone = ThreatZone(map_obstacle, nodes, some_map)
+ threat_zone.mark_nodes_as_members_of_threat_zone()
+
+ some_map.nodes = nodes
+
+ return nodes
+
+class Map(object):
+ """
+ The ``Map`` class provides an interface for reading the currently loaded
+ map.
+ """
+
+ def __init__(self, cry, parsed_map, height, width, map_group_id, map_id, config=config):
+ """
+ :param cry: pokemon crystal emulation interface
+ :type cry: crystal
+ """
+ self.config = config
+ self.cry = cry
+
+ self.threat_zones = set()
+ self.obstacles = set()
+
+ self.parsed_map = parsed_map
+ self.map_group_id = map_group_id
+ self.map_id = map_id
+ self.height = height
+ self.width = width
+
+ def travel_to(self, destination_location):
+ """
+ Does path planning and figures out the quickest way to get to the
+ destination.
+ """
+ raise NotImplementedError
+
+ @staticmethod
+ def from_rom(cry, address):
+ """
+ Loads a map from bytes in ROM at the given address.
+
+ :param cry: pokemon crystal wrapper
+ """
+ raise NotImplementedError
+
+ @staticmethod
+ def from_wram(cry):
+ """
+ Loads a map from bytes in WRAM.
+
+ :param cry: pokemon crystal wrapper
+ """
+ raise NotImplementedError
+
+ def draw_path(self, path):
+ """
+ Draws a path on an image of the current map. The path must be an
+ iterable of nodes to visit in (y, x) format.
+ """
+ palettes = pokemontools.map_gfx.read_palettes(self.config)
+ map_image = pokemontools.map_gfx.draw_map(self.map_group_id, self.map_id, palettes, show_sprites=True, config=self.config)
+
+ for coordinates in path:
+ y = coordinates[0]
+ x = coordinates[1]
+
+ some_image = Image.new("RGBA", (32, 32))
+ draw = ImageDraw.Draw(some_image, "RGBA")
+ draw.rectangle([(0, 0), (32, 32)], fill=(0, 0, 0, 127))
+
+ target = [(x * 4, y * 4), ((x + 32) * 4, (y + 32) * 4)]
+
+ map_image.paste(some_image, target, mask=some_image)
+
+ return map_image
+
+class PathPlanner(object):
+ """
+ Generic path finding implementation.
+ """
+
+ def __init__(self, some_map, initial_location, target_location):
+ self.some_map = some_map
+ self.initial_location = initial_location
+ self.target_location = target_location
+
+ def plan(self):
+ """
+ Runs the path planner and returns a list of positions making up the
+ path.
+ """
+ return [(0, 0), (1, 0), (1, 1), (1, 2), (1, 3)]
+
+def plan_and_draw_path_on(map_group_id=1, map_id=1, initial_location=(0, 0), final_location=(2, 2), config=config):
+ """
+ An attempt at an entry point. This hasn't been sufficiently considered yet.
+ """
+ initial_location = (0, 0)
+ final_location = (2, 2)
+ map_group_id = 1
+ map_id = 1
+
+ pokemontools.crystal.cachably_parse_rom()
+ pokemontools.map_gfx.add_pokecrystal_paths_to_configuration(config)
+
+ # get the map based on data from the rom
+ parsed_map = pokemontools.crystal.map_names[map_group_id][map_id]["header_new"]
+
+ # convert this map into a different structure
+ current_map = Map(cry=None, parsed_map=parsed_map, height=parsed_map.height.byte, width=parsed_map.width.byte, map_group_id=map_group_id, map_id=map_id, config=config)
+
+ # make a graph based on the map data
+ nodes = create_graph(current_map)
+
+ # make an instance of the planner implementation
+ planner = PathPlanner(current_map, initial_location, final_location)
+
+ # Make that planner do its planning based on the current configuration. The
+ # planner should be callable in the future and still have
+ # previously-calculated state, like cached pre-computed routes or
+ # something.
+ path = planner.plan()
+
+ # show the path on the map
+ drawn = current_map.draw_path(path)
+
+ return drawn
diff --git a/pokemontools/wram.py b/pokemontools/wram.py
index e1b9212..2133444 100644
--- a/pokemontools/wram.py
+++ b/pokemontools/wram.py
@@ -9,6 +9,11 @@ import os
NUM_OBJECTS = 0x10
OBJECT_LENGTH = 0x10
+
+def rgbasm_to_py(text):
+ return text.replace('$', '0x').replace('%', '0b')
+
+
def make_wram_labels(wram_sections):
wram_labels = {}
for section in wram_sections:
@@ -18,6 +23,9 @@ def make_wram_labels(wram_sections):
wram_labels[label['address']] += [label['label']]
return wram_labels
+def bracket_value(string, i=0):
+ return string.split('[')[1 + i*2].split(']')[0]
+
def read_bss_sections(bss):
sections = []
section = {
@@ -30,10 +38,40 @@ def read_bss_sections(bss):
if 'SECTION' in line:
if section: sections.append(section) # last section
- address = eval(line[line.find('[')+1:line.find(']')].replace('$','0x'))
+ comment_index = line.find(';')
+ line, comment = line[:comment_index].lstrip(), line[comment_index:]
+
+ if 'SECTION' == line[:7]:
+ if section: # previous
+ sections += [section]
+
+ section_def = line.split(',')
+ name = section_def[0].split('"')[1]
+ type_ = section_def[1].strip()
+ if len(section_def) > 2:
+ bank = bracket_value(section_def[2])
+ else:
+ bank = None
+
+ if '[' in type_:
+ address = int(rgbasm_to_py(bracket_value(type_)), 16)
+ else:
+ types = {
+ 'VRAM': 0x8000,
+ 'SRAM': 0xa000,
+ 'WRAM0': 0xc000,
+ 'WRAMX': 0xd000,
+ 'HRAM': 0xff80,
+ }
+ if address == None or bank != section['bank'] or section['type'] != type_:
+ if type_ in types.keys():
+ address = types[type_]
+ # else: keep going from this address
+
section = {
- 'name': line.split('"')[1],
- #'type': line.split(',')[1].split('[')[0].strip(),
+ 'name': name,
+ 'type': type_,
+ 'bank': bank,
'start': address,
'labels': [],
}
@@ -49,7 +87,7 @@ def read_bss_sections(bss):
}]
elif line[:3] == 'ds ':
- length = eval(line[3:line.find(';')].replace('$','0x'))
+ length = eval(rgbasm_to_py(line[3:]))
address += length
# adjacent labels use the same space
for label in section['labels'][::-1]:
@@ -68,7 +106,7 @@ def read_bss_sections(bss):
return sections
def constants_to_dict(constants):
- return dict((eval(constant[constant.find('EQU')+3:constant.find(';')].replace('$','0x')), constant[:constant.find('EQU')].strip()) for constant in constants)
+ return dict((eval(rgbasm_to_py(constant[constant.find('EQU')+3:constant.find(';')])), constant[:constant.find('EQU')].strip()) for constant in constants)
def scrape_constants(text):
if type(text) is not list:
@@ -79,10 +117,10 @@ def read_constants(filepath):
"""
Load lines from a file and call scrape_constants.
"""
- lines = None
-
- with open(filepath, "r") as file_handler:
- lines = file_handler.readlines()
+ lines = []
+ if os.path.exists(filepath):
+ with open(filepath, "r") as file_handler:
+ lines = file_handler.readlines()
constants = scrape_constants(lines)
return constants
@@ -99,9 +137,21 @@ class WRAMProcessor(object):
self.config = config
self.paths = {}
- self.paths["wram"] = os.path.join(self.config.path, "wram.asm")
- self.paths["hram"] = os.path.join(self.config.path, "hram.asm")
- self.paths["gbhw"] = os.path.join(self.config.path, "gbhw.asm")
+
+ if hasattr(self.config, "wram"):
+ self.paths["wram"] = self.config.wram
+ else:
+ self.paths["wram"] = os.path.join(self.config.path, "wram.asm")
+
+ if hasattr(self.config, "hram"):
+ self.paths["hram"] = self.config.hram
+ else:
+ self.paths["hram"] = os.path.join(self.config.path, "hram.asm")
+
+ if hasattr(self.config, "gbhw"):
+ self.paths["gbhw"] = self.config.gbhw
+ else:
+ self.paths["gbhw"] = os.path.join(self.config.path, "gbhw.asm")
def initialize(self):
"""