summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--pokemontools/crystal.py71
-rw-r--r--pokemontools/data/pokecrystal/gbhw.asm102
-rw-r--r--pokemontools/data/pokecrystal/hram.asm71
-rw-r--r--pokemontools/map_gfx.py400
-rw-r--r--pokemontools/vba/path.py664
-rw-r--r--pokemontools/wram.py18
6 files changed, 1309 insertions, 17 deletions
diff --git a/pokemontools/crystal.py b/pokemontools/crystal.py
index cdab01f..eb88b6b 100644
--- a/pokemontools/crystal.py
+++ b/pokemontools/crystal.py
@@ -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
diff --git a/pokemontools/data/pokecrystal/gbhw.asm b/pokemontools/data/pokecrystal/gbhw.asm
new file mode 100644
index 0000000..0f12e48
--- /dev/null
+++ b/pokemontools/data/pokecrystal/gbhw.asm
@@ -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)
+
diff --git a/pokemontools/data/pokecrystal/hram.asm b/pokemontools/data/pokecrystal/hram.asm
new file mode 100644
index 0000000..051d418
--- /dev/null
+++ b/pokemontools/data/pokecrystal/hram.asm
@@ -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
+
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)
diff --git a/pokemontools/vba/path.py b/pokemontools/vba/path.py
new file mode 100644
index 0000000..2e50d1b
--- /dev/null
+++ b/pokemontools/vba/path.py
@@ -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
diff --git a/pokemontools/wram.py b/pokemontools/wram.py
index dbff309..a132289 100644
--- a/pokemontools/wram.py
+++ b/pokemontools/wram.py
@@ -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):
"""