mirror of
https://github.com/pret/pokemon-reverse-engineering-tools.git
synced 2026-08-27 20:54:08 -05:00
Merge pull request #62 from yenatch/battle-animations
* event command macro storetext actually took one param
* PointerLabel{Before,After}Bank now take a label instead of the redundant BANK(label), label
* use the from_asm() method for all macros
* no more macro logic in the preprocessor
* read and write RGB macros for palettes instead of binary chunks
* battle animation macros
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)
|
||||
|
||||
@@ -853,8 +853,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"]))
|
||||
@@ -927,8 +925,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
|
||||
@@ -944,13 +945,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
|
||||
@@ -2358,7 +2368,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]],
|
||||
|
||||
@@ -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):
|
||||
@@ -1566,6 +1616,8 @@ def expand_pic_palettes():
|
||||
with open(filename, 'wb') as out:
|
||||
out.write(w + palette + b)
|
||||
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
debug = False
|
||||
|
||||
@@ -1627,6 +1679,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]
|
||||
|
||||
@@ -445,7 +445,7 @@ class Tileset:
|
||||
def get_tiles(self):
|
||||
filename = self.get_tileset_gfx_filename()
|
||||
if not os.path.exists(filename):
|
||||
gfx.to_png(filename.replace('.png','.2bpp'), filename)
|
||||
gfx.export_lz_to_png(filename.replace('.png','.lz'))
|
||||
self.img = Image.open(filename)
|
||||
self.img.width, self.img.height = self.img.size
|
||||
self.tiles = []
|
||||
@@ -505,30 +505,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):
|
||||
|
||||
@@ -178,29 +178,9 @@ def read_palettes(time_of_day=1, config=config):
|
||||
filename = "{}.pal".format(actual_time_of_day)
|
||||
filepath = os.path.join(config.palette_dir, filename)
|
||||
|
||||
num_colors = 4
|
||||
color_length = 2
|
||||
palette_length = num_colors * color_length
|
||||
|
||||
pals = bytearray(open(filepath, "rb").read())
|
||||
num_pals = len(pals) / palette_length
|
||||
|
||||
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(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):
|
||||
|
||||
@@ -549,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:
|
||||
@@ -600,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)
|
||||
|
||||
|
||||
@@ -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:
|
||||
@@ -24,15 +29,14 @@ def bracket_value(string, i=0):
|
||||
def read_bss_sections(bss):
|
||||
sections = []
|
||||
section = {
|
||||
'name': None,
|
||||
'type': None,
|
||||
'bank': None,
|
||||
'start': None,
|
||||
'labels': [],
|
||||
"labels": [],
|
||||
}
|
||||
address = None
|
||||
if type(bss) is not list: bss = bss.split('\n')
|
||||
for line in bss:
|
||||
line = line.lstrip()
|
||||
if 'SECTION' in line:
|
||||
if section: sections.append(section) # last section
|
||||
|
||||
comment_index = line.find(';')
|
||||
line, comment = line[:comment_index].lstrip(), line[comment_index:]
|
||||
@@ -50,7 +54,7 @@ def read_bss_sections(bss):
|
||||
bank = None
|
||||
|
||||
if '[' in type_:
|
||||
address = int(bracket_value(type_).replace('$','0x'), 16)
|
||||
address = int(rgbasm_to_py(bracket_value(type_)), 16)
|
||||
else:
|
||||
types = {
|
||||
'VRAM': 0x8000,
|
||||
@@ -83,7 +87,7 @@ def read_bss_sections(bss):
|
||||
}]
|
||||
|
||||
elif line[:3] == 'ds ':
|
||||
length = eval(line[3:].replace('$','0x'))
|
||||
length = eval(rgbasm_to_py(line[3:]))
|
||||
address += length
|
||||
# adjacent labels use the same space
|
||||
for label in section['labels'][::-1]:
|
||||
@@ -102,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:
|
||||
@@ -113,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
|
||||
|
||||
Reference in New Issue
Block a user