Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP

Comparing changes

Choose two branches to see what's changed or to start a new pull request. If you need to, you can also compare across forks.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also compare across forks.
base fork: Zhorken/kaomado
base: 3d5fc74d44
...
head fork: Zhorken/kaomado
compare: 2551bdd605
Checking mergeability… Don't worry, you can still create the pull request.
  • 4 commits
  • 5 files changed
  • 0 commit comments
  • 1 contributor
View
157 kaomado.py
@@ -1,9 +1,18 @@
#!/usr/bin/env python3
"""Extract the portrait sprites from Pokémon Mystery Dungeon: Explorers of Sky.
+PMD: Blue Rescue Team is also partially supported.
+
+Requires pypng 0.0.13 or later.
This program does not actually rip sprites directly from a ROM. You'll have to
-provide the portrait file yourself. In a PMD: Sky ROM (or at least a European
-multilingual one) the file can be found at /FONT/kaomado.kao.
+provide the portrait file yourself. In a European PMD: Sky ROM, the file is
+located at /FONT/kaomado.kao; in an American PMD: Blue ROM, the file this
+script requires is /monster.sbin. I assume these files are the same for other
+regions.
+
+/monster.sbin contains at least the portraits for Pokémon with multiple
+portraits. They only take up part of the file, so the others might be in the
+same file, but if so, they don't use the same compression scheme.
"kaomado" means "face window", as far as I can tell.
@@ -18,7 +27,9 @@
from struct import unpack
from sys import argv
-from tables import expressions, pokemon_ids, Pokemon
+import tables
+
+VERSION = None
def build_filename(pokemon, sprite_num, output_dir):
"""Determine an output filename for a sprite given the Pokémon it
@@ -36,13 +47,20 @@ def build_filename(pokemon, sprite_num, output_dir):
if pokemon.female:
filename.append('female')
- # Sprite pointers come in pairs of two: facing left and (sometimes) right
- # for a given facial expression.
- expression = expressions[sprite_num // 2]
+ # XXX Expressions are only correct for protagonist/partner Pokémon
+ if VERSION == 'sky':
+ # Sprite pointers come in pairs of two: facing left and (sometimes)
+ # right for a given facial expression.
+ expression = tables.expressions.sky[sprite_num // 2]
+ right = sprite_num % 2 == 1
+ elif VERSION == 'blue':
+ expression = tables.expressions.blue[sprite_num]
+ right = False
+
if expression != 'standard':
filename.append(expression)
- if sprite_num % 2 != 0:
+ if right:
filename.append('right')
# Figure out the base filename
@@ -137,6 +155,9 @@ def parse_palette(kaomado):
Each channel gets its own byte, for some reason, as opposed to the usual
NTFP color format which actually fits all three into fifteen bits.
+
+ In PMD: Blue, each palette entry is padded to four bytes with 0x80 and I
+ don't know why.
"""
palette = []
@@ -145,6 +166,9 @@ def parse_palette(kaomado):
channel >> 3 for channel in kaomado.read(3)
))
+ if VERSION == 'blue':
+ kaomado.seek(1, os.SEEK_CUR)
+
return palette
def pixel_iterator(sprite):
@@ -154,6 +178,84 @@ def pixel_iterator(sprite):
yield pixel_pair & 0xf
yield pixel_pair >> 4
+def rip_blue(kaomado, output_dir):
+ for pokemon in range(0x1a70, 0x1ef0, 0x10):
+ # Deliberately skipping the last one because it's a dupe Rayquaza
+ kaomado.seek(pokemon)
+
+ label = kaomado.read(8).rstrip(b'\x00').decode('ASCII')
+ pokemon = int(label[3:])
+ pokemon = tables.pokemon.blue[pokemon]
+
+ pointer, length = unpack('<2L', kaomado.read(8))
+ end = pointer + length
+
+ kaomado.seek(pointer)
+ assert kaomado.read(4) == b'SIR0'
+ sprites_length, = unpack('<L', kaomado.read(4))
+ sprites_end = pointer + sprites_length
+ kaomado.seek(8, os.SEEK_CUR)
+
+ sprite_num = 0
+ while kaomado.tell() < sprites_end:
+ palette = parse_palette(kaomado)
+
+ try:
+ sprite = decompress(kaomado)
+ except ValueError:
+ print(hex(kaomado.tell()), hex(end), end - kaomado.tell())
+ break
+
+ # Each compressed sprite is padded to a multiple of four bytes
+ four_offset = kaomado.tell() % 4
+ if four_offset:
+ kaomado.seek(4 - four_offset, os.SEEK_CUR)
+
+ sprite = unscramble(sprite, palette)
+ sprite = png.from_array(sprite, mode='RGB;5')
+
+ filename = build_filename(pokemon, sprite_num, output_dir)
+ makedirs_if_need_be(filename)
+ sprite.save(filename)
+
+ sprite_num += 1
+
+ print(kaomado.read(end - sprites_end))
+
+
+def rip_sky(kaomado, output_dir):
+ for pokemon in range(1, 1155):
+ kaomado.seek(0xa0 * pokemon)
+
+ try:
+ pokemon = tables.pokemon.sky[pokemon]
+ except KeyError:
+ pokemon = tables.pokemon.Pokemon(pokemon, 'other', None, False)
+
+ # Each Pokémon has a sprite pointer for each facial expression and
+ # direction, even if they're not all used
+ pointers = unpack('<40L', kaomado.read(0xa0))
+
+ for sprite_num, pointer in enumerate(pointers):
+ if not 0x2d1e0 <= pointer <= 0x1968c0:
+ # Nonexistent sprites have consistent junk pointers, thankfully
+ continue
+
+ kaomado.seek(pointer)
+
+ # Get the palette
+ palette = parse_palette(kaomado)
+
+ # Extract the actual sprite
+ sprite = decompress(kaomado)
+ sprite = unscramble(sprite, palette)
+
+ # Save it as a PNG
+ sprite = png.from_array(sprite, mode='RGB;5')
+ filename = build_filename(pokemon, sprite_num, output_dir)
+ makedirs_if_need_be(filename)
+ sprite.save(filename)
+
def unscramble(sprite, palette):
"""Unscramble the raw sprite data into something pypng can swallow."""
@@ -181,40 +283,19 @@ def unscramble(sprite, palette):
if len(argv) != 3:
- print("Usage: kaomado.py /path/to/kaomado.kao output-dir")
+ print("Usage: kaomado.py portrait-file output-dir")
exit(1)
-kaomado = open(argv[1], 'br')
+kaomado = open(argv[1], 'rb')
output_dir = argv[2]
-for pokemon in range(1, 1155):
- kaomado.seek(0xa0 * pokemon)
-
- try:
- pokemon = pokemon_ids[pokemon]
- except KeyError:
- pokemon = Pokemon(pokemon, 'other', None, False)
-
- # Each Pokémon has a sprite pointer for each facial expression and
- # direction, even if they're not all used
- pointers = unpack('<40L', kaomado.read(0xa0))
-
- for sprite_num, pointer in enumerate(pointers):
- if not 0x2d1e0 <= pointer <= 0x1968c0:
- # Nonexistent sprites have consistent junk pointers, thankfully
- continue
-
- kaomado.seek(pointer)
-
- # Get the palette
- palette = parse_palette(kaomado)
+magic = kaomado.read(5)
- # Extract the actual sprite
- sprite = decompress(kaomado)
- sprite = unscramble(sprite, palette)
+if magic == b'\x00\x00\x00\x00\x00':
+ VERSION = 'sky'
+ rip = rip_sky
+elif magic == b'ax001':
+ VERSION = 'blue'
+ rip = rip_blue
- # Save it as a PNG
- sprite = png.from_array(sprite, mode='RGB;5')
- filename = build_filename(pokemon, sprite_num, output_dir)
- makedirs_if_need_be(filename)
- sprite.save(filename)
+rip(kaomado, output_dir)
View
2  rip.sh
@@ -10,7 +10,7 @@ fi
$(dirname $0)/kaomado.py $*
# Make duplicates for named default forms
-for form in 201-a 386-attack 412-plant 413-plant 421-overcast 422-west \
+for form in 201-a 386-normal 412-plant 413-plant 421-overcast 422-west \
423-west 487-altered 492-land
do
id=${form%%-*}
View
2  tables/__init__.py
@@ -0,0 +1,2 @@
+from tables import pokemon
+from tables import expressions
View
65 tables/expressions.py
@@ -0,0 +1,65 @@
+from collections import defaultdict
+
+### Assign all the expressions to constants to minimize opportunity for typos
+# Expressions with "official" names from the Sky debug menu
+STANDARD = 'standard'
+GRIN = 'grin'
+PAINED = 'pained'
+ANGRY = 'angry'
+WORRIED = 'worried'
+SAD = 'sad'
+CRYING = 'crying'
+SHOUTING = 'shouting'
+TEARY_EYED = 'teary-eyed'
+DETERMINED = 'determined'
+JOYOUS = 'joyous'
+INSPIRED = 'inspired'
+SURPRISED = 'surprised'
+DIZZY = 'dizzy'
+
+# Other Sky categories
+SIGH = 'sigh'
+SWEATDROP = 'sweatdrop' # XXX better name?
+MISC = 'misc' # XXX temporary; sort these properly
+
+# Other Blue categories
+HAPPY = 'happy'
+
+sky = [
+ STANDARD,
+ GRIN,
+ PAINED,
+ ANGRY,
+ WORRIED,
+ SAD,
+ CRYING,
+ SHOUTING,
+ TEARY_EYED,
+ DETERMINED,
+ JOYOUS,
+ INSPIRED,
+ SURPRISED,
+ DIZZY,
+ None,
+ None,
+ SIGH,
+ SWEATDROP,
+ MISC,
+ None
+]
+
+blue = [
+ STANDARD,
+ GRIN,
+ PAINED,
+ ANGRY,
+ WORRIED, # Some of these are different from Sky; they look more annoyed
+ SAD,
+ CRYING,
+ SHOUTING,
+ TEARY_EYED,
+ HAPPY,
+ JOYOUS,
+ INSPIRED,
+ SURPRISED,
+]
View
55 tables.py → tables/pokemon.py 100755 → 100644
@@ -1,34 +1,10 @@
from collections import namedtuple
-expressions = [
- 'standard',
- 'grin',
- 'pained',
- 'angry',
- 'worried',
- 'sad',
- 'crying',
- 'shouting',
- 'teary-eyed',
- 'determined',
- 'joyous',
- 'inspired',
- 'surprised',
- 'dizzy',
- 'sigh', # Shaymin only
- None,
- 'sigh',
- 'sweatdrop',
- 'misc',
- None
-]
-
-
# female is true iff the sprite is a SEPARATE female sprite
# e.g. True for female Rattata, false for Latias
Pokemon = namedtuple('Pokemon', 'national_id species form female')
-pokemon_ids = {
+sky = {
1: Pokemon(1, 'bulbasaur', None, False),
2: Pokemon(2, 'ivysaur', None, False),
3: Pokemon(3, 'venusaur', None, False),
@@ -628,3 +604,32 @@
1106: Pokemon(464, 'rhyperior', None, True),
1115: Pokemon(473, 'mamoswine', None, True)
}
+
+def _sky_to_blue_iterator():
+ """Yield tuples for a dict of Blue Rescue Team Pokémon IDs based on the Sky
+ Pokémon ID dict.
+
+ This information is based on a list of Pokémon names I found in Blue's
+ /system.sbin at 0x5f070. It holds for all the sprites we need, at least.
+ """
+
+ # Skip: Unown !, Unown ?, shiny Celebi, purple Kecleon
+ skip = [227, 228, 279, 384]
+ skipped = 0
+
+ # Everything up to Normal Deoxys is the same aside from the skips
+ for sky_id in range(1, 419):
+ if sky_id in skip:
+ skipped += 1
+ else:
+ yield (sky_id - skipped, sky[sky_id])
+
+ # Final oddities; I might have the Unowns and Deoxyses in the wrong order
+ yield (415, sky[227]) # Unown !
+ yield (416, sky[228]) # Unown ?
+ yield (417, sky[419]) # Attack Deoxys
+ yield (418, sky[420]) # Defense Deoxys
+ yield (419, sky[421]) # Speed Deoxys
+ yield (420, sky[488]) # Munchlax
+
+blue = dict(_sky_to_blue_iterator())

No commit comments for this range

Something went wrong with that request. Please try again.