mirror of
https://github.com/pret/pokemon-reverse-engineering-tools.git
synced 2026-08-27 20:54:08 -05:00
Merge branch 'kanzure/master' into merge-old-commits
Conflicts: pokemontools/map_editor.py
This commit is contained in:
@@ -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()
|
||||
|
||||
|
||||
324
pokemontools/battle_animations.py
Normal file
324
pokemontools/battle_animations.py
Normal 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)
|
||||
|
||||
@@ -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
|
||||
|
||||
102
pokemontools/data/pokecrystal/gbhw.asm
Normal file
102
pokemontools/data/pokecrystal/gbhw.asm
Normal 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)
|
||||
|
||||
71
pokemontools/data/pokecrystal/hram.asm
Normal file
71
pokemontools/data/pokecrystal/hram.asm
Normal 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
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
380
pokemontools/map_gfx.py
Normal 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)
|
||||
@@ -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)
|
||||
|
||||
|
||||
36
pokemontools/scan_includes.py
Normal file
36
pokemontools/scan_includes.py
Normal 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))
|
||||
|
||||
@@ -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
664
pokemontools/vba/path.py
Normal 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
|
||||
@@ -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):
|
||||
"""
|
||||
|
||||
Reference in New Issue
Block a user