Merge branch 'kanzure/master' into merge-old-commits

Conflicts:
	pokemontools/map_editor.py
This commit is contained in:
yenatch
2014-02-06 19:58:21 -05:00
14 changed files with 1916 additions and 220 deletions

View File

@@ -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()

View File

@@ -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)

View File

@@ -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

View File

@@ -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)

View File

@@ -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

View File

@@ -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]

View File

@@ -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

View File

@@ -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):

380
pokemontools/map_gfx.py Normal file
View File

@@ -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)

View File

@@ -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]
has_tab = line[0] == "\t"
# remove first tab
has_tab = False
if line[0] == "\t":
has_tab = True
line = line[1:]
# 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.
if "from_asm" in dir(param_klass):
param = param_klass.from_asm(param)
# 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")
index += 1
output += ("\t" + byte_type + " " + param + " ; " + description + "\n")
sys.stdout.write(output)

View File

@@ -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))

View File

@@ -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,

664
pokemontools/vba/path.py Normal file
View File

@@ -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

View File

@@ -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):
"""