summaryrefslogtreecommitdiff
path: root/pokemontools/map_gfx.py
diff options
context:
space:
mode:
Diffstat (limited to 'pokemontools/map_gfx.py')
-rw-r--r--pokemontools/map_gfx.py400
1 files changed, 400 insertions, 0 deletions
diff --git a/pokemontools/map_gfx.py b/pokemontools/map_gfx.py
new file mode 100644
index 0000000..77e7d56
--- /dev/null
+++ b/pokemontools/map_gfx.py
@@ -0,0 +1,400 @@
+"""
+Map-related graphic functions.
+"""
+
+import os
+import png
+from io import BytesIO
+
+from PIL import (
+ Image,
+ ImageDraw,
+)
+
+import crystal
+import gfx
+
+tile_width = 8
+tile_height = 8
+block_width = 4
+block_height = 4
+
+WALKING_SPRITE = 1
+STANDING_SPRITE = 2
+STILL_SPRITE = 3
+
+# use the same configuration
+gfx.config = crystal.conf
+config = gfx.config
+
+def add_pokecrystal_paths_to_configuration(config=config):
+ """
+ Assumes that the current working directory is the pokecrystal project path.
+ """
+ config.gfx_dir = os.path.join(os.path.abspath("."), "gfx/tilesets/")
+ config.block_dir = os.path.join(os.path.abspath("."), "tilesets/")
+ config.palmap_dir = config.block_dir
+ config.palette_dir = config.block_dir
+ config.sprites_dir = os.path.join(os.path.abspath("."), "gfx/overworld/")
+
+add_pokecrystal_paths_to_configuration(config=config)
+
+def read_map_blockdata(map_header):
+ """
+ Reads out the list of bytes representing the blockdata for the current map.
+ """
+ width = map_header.second_map_header.blockdata.width.byte
+ height = map_header.second_map_header.blockdata.height.byte
+
+ start_address = map_header.second_map_header.blockdata.address
+ end_address = start_address + (width * height)
+
+ blockdata = crystal.rom[start_address : end_address]
+
+ return [ord(x) for x in blockdata]
+
+def load_png(filepath):
+ """
+ Makes an image object from file.
+ """
+ return Image.open(filepath)
+
+all_blocks = {}
+def read_blocks(tileset_id, config=config):
+ """
+ Makes a list of blocks, such that each block is a list of tiles by id, for
+ the given tileset.
+ """
+ if tileset_id in all_blocks.keys():
+ return all_blocks[tileset_id]
+
+ blocks = []
+
+ block_width = 4
+ block_height = 4
+ block_length = block_width * block_height
+
+ filename = "{id}{ext}".format(id=str(tileset_id).zfill(2), ext="_metatiles.bin")
+ filepath = os.path.join(config.block_dir, filename)
+
+ blocksetdata = bytearray(open(filepath, "rb").read())
+
+ for blockbyte in xrange(len(blocksetdata) / block_length):
+ block_num = blockbyte * block_length
+ block = blocksetdata[block_num : block_num + block_length]
+ blocks += [block]
+
+ all_blocks[tileset_id] = blocks
+
+ return blocks
+
+def colorize_tile(tile, palette):
+ """
+ Make the tile have colors.
+ """
+ (width, height) = tile.size
+ tile = tile.convert("RGB")
+ px = tile.load()
+
+ for y in xrange(height):
+ for x in xrange(width):
+ # assume greyscale
+ which_color = 3 - (px[x, y][0] / 0x55)
+ (r, g, b) = [v * 8 for v in palette[which_color]]
+ px[x, y] = (r, g, b)
+
+ return tile
+
+pre_cropped = {}
+def read_tiles(tileset_id, palette_map, palettes, config=config):
+ """
+ Opens the tileset png file and reads bytes for each tile in the tileset.
+ """
+
+ if tileset_id not in pre_cropped.keys():
+ pre_cropped[tileset_id] = {}
+
+ tile_width = 8
+ tile_height = 8
+
+ tiles = []
+
+ filename = "{id}.{ext}".format(id=str(tileset_id).zfill(2), ext="png")
+ filepath = os.path.join(config.gfx_dir, filename)
+
+ image = load_png(filepath)
+ (image.width, image.height) = image.size
+
+ cur_tile = 0
+
+ for y in xrange(0, image.height, tile_height):
+ for x in xrange(0, image.width, tile_width):
+ if (x, y) in pre_cropped[tileset_id].keys():
+ tile = pre_cropped[tileset_id][(x, y)]
+ else:
+ tile = image.crop((x, y, x + tile_width, y + tile_height))
+ pre_cropped[tileset_id][(x, y)] = tile
+
+ # palette maps are padded to make vram mapping easier
+ pal = palette_map[cur_tile + 0x20 if cur_tile > 0x60 else cur_tile] & 0x7
+ tile = colorize_tile(tile, palettes[pal])
+
+ tiles.append(tile)
+
+ cur_tile += 1
+
+ return tiles
+
+all_palette_maps = {}
+def read_palette_map(tileset_id, config=config):
+ """
+ Loads a palette map.
+ """
+ if tileset_id in all_palette_maps.keys():
+ return all_palette_maps[tileset_id]
+
+ filename = "{id}{ext}".format(id=str(tileset_id).zfill(2), ext="_palette_map.bin")
+ filepath = os.path.join(config.palmap_dir, filename)
+
+ palette_map = []
+
+ palmap = bytearray(open(filepath, "rb").read())
+
+ for i in xrange(len(palmap)):
+ palette_map += [palmap[i] & 0xf]
+ palette_map += [(palmap[i] >> 4) & 0xf]
+
+ all_palette_maps[tileset_id] = palette_map
+
+ return palette_map
+
+def read_palettes(time_of_day=1, config=config):
+ """
+ Loads up the .pal file?
+ """
+ palettes = []
+
+ actual_time_of_day = ["morn", "day", "nite"][time_of_day]
+ 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,
+ ]
+ ]]
+
+ return palettes
+
+def load_sprite_image(address, config=config):
+ """
+ Make standard file path.
+ """
+ pal_file = os.path.join(config.block_dir, "day.pal")
+
+ length = 0x40
+
+ image = crystal.rom[address:address + length]
+ width, height, palette, greyscale, bitdepth, px_map = gfx.convert_2bpp_to_png(image, width=16, height=16, pal_file=pal_file)
+ w = png.Writer(16, 16, palette=palette, compression=9, greyscale=greyscale, bitdepth=bitdepth)
+ some_buffer = BytesIO()
+ w.write(some_buffer, px_map)
+ some_buffer.seek(0)
+
+ sprite_image = Image.open(some_buffer)
+
+ return sprite_image
+
+sprites = {}
+def load_all_sprite_images(config=config):
+ """
+ Loads all images for each sprite in each direction.
+ """
+ crystal.direct_load_rom()
+
+ sprite_headers_address = 0x14736
+ sprite_header_size = 6
+ sprite_count = 102
+ frame_size = 0x40
+
+ current_address = sprite_headers_address
+
+ current_image_id = 0
+
+ for sprite_id in xrange(1, sprite_count):
+ rom_bytes = crystal.rom[current_address : current_address + sprite_header_size]
+ header = [ord(x) for x in rom_bytes]
+
+ bank = header[3]
+
+ lo = header[0]
+ hi = header[1]
+ sprite_address = (hi * 0x100) + lo - 0x4000
+ sprite_address += 0x4000 * bank
+
+ sprite_size = header[2]
+ sprite_type = header[4]
+ sprite_palette = header[5]
+ image_count = sprite_size / frame_size
+
+ sprite = {
+ "size": sprite_size,
+ "image_count": image_count,
+ "type": sprite_type,
+ "palette": sprite_palette,
+ "images": {},
+ }
+
+ if sprite_type in [WALKING_SPRITE, STANDING_SPRITE]:
+ # down, up, left, move down, move up, move left
+ sprite["images"]["down"] = load_sprite_image(sprite_address, config=config)
+ sprite["images"]["up"] = load_sprite_image(sprite_address + 0x40, config=config)
+ sprite["images"]["left"] = load_sprite_image(sprite_address + (0x40 * 2), config=config)
+
+ if sprite_type == WALKING_SPRITE:
+ current_image_id += image_count * 2
+ elif sprite_type == STANDING_SPRITE:
+ current_image_id += image_count * 1
+ elif sprite_type == STILL_SPRITE:
+ # just one image
+ sprite["images"]["still"] = load_sprite_image(sprite_address, config=config)
+
+ current_image_id += image_count * 1
+
+ # store the actual metadata
+ sprites[sprite_id] = sprite
+
+ current_address += sprite_header_size
+
+ return sprites
+
+def draw_map_sprites(map_header, map_image, config=config):
+ """
+ Show NPCs and items on the map.
+ """
+
+ events = map_header.second_map_header.event_header.people_events
+
+ for event in events:
+ sprite_image_id = event.params[0].byte
+ y = (event.params[1].byte - 4) * 4
+ x = (event.params[2].byte - 4) * 4
+ facing = event.params[3].byte
+ movement = event.params[4].byte
+ sight_range = event.params[8].byte
+ some_pointer = event.params[9]
+ bit_table_bit_number = event.params[10]
+
+ other_args = {}
+
+ if sprite_image_id not in sprites.keys() or sprite_image_id > 0x66:
+ print "sprite_image_id {} is not in sprites".format(sprite_image_id)
+
+ sprite_image = Image.new("RGBA", (16, 16))
+
+ draw = ImageDraw.Draw(sprite_image, "RGBA")
+ draw.rectangle([(0, 0), (16, 16)], fill=(0, 0, 0, 127))
+
+ other_args["mask"] = sprite_image
+ else:
+ sprite = sprites[sprite_image_id]
+
+ # TODO: pick the correct direction based on "facing"
+ sprite_image = sprite["images"].values()[0]
+
+ # TODO: figure out how to calculate the correct position
+ map_image.paste(sprite_image, (x * 4, y * 4), **other_args)
+
+def draw_map(map_group_id, map_id, palettes, show_sprites=True, config=config):
+ """
+ Makes a picture of a map.
+ """
+ # extract data from the ROM
+ crystal.cachably_parse_rom()
+
+ map_header = crystal.map_names[map_group_id][map_id]["header_new"]
+ second_map_header = map_header.second_map_header
+
+ width = second_map_header.blockdata.width.byte
+ height = second_map_header.blockdata.height.byte
+
+ tileset_id = map_header.tileset.byte
+ blockdata = read_map_blockdata(map_header)
+
+ palette_map = read_palette_map(tileset_id, config=config)
+
+ tileset_blocks = read_blocks(tileset_id, config=config)
+ tileset_images = read_tiles(tileset_id, palette_map, palettes, config=config)
+
+ map_image = Image.new("RGB", (width * tile_width * block_width, height * tile_height * block_height))
+
+ # draw each block on the map
+ for block_num in xrange(len(blockdata)):
+ block_x = block_num % width
+ block_y = block_num / width
+
+ block = blockdata[block_y * width + block_x]
+
+ for (tile_num, tile) in enumerate(tileset_blocks[block]):
+ # tile gfx are split in half to make vram mapping easier
+ if tile >= 0x80:
+ tile -= 0x20
+
+ tile_x = block_x * 32 + (tile_num % 4) * 8
+ tile_y = block_y * 32 + (tile_num / 4) * 8
+
+ tile_image = tileset_images[tile]
+
+ map_image.paste(tile_image, (tile_x, tile_y))
+
+ # draw each sprite on the map
+ draw_map_sprites(map_header, map_image, config=config)
+
+ return map_image
+
+def save_map(map_group_id, map_id, savedir, show_sprites=True, config=config):
+ """
+ Makes a map and saves it to a file in savedir.
+ """
+ # this could be moved into a decorator
+ crystal.cachably_parse_rom()
+
+ map_name = crystal.map_names[map_group_id][map_id]["label"]
+ filename = "{name}.{ext}".format(name=map_name, ext="png")
+ filepath = os.path.join(savedir, filename)
+
+ palettes = read_palettes(config=config)
+
+ print "Drawing {}".format(map_name)
+ map_image = draw_map(map_group_id, map_id, palettes, show_sprites=show_sprites, config=config)
+ map_image.save(filepath)
+
+ return map_image
+
+def save_maps(savedir, show_sprites=True, config=config):
+ """
+ Draw as many maps as possible.
+ """
+ crystal.cachably_parse_rom()
+
+ for map_group_id in crystal.map_names.keys():
+ for map_id in crystal.map_names[map_group_id].keys():
+ if isinstance(map_id, int):
+ image = save_map(map_group_id, map_id, savedir, show_sprites=show_sprites, config=config)