Merge pull request #61 from kanzure/path-finding

Some basic path finding stuff
This commit is contained in:
Bryan Bishop 2013-12-18 18:22:42 -08:00
commit f0aaf3cd56
6 changed files with 1309 additions and 17 deletions

View File

@ -70,6 +70,11 @@ OldTextScript = old_text_script
import configuration
conf = configuration.Config()
data_path = os.path.join(os.path.abspath(os.path.dirname(__file__)), "data/pokecrystal/")
conf.wram = os.path.join(data_path, "wram.asm")
conf.gbhw = os.path.join(data_path, "gbhw.asm")
conf.hram = os.path.join(data_path, "hram.asm")
from map_names import map_names
# ---- script_parse_table explanation ----
@ -174,7 +179,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)
data = helpers.grouper(data)
for pointer_parts in data:
pointer = pointer_parts[0] + (pointer_parts[1] << 8)
@ -249,7 +254,10 @@ class TextScript:
see: http://hax.iimarck.us/files/scriptingcodes_eng.htm#InText
"""
base_label = "UnknownText_"
def __init__(self, address, map_group=None, map_id=None, debug=False, label=None, force=False, show=None):
def __init__(self, address, map_group=None, map_id=None, debug=False, label=None, force=False, show=None, script_parse_table=None, text_command_classes=None):
self.text_command_classes = text_command_classes
self.script_parse_table = script_parse_table
self.address = address
# $91, $84, $82, $54, $8c
# 0x19768c is a a weird problem?
@ -425,7 +433,7 @@ def parse_text_engine_script_at(address, map_group=None, map_id=None, debug=True
"""
if is_script_already_parsed_at(address) and not force:
return script_parse_table[address]
return TextScript(address, map_group=map_group, map_id=map_id, debug=debug, show=show, force=force)
return TextScript(address, map_group=map_group, map_id=map_id, debug=debug, show=show, force=force, script_parse_table=script_parse_table, text_command_classes=text_command_classes)
def find_text_addresses():
"""returns a list of text pointers
@ -560,7 +568,7 @@ def parse_text_at3(address, map_group=None, map_id=None, debug=False):
if deh:
return deh
else:
text = TextScript(address, map_group=map_group, map_id=map_id, debug=debug)
text = TextScript(address, map_group=map_group, map_id=map_id, debug=debug, script_parse_table=script_parse_table, text_command_classes=text_command_classes)
if text.is_valid():
return text
else:
@ -775,7 +783,7 @@ HexByte=DollarSignByte
class ItemLabelByte(DollarSignByte):
def to_asm(self):
label = item_constants.item_constants.find_item_label_by_id(self.byte)
label = item_constants.find_item_label_by_id(self.byte)
if label:
return label
elif not label:
@ -2925,7 +2933,7 @@ class Script:
if start_address in stop_points and force == False:
if debug:
logging.debug(
"script parsing is stopping at stop_point={address} at map_group={map_group} map_id={map_id}"
"script parsing is stopping at stop_point={stop_point} at map_group={map_group} map_id={map_id}"
.format(
stop_point=hex(start_address),
map_group=str(map_group),
@ -6596,7 +6604,7 @@ def list_texts_in_bank(bank):
Narrows down the list of objects that you will be inserting into Asm.
"""
if len(all_texts) == 0:
raise Exception("all_texts is blank.. main() will populate it")
raise Exception("all_texts is blank.. parse_rom() will populate it")
assert bank != None, "list_texts_in_banks must be given a particular bank"
@ -6614,7 +6622,7 @@ def list_movements_in_bank(bank, all_movements):
Narrows down the list of objects to speed up Asm insertion.
"""
if len(all_movements) == 0:
raise Exception("all_movements is blank.. main() will populate it")
raise Exception("all_movements is blank.. parse_rom() will populate it")
assert bank != None, "list_movements_in_bank must be given a particular bank"
assert 0 <= bank < 0x80, "bank doesn't exist in the ROM (out of bounds)"
@ -6633,7 +6641,7 @@ def dump_asm_for_texts_in_bank(bank, start=50, end=100, rom=None):
# load and parse the ROM if necessary
if rom == None or len(rom) <= 4:
rom = load_rom()
main()
parse_rom()
# get all texts
# first 100 look okay?
@ -6653,7 +6661,7 @@ def dump_asm_for_texts_in_bank(bank, start=50, end=100, rom=None):
def dump_asm_for_movements_in_bank(bank, start=0, end=100, all_movements=None):
if rom == None or len(rom) <= 4:
rom = load_rom()
main()
parse_rom()
movements = list_movements_in_bank(bank, all_movements)[start:end]
@ -6669,7 +6677,7 @@ def dump_things_in_bank(bank, start=50, end=100):
# load and parse the ROM if necessary
if rom == None or len(rom) <= 4:
rom = load_rom()
main()
parse_rom()
things = list_things_in_bank(bank)[start:end]
@ -6706,6 +6714,14 @@ def write_all_labels(all_labels, filename="labels.json"):
fh.close()
return True
def setup_wram_labels(config=conf):
"""
Get all wram labels and store it on the module.
"""
wramproc = wram.WRAMProcessor(config=config)
wramproc.initialize()
wram.wram_labels = wramproc.wram_labels
def get_ram_label(address):
"""
returns a label assigned to a particular ram address
@ -6953,16 +6969,28 @@ Command.trainer_group_maximums = trainer_group_maximums
SingleByteParam.map_internal_ids = map_internal_ids
MultiByteParam.map_internal_ids = map_internal_ids
def main(rom=None):
def add_map_offsets_into_map_names(map_group_offsets, map_names=None):
"""
Add the offsets for each map into the map_names variable.
"""
# add the offsets into our map structure, why not (johto maps only)
return [map_names[map_group_id+1].update({"offset": offset}) for map_group_id, offset in enumerate(map_group_offsets)]
rom_parsed = False
def parse_rom(rom=None):
if not rom:
# read the rom and figure out the offsets for maps
rom = direct_load_rom()
# make wram.wram_labels available
setup_wram_labels()
# figure out the map offsets
map_group_offsets = load_map_group_offsets(map_group_pointer_table=map_group_pointer_table, map_group_count=map_group_count, rom=rom)
# add the offsets into our map structure, why not (johto maps only)
[map_names[map_group_id+1].update({"offset": offset}) for map_group_id, offset in enumerate(map_group_offsets)]
# populate the map_names structure with the offsets
add_map_offsets_into_map_names(map_group_offsets, map_names=map_names)
# parse map header bytes for each map
parse_all_map_headers(map_names, all_map_headers=all_map_headers)
@ -6978,5 +7006,20 @@ def main(rom=None):
# improve duplicate trainer names
make_trainer_group_name_trainer_ids(trainer_group_table)
global rom_parsed
rom_parsed = True
return map_names
def cachably_parse_rom(rom=None):
"""
Calls parse_rom if it hasn't been called and completed yet.
"""
global rom_parsed
if not rom_parsed:
return parse_rom(rom=rom)
else:
return map_names
if __name__ == "crystal":
pass

