diff options
-rw-r--r-- | pokemontools/audio.py | 2 | ||||
-rw-r--r-- | pokemontools/battle_animations.py | 294 | ||||
-rw-r--r-- | pokemontools/crystal.py | 24 | ||||
-rw-r--r-- | pokemontools/gfx.py | 108 | ||||
-rw-r--r-- | pokemontools/preprocessor.py | 84 | ||||
-rw-r--r-- | pokemontools/wram.py | 20 |
6 files changed, 414 insertions, 118 deletions
diff --git a/pokemontools/audio.py b/pokemontools/audio.py index 38fd65f..0e7d375 100644 --- a/pokemontools/audio.py +++ b/pokemontools/audio.py @@ -26,7 +26,7 @@ conf = configuration.Config() 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)) + return sorted(set(asms), key=lambda (x,y,z):(x,z,not y.startswith(';'), ':' not in y, y)) class NybbleParam: size = 0.5 diff --git a/pokemontools/battle_animations.py b/pokemontools/battle_animations.py new file mode 100644 index 0000000..96f0090 --- /dev/null +++ b/pokemontools/battle_animations.py @@ -0,0 +1,294 @@ +# 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 Wait(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: + + def __init__(self, address, base_label=None, label=None, used_labels=[]): + 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.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) + 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 = sorted(self.output + self.labels, key = lambda (x, y, z): (x, z)) + text = '' + for (address, asm, last_address) in output: + text += asm + '\n' + #text += '; %x\n' % last_address + return text + + def get_command_class(self, cmd): + if cmd < 0xd0: + return Wait + for class_ in battle_animation_classes: + if class_.id == cmd: + return class_ + return None + + +def battle_anim_label(i): + 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): + """ + 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) + asms += anim.output + anim.labels + + asms += [(address, '; %x\n' % address, address)] + + # jp sonicboom + anim = BattleAnim(address=0xc9c00, base_label='BattleAnim_Sonicboom_JP') + asms += anim.output + anim.labels + + asms = sort_asms(asms) + return asms + +def print_asm_list(asms): + # incbin any unknown areas + # not really needed since there are no gaps + last = asms[0][0] + for addr, asm, last_addr in asms: + if addr > last: + print '\nINCBIN "baserom.gbc", $%x, $%x - $%x\n\n' % (last, addr, last) + if addr >= last: + print asm + last = last_addr + +if __name__ == '__main__': + asms = dump_battle_anims() + print_asm_list(asms) + diff --git a/pokemontools/crystal.py b/pokemontools/crystal.py index eb88b6b..eed7c32 100644 --- a/pokemontools/crystal.py +++ b/pokemontools/crystal.py @@ -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]], diff --git a/pokemontools/gfx.py b/pokemontools/gfx.py index 8397337..e452da4 100644 --- a/pokemontools/gfx.py +++ b/pokemontools/gfx.py @@ -1012,7 +1012,7 @@ def get_uncompressed_gfx(start, num_tiles, filename): -def hex_to_rgb(word): +def bin_to_rgb(word): red = word & 0b11111 word >>= 5 green = word & 0b11111 @@ -1020,23 +1020,39 @@ def hex_to_rgb(word): blue = word & 0b11111 return (red, green, blue) -def grab_palettes(address, length=0x80): +def rgb_from_rom(address, length=0x80): + return convert_binary_pal_to_text(rom[address:address+length]) + +def convert_binary_pal_to_text_by_filename(filename): + with open(filename) as f: + pal = bytearray(f.read()) + return convert_binary_pal_to_text(pal) + +def convert_binary_pal_to_text(pal): output = '' - for word in range(length/2): - color = ord(rom[address+1])*0x100 + ord(rom[address]) - address += 2 - color = hex_to_rgb(color) - red = str(color[0]).zfill(2) - green = str(color[1]).zfill(2) - blue = str(color[2]).zfill(2) - output += '\tRGB '+red+', '+green+', '+blue + words = [hi * 0x100 + lo for lo, hi in zip(pal[::2], pal[1::2])] + for word in words: + red, green, blue = ['%.2d' % c for c in bin_to_rgb(word)] + output += '\tRGB ' + ', '.join((red, green, blue)) output += '\n' return output +def read_rgb_macros(lines): + colors = [] + for line in lines: + macro = line.split(" ")[0].strip() + if macro == 'RGB': + params = ' '.join(line.split(" ")[1:]).split(',') + red, green, blue = [int(v) for v in params] + colors += [[red, green, blue]] + return colors - - +def rewrite_binary_pals_to_text(filenames): + for filename in filenames: + pal_text = convert_binary_pal_to_text_by_filename(filename) + with open(filename, 'w') as out: + out.write(pal_text) def dump_monster_pals(): @@ -1147,6 +1163,9 @@ def to_lines(image, width): def dmg2rgb(word): + """ + For PNGs. + """ def shift(value): while True: yield value & (2**5 - 1) @@ -1159,27 +1178,49 @@ def dmg2rgb(word): def rgb_to_dmg(color): + """ + For PNGs. + """ word = (color['r'] / 8) word += (color['g'] / 8) << 5 word += (color['b'] / 8) << 10 return word -def png_pal(filename): - with open(filename, 'rb') as pal_data: - words = pal_data.read() - dmg_pals = [] - for word in range(len(words)/2): - dmg_pals.append(ord(words[word*2]) + ord(words[word*2+1])*0x100) +def pal_to_png(filename): + """ + Interpret a .pal file as a png palette. + """ + with open(filename) as rgbs: + colors = read_rgb_macros(rgbs.readlines()) + a = 255 palette = [] + for color in colors: + # even distribution over 000-255 + r, g, b = [int(hue * 8.25) for hue in color] + palette += [(r, g, b, a)] white = (255,255,255,255) black = (000,000,000,255) - for word in dmg_pals: palette += [dmg2rgb(word)] - if white not in dmg_pals and len(palette) < 4: palette = [white] + palette - if black not in dmg_pals and len(palette) < 4: palette += [black] + if white not in palette and len(palette) < 4: + palette = [white] + palette + if black not in palette and len(palette) < 4: + palette = palette + [black] return palette +def png_to_rgb(palette): + """ + Convert a png palette to rgb macros. + """ + output = '' + for color in palette: + r, g, b = [color[c] / 8 for c in 'rgb'] + output += '\tRGB ' + ', '.join(['%.2d' % hue for hue in (r, g, b)]) + output += '\n' + return output + + + def export_2bpp_to_png(filein, fileout=None, pal_file=None, height=0, width=0): if fileout == None: fileout = os.path.splitext(filein)[0] + '.png' @@ -1234,7 +1275,7 @@ def convert_2bpp_to_png(image, width=0, height=0, pal_file=None): px_map = [[3 - pixel for pixel in line] for line in lines] else: # gbc color - palette = png_pal(pal_file) + palette = pal_to_png(pal_file) greyscale = False bitdepth = 8 px_map = [[pixel for pixel in line] for line in lines] @@ -1371,13 +1412,22 @@ def png_to_2bpp(filein): def export_palette(palette, filename): + """ + Export a palette from png to rgb macros in a .pal file. + """ + if os.path.exists(filename): - output = [] - for color in palette: - word = rgb_to_dmg(color) - output += [word & 0xff] - output += [word >> 8] - to_file(filename, output) + + # Pic palettes are 2 colors (black/white are added later). + with open(filename) as rgbs: + colors = read_rgb_macros(rgbs.readlines()) + + if len(colors) == 2: + palette = palette[1:3] + + text = png_to_rgb(palette) + with open(filename, 'w') as out: + out.write(text) def png_to_lz(filein): @@ -1566,6 +1616,8 @@ def expand_pic_palettes(): with open(filename, 'wb') as out: out.write(w + palette + b) + + if __name__ == "__main__": debug = False diff --git a/pokemontools/preprocessor.py b/pokemontools/preprocessor.py index bde5f70..e7de46d 100644 --- a/pokemontools/preprocessor.py +++ b/pokemontools/preprocessor.py @@ -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. - - # some parameters are really multiple types of bytes - if (byte_type == "dw" and size != 2) or \ - (byte_type == "db" and size != 1): - - output += ("; " + description + "\n") - - if size == 3 and is_based_on(param_klass, "PointerLabelBeforeBank"): - # write the bank first - output += ("db " + param + "\n") - # write the pointer second - output += ("dw " + params[index+1].strip() + "\n") - index += 2 - correction += 1 - elif size == 3 and is_based_on(param_klass, "PointerLabelAfterBank"): - # write the pointer first - output += ("dw " + param + "\n") - # write the bank second - output += ("db " + params[index+1].strip() + "\n") - index += 2 - correction += 1 - elif size == 3 and "from_asm" in dir(param_klass): - output += ("\t" + byte_type + " " + param_klass.from_asm(param) + "\n") - index += 1 - else: - raise exceptions.MacroException( - "dunno what to do with this macro param ({klass}) in line: {line}" - .format( - klass=param_klass, - line=original_line, - ) - ) - - elif "from_asm" in dir(param_klass): - output += ("\t" + byte_type + " " + param_klass.from_asm(param) + " ; " + description + "\n") - index += 1 - - # or just print out the byte - else: - output += ("\t" + byte_type + " " + param + " ; " + description + "\n") + if "from_asm" in dir(param_klass): + param = param_klass.from_asm(param) - index += 1 + output += ("\t" + byte_type + " " + param + " ; " + description + "\n") sys.stdout.write(output) diff --git a/pokemontools/wram.py b/pokemontools/wram.py index a132289..1029c8e 100644 --- a/pokemontools/wram.py +++ b/pokemontools/wram.py @@ -9,6 +9,11 @@ import os NUM_OBJECTS = 0x10 OBJECT_LENGTH = 0x10 + +def rgbasm_to_py(text): + return text.replace('$', '0x').replace('%', '0b') + + def make_wram_labels(wram_sections): wram_labels = {} for section in wram_sections: @@ -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: |