mirror of
https://github.com/pret/pokemon-reverse-engineering-tools.git
synced 2026-08-27 20:54:08 -05:00
Merge pull request #51 from yenatch/master
sound dumps and from_asm() method in preprocessor macros
This commit is contained in:
385
pokemontools/audio.py
Normal file
385
pokemontools/audio.py
Normal file
@@ -0,0 +1,385 @@
|
||||
# coding: utf-8
|
||||
|
||||
import os
|
||||
|
||||
from math import ceil
|
||||
|
||||
from gbz80disasm import get_global_address, get_local_address
|
||||
|
||||
import crystal
|
||||
from crystal import music_classes as sound_classes
|
||||
from crystal import Command
|
||||
|
||||
from crystal import load_rom
|
||||
rom = load_rom()
|
||||
rom = bytearray(rom)
|
||||
|
||||
import config
|
||||
conf = config.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))
|
||||
|
||||
class NybbleParam:
|
||||
size = 0.5
|
||||
byte_type = 'dn'
|
||||
which = None
|
||||
|
||||
def __init__(self, address, name):
|
||||
if self.which == None:
|
||||
self.which = {0.0: 'lo', 0.5: 'hi'}[address % 1]
|
||||
self.address = int(address)
|
||||
self.name = name
|
||||
self.parse()
|
||||
|
||||
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
|
||||
|
||||
class LoNybbleParam(NybbleParam):
|
||||
which = 'lo'
|
||||
def to_asm(self):
|
||||
return '%d' % self.nybble
|
||||
|
||||
class PitchParam(HiNybbleParam):
|
||||
def to_asm(self):
|
||||
"""E and B cant be sharp"""
|
||||
if self.nybble == 0:
|
||||
pitch = '__'
|
||||
else:
|
||||
pitch = 'CCDDEFFGGAAB'[(self.nybble - 1)]
|
||||
if self.nybble in [2, 4, 7, 9, 11]:
|
||||
pitch += '#'
|
||||
else:
|
||||
pitch += '_'
|
||||
return pitch
|
||||
|
||||
|
||||
class Note(Command):
|
||||
macro_name = "note"
|
||||
size = 1
|
||||
end = False
|
||||
param_types = {
|
||||
0: {"name": "pitch", "class": PitchParam},
|
||||
1: {"name": "duration", "class": LoNybbleParam},
|
||||
}
|
||||
allowed_lengths = [2]
|
||||
override_byte_check = True
|
||||
is_rgbasm_macro = True
|
||||
|
||||
def parse(self):
|
||||
self.params = []
|
||||
byte = rom[self.address]
|
||||
current_address = self.address
|
||||
for (key, param_type) in self.param_types.items():
|
||||
name = param_type["name"]
|
||||
class_ = param_type["class"]
|
||||
|
||||
# by making an instance, obj.parse() is called
|
||||
obj = class_(address=int(current_address), name=name)
|
||||
self.params += [obj]
|
||||
|
||||
current_address += obj.size
|
||||
|
||||
self.params = dict(enumerate(self.params))
|
||||
|
||||
# obj sizes were 0.5, but were working with ints
|
||||
current_address = int(ceil(current_address))
|
||||
self.size = int(ceil(self.size))
|
||||
|
||||
self.last_address = current_address
|
||||
return True
|
||||
|
||||
|
||||
class Noise(Note):
|
||||
macro_name = "noise"
|
||||
size = 0
|
||||
end = False
|
||||
param_types = {
|
||||
0: {"name": "duration", "class": LoNybbleParam},
|
||||
1: {"name": "intensity", "class": SingleByteParam},
|
||||
2: {"name": "frequency", "class": MultiByteParam},
|
||||
}
|
||||
allowed_lengths = [2,3]
|
||||
override_byte_check = True
|
||||
is_rgbasm_macro = False
|
||||
|
||||
|
||||
|
||||
class Channel:
|
||||
"""A sound channel data parser."""
|
||||
|
||||
def __init__(self, address, channel=1, base_label='Sound'):
|
||||
self.start_address = address
|
||||
self.address = address
|
||||
self.channel = channel
|
||||
self.base_label = base_label
|
||||
self.output = []
|
||||
self.labels = []
|
||||
self.parse()
|
||||
|
||||
def parse(self):
|
||||
noise = False
|
||||
done = False
|
||||
while not done:
|
||||
cmd = rom[self.address]
|
||||
|
||||
class_ = self.get_sound_class(cmd)(address=self.address, channel=self.channel)
|
||||
|
||||
# notetype loses the intensity param on channel 4
|
||||
if class_.macro_name == 'notetype':
|
||||
if self.channel in [4, 8]:
|
||||
class_.size -= 1
|
||||
del class_.params[class_.size - 1]
|
||||
|
||||
# togglenoise only has a param when toggled on
|
||||
elif class_.macro_name in ['togglenoise', 'sfxtogglenoise']:
|
||||
if noise:
|
||||
class_.size -= 1
|
||||
del class_.params[class_.size - 1]
|
||||
noise = not noise
|
||||
|
||||
asm = class_.to_asm()
|
||||
|
||||
# label any jumps or calls
|
||||
for key, param in class_.param_types.items():
|
||||
if param['class'] == crystal.PointerLabelParam:
|
||||
label_address = class_.params[key].parsed_address
|
||||
label = '%s_branch_%x' % (
|
||||
self.base_label,
|
||||
label_address
|
||||
)
|
||||
label_output = (
|
||||
label_address,
|
||||
'\n%s: ; %x' % (label, label_address),
|
||||
label_address
|
||||
)
|
||||
self.labels += [label_output]
|
||||
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 == 'loopchannel':
|
||||
if class_.params[0].byte == 0:
|
||||
done = True
|
||||
|
||||
# keep going past enders if theres more to parse
|
||||
if any(self.address <= address for address, asm, last_address in self.output + self.labels):
|
||||
if done:
|
||||
self.output += [(self.address, '; %x' % self.address, self.address)]
|
||||
done = False
|
||||
|
||||
# dumb safety checks
|
||||
if (
|
||||
self.address >= len(rom) or
|
||||
self.address / 0x4000 != self.start_address / 0x4000
|
||||
) and not done:
|
||||
done = True
|
||||
raise Exception, 'reached the end of the bank without finishing!'
|
||||
|
||||
def to_asm(self):
|
||||
output = sort_asms(self.output + self.labels)
|
||||
text = ''
|
||||
for i, (address, asm, last_address) in enumerate(output):
|
||||
if ':' in asm:
|
||||
# dont print labels for empty chunks
|
||||
for (address_, asm_, last_address_) in output[i:]:
|
||||
if ':' not in asm_:
|
||||
text += '\n' + asm + '\n'
|
||||
break
|
||||
else:
|
||||
text += asm + '\n'
|
||||
text += '; %x' % (last_address) + '\n'
|
||||
return text
|
||||
|
||||
def get_sound_class(self, i):
|
||||
for class_ in sound_classes:
|
||||
if class_.id == i:
|
||||
return class_
|
||||
if self.channel in [4. 8]: return Noise
|
||||
return Note
|
||||
|
||||
|
||||
class Sound:
|
||||
"""Interprets a sound data header."""
|
||||
|
||||
def __init__(self, address, name=''):
|
||||
self.start_address = address
|
||||
self.bank = address / 0x4000
|
||||
self.address = address
|
||||
|
||||
self.name = name
|
||||
self.base_label = 'Sound_%x' % self.start_address
|
||||
if self.name != '':
|
||||
self.base_label = self.name
|
||||
|
||||
self.output = []
|
||||
self.labels = []
|
||||
self.asms = []
|
||||
self.parse()
|
||||
|
||||
def parse(self):
|
||||
self.num_channels = (rom[self.address] >> 6) + 1
|
||||
self.channels = []
|
||||
for ch in xrange(self.num_channels):
|
||||
current_channel = (rom[self.address] & 0xf) + 1
|
||||
self.address += 1
|
||||
address = rom[self.address] + rom[self.address + 1] * 0x100
|
||||
address = self.bank * 0x4000 + address % 0x4000
|
||||
self.address += 2
|
||||
channel = Channel(address, current_channel, self.base_label)
|
||||
self.channels += [(current_channel, channel)]
|
||||
|
||||
self.labels += channel.labels
|
||||
|
||||
label_text = '\n%s_Ch%d: ; %x' % (
|
||||
self.base_label,
|
||||
current_channel,
|
||||
channel.start_address
|
||||
)
|
||||
label_output = (channel.start_address, label_text, channel.start_address)
|
||||
self.labels += [label_output]
|
||||
|
||||
asms = []
|
||||
|
||||
text = '%s: ; %x' % (self.base_label, self.start_address) + '\n'
|
||||
for i, (num, channel) in enumerate(self.channels):
|
||||
channel_id = num - 1
|
||||
if i == 0:
|
||||
channel_id += (len(self.channels) - 1) << 6
|
||||
text += '\tdbw $%.2x, %s_Ch%d' % (channel_id, self.base_label, num) + '\n'
|
||||
text += '; %x\n' % self.address
|
||||
asms += [(self.start_address, text, self.start_address + len(self.channels) * 3)]
|
||||
|
||||
for num, channel in self.channels:
|
||||
asms += channel.output
|
||||
|
||||
asms = sort_asms(asms)
|
||||
self.last_address = asms[-1][2]
|
||||
asms += [(self.last_address,'; %x' % self.last_address, self.last_address)]
|
||||
|
||||
self.asms += asms
|
||||
|
||||
def to_asm(self, labels=[]):
|
||||
"""insert outside labels here"""
|
||||
asms = self.asms
|
||||
|
||||
# incbins dont really count as parsed data
|
||||
incbins = []
|
||||
for i, (address, asm, last_address) in enumerate(asms):
|
||||
if i + 1 < len(asms):
|
||||
next_address = asms[i + 1][0]
|
||||
if last_address != next_address:
|
||||
incbins += [(last_address, 'INCBIN "baserom.gbc", $%x, $%x - $%x' % (last_address, next_address, last_address), next_address)]
|
||||
asms += incbins
|
||||
for label in self.labels + labels:
|
||||
if self.start_address <= label[0] < self.last_address:
|
||||
asms += [label]
|
||||
|
||||
return '\n'.join(asm for address, asm, last_address in sort_asms(asms))
|
||||
|
||||
|
||||
def read_bank_address_pointer(addr):
|
||||
bank, address = rom[addr], rom[addr+1] + rom[addr+2] * 0x100
|
||||
return get_global_address(address, bank)
|
||||
|
||||
|
||||
def dump_sounds(origin, names, base_label='Sound_'):
|
||||
"""Dump sound data from a pointer table."""
|
||||
|
||||
# first pass to grab labels and boundaries
|
||||
labels = []
|
||||
addresses = []
|
||||
for i, name in enumerate(names):
|
||||
sound_at = read_bank_address_pointer(origin + i * 3)
|
||||
sound = Sound(sound_at, base_label + name)
|
||||
labels += sound.labels
|
||||
addresses += [(sound.start_address, sound.last_address)]
|
||||
addresses = sorted(addresses)
|
||||
|
||||
outputs = []
|
||||
for i, name in enumerate(names):
|
||||
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):
|
||||
next_address = addresses[index + 1][0]
|
||||
if 5 > next_address - sound.last_address > 0:
|
||||
if next_address / 0x4000 == sound.last_address / 0x4000:
|
||||
output += '\nINCBIN "baserom.gbc", $%x, $%x - $%x\n' % (sound.last_address, next_address, sound.last_address)
|
||||
|
||||
filename = name.lower() + '.asm'
|
||||
outputs += [(filename, output)]
|
||||
return outputs
|
||||
|
||||
|
||||
def export_sounds(origin, names, path, base_label='Sound_'):
|
||||
for filename, output in dump_sounds(origin, names, base_label):
|
||||
with open(os.path.join(path, filename), 'w') as out:
|
||||
out.write(output)
|
||||
|
||||
|
||||
def dump_sound_clump(origin, names, base_label='Sound_'):
|
||||
"""some sounds are grouped together and/or share most components.
|
||||
these can't reasonably be split into files for each sound."""
|
||||
|
||||
output = []
|
||||
for i, name in enumerate(names):
|
||||
sound_at = read_bank_address_pointer(origin + i * 3)
|
||||
sound = Sound(sound_at, base_label + name)
|
||||
output += sound.asms + sound.labels
|
||||
output = sort_asms(output)
|
||||
return output
|
||||
|
||||
|
||||
def export_sound_clump(origin, names, path, base_label='Sound_'):
|
||||
output = dump_sound_clump(origin, names, base_label)
|
||||
with open(path, 'w') as out:
|
||||
out.write('\n'.join(asm for address, asm, last_address in output))
|
||||
|
||||
|
||||
def dump_crystal_music():
|
||||
from song_names import song_names
|
||||
export_sounds(0xe906e, song_names, os.path.join(conf.path, 'audio', 'music'), 'Music_')
|
||||
|
||||
def generate_crystal_music_pointers():
|
||||
from song_names import song_names
|
||||
return '\n'.join('\tdbw BANK({0}), {0}'.format('Music_' + label) for label in song_names)
|
||||
|
||||
def dump_crystal_sfx():
|
||||
from sfx_names import sfx_names
|
||||
export_sound_clump(0xe927c, sfx_names, os.path.join(conf.path, 'audio', 'sfx.asm'), 'Sfx_')
|
||||
|
||||
def generate_crystal_sfx_pointers():
|
||||
from sfx_names import sfx_names
|
||||
return '\n'.join('\tdbw BANK({0}), {0}'.format('Sfx_' + label) for label in sfx_names)
|
||||
|
||||
def dump_crystal_cries():
|
||||
from cry_names import cry_names
|
||||
export_sound_clump(0xe91b0, cry_names, os.path.join(conf.path, 'audio', 'cries.asm'), 'Cry_')
|
||||
|
||||
def generate_crystal_cry_pointers():
|
||||
from cry_names import cry_names
|
||||
return '\n'.join('\tdbw BANK({0}), {0}'.format('Cry_' + label) for label in cry_names)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
dump_crystal_music()
|
||||
dump_crystal_sfx()
|
||||
|
||||
4
pokemontools/cry_names.py
Normal file
4
pokemontools/cry_names.py
Normal file
@@ -0,0 +1,4 @@
|
||||
# coding: utf-8
|
||||
|
||||
cry_names = ['%.2X' % x for x in xrange(0x44)]
|
||||
|
||||
@@ -174,7 +174,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, rom=rom)
|
||||
data = helpers.grouper(data)
|
||||
for pointer_parts in data:
|
||||
pointer = pointer_parts[0] + (pointer_parts[1] << 8)
|
||||
@@ -548,7 +548,7 @@ def parse_text_from_bytes(bytes, debug=True, japanese=False):
|
||||
def parse_text_at(address, count=10, debug=True):
|
||||
"""returns a list of bytes from an address
|
||||
see parse_text_at2 for pretty printing"""
|
||||
return parse_text_from_bytes(rom_interval(address, count, strings=False), debug=debug)
|
||||
return parse_text_from_bytes(rom.interval(address, count, strings=False), debug=debug)
|
||||
|
||||
def parse_text_at2(address, count=10, debug=True, japanese=False):
|
||||
"""returns a string of text from an address
|
||||
@@ -569,7 +569,7 @@ def parse_text_at3(address, map_group=None, map_id=None, debug=False):
|
||||
def rom_text_at(address, count=10):
|
||||
"""prints out raw text from the ROM
|
||||
like for 0x112110"""
|
||||
return "".join([chr(x) for x in rom_interval(address, count, strings=False)])
|
||||
return "".join([chr(x) for x in rom.interval(address, count, strings=False)])
|
||||
|
||||
def get_map_constant_label(map_group=None, map_id=None, map_internal_ids=None):
|
||||
"""returns PALLET_TOWN for some map group/id pair"""
|
||||
@@ -764,6 +764,10 @@ class SingleByteParam():
|
||||
else:
|
||||
return str(self.byte)
|
||||
|
||||
@staticmethod
|
||||
def from_asm(value):
|
||||
return value
|
||||
|
||||
class DollarSignByte(SingleByteParam):
|
||||
def to_asm(self):
|
||||
return hex(self.byte).replace("0x", "$")
|
||||
@@ -803,7 +807,7 @@ class MultiByteParam():
|
||||
self.parse()
|
||||
|
||||
def parse(self):
|
||||
self.bytes = rom_interval(self.address, self.size, strings=False)
|
||||
self.bytes = rom.interval(self.address, self.size, strings=False)
|
||||
self.parsed_number = self.bytes[0] + (self.bytes[1] << 8)
|
||||
if hasattr(self, "bank"):
|
||||
self.parsed_address = calculate_pointer_from_bytes_at(self.address, bank=self.bank)
|
||||
@@ -822,6 +826,10 @@ class MultiByteParam():
|
||||
decimal = int("0x"+"".join([("%.2x")%x for x in reversed(self.bytes)]), 16)
|
||||
return str(decimal)
|
||||
|
||||
@staticmethod
|
||||
def from_asm(value):
|
||||
return value
|
||||
|
||||
|
||||
class PointerLabelParam(MultiByteParam):
|
||||
# default size is 2 bytes
|
||||
@@ -1000,6 +1008,7 @@ class RAMAddressParam(MultiByteParam):
|
||||
|
||||
class MoneyByteParam(MultiByteParam):
|
||||
size = 3
|
||||
byte_type = "db"
|
||||
max_value = 0x0F423F
|
||||
should_be_decimal = True
|
||||
def parse(self):
|
||||
@@ -1236,6 +1245,7 @@ class Command:
|
||||
# use this when the "byte id" doesn't matter
|
||||
# .. for example, a non-script command doesn't use the "byte id"
|
||||
override_byte_check = False
|
||||
is_rgbasm_macro = False
|
||||
base_label = "UnseenLabel_"
|
||||
|
||||
def __init__(self, address=None, *pargs, **kwargs):
|
||||
@@ -1723,6 +1733,7 @@ class TextCommand(Command):
|
||||
#def get_dependencies(self, recompute=False, global_dependencies=set()):
|
||||
# return []
|
||||
|
||||
|
||||
# this is a regular command in a TextScript for writing text
|
||||
# but unlike other macros that preprocessor.py handles,
|
||||
# the preprocessor-parser is custom and MainText is not
|
||||
@@ -1769,7 +1780,7 @@ class MainText(TextCommand):
|
||||
|
||||
# read the text bytes into a structure
|
||||
# skip the first offset byte because that's the command byte
|
||||
self.bytes = rom_interval(offset, jump, strings=False)
|
||||
self.bytes = rom.interval(offset, jump, strings=False)
|
||||
|
||||
# include the original command in the size calculation
|
||||
self.size = jump
|
||||
@@ -2372,41 +2383,70 @@ def create_command_classes(debug=False):
|
||||
command_classes = create_command_classes()
|
||||
|
||||
|
||||
class BigEndianParam:
|
||||
"""big-endian word"""
|
||||
size = 2
|
||||
should_be_decimal = False
|
||||
byte_type = "bigdw"
|
||||
|
||||
music_commands_new = {
|
||||
0xD0: ["octave8"],
|
||||
0xD1: ["octave7"],
|
||||
0xD2: ["octave6"],
|
||||
0xD3: ["octave5"],
|
||||
0xD4: ["octave4"],
|
||||
0xD5: ["octave3"],
|
||||
0xD6: ["octave2"],
|
||||
0xD7: ["octave1"],
|
||||
0xD8: ["notetype", ["note_length", SingleByteParam], ["intensity", SingleByteParam]], # only 1 param on ch3
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.prefix = '$'
|
||||
for (key, value) in kwargs.items():
|
||||
setattr(self, key, value)
|
||||
self.parse()
|
||||
|
||||
def parse(self):
|
||||
self.bytes = rom.interval(self.address, 2, strings=False)
|
||||
self.parsed_number = self.bytes[0] * 0x100 + self.bytes[1]
|
||||
|
||||
def to_asm(self):
|
||||
if not self.should_be_decimal:
|
||||
return self.prefix+"".join([("%.2x")%x for x in self.bytes])
|
||||
elif self.should_be_decimal:
|
||||
decimal = int("0x"+"".join([("%.2x")%x for x in self.bytes]), 16)
|
||||
return str(decimal)
|
||||
|
||||
@staticmethod
|
||||
def from_asm(value):
|
||||
return value
|
||||
|
||||
class DecimalBigEndianParam(BigEndianParam):
|
||||
should_be_decimal = True
|
||||
|
||||
music_commands = {
|
||||
0xD0: ["octave 8"],
|
||||
0xD1: ["octave 7"],
|
||||
0xD2: ["octave 6"],
|
||||
0xD3: ["octave 5"],
|
||||
0xD4: ["octave 4"],
|
||||
0xD5: ["octave 3"],
|
||||
0xD6: ["octave 2"],
|
||||
0xD7: ["octave 1"],
|
||||
0xD8: ["notetype", ["note_length", SingleByteParam], ["intensity", SingleByteParam]], # no intensity on channel 4/8
|
||||
0xD9: ["forceoctave", ["octave", SingleByteParam]],
|
||||
0xDA: ["tempo", ["tempo", MultiByteParam]],
|
||||
0xDA: ["tempo", ["tempo", DecimalBigEndianParam]],
|
||||
0xDB: ["dutycycle", ["duty_cycle", SingleByteParam]],
|
||||
0xDC: ["intensity", ["intensity", SingleByteParam]],
|
||||
0xDD: ["soundinput", ["input", SingleByteParam]],
|
||||
0xDE: ["unknownmusic0xde", ["unknown", SingleByteParam]], # also updates duty cycle
|
||||
0xDE: ["unknownmusic0xde", ["unknown", SingleByteParam]],
|
||||
0xDF: ["unknownmusic0xdf"],
|
||||
0xE0: ["unknownmusic0xe0", ["unknown", SingleByteParam], ["unknown", SingleByteParam]],
|
||||
0xE1: ["vibrato", ["delay", SingleByteParam], ["extent", SingleByteParam]],
|
||||
0xE2: ["unknownmusic0xe2", ["unknown", SingleByteParam]],
|
||||
0xE3: ["togglenoise", ["id", SingleByteParam]], # this can have 0-1 params!
|
||||
0xE3: ["togglenoise", ["id", SingleByteParam]], # no parameters on toggle off
|
||||
0xE4: ["panning", ["tracks", SingleByteParam]],
|
||||
0xE5: ["volume", ["volume", SingleByteParam]],
|
||||
0xE6: ["tone", ["tone", MultiByteParam]], # big endian
|
||||
0xE6: ["tone", ["tone", BigEndianParam]],
|
||||
0xE7: ["unknownmusic0xe7", ["unknown", SingleByteParam]],
|
||||
0xE8: ["unknownmusic0xe8", ["unknown", SingleByteParam]],
|
||||
0xE9: ["globaltempo", ["value", MultiByteParam]],
|
||||
0xE9: ["globaltempo", ["value", DecimalBigEndianParam]],
|
||||
0xEA: ["restartchannel", ["address", PointerLabelParam]],
|
||||
0xEB: ["newsong", ["id", MultiByteParam]],
|
||||
0xEB: ["newsong", ["id", DecimalBigEndianParam]],
|
||||
0xEC: ["sfxpriorityon"],
|
||||
0xED: ["sfxpriorityoff"],
|
||||
0xEE: ["unknownmusic0xee", ["address", PointerLabelParam]],
|
||||
0xEF: ["stereopanning", ["tracks", SingleByteParam]],
|
||||
0xF0: ["sfxtogglenoise", ["id", SingleByteParam]], # 0-1 params
|
||||
0xF0: ["sfxtogglenoise", ["id", SingleByteParam]], # no parameters on toggle off
|
||||
0xF1: ["music0xf1"], # nothing
|
||||
0xF2: ["music0xf2"], # nothing
|
||||
0xF3: ["music0xf3"], # nothing
|
||||
@@ -2419,19 +2459,29 @@ music_commands_new = {
|
||||
0xFA: ["setcondition", ["condition", SingleByteParam]],
|
||||
0xFB: ["jumpif", ["condition", SingleByteParam], ["address", PointerLabelParam]],
|
||||
0xFC: ["jumpchannel", ["address", PointerLabelParam]],
|
||||
0xFD: ["loopchannel", ["count", SingleByteParam], ["address", PointerLabelParam]],
|
||||
0xFD: ["loopchannel", ["count", DecimalParam], ["address", PointerLabelParam]],
|
||||
0xFE: ["callchannel", ["address", PointerLabelParam]],
|
||||
0xFF: ["endchannel"],
|
||||
}
|
||||
|
||||
music_command_enders = [0xEA, 0xEB, 0xEE, 0xFC, 0xFF,]
|
||||
# special case for 0xFD (if loopchannel.count = 0, break)
|
||||
music_command_enders = [
|
||||
"restartchannel",
|
||||
"newsong",
|
||||
"unknownmusic0xee",
|
||||
"jumpchannel",
|
||||
"endchannel",
|
||||
]
|
||||
|
||||
def create_music_command_classes(debug=False):
|
||||
klasses = []
|
||||
for (byte, cmd) in music_commands_new.items():
|
||||
for (byte, cmd) in music_commands.items():
|
||||
cmd_name = cmd[0].replace(" ", "_")
|
||||
params = {"id": byte, "size": 1, "end": byte in music_command_enders, "macro_name": cmd_name}
|
||||
params = {
|
||||
"id": byte,
|
||||
"size": 1,
|
||||
"end": cmd[0] in music_command_enders,
|
||||
"macro_name": cmd[0]
|
||||
}
|
||||
params["param_types"] = {}
|
||||
if len(cmd) > 1:
|
||||
param_types = cmd[1:]
|
||||
@@ -2451,24 +2501,54 @@ def create_music_command_classes(debug=False):
|
||||
klasses.append(klass)
|
||||
# later an individual klass will be instantiated to handle something
|
||||
return klasses
|
||||
|
||||
music_classes = create_music_command_classes()
|
||||
|
||||
class OctaveParam(DecimalParam):
|
||||
@staticmethod
|
||||
def from_asm(value):
|
||||
value = int(value)
|
||||
return hex(0xd8 - value).replace("0x", "$")
|
||||
|
||||
class OctaveCommand(Command):
|
||||
macro_name = "octave"
|
||||
size = 0
|
||||
end = False
|
||||
param_types = {
|
||||
0: {"name": "octave", "class": OctaveParam},
|
||||
}
|
||||
allowed_lengths = [1]
|
||||
override_byte_check = True
|
||||
|
||||
class ChannelCommand(Command):
|
||||
macro_name = "channel"
|
||||
size = 3
|
||||
override_byte_check = True
|
||||
param_types = {
|
||||
0: {"name": "id", "class": DecimalParam},
|
||||
1: {"name": "address", "class": PointerLabelParam},
|
||||
}
|
||||
|
||||
|
||||
# pokered
|
||||
|
||||
class callchannel(Command):
|
||||
id = 0xFD
|
||||
macro_name = "callchannel"
|
||||
size = 3
|
||||
param_types = {
|
||||
0: {"name": "address", "class": PointerLabelParam},
|
||||
}
|
||||
id = 0xFD
|
||||
macro_name = "callchannel"
|
||||
size = 3
|
||||
param_types = {
|
||||
0: {"name": "address", "class": PointerLabelParam},
|
||||
}
|
||||
|
||||
class loopchannel(Command):
|
||||
id = 0xFE
|
||||
macro_name = "loopchannel"
|
||||
size = 4
|
||||
param_types = {
|
||||
0: {"name": "count", "class": SingleByteParam},
|
||||
1: {"name": "address", "class": PointerLabelParam},
|
||||
}
|
||||
id = 0xFE
|
||||
macro_name = "loopchannel"
|
||||
size = 4
|
||||
param_types = {
|
||||
0: {"name": "count", "class": SingleByteParam},
|
||||
1: {"name": "address", "class": PointerLabelParam},
|
||||
}
|
||||
|
||||
|
||||
effect_commands = {
|
||||
0x1: ['checkturn'],
|
||||
@@ -4272,7 +4352,7 @@ class Signpost(Command):
|
||||
address = self.address
|
||||
bank = self.bank
|
||||
self.last_address = self.address + self.size
|
||||
bytes = rom_interval(self.address, self.size) #, signpost_byte_size)
|
||||
bytes = rom.interval(self.address, self.size) #, signpost_byte_size)
|
||||
|
||||
self.y = int(bytes[0], 16)
|
||||
self.x = int(bytes[1], 16)
|
||||
@@ -4526,7 +4606,7 @@ class SecondMapHeader:
|
||||
|
||||
def parse(self):
|
||||
address = self.address
|
||||
bytes = rom_interval(address, second_map_header_byte_size, strings=False)
|
||||
bytes = rom.interval(address, second_map_header_byte_size, strings=False)
|
||||
size = second_map_header_byte_size
|
||||
|
||||
# for later
|
||||
@@ -5339,7 +5419,7 @@ class MapBlockData:
|
||||
os.mkdir(self.maps_path)
|
||||
if not os.path.exists(map_path):
|
||||
# dump to file
|
||||
#bytes = rom_interval(self.address, self.width.byte*self.height.byte, strings=True)
|
||||
#bytes = rom.interval(self.address, self.width.byte*self.height.byte, strings=True)
|
||||
bytes = rom[self.address : self.address + self.width.byte*self.height.byte]
|
||||
file_handler = open(map_path, "w")
|
||||
file_handler.write(bytes)
|
||||
@@ -5407,7 +5487,7 @@ class MapEventHeader:
|
||||
# signposts
|
||||
signpost_count = ord(rom[after_triggers])
|
||||
signpost_byte_count = signpost_byte_size * signpost_count
|
||||
# signposts = rom_interval(after_triggers+1, signpost_byte_count)
|
||||
# signposts = rom.interval(after_triggers+1, signpost_byte_count)
|
||||
signposts = parse_signposts(after_triggers+1, signpost_count, bank=bank, map_group=map_group, map_id=map_id, debug=debug)
|
||||
after_signposts = after_triggers + 1 + signpost_byte_count
|
||||
self.signpost_count = signpost_count
|
||||
@@ -5416,7 +5496,7 @@ class MapEventHeader:
|
||||
# people events
|
||||
people_event_count = ord(rom[after_signposts])
|
||||
people_event_byte_count = people_event_byte_size * people_event_count
|
||||
# people_events_bytes = rom_interval(after_signposts+1, people_event_byte_count)
|
||||
# people_events_bytes = rom.interval(after_signposts+1, people_event_byte_count)
|
||||
# people_events = parse_people_event_bytes(people_events_bytes, address=after_signposts+1, map_group=map_group, map_id=map_id)
|
||||
people_events = parse_people_events(after_signposts+1, people_event_count, bank=pointers.calculate_bank(after_signposts+2), map_group=map_group, map_id=map_id, debug=debug)
|
||||
self.people_event_count = people_event_count
|
||||
@@ -5577,7 +5657,7 @@ class MapScriptHeader:
|
||||
self.trigger_count = ord(rom[address])
|
||||
self.triggers = []
|
||||
ptr_line_size = 4
|
||||
groups = helpers.grouper(rom_interval(address+1, self.trigger_count * ptr_line_size, strings=False), count=ptr_line_size)
|
||||
groups = helpers.grouper(rom.interval(address+1, self.trigger_count * ptr_line_size, strings=False), count=ptr_line_size)
|
||||
current_address = address+1
|
||||
for (index, trigger_bytes) in enumerate(groups):
|
||||
logging.debug(
|
||||
|
||||
@@ -556,8 +556,6 @@ class Preprocessor(object):
|
||||
# remove trailing newline
|
||||
if line[-1] == "\n":
|
||||
line = line[:-1]
|
||||
else:
|
||||
original_line += "\n"
|
||||
|
||||
# remove first tab
|
||||
has_tab = False
|
||||
@@ -589,6 +587,12 @@ class Preprocessor(object):
|
||||
sys.stdout.write(original_line)
|
||||
return
|
||||
|
||||
# rgbasm can handle other macros too
|
||||
if "is_rgbasm_macro" in dir(macro):
|
||||
if macro.is_rgbasm_macro:
|
||||
sys.stdout.write(original_line)
|
||||
return
|
||||
|
||||
# certain macros don't need an initial byte written
|
||||
# do: all scripting macros
|
||||
# don't: signpost, warp_def, person_event, xy_trigger
|
||||
@@ -608,7 +612,7 @@ class Preprocessor(object):
|
||||
index = 0
|
||||
while index < len(params):
|
||||
param_type = macro.param_types[index - correction]
|
||||
description = param_type["name"]
|
||||
description = param_type["name"].strip()
|
||||
param_klass = param_type["class"]
|
||||
byte_type = param_klass.byte_type # db or dw
|
||||
size = param_klass.size
|
||||
@@ -638,7 +642,7 @@ class Preprocessor(object):
|
||||
index += 2
|
||||
correction += 1
|
||||
elif size == 3 and "from_asm" in dir(param_klass):
|
||||
output += ("db " + param_klass.from_asm(param) + "\n")
|
||||
output += ("\t" + byte_type + " " + param_klass.from_asm(param) + "\n")
|
||||
index += 1
|
||||
else:
|
||||
raise exceptions.MacroException(
|
||||
@@ -649,9 +653,13 @@ class Preprocessor(object):
|
||||
)
|
||||
)
|
||||
|
||||
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 += (byte_type + " " + param + " ; " + description + "\n")
|
||||
output += ("\t" + byte_type + " " + param + " ; " + description + "\n")
|
||||
|
||||
index += 1
|
||||
|
||||
|
||||
211
pokemontools/sfx_names.py
Normal file
211
pokemontools/sfx_names.py
Normal file
@@ -0,0 +1,211 @@
|
||||
# coding: utf-8
|
||||
|
||||
sfx_names = [
|
||||
'DexFanfare5079',
|
||||
'Item',
|
||||
'CaughtMon',
|
||||
'PokeballsPlacedOnTable',
|
||||
'Potion',
|
||||
'FullHeal',
|
||||
'Menu',
|
||||
'ReadText',
|
||||
'ReadText2',
|
||||
'DexFanfare2049',
|
||||
'DexFanfare80109',
|
||||
'Poison',
|
||||
'GotSafariBalls',
|
||||
'BootPc',
|
||||
'ShutDownPc',
|
||||
'ChoosePcOption',
|
||||
'EscapeRope',
|
||||
'PushButton',
|
||||
'SecondPartOfItemfinder',
|
||||
'WarpTo',
|
||||
'WarpFrom',
|
||||
'ChangeDexMode',
|
||||
'JumpOverLedge',
|
||||
'GrassRustle',
|
||||
'Fly',
|
||||
'Wrong',
|
||||
'Squeak',
|
||||
'Strength',
|
||||
'Boat',
|
||||
'WallOpen',
|
||||
'PlacePuzzlePieceDown',
|
||||
'EnterDoor',
|
||||
'SwitchPokemon',
|
||||
'Tally',
|
||||
'Transaction',
|
||||
'ExitBuilding',
|
||||
'Bump',
|
||||
'Save',
|
||||
'Pokeflute',
|
||||
'ElevatorEnd',
|
||||
'ThrowBall',
|
||||
'BallPoof',
|
||||
'Unknown3A',
|
||||
'Run',
|
||||
'SlotMachineStart',
|
||||
'Fanfare',
|
||||
'Peck',
|
||||
'Kinesis',
|
||||
'Lick',
|
||||
'Pound',
|
||||
'MovePuzzlePiece',
|
||||
'CometPunch',
|
||||
'MegaPunch',
|
||||
'Scratch',
|
||||
'Vicegrip',
|
||||
'RazorWind',
|
||||
'Cut',
|
||||
'WingAttack',
|
||||
'Whirlwind',
|
||||
'Bind',
|
||||
'VineWhip',
|
||||
'DoubleKick',
|
||||
'MegaKick',
|
||||
'Headbutt',
|
||||
'HornAttack',
|
||||
'Tackle',
|
||||
'PoisonSting',
|
||||
'Powder',
|
||||
'Doubleslap',
|
||||
'Bite',
|
||||
'JumpKick',
|
||||
'Stomp',
|
||||
'TailWhip',
|
||||
'KarateChop',
|
||||
'Submission',
|
||||
'WaterGun',
|
||||
'SwordsDance',
|
||||
'Thunder',
|
||||
'Supersonic',
|
||||
'Leer',
|
||||
'Ember',
|
||||
'Bubblebeam',
|
||||
'HydroPump',
|
||||
'Surf',
|
||||
'Psybeam',
|
||||
'Charge',
|
||||
'Thundershock',
|
||||
'Psychic',
|
||||
'Screech',
|
||||
'BoneClub',
|
||||
'Sharpen',
|
||||
'EggBomb',
|
||||
'Sing',
|
||||
'HyperBeam',
|
||||
'Shine',
|
||||
'Unknown5F',
|
||||
'Unknown60',
|
||||
'Unknown61',
|
||||
'Unknown62',
|
||||
'Unknown63',
|
||||
'Burn',
|
||||
'TitleScreenEntrance',
|
||||
'Unknown66',
|
||||
'GetCoinFromSlots',
|
||||
'PayDay',
|
||||
'Metronome',
|
||||
'Call',
|
||||
'HangUp',
|
||||
'NoSignal',
|
||||
'Sandstorm',
|
||||
'Elevator',
|
||||
'Protect',
|
||||
'Sketch',
|
||||
'RainDance',
|
||||
'Aeroblast',
|
||||
'Spark',
|
||||
'Curse',
|
||||
'Rage',
|
||||
'Thief',
|
||||
'Thief2',
|
||||
'SpiderWeb',
|
||||
'MindReader',
|
||||
'Nightmare',
|
||||
'Snore',
|
||||
'SweetKiss',
|
||||
'SweetKiss2',
|
||||
'BellyDrum',
|
||||
'Unknown7F',
|
||||
'SludgeBomb',
|
||||
'Foresight',
|
||||
'Spite',
|
||||
'Outrage',
|
||||
'PerishSong',
|
||||
'GigaDrain',
|
||||
'Attract',
|
||||
'Kinesis2',
|
||||
'ZapCannon',
|
||||
'MeanLook',
|
||||
'HealBell',
|
||||
'Return',
|
||||
'ExpBar',
|
||||
'MilkDrink',
|
||||
'Present',
|
||||
'MorningSun',
|
||||
'LevelUp',
|
||||
'KeyItem',
|
||||
'Fanfare2',
|
||||
'RegisterPhoneNumber',
|
||||
'3RdPlace',
|
||||
'GetEggFromDaycareMan',
|
||||
'GetEggFromDaycareLady',
|
||||
'MoveDeleted',
|
||||
'2NdPlace',
|
||||
'1StPlace',
|
||||
'ChooseACard',
|
||||
'GetTm',
|
||||
'GetBadge',
|
||||
'QuitSlots',
|
||||
'EggCrack',
|
||||
'DexFanfareLessThan20',
|
||||
'DexFanfare140169',
|
||||
'DexFanfare170199',
|
||||
'DexFanfare200229',
|
||||
'DexFanfare230Plus',
|
||||
'Evolved',
|
||||
'MasterBall',
|
||||
'EggHatch',
|
||||
'GsIntroCharizardFireball',
|
||||
'GsIntroPokemonAppears',
|
||||
'Flash',
|
||||
'GameFreakLogoGs',
|
||||
'NotVeryEffective',
|
||||
'Damage',
|
||||
'SuperEffective',
|
||||
'BallBounce',
|
||||
'Moonlight',
|
||||
'Encore',
|
||||
'BeatUp',
|
||||
'BatonPass',
|
||||
'BallWiggle',
|
||||
'SweetScent',
|
||||
'SweetScent2',
|
||||
'HitEndOfExpBar',
|
||||
'GiveTrademon',
|
||||
'GetTrademon',
|
||||
'TrainArrived',
|
||||
'StopSlot',
|
||||
'2Boops',
|
||||
'GlassTing',
|
||||
'GlassTing2',
|
||||
'IntroUnown1',
|
||||
'IntroUnown2',
|
||||
'IntroUnown3',
|
||||
'DittoPopUp',
|
||||
'DittoTransform',
|
||||
'IntroSuicune1',
|
||||
'IntroPichu',
|
||||
'IntroSuicune2',
|
||||
'IntroSuicune3',
|
||||
'DittoBounce',
|
||||
'IntroSuicune4',
|
||||
'GameFreakPresents',
|
||||
'Tingle',
|
||||
'UnknownCb',
|
||||
'TwoPcBeeps',
|
||||
'4NoteDitty',
|
||||
'Twinkle',
|
||||
]
|
||||
107
pokemontools/song_names.py
Normal file
107
pokemontools/song_names.py
Normal file
@@ -0,0 +1,107 @@
|
||||
# coding: utf-8
|
||||
|
||||
song_names = [
|
||||
'Nothing',
|
||||
'TitleScreen',
|
||||
'Route1',
|
||||
'Route3',
|
||||
'Route12',
|
||||
'MagnetTrain',
|
||||
'KantoGymBattle',
|
||||
'KantoTrainerBattle',
|
||||
'KantoWildBattle',
|
||||
'PokemonCenter',
|
||||
'LookHiker',
|
||||
'LookLass',
|
||||
'LookOfficer',
|
||||
'HealPokemon',
|
||||
'LavenderTown',
|
||||
'Route2',
|
||||
'MtMoon',
|
||||
'ShowMeAround',
|
||||
'GameCorner',
|
||||
'Bicycle',
|
||||
'HallOfFame',
|
||||
'ViridianCity',
|
||||
'CeladonCity',
|
||||
'TrainerVictory',
|
||||
'WildPokemonVictory',
|
||||
'GymLeaderVictory',
|
||||
'MtMoonSquare',
|
||||
'Gym',
|
||||
'PalletTown',
|
||||
'ProfOaksPokemonTalk',
|
||||
'ProfOak',
|
||||
'LookRival',
|
||||
'AfterTheRivalFight',
|
||||
'Surf',
|
||||
'Evolution',
|
||||
'NationalPark',
|
||||
'Credits',
|
||||
'AzaleaTown',
|
||||
'CherrygroveCity',
|
||||
'LookKimonoGirl',
|
||||
'UnionCave',
|
||||
'JohtoWildBattle',
|
||||
'JohtoTrainerBattle',
|
||||
'Route30',
|
||||
'EcruteakCity',
|
||||
'VioletCity',
|
||||
'JohtoGymBattle',
|
||||
'ChampionBattle',
|
||||
'RivalBattle',
|
||||
'RocketBattle',
|
||||
'ElmsLab',
|
||||
'DarkCave',
|
||||
'Route29',
|
||||
'Route36',
|
||||
'SSAqua',
|
||||
'LookYoungster',
|
||||
'LookBeauty',
|
||||
'LookRocket',
|
||||
'LookPokemaniac',
|
||||
'LookSage',
|
||||
'NewBarkTown',
|
||||
'GoldenrodCity',
|
||||
'VermilionCity',
|
||||
'PokemonChannel',
|
||||
'PokeFluteChannel',
|
||||
'TinTower',
|
||||
'SproutTower',
|
||||
'BurnedTower',
|
||||
'Lighthouse',
|
||||
'LakeOfRage',
|
||||
'IndigoPlateau',
|
||||
'Route37',
|
||||
'RocketHideout',
|
||||
'DragonsDen',
|
||||
'JohtoWildBattleNight',
|
||||
'RuinsOfAlphRadio',
|
||||
'SuccessfulCapture',
|
||||
'Route26',
|
||||
'Mom',
|
||||
'VictoryRoad',
|
||||
'PokemonLullaby',
|
||||
'PokemonMarch',
|
||||
'GoldSilverOpening',
|
||||
'GoldSilverOpening2',
|
||||
'MainMenu',
|
||||
'RuinsOfAlphInterior',
|
||||
'RocketTheme',
|
||||
'DancingHall',
|
||||
'ContestResults',
|
||||
'BugCatchingContest',
|
||||
'LakeOfRageRocketRadio',
|
||||
'Printer',
|
||||
'PostCredits',
|
||||
'Clair',
|
||||
'MobileAdapterMenu',
|
||||
'MobileAdapter',
|
||||
'BuenasPassword',
|
||||
'LookMysticalMan',
|
||||
'CrystalOpening',
|
||||
'BattleTowerTheme',
|
||||
'SuicuneBattle',
|
||||
'BattleTowerLobby',
|
||||
'MobileCenter',
|
||||
]
|
||||
Reference in New Issue
Block a user