View File

@ -0,0 +1,102 @@
; Graciously aped from http://nocash.emubase.de/pandocs.htm .
; MBC3
MBC3SRamEnable EQU $0000
MBC3RomBank EQU $2000
MBC3SRamBank EQU $4000
MBC3LatchClock EQU $6000
MBC3RTC EQU $a000
SRAM_DISABLE EQU $00
SRAM_ENABLE EQU $0a
NUM_SRAM_BANKS EQU 4
RTC_S EQU $08 ; Seconds 0-59 (0-3Bh)
RTC_M EQU $09 ; Minutes 0-59 (0-3Bh)
RTC_H EQU $0a ; Hours 0-23 (0-17h)
RTC_DL EQU $0b ; Lower 8 bits of Day Counter (0-FFh)
RTC_DH EQU $0c ; Upper 1 bit of Day Counter, Carry Bit, Halt Flag
; Bit 0 Most significant bit of Day Counter (Bit 8)
; Bit 6 Halt (0=Active, 1=Stop Timer)
; Bit 7 Day Counter Carry Bit (1=Counter Overflow)
; interrupt flags
VBLANK EQU 0
LCD_STAT EQU 1
TIMER EQU 2
SERIAL EQU 3
JOYPAD EQU 4
; Hardware registers
rJOYP EQU $ff00 ; Joypad (R/W)
rSB EQU $ff01 ; Serial transfer data (R/W)
rSC EQU $ff02 ; Serial Transfer Control (R/W)
rSC_ON EQU 7
rSC_CGB EQU 1
rSC_CLOCK EQU 0
rDIV EQU $ff04 ; Divider Register (R/W)
rTIMA EQU $ff05 ; Timer counter (R/W)
rTMA EQU $ff06 ; Timer Modulo (R/W)
rTAC EQU $ff07 ; Timer Control (R/W)
rTAC_ON EQU 2
rTAC_4096_HZ EQU 0
rTAC_262144_HZ EQU 1
rTAC_65536_HZ EQU 2
rTAC_16384_HZ EQU 3
rIF EQU $ff0f ; Interrupt Flag (R/W)
rNR10 EQU $ff10 ; Channel 1 Sweep register (R/W)
rNR11 EQU $ff11 ; Channel 1 Sound length/Wave pattern duty (R/W)
rNR12 EQU $ff12 ; Channel 1 Volume Envelope (R/W)
rNR13 EQU $ff13 ; Channel 1 Frequency lo (Write Only)
rNR14 EQU $ff14 ; Channel 1 Frequency hi (R/W)
rNR21 EQU $ff16 ; Channel 2 Sound Length/Wave Pattern Duty (R/W)
rNR22 EQU $ff17 ; Channel 2 Volume Envelope (R/W)
rNR23 EQU $ff18 ; Channel 2 Frequency lo data (W)
rNR24 EQU $ff19 ; Channel 2 Frequency hi data (R/W)
rNR30 EQU $ff1a ; Channel 3 Sound on/off (R/W)
rNR31 EQU $ff1b ; Channel 3 Sound Length
rNR32 EQU $ff1c ; Channel 3 Select output level (R/W)
rNR33 EQU $ff1d ; Channel 3 Frequency's lower data (W)
rNR34 EQU $ff1e ; Channel 3 Frequency's higher data (R/W)
rNR41 EQU $ff20 ; Channel 4 Sound Length (R/W)
rNR42 EQU $ff21 ; Channel 4 Volume Envelope (R/W)
rNR43 EQU $ff22 ; Channel 4 Polynomial Counter (R/W)
rNR44 EQU $ff23 ; Channel 4 Counter/consecutive; Inital (R/W)
rNR50 EQU $ff24 ; Channel control / ON-OFF / Volume (R/W)
rNR51 EQU $ff25 ; Selection of Sound output terminal (R/W)
rNR52 EQU $ff26 ; Sound on/off
rLCDC EQU $ff40 ; LCD Control (R/W)
rSTAT EQU $ff41 ; LCDC Status (R/W)
rSCY EQU $ff42 ; Scroll Y (R/W)
rSCX EQU $ff43 ; Scroll X (R/W)
rLY EQU $ff44 ; LCDC Y-Coordinate (R)
rLYC EQU $ff45 ; LY Compare (R/W)
rDMA EQU $ff46 ; DMA Transfer and Start Address (W)
rBGP EQU $ff47 ; BG Palette Data (R/W) - Non CGB Mode Only
rOBP0 EQU $ff48 ; Object Palette 0 Data (R/W) - Non CGB Mode Only
rOBP1 EQU $ff49 ; Object Palette 1 Data (R/W) - Non CGB Mode Only
rWY EQU $ff4a ; Window Y Position (R/W)
rWX EQU $ff4b ; Window X Position minus 7 (R/W)
rKEY1 EQU $ff4d ; CGB Mode Only - Prepare Speed Switch
rVBK EQU $ff4f ; CGB Mode Only - VRAM Bank
rHDMA1 EQU $ff51 ; CGB Mode Only - New DMA Source, High
rHDMA2 EQU $ff52 ; CGB Mode Only - New DMA Source, Low
rHDMA3 EQU $ff53 ; CGB Mode Only - New DMA Destination, High
rHDMA4 EQU $ff54 ; CGB Mode Only - New DMA Destination, Low
rHDMA5 EQU $ff55 ; CGB Mode Only - New DMA Length/Mode/Start
rRP EQU $ff56 ; CGB Mode Only - Infrared Communications Port
rBGPI EQU $ff68 ; CGB Mode Only - Background Palette Index
rBGPD EQU $ff69 ; CGB Mode Only - Background Palette Data
rOBPI EQU $ff6a ; CGB Mode Only - Sprite Palette Index
rOBPD EQU $ff6b ; CGB Mode Only - Sprite Palette Data
rUNKNOWN1 EQU $ff6c ; (FEh) Bit 0 (Read/Write) - CGB Mode Only
rSVBK EQU $ff70 ; CGB Mode Only - WRAM Bank
rUNKNOWN2 EQU $ff72 ; (00h) - Bit 0-7 (Read/Write)
rUNKNOWN3 EQU $ff73 ; (00h) - Bit 0-7 (Read/Write)
rUNKNOWN4 EQU $ff74 ; (00h) - Bit 0-7 (Read/Write) - CGB Mode Only
rUNKNOWN5 EQU $ff75 ; (8Fh) - Bit 4-6 (Read/Write)
rUNKNOWN6 EQU $ff76 ; (00h) - Always 00h (Read Only)
rUNKNOWN7 EQU $ff77 ; (00h) - Always 00h (Read Only)
rIE EQU $ffff ; Interrupt Enable (R/W)

