Port RBY dumping code to Construct 2.8

This commit is contained in:
Eevee (Lexy Munroe) 2016-12-19 16:12:54 -08:00
parent 508b5cd88f
commit b8a5f634d5

View file

@ -19,7 +19,14 @@ import sys
from camel import Camel from camel import Camel
from classtools import reify from classtools import reify
from construct import * from construct import (
# Simple fields
BitsInteger, Byte, Bytes, CString, Const, Int8ul, Int16ub, Int16ul,
Padding, String,
# Structures and meta stuff
Adapter, Array, Bitwise, Construct, Embedded, Enum, Peek, Pointer, Struct,
Subconstruct, Switch,
)
from pokedex.extract.lib.gbz80 import find_code from pokedex.extract.lib.gbz80 import find_code
import pokedex.schema as schema import pokedex.schema as schema
@ -742,13 +749,13 @@ class PokemonString:
class PokemonCString(Adapter): class PokemonCString(Adapter):
"""Construct thing for `PokemonString`.""" """Construct thing for `PokemonString`."""
def __init__(self, name, length=None): def __init__(self, length=None):
# No matter which charmap, the "end of string" character is always # No matter which charmap, the "end of string" character is always
# encoded as P # encoded as P
if length is None: if length is None:
subcon = CString(name, terminators=b'P') subcon = CString(terminators=b'P')
else: else:
subcon = String(name, length, padchar=b'P') subcon = String(length, padchar=b'P')
super().__init__(subcon) super().__init__(subcon)
def _encode(self, obj, context): def _encode(self, obj, context):
@ -762,7 +769,7 @@ class MacroPokemonCString(Construct):
"""Similar to the above, but for strings that may contain the 0x17 "far """Similar to the above, but for strings that may contain the 0x17 "far
load" macro, whose parameters may in turn contain the NUL byte 0x50 without load" macro, whose parameters may in turn contain the NUL byte 0x50 without
marking the end of the string. Yikes.""" marking the end of the string. Yikes."""
def _parse(self, stream, context): def _parse(self, stream, context, path):
buf = bytearray() buf = bytearray()
while True: while True:
byte, = stream.read(1) byte, = stream.read(1)
@ -773,7 +780,7 @@ class MacroPokemonCString(Construct):
pos = stream.tell() pos = stream.tell()
try: try:
stream.seek(offset) stream.seek(offset)
buf.extend(self._parse(stream, context).raw) buf.extend(self._parse(stream, context, path).raw)
finally: finally:
stream.seek(pos) stream.seek(pos)
elif byte == 0x50: elif byte == 0x50:
@ -789,28 +796,19 @@ class MacroPokemonCString(Construct):
class NullTerminatedArray(Subconstruct): class NullTerminatedArray(Subconstruct):
_peeker = Peek(ULInt8('___')) _peeker = Peek(Int8ul)
__slots__ = () __slots__ = ()
def __init__(self, subcon): def _parse(self, stream, context, path):
super().__init__(subcon)
self._clear_flag(self.FLAG_COPY_CONTEXT)
self._set_flag(self.FLAG_DYNAMIC)
def _parse(self, stream, context):
from construct.lib import ListContainer from construct.lib import ListContainer
obj = ListContainer() obj = ListContainer()
orig_context = context
while True: while True:
nextbyte = self._peeker.parse_stream(stream) nextbyte = self._peeker.parse_stream(stream)
if nextbyte == 0: if nextbyte == 0:
break break
if self.subcon.conflags & self.FLAG_COPY_CONTEXT:
context = orig_context.__copy__()
# TODO what if we hit the end of the stream # TODO what if we hit the end of the stream
obj.append(self.subcon._parse(stream, context)) obj.append(self.subcon._parse(stream, context, path))
# Consume the trailing zero # Consume the trailing zero
stream.read(1) stream.read(1)
@ -832,108 +830,95 @@ def IdentEnum(subcon, mapping):
# http://gbdev.gg8.se/wiki/articles/The_Cartridge_Header # http://gbdev.gg8.se/wiki/articles/The_Cartridge_Header
# TODO hey! i wish i had a little cli entry point that would spit this out for a game. and do other stuff like scan for likely pokemon text or graphics. that would be really cool in fact. maybe put this in a gb module and make that exist sometime. # TODO hey! i wish i had a little cli entry point that would spit this out for a game. and do other stuff like scan for likely pokemon text or graphics. that would be really cool in fact. maybe put this in a gb module and make that exist sometime.
game_boy_header_struct = Struct( game_boy_header_struct = Struct(
'game_boy_header',
# Entry point for the game; generally contains a jump to 0x0150 # Entry point for the game; generally contains a jump to 0x0150
String('entry_point', 4), 'entry_point' / String(4),
# Nintendo logo; must be exactly this or booting will not continue # Nintendo logo; must be exactly this or booting will not continue
Const( 'nintendo_logo' / Const(
String('nintendo_logo', 48),
bytes.fromhex(""" bytes.fromhex("""
CE ED 66 66 CC 0D 00 0B 03 73 00 83 00 0C 00 0D CE ED 66 66 CC 0D 00 0B 03 73 00 83 00 0C 00 0D
00 08 11 1F 88 89 00 0E DC CC 6E E6 DD DD D9 99 00 08 11 1F 88 89 00 0E DC CC 6E E6 DD DD D9 99
BB BB 67 63 6E 0E EC CC DD DC 99 9F BB B9 33 3E BB BB 67 63 6E 0E EC CC DD DC 99 9F BB B9 33 3E
""".replace('\n', '')), """.replace('\n', '')),
), ),
String('title', 11, padchar=b'\x00'), 'title' / Bytes(11),
String('manufacturer_code', 4), 'manufacturer_code' / Bytes(4),
ULInt8('cgb_flag'), 'cgb_flag' / Int8ul,
String('new_licensee_code', 2), 'new_licensee_code' / Bytes(2),
ULInt8('sgb_flag'), # 3 for super game boy support 'sgb_flag' / Int8ul, # 3 for super game boy support
ULInt8('cartridge_type'), 'cartridge_type' / Int8ul,
ULInt8('rom_size'), 'rom_size' / Int8ul,
ULInt8('ram_size'), 'ram_size' / Int8ul,
ULInt8('region_code'), # 0 for japan, 1 for not japan 'region_code' / Int8ul, # 0 for japan, 1 for not japan
ULInt8('old_licensee_code'), # 0x33 means to use licensee_code 'old_licensee_code' / Int8ul, # 0x33 means to use licensee_code
ULInt8('game_version'), 'game_version' / Int8ul,
ULInt8('header_checksum'), 'header_checksum' / Int8ul,
UBInt16('cart_checksum'), 'cart_checksum' / Int16ub,
) )
# The mother lode — Pokémon base stats # The mother lode — Pokémon base stats
pokemon_struct = Struct( pokemon_struct = Struct(
'pokemon', 'pokedex_number' / Byte,
ULInt8('pokedex_number'), 'base_hp' / Byte,
ULInt8('base_hp'), 'base_attack' / Byte,
ULInt8('base_attack'), 'base_defense' / Byte,
ULInt8('base_defense'), 'base_speed' / Byte,
ULInt8('base_speed'), 'base_special' / Byte,
ULInt8('base_special'), 'type1' / IdentEnum(Byte, TYPE_IDENTIFIERS),
IdentEnum(ULInt8('type1'), TYPE_IDENTIFIERS), 'type2' / IdentEnum(Byte, TYPE_IDENTIFIERS),
IdentEnum(ULInt8('type2'), TYPE_IDENTIFIERS), 'catch_rate' / Byte,
ULInt8('catch_rate'), 'base_experience' / Byte,
ULInt8('base_experience'),
# TODO ???? "sprite dimensions" # TODO ???? "sprite dimensions"
ULInt8('_sprite_dimensions'), '_sprite_dimensions' / Byte,
ULInt16('front_sprite_pointer'), 'front_sprite_pointer' / Int16ul,
ULInt16('back_sprite_pointer'), 'back_sprite_pointer' / Int16ul,
# TODO somehow rig this to discard trailing zeroes; there's a paddedstring that does it # TODO somehow rig this to discard trailing zeroes; there's a paddedstring that does it
Array(4, IdentEnum(ULInt8('initial_moveset'), MOVE_IDENTIFIERS)), 'initial_moveset' / Array(4, IdentEnum(Byte, MOVE_IDENTIFIERS)),
IdentEnum(ULInt8('growth_rate'), GROWTH_RATES), 'growth_rate' / IdentEnum(Byte, GROWTH_RATES),
# TODO argh, this is a single huge integer; i want an array, but then i lose the byteswapping! # TODO argh, this is a single huge integer; i want an array, but then i lose the byteswapping!
Bitwise( 'machines' / Bitwise(BitsInteger(7 * 8, swapped=True)),
BitField('machines', 7 * 8, swapped=True),
),
Padding(1), Padding(1),
) )
evos_moves_struct = Struct( evos_moves_struct = Struct(
'evos_moves', 'evolutions' / NullTerminatedArray(
NullTerminatedArray(
Struct( Struct(
'evolutions', 'evo_trigger' / IdentEnum(Byte, EVOLUTION_TRIGGERS),
IdentEnum(ULInt8('evo_trigger'), EVOLUTION_TRIGGERS), 'evo_arguments' / Embedded(Switch(
Embedded(Switch(
'evo_arguments',
lambda ctx: ctx.evo_trigger, { lambda ctx: ctx.evo_trigger, {
'evolution-trigger.level-up': Struct( 'evolution-trigger.level-up': Struct(
'---', 'evo_level' / Byte,
ULInt8('evo_level'),
), ),
'evolution-trigger.use-item': Struct( 'evolution-trigger.use-item': Struct(
'---',
# TODO item enum too wow! # TODO item enum too wow!
ULInt8('evo_item'), 'evo_item' / Byte,
# TODO ??? always seems to be 1 # TODO ??? always seems to be 1
ULInt8('evo_level'), 'evo_level' / Byte,
), ),
# TODO ??? always seems to be 1 here too # TODO ??? always seems to be 1 here too
'evolution-trigger.trade': Struct( 'evolution-trigger.trade': Struct(
'---', 'evo_level' / Byte,
ULInt8('evo_level'),
), ),
}, },
)), )),
# TODO alas, the species here is a number, because it's an internal # TODO alas, the species here is a number, because it's an internal
# id and we switch those back using data from the game... # id and we switch those back using data from the game...
ULInt8('evo_species'), 'evo_species' / Byte,
), ),
), ),
NullTerminatedArray( 'level_up_moves' / NullTerminatedArray(
Struct( Struct(
'level_up_moves', 'level' / Byte,
ULInt8('level'), 'move' / IdentEnum(Byte, MOVE_IDENTIFIERS),
IdentEnum(ULInt8('move'), MOVE_IDENTIFIERS), '_end' / Peek(Byte), # TODO what, what is this
Peek(ULInt8('_end')),
), ),
), ),
) )
evos_moves_pointer = Struct( evos_moves_pointer = Struct(
'xxx', 'offset' / Int16ul,
ULInt16('offset'),
# TODO hardcoded as the same bank, ugh # TODO hardcoded as the same bank, ugh
Pointer(lambda ctx: ctx.offset + (0xE - 1) * 0x4000, evos_moves_struct), 'evos_moves' / Pointer(lambda ctx: ctx.offset + (0xE - 1) * 0x4000, evos_moves_struct),
) )
# There are actually two versions of this struct... an imperial one, used by # There are actually two versions of this struct... an imperial one, used by
@ -941,19 +926,17 @@ evos_moves_pointer = Struct(
# one has separate bytes for feet and inches, so it's a different size, making # one has separate bytes for feet and inches, so it's a different size, making
# it completely incompatible. # it completely incompatible.
pokedex_flavor_struct_metric = Struct( pokedex_flavor_struct_metric = Struct(
'pokedex_flavor_metric', 'genus' / PokemonCString(),
PokemonCString('genus'), 'height_decimeters' / Int8ul,
ULInt8('height_decimeters'), 'weight_hectograms' / Int16ul,
ULInt16('weight_hectograms'), 'flavor_text' / MacroPokemonCString(),
MacroPokemonCString('flavor_text'),
) )
pokedex_flavor_struct_imperial = Struct( pokedex_flavor_struct_imperial = Struct(
'pokedex_flavor_imperial', 'genus' / PokemonCString(),
PokemonCString('genus'), 'height_feet' / Int8ul,
ULInt8('height_feet'), 'height_inches' / Int8ul,
ULInt8('height_inches'), 'weight_decipounds' / Int16ul,
ULInt16('weight_decipounds'), 'flavor_text' / MacroPokemonCString(),
MacroPokemonCString('flavor_text'),
) )
@ -1107,7 +1090,7 @@ class RBYCart:
raise CartDetectionError("Can't find name array") raise CartDetectionError("Can't find name array")
rem, inputs = match rem, inputs = match
start = inputs['NamePointers'] start = inputs['NamePointers']
name_pointers = Array(7, ULInt16('dummy')).parse( name_pointers = Array(7, Int16ul).parse(
self.data[start:start + 14]) self.data[start:start + 14])
# One downside to the Game Boy memory structure is that banks are # One downside to the Game Boy memory structure is that banks are
# not stored anywhere near their corresponding addresses. Most # not stored anywhere near their corresponding addresses. Most
@ -1437,7 +1420,7 @@ class RBYCart:
self.stream.seek(self.addrs['ItemNames']) self.stream.seek(self.addrs['ItemNames'])
# Item 0 is MASTER BALL. The first item with a different name in every # Item 0 is MASTER BALL. The first item with a different name in every
# single language is item 4, TOWN MAP, so chew through five names. # single language is item 4, TOWN MAP, so chew through five names.
single_string_struct = PokemonCString('dummy') single_string_struct = PokemonCString()
for _ in range(5): for _ in range(5):
name = single_string_struct.parse_stream(self.stream) name = single_string_struct.parse_stream(self.stream)
@ -1515,7 +1498,7 @@ class RBYCart:
name_length = 5 name_length = 5
else: else:
name_length = 10 name_length = 10
for index, pokemon_name in enumerate(Array(self.max_pokemon_index, PokemonCString('...', name_length)).parse_stream(self.stream), start=1): for index, pokemon_name in enumerate(Array(self.max_pokemon_index, PokemonCString(name_length)).parse_stream(self.stream), start=1):
try: try:
id = self.pokedex_order[index] id = self.pokedex_order[index]
except KeyError: except KeyError:
@ -1528,7 +1511,7 @@ class RBYCart:
def machine_moves(self): def machine_moves(self):
"""List of move identifiers corresponding to TMs/HMs.""" """List of move identifiers corresponding to TMs/HMs."""
self.stream.seek(self.addrs['TechnicalMachines']) self.stream.seek(self.addrs['TechnicalMachines'])
return Array(self.NUM_MACHINES, IdentEnum(ULInt8('move'), MOVE_IDENTIFIERS)).parse_stream(self.stream) return Array(self.NUM_MACHINES, IdentEnum(Byte, MOVE_IDENTIFIERS)).parse_stream(self.stream)
@reify @reify
def pokemon_records(self): def pokemon_records(self):
@ -1572,7 +1555,7 @@ class RBYCart:
self.stream.seek(self.addrs['PokedexEntryPointers']) self.stream.seek(self.addrs['PokedexEntryPointers'])
# This address is just an array of pointers # This address is just an array of pointers
for index, address in enumerate(Array(self.max_pokemon_index, ULInt16('pointer')).parse_stream(self.stream), start=1): for index, address in enumerate(Array(self.max_pokemon_index, Int16ul).parse_stream(self.stream), start=1):
try: try:
id = self.pokedex_order[index] id = self.pokedex_order[index]
except KeyError: except KeyError:
@ -1586,7 +1569,7 @@ class RBYCart:
@reify @reify
def move_names(self): def move_names(self):
self.stream.seek(self.addrs['MoveNames']) self.stream.seek(self.addrs['MoveNames'])
return Array(NUM_MOVES, PokemonCString('move_name')).parse_stream(self.stream) return Array(self.NUM_MOVES, PokemonCString()).parse_stream(self.stream)
# TODO would be slick to convert this to a construct... construct # TODO would be slick to convert this to a construct... construct