View File

@ -0,0 +1,71 @@
hPushOAM EQU $ff80
hBuffer EQU $ff8b
hRTCDayHi EQU $ff8d
hRTCDayLo EQU $ff8e
hRTCHours EQU $ff8f
hRTCMinutes EQU $ff90
hRTCSeconds EQU $ff91
hHours EQU $ff94
hMinutes EQU $ff96
hSeconds EQU $ff98
hROMBank EQU $ff9d
hJoypadReleased EQU $ffa2
hJoypadPressed EQU $ffa3
hJoypadDown EQU $ffa4
hJoypadSum EQU $ffa5
hJoyReleased EQU $ffa6
hJoyPressed EQU $ffa7
hJoyDown EQU $ffa8
hConnectionStripLength EQU $ffaf
hConnectedMapWidth EQU $ffb0
hPastLeadingZeroes EQU $ffb3
hDividend EQU $ffb3
hDivisor EQU $ffb7
hQuotient EQU $ffb4
hMultiplicand EQU $ffb4
hMultiplier EQU $ffb7
hProduct EQU $ffb3
hMathBuffer EQU $ffb8
hLCDStatCustom EQU $ffc6
hSerialSend EQU $ffcd
hSerialReceive EQU $ffce
hSCX EQU $ffcf
hSCY EQU $ffd0
hWX EQU $ffd1
hWY EQU $ffd2
hBGMapMode EQU $ffd4
hBGMapThird EQU $ffd5
hBGMapAddress EQU $ffd6
hOAMUpdate EQU $ffd8
hSPBuffer EQU $ffd9
hBGMapUpdate EQU $ffdb
hTileAnimFrame EQU $ffdf
hRandomAdd EQU $ffe1
hRandomSub EQU $ffe2
hBattleTurn EQU $ffe4
hCGBPalUpdate EQU $ffe5
hCGB EQU $ffe6
hSGB EQU $ffe7
hDMATransfer EQU $ffe8

400
pokemontools/map_gfx.py Normal file
View File

@ -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)

664
pokemontools/vba/path.py Normal file
View File

@ -0,0 +1,664 @@
"""
path finding implementation
1) For each position on the map, create a node representing the position.
2) For each NPC/item, mark nearby nodes as members of that NPC's threat zone
(note that they can be members of multiple zones simultaneously).
"""
import pokemontools.configuration
config = pokemontools.configuration.Config()
import pokemontools.crystal
import pokemontools.map_gfx
from PIL import (
Image,
ImageDraw,
)
PENALTIES = {
# The minimum cost for a step must be greater than zero or else the path
# finding implementation might take the player through elaborate routes
# through nowhere.
"NONE": 1,
# for any area that might be near a trainer or moving object
"THREAT_ZONE": 50,
# for any nodes that might be under active observation (sight) by a trainer
"SIGHT_RANGE": 80,
# active sight range is where the trainer will definitely see the player
"ACTIVE_SIGHT_RANGE": 100,
# This is impossible, but the pathfinder might have a bug, and it would be
# nice to know about such a bug very soon.
"COLLISION": -999999,
}
DIRECTIONS = {
"UP": "UP",
"DOWN": "DOWN",
"LEFT": "LEFT",
"RIGHT": "RIGHT",
}
class Node(object):
"""
A ``Node`` represents a position on the map.
"""
def __init__(self, position, threat_zones=None, contents=None):
self.position = position
self.y = position[0]
self.x = position[1]
# by default a node is not a member of any threat zones
self.threat_zones = threat_zones or set()
# by default a node does not have any objects at this location
self.contents = contents or set()
self.cost = self.calculate_cost()
def calculate_cost(self, PENALTIES=PENALTIES):
"""
Calculates a cost associated with passing through this node.
"""
penalty = PENALTIES["NONE"]
# 1) assign a penalty based on whether or not this object is passable,
# if it's a collision then return a priority immediately
if self.is_collision_by_map_data() or self.is_collision_by_map_obstacle():
penalty += PENALTIES["COLLISION"]
return penalty
# 2) assign a penalty based on whether or not this object is grass/water
# 3) assign a penalty based on whether or not there is a map_obstacle here,
# check each of the contents to see if there are any objects that exist
# at this location, if anything exists here then return a priority immediately
# 4) consider any additional penalties due to the presence of a threat
# zone. Only calculate detailed penalties about the threat zone if the
# player is within range.
for threat_zone in self.threat_zones:
# the player might be inside the threat zone or the player might be
# just on the boundary
player_y = get_player_y()
player_x = get_player_x()
if threat_zone.is_player_near(player_y, player_x):
consider_sight_range = True
else:
consider_sight_range = False
penalty += threat_zone.calculate_node_cost(self.y, self.x, consider_sight_range=consider_sight_range, PENALTIES=PENALTIES)
return penalty
def is_collision_by_map_data(self):
"""
Checks if the player can walk on this location.
"""
raise NotImplementedError
def is_collision_by_map_obstacle(self):
"""
Checks if there is a map_obstacle on the current position that prevents
the player walking here.
"""
for content in self.contents:
if self.content.y == self.y and self.content.x == self.x:
return True
else:
return False
class MapObstacle(object):
"""
A ``MapObstacle`` represents an item, npc or trainer on the map.
"""
def __init__(self, some_map, identifier, sight_range=None, movement=None, turn=None, simulation=False, facing_direction=DIRECTIONS["DOWN"]):
"""
:param some_map: a reference to the map that this object belongs to
:param identifier: which object on the map does this correspond to?
:param simulation: set to False to not read from RAM
"""
self.simulation = simulation
self.some_map = some_map
self.identifier = identifier
self._sight_range = sight_range
if self._sight_range is None:
self._sight_range = self._get_sight_range()
self._movement = movement
if self._movement is None:
self._movement = self._get_movement()
self._turn = turn
if self._turn is None:
self._turn = self._get_turn()
self.facing_direction = facing_direction
if not self.facing_direction:
self.facing_direction = self.get_current_facing_direction()
self.update_location()
def update_location(self):
"""
Determines the (y, x) location of the given map_obstacle object, which
can be a reference to an item, npc or trainer npc.
"""
if self.simulation:
return (self.y, self.x)
else:
raise NotImplementedError
self.y = new_y
self.x = new_x
return (new_y, new_x)
def _get_current_facing_direction(self, DIRECTIONS=DIRECTIONS):
"""
Get the current facing direction of the map_obstacle.
"""
raise NotImplementedError
def get_current_facing_direction(self, DIRECTIONS=DIRECTIONS):
"""
Get the current facing direction of the map_obstacle.
"""
if not self.simulation:
self.facing_direction = self._get_current_facing_direction(DIRECTIONS=DIRECTIONS)
return self.facing_direction
def _get_movement(self):
"""
Figures out the "movement" variable. Also, this converts from the
internal game's format into True or False for whether or not the object
is capable of moving.
"""
raise NotImplementedError
@property
def movement(self):
if self._movement is None:
self._movement = self._get_movement()
return self._movement
def can_move(self):
"""
Checks if this map_obstacle is capable of movement.
"""
return self.movement
def _get_turn(self):
"""
Checks whether or not the map_obstacle can turn. This only matters for
trainers.
"""
raise NotImplementedError
@property
def turn(self):
if self._turn is None:
self._turn = self._get_turn()
return self._turn
def can_turn_without_moving(self):
"""
Checks whether or not the map_obstacle can turn. This only matters for
trainers.
"""
return self.turn
def _get_sight_range(self):
"""
Figure out the sight range of this map_obstacle.
"""
raise NotImplementedError
@property
def sight_range(self):
if self._sight_range is None:
self._sight_range = self._get_sight_range()
return self._sight_range
class ThreatZone(object):
"""
A ``ThreatZone`` represents the area surrounding a moving or turning object
that the player can try to avoid.
"""
def __init__(self, map_obstacle, main_graph):
"""
Constructs a ``ThreatZone`` based on a graph of a map and a particular
object on that map.
:param map_obstacle: the subject based on which to build a threat zone
:param main_graph: a reference to the map's nodes
"""
self.map_obstacle = map_obstacle
self.main_graph = main_graph
self.sight_range = self.calculate_sight_range()
self.top_left_y = None
self.top_left_x = None
self.bottom_right_y = None
self.bottom_right_x = None
self.height = None
self.width = None
self.size = self.calculate_size()
# nodes specific to this threat zone
self.nodes = []
def calculate_size(self):
"""
Calculate the bounds of the threat zone based on the map obstacle.
Returns the top left corner (y, x) and the bottom right corner (y, x)
in the form of ((y, x), (y, x), height, width).
"""
top_left_y = 0
top_left_x = 0
bottom_right_y = 1
bottom_right_x = 1
# TODO: calculate the correct bounds of the threat zone.
raise NotImplementedError
# if there is a sight_range for this map_obstacle then increase the size of the zone.
if self.sight_range > 0:
top_left_y += self.sight_range
top_left_x += self.sight_range
bottom_right_y += self.sight_range
bottom_right_x += self.sight_range
top_left = (top_left_y, top_left_x)
bottom_right = (bottom_right_y, bottom_right_x)
height = bottom_right_y - top_left_y
width = bottom_right_x - top_left_x
self.top_left_y = top_left_y
self.top_left_x = top_left_x
self.bottom_right_y = bottom_right_y
self.bottom_right_x = bottom_right_x
self.height = height
self.width = width
return (top_left, bottom_right, height, width)
def is_player_near(self, y, x):
"""
Applies a boundary of one around the threat zone, then checks if the
player is inside. This is how the threatzone activates to calculate an
updated graph or set of penalties for each step.
"""
y_condition = (self.top_left_y - 1) <= y < (self.bottom_right_y + 1)
x_condition = (self.top_left_x - 1) <= x < (self.bottom_right_x + 1)
return y_condition and x_condition
def check_map_obstacle_has_sight(self):
"""
Determines if the map object has the sight feature.
"""
return self.map_obstacle.sight_range > 0
def calculate_sight_range(self):
"""
Calculates the range that the object is able to see.
"""
if not self.check_map_obstacle_has_sight():
return 0
else:
return self.map_obstacle.sight_range
def get_current_facing_direction(self, DIRECTIONS=DIRECTIONS):
"""
Get the current facing direction of the map_obstacle.
"""
return self.map_obstacle.get_current_facing_direction(DIRECTIONS=DIRECTIONS)
# this isn't used anywhere yet
def is_map_obstacle_in_screen_range(self):
"""
Determines if the map_obstacle is within the bounds of whatever is on
screen at the moment. If the object is of a type that is capable of
moving, and it is not on screen, then it is not moving.
"""
raise NotImplementedError
def mark_nodes_as_members_of_threat_zone(self):
"""
Based on the nodes in this threat zone, mark each main graph's nodes as
members of this threat zone.
"""
for y in range(self.top_left_y, self.top_left_y + self.height):
for x in range(self.top_left_x, self.top_left_x + self.width):
main_node = self.main_graph[y][x]
main_node.threat_zones.add(self)
self.nodes.append(main_node)
def update_obstacle_location(self):
"""
Updates which node has the obstacle. This does not recompute the graph
based on this new information.
Each threat zone is responsible for updating its own map objects. So
there will never be a time when the current x value attached to the
map_obstacle does not represent the actual previous location.
"""
# find the previous location of the obstacle
old_y = self.map_obstacle.y
old_x = self.map_obstacle.x
# remove it from the main graph
self.main_graph[old_y][old_x].contents.remove(self.map_obstacle)
# get the latest location
self.map_obstacle.update_location()
(new_y, new_x) = (self.map_obstacle.y, self.map_obstacle.x)
# add it back into the main graph
self.main_graph[new_y][new_x].contents.add(self.map_obstacle)
# update the map obstacle (not necessary, but it doesn't hurt)
self.map_obstacle.y = new_y
self.map_obstacle.x = new_x
def is_node_in_threat_zone(self, y, x):
"""
Checks if the node is in the range of the threat zone.
"""
y_condition = self.top_left_y <= y < self.top_left_y + self.height
x_condition = self.top_left_x <= x < self.top_left_x + self.width
return y_condition and x_condition
def is_node_in_sight_range(self, y, x, skip_range_check=False):
"""
Checks if the node is in the sight range of the threat.
"""
if not skip_range_check:
if not self.is_node_in_threat_zone(y, x):
return False
if self.sight_range == 0:
return False
# TODO: sight range can be blocked by collidable map objects. But this
# node wouldn't be in the threat zone anyway.
y_condition = self.map_obstacle.y == y
x_condition = self.map_obstacle.x == x
# this probably only happens if the player warps to the exact spot
if y_condition and x_condition:
raise Exception(
"Don't know the meaning of being on top of the map_obstacle."
)
# check if y or x matches the map object
return y_condition or x_condition
def is_node_in_active_sight_range(self,
y,
x,
skip_sight_range_check=False,
skip_range_check=False,
DIRECTIONS=DIRECTIONS):
"""
Checks if the node has active sight range lock.
"""
if not skip_sight_range_check:
# can't be in active sight range if not in sight range
if not self.is_in_sight_range(y, x, skip_range_check=skip_range_check):
return False
y_condition = self.map_obstacle.y == y
x_condition = self.map_obstacle.x == x
# this probably only happens if the player warps to the exact spot
if y_condition and x_condition:
raise Exception(
"Don't know the meaning of being on top of the map_obstacle."
)
current_facing_direction = self.get_current_facing_direction(DIRECTIONS=DIRECTIONS)
if current_facing_direction not in DIRECTIONS.keys():
raise Exception(
"Invalid direction."
)
if current_facing_direction in [DIRECTIONS["UP"], DIRECTIONS["DOWN"]]:
# map_obstacle is looking up/down but player doesn't match y
if not y_condition:
return False
if current_facing_direction == DIRECTIONS["UP"]:
return y < self.map_obstacle.y
elif current_facing_direction == DIRECTIONS["DOWN"]:
return y > self.map_obstacle.y
else:
# map_obstacle is looking left/right but player doesn't match x
if not x_condition:
return False
if current_facing_direction == DIRECTIONS["LEFT"]:
return x < self.map_obstacle.x
elif current_facing_direction == DIRECTIONS["RIGHT"]:
return x > self.map_obstacle.x
def calculate_node_cost(self, y, x, consider_sight_range=True, PENALTIES=PENALTIES):
"""
Calculates the cost of the node w.r.t this threat zone. Turn off
consider_sight_range when not in the threat zone.
"""
penalty = 0
# The node is probably in the threat zone because otherwise why would
# this cost function be called? Only the nodes that are members of the
# current threat zone would have a reference to this threat zone and
# this function.
if not self.is_node_in_threat_zone(y, x):
penalty += PENALTIES["NONE"]
# Additionally, if htis codepath is ever hit, the other node cost
# function will have already used the "NONE" penalty, so this would
# really be doubling the penalty of the node..
raise Exception(
"Didn't expect to calculate a non-threat-zone node's cost, "
"since this is a threat zone function."
)
else:
penalty += PENALTIES["THREAT_ZONE"]
if consider_sight_range:
if self.is_node_in_sight_range(y, x, skip_range_check=True):
penalty += PENALTIES["SIGHT_RANGE"]
params = {
"skip_sight_range_check": True,
"skip_range_check": True,
}
active_sight_range = self.is_node_in_active_sight_range(y, x, **params)
if active_sight_range:
penalty += PENALTIES["ACTIVE_SIGHT_RANGE"]
return penalty
def create_graph(some_map):
"""
Creates the array of nodes representing the in-game map.
"""
map_height = some_map.height
map_width = some_map.width
map_obstacles = some_map.obstacles
nodes = [[None] * map_width] * map_height
# create a node representing each position on the map
for y in range(0, map_height):
for x in range(0, map_width):
position = (y, x)
# create a node describing this position
node = Node(position=position)
# store it on the graph
nodes[y][x] = node
# look through all moving characters, non-moving characters, and items
for map_obstacle in map_obstacles:
# all characters must start somewhere
node = nodes[map_obstacle.y][map_obstacle.x]
# store the map_obstacle on this node.
node.contents.add(map_obstacle)
# only create threat zones for moving/turning entities
if map_obstacle.can_move() or map_obstacle.can_turn_without_moving():
threat_zone = ThreatZone(map_obstacle, nodes, some_map)
threat_zone.mark_nodes_as_members_of_threat_zone()
some_map.nodes = nodes
return nodes
class Map(object):
"""
The ``Map`` class provides an interface for reading the currently loaded
map.
"""
def __init__(self, cry, parsed_map, height, width, map_group_id, map_id, config=config):
"""
:param cry: pokemon crystal emulation interface
:type cry: crystal
"""
self.config = config
self.cry = cry
self.threat_zones = set()
self.obstacles = set()
self.parsed_map = parsed_map
self.map_group_id = map_group_id
self.map_id = map_id
self.height = height
self.width = width
def travel_to(self, destination_location):
"""
Does path planning and figures out the quickest way to get to the
destination.
"""
raise NotImplementedError
@staticmethod
def from_rom(cry, address):
"""
Loads a map from bytes in ROM at the given address.
:param cry: pokemon crystal wrapper
"""
raise NotImplementedError
@staticmethod
def from_wram(cry):
"""
Loads a map from bytes in WRAM.
:param cry: pokemon crystal wrapper
"""
raise NotImplementedError
def draw_path(self, path):
"""
Draws a path on an image of the current map. The path must be an
iterable of nodes to visit in (y, x) format.
"""
palettes = pokemontools.map_gfx.read_palettes(self.config)
map_image = pokemontools.map_gfx.draw_map(self.map_group_id, self.map_id, palettes, show_sprites=True, config=self.config)
for coordinates in path:
y = coordinates[0]
x = coordinates[1]
some_image = Image.new("RGBA", (32, 32))
draw = ImageDraw.Draw(some_image, "RGBA")
draw.rectangle([(0, 0), (32, 32)], fill=(0, 0, 0, 127))
target = [(x * 4, y * 4), ((x + 32) * 4, (y + 32) * 4)]
map_image.paste(some_image, target, mask=some_image)
return map_image
class PathPlanner(object):
"""
Generic path finding implementation.
"""
def __init__(self, some_map, initial_location, target_location):
self.some_map = some_map
self.initial_location = initial_location
self.target_location = target_location
def plan(self):
"""
Runs the path planner and returns a list of positions making up the
path.
"""
return [(0, 0), (1, 0), (1, 1), (1, 2), (1, 3)]
def plan_and_draw_path_on(map_group_id=1, map_id=1, initial_location=(0, 0), final_location=(2, 2), config=config):
"""
An attempt at an entry point. This hasn't been sufficiently considered yet.
"""
initial_location = (0, 0)
final_location = (2, 2)
map_group_id = 1
map_id = 1
pokemontools.crystal.cachably_parse_rom()
pokemontools.map_gfx.add_pokecrystal_paths_to_configuration(config)
# get the map based on data from the rom
parsed_map = pokemontools.crystal.map_names[map_group_id][map_id]["header_new"]
# convert this map into a different structure
current_map = Map(cry=None, parsed_map=parsed_map, height=parsed_map.height.byte, width=parsed_map.width.byte, map_group_id=map_group_id, map_id=map_id, config=config)
# make a graph based on the map data
nodes = create_graph(current_map)
# make an instance of the planner implementation
planner = PathPlanner(current_map, initial_location, final_location)
# Make that planner do its planning based on the current configuration. The
# planner should be callable in the future and still have
# previously-calculated state, like cached pre-computed routes or
# something.
path = planner.plan()
# show the path on the map
drawn = current_map.draw_path(path)
return drawn

View File

@ -133,9 +133,21 @@ class WRAMProcessor(object):
self.config = config
self.paths = {}
self.paths["wram"] = os.path.join(self.config.path, "wram.asm")
self.paths["hram"] = os.path.join(self.config.path, "hram.asm")
self.paths["gbhw"] = os.path.join(self.config.path, "gbhw.asm")
if hasattr(self.config, "wram"):
self.paths["wram"] = self.config.wram
else:
self.paths["wram"] = os.path.join(self.config.path, "wram.asm")
if hasattr(self.config, "hram"):
self.paths["hram"] = self.config.hram
else:
self.paths["hram"] = os.path.join(self.config.path, "hram.asm")
if hasattr(self.config, "gbhw"):
self.paths["gbhw"] = self.config.gbhw
else:
self.paths["gbhw"] = os.path.join(self.config.path, "gbhw.asm")
def initialize(self):
"""