mirror of
https://github.com/pret/pokemon-reverse-engineering-tools.git
synced 2026-04-24 15:16:52 -05:00
The map editor can now be invoked in ipython: import extras.pokemontools.map_editor as ed app = ed.init() Then <app> can be modified on the fly to make up for lacking functionality. Also see app.map.crop(). Now works with all maps in both Red and Crystal. The version defaults to Crystal. To use with pokered, invoke from the command line: python extras/pokemontools/map_editor.py red Or in ipython: import extras.pokemontools.map_editor as ed app = ed.init(version='red') Also displays connections, but they're unaligned.
841 lines
28 KiB
Python
841 lines
28 KiB
Python
import os
|
|
import sys
|
|
import logging
|
|
import argparse
|
|
|
|
from Tkinter import (
|
|
Tk,
|
|
Button,
|
|
Canvas,
|
|
Scrollbar,
|
|
VERTICAL,
|
|
HORIZONTAL,
|
|
RIGHT,
|
|
LEFT,
|
|
TOP,
|
|
BOTTOM,
|
|
BOTH,
|
|
Y,
|
|
X,
|
|
N, S, E, W,
|
|
TclError,
|
|
Menu,
|
|
)
|
|
import tkFileDialog
|
|
|
|
from ttk import (
|
|
Frame,
|
|
Style,
|
|
Combobox,
|
|
)
|
|
|
|
# This is why requirements.txt says to install pillow instead of the original
|
|
# PIL.
|
|
from PIL import (
|
|
Image,
|
|
ImageTk,
|
|
)
|
|
|
|
import gfx
|
|
import wram
|
|
import preprocessor
|
|
import configuration
|
|
config = configuration.Config()
|
|
|
|
|
|
def config_open(self, filename):
|
|
return open(os.path.join(self.path, filename))
|
|
|
|
configuration.Config.open = config_open
|
|
|
|
|
|
def setup_logging():
|
|
"""
|
|
Temporary function that configures logging to go straight to console.
|
|
"""
|
|
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
|
|
console = logging.StreamHandler(sys.stdout)
|
|
console.setLevel(logging.DEBUG)
|
|
console.setFormatter(formatter)
|
|
root = logging.getLogger()
|
|
root.addHandler(console)
|
|
root.setLevel(logging.DEBUG)
|
|
|
|
|
|
def read_incbin_in_file(label, filename='main.asm', config=config):
|
|
asm = config.open(filename).read()
|
|
return read_incbin(asm, label)
|
|
|
|
def read_incbin(asm, label):
|
|
incbin = asm_at_label(asm, label)
|
|
filename = read_header_macros_2(
|
|
incbin,
|
|
[('filename', 'INCBIN')]
|
|
)[0]['filename']
|
|
filename = filename.split('"')[1]
|
|
return filename
|
|
|
|
|
|
def red_gfx_name(tset):
|
|
if type(tset) is int:
|
|
return [
|
|
'overworld',
|
|
'redshouse1',
|
|
'mart',
|
|
'forest',
|
|
'redshouse2',
|
|
'dojo',
|
|
'pokecenter',
|
|
'gym',
|
|
'house',
|
|
'forestgate',
|
|
'museum',
|
|
'underground',
|
|
'gate',
|
|
'ship',
|
|
'shipport',
|
|
'cemetery',
|
|
'interior',
|
|
'cavern',
|
|
'lobby',
|
|
'mansion',
|
|
'lab',
|
|
'club',
|
|
'facility',
|
|
'plateau',
|
|
][tset]
|
|
|
|
elif type(tset) is str:
|
|
return tset.lower().replace('_', '')
|
|
|
|
|
|
def configure_for_pokered(config=config):
|
|
"""
|
|
Sets default configuration values for pokered. These should eventually be
|
|
moved into the configuration module.
|
|
"""
|
|
attrs = {
|
|
"version": "red",
|
|
|
|
"map_dir": os.path.join(config.path, 'maps/'),
|
|
"gfx_dir": os.path.join(config.path, 'gfx/tilesets/'),
|
|
"to_gfx_name": red_gfx_name,
|
|
"block_dir": os.path.join(config.path, 'gfx/blocksets/'), # not used
|
|
"block_ext": '.bst', # not used
|
|
|
|
"palettes_on": False,
|
|
|
|
"constants_filename": os.path.join(config.path, 'constants.asm'),
|
|
|
|
"time_of_day": 1,
|
|
}
|
|
return attrs
|
|
|
|
def configure_for_pokecrystal(config=config):
|
|
"""
|
|
Sets default configuration values for pokecrystal. These should eventually
|
|
be moved into the configuration module.
|
|
"""
|
|
attrs = {
|
|
"version": "crystal",
|
|
|
|
"map_dir": os.path.join(config.path, 'maps/'),
|
|
"gfx_dir": os.path.join(config.path, 'gfx/tilesets/'),
|
|
"to_gfx_name": lambda x : '%.2d' % x,
|
|
"block_dir": os.path.join(config.path, 'tilesets/'),
|
|
"block_ext": '_metatiles.bin',
|
|
|
|
"palettes_on": True,
|
|
"palmap_dir": os.path.join(config.path, 'tilesets/'),
|
|
"palette_dir": os.path.join(config.path, 'tilesets/'),
|
|
|
|
"asm_dir": os.path.join(config.path, 'maps/'),
|
|
|
|
"constants_filename": os.path.join(config.path, 'constants.asm'),
|
|
|
|
"header_dir": os.path.join(config.path, 'maps/'),
|
|
|
|
"time_of_day": 1,
|
|
}
|
|
return attrs
|
|
|
|
def configure_for_version(version, config=config):
|
|
"""
|
|
Overrides default values from the configuration with additional attributes.
|
|
"""
|
|
if version == "red":
|
|
attrs = configure_for_pokered(config)
|
|
elif version == "crystal":
|
|
attrs = configure_for_pokecrystal(config)
|
|
else:
|
|
# TODO: pick a better exception
|
|
raise Exception(
|
|
"Can't configure for this version."
|
|
)
|
|
|
|
for (key, value) in attrs.iteritems():
|
|
setattr(config, key, value)
|
|
|
|
# not really needed since it's modifying the same object
|
|
return config
|
|
|
|
def get_constants(config=config):
|
|
bss = wram.BSSReader()
|
|
bss.read_bss_sections(open(config.constants_filename).readlines())
|
|
config.constants = bss.constants
|
|
return config.constants
|
|
|
|
|
|
class Application(Frame):
|
|
def __init__(self, master=None, config=config):
|
|
self.config = config
|
|
self.log = logging.getLogger("{0}.{1}".format(self.__class__.__name__, id(self)))
|
|
self.display_connections = True
|
|
|
|
Frame.__init__(self, master)
|
|
self.pack(fill=BOTH, expand=True)
|
|
Style().configure("TFrame", background="#444")
|
|
|
|
self.paint_tile = 1
|
|
self.init_ui()
|
|
|
|
def init_ui(self):
|
|
self.connections = {}
|
|
self.button_frame = Frame(self)
|
|
self.button_frame.grid(row=0, column=0, columnspan=2)
|
|
self.map_frame = Frame(self)
|
|
self.map_frame.grid(row=1, column=0, padx=5, pady=5, sticky=N+S+E+W)
|
|
self.picker_frame = Frame(self)
|
|
self.picker_frame.grid(row=1, column=1)
|
|
|
|
self.button_new = Button(self.button_frame)
|
|
self.button_new["text"] = "New"
|
|
self.button_new["command"] = self.new_map
|
|
self.button_new.grid(row=0, column=0, padx=2)
|
|
|
|
self.menubar = Menu(self)
|
|
|
|
menu = Menu(self.menubar, tearoff=0)
|
|
self.menubar.add_cascade(label="File", menu=menu)
|
|
menu.add_command(label="New")
|
|
menu.add_command(label="Open")
|
|
menu.add_command(label="Save")
|
|
|
|
self.open = Button(self.button_frame)
|
|
self.open["text"] = "Open"
|
|
self.open["command"] = self.open_map
|
|
self.open.grid(row=0, column=1, padx=2)
|
|
|
|
self.save = Button(self.button_frame)
|
|
self.save["text"] = "Save"
|
|
self.save["command"] = self.save_map
|
|
self.save.grid(row=0, column=2, padx=2)
|
|
|
|
self.get_map_list()
|
|
self.map_list.grid(row=0, column=3, padx=2)
|
|
|
|
|
|
def get_map_list(self):
|
|
self.available_maps = sorted(m for m in get_available_maps(config=self.config))
|
|
self.map_list = Combobox(self.button_frame, height=24, width=24, values=self.available_maps)
|
|
if len(self.available_maps):
|
|
self.map_list.set(self.available_maps[0])
|
|
|
|
def new_map(self):
|
|
self.map_name = None
|
|
self.init_map()
|
|
self.map.map.blockdata = bytearray([self.paint_tile] * 20 * 20)
|
|
self.map.map.width = 20
|
|
self.map.map.height = 20
|
|
self.draw_map()
|
|
self.init_picker()
|
|
|
|
def open_map(self):
|
|
self.map_name = self.map_list.get()
|
|
self.init_map()
|
|
self.draw_map()
|
|
self.init_picker()
|
|
|
|
def save_map(self):
|
|
if hasattr(self, 'map'):
|
|
if self.map.map.blk_path:
|
|
initial = self.map.map.blk_path
|
|
else:
|
|
initial = self.config.path
|
|
filename = tkFileDialog.asksaveasfilename(initialfile=initial)
|
|
if filename:
|
|
with open(filename, 'wb') as save:
|
|
save.write(self.map.map.blockdata)
|
|
self.log.info('blockdata saved as {}'.format(filename))
|
|
else:
|
|
self.log.info('nothing to save')
|
|
|
|
def init_map(self):
|
|
if hasattr(self, 'map'):
|
|
self.map.kill_canvas()
|
|
self.map = MapRenderer(self.config, parent=self.map_frame, name=self.map_name)
|
|
self.init_map_connections()
|
|
|
|
def draw_map(self):
|
|
self.map.init_canvas(self.map_frame)
|
|
self.map.canvas.pack() #.grid(row=1,column=1)
|
|
self.map.draw()
|
|
self.map.canvas.bind('<Button-1>', self.paint)
|
|
self.map.canvas.bind('<B1-Motion>', self.paint)
|
|
|
|
def init_picker(self):
|
|
"""This should really be its own class."""
|
|
self.current_tile = MapRenderer(self.config, parent=self.button_frame, tileset=Tileset(id=self.map.map.tileset.id))
|
|
self.current_tile.map.blockdata = [self.paint_tile]
|
|
self.current_tile.map.width = 1
|
|
self.current_tile.map.height = 1
|
|
self.current_tile.init_canvas()
|
|
self.current_tile.draw()
|
|
self.current_tile.canvas.grid(row=0, column=4, padx=4)
|
|
|
|
if hasattr(self, 'picker'):
|
|
self.picker.kill_canvas()
|
|
self.picker = MapRenderer(self.config, parent=self, tileset=Tileset(id=self.map.map.tileset.id))
|
|
self.picker.map.blockdata = range(len(self.picker.map.tileset.blocks))
|
|
self.picker.map.width = 4
|
|
self.picker.map.height = len(self.picker.map.blockdata) / self.picker.map.width
|
|
self.picker.init_canvas(self.picker_frame)
|
|
|
|
if hasattr(self.picker_frame, 'vbar'):
|
|
self.picker_frame.vbar.destroy()
|
|
self.picker_frame.vbar = Scrollbar(self.picker_frame, orient=VERTICAL)
|
|
self.picker_frame.vbar.pack(side=RIGHT, fill=Y)
|
|
self.picker_frame.vbar.config(command=self.picker.canvas.yview)
|
|
|
|
self.picker.canvas.config(scrollregion=(0,0,self.picker.canvas_width, self.picker.canvas_height))
|
|
self.map_frame.update()
|
|
|
|
# overwriting a property is probably a bad idea
|
|
self.picker.canvas_height = self.map_frame.winfo_height()
|
|
|
|
self.picker.canvas.config(yscrollcommand=self.picker_frame.vbar.set)
|
|
self.picker.canvas.pack(side=LEFT, expand=True)
|
|
|
|
self.picker.canvas.bind('<4>', lambda event : self.scroll_picker(event))
|
|
self.picker.canvas.bind('<5>', lambda event : self.scroll_picker(event))
|
|
self.picker_frame.vbar.bind('<4>', lambda event : self.scroll_picker(event))
|
|
self.picker_frame.vbar.bind('<5>', lambda event : self.scroll_picker(event))
|
|
|
|
self.picker.draw()
|
|
self.picker.canvas.bind('<Button-1>', self.pick_block)
|
|
|
|
def scroll_picker(self, event):
|
|
if event.num == 4:
|
|
self.picker.canvas.yview('scroll', -1, 'units')
|
|
elif event.num == 5:
|
|
self.picker.canvas.yview('scroll', 1, 'units')
|
|
|
|
|
|
def pick_block(self, event):
|
|
block_x = int(self.picker.canvas.canvasx(event.x)) / (self.picker.map.tileset.block_width * self.picker.map.tileset.tile_width)
|
|
block_y = int(self.picker.canvas.canvasy(event.y)) / (self.picker.map.tileset.block_height * self.picker.map.tileset.tile_height)
|
|
i = block_y * self.picker.map.width + block_x
|
|
self.paint_tile = self.picker.map.blockdata[i]
|
|
|
|
self.current_tile.map.blockdata = [self.paint_tile]
|
|
self.current_tile.draw()
|
|
|
|
def paint(self, event):
|
|
block_x = event.x / (self.map.map.tileset.block_width * self.map.map.tileset.tile_width)
|
|
block_y = event.y / (self.map.map.tileset.block_height * self.map.map.tileset.tile_height)
|
|
i = block_y * self.map.map.width + block_x
|
|
if 0 <= i < len(self.map.map.blockdata):
|
|
self.map.map.blockdata[i] = self.paint_tile
|
|
self.map.draw_block(block_x, block_y)
|
|
|
|
def init_map_connections(self):
|
|
if not self.display_connections:
|
|
return
|
|
|
|
for direction in self.map.map.connections.keys():
|
|
|
|
if direction in self.connections.keys():
|
|
if hasattr(self.connections[direction], 'canvas'):
|
|
self.connections[direction].kill_canvas()
|
|
|
|
if self.map.map.connections[direction] == {}:
|
|
self.connections[direction] = {}
|
|
continue
|
|
|
|
self.connections[direction] = MapRenderer(self.config, parent=self, name=self.map.map.connections[direction]['map_name'])
|
|
|
|
attrs = self.map.map.connections[direction]
|
|
if direction in ['north', 'south']:
|
|
if direction == 'north':
|
|
x1 = 0
|
|
if self.config.version == 'red':
|
|
y1 = eval(attrs['other_height'], self.config.constants) - 3
|
|
elif self.config.version == 'crystal':
|
|
y1 = eval(attrs['map'] + '_HEIGHT', self.config.constants) - 3
|
|
else: # south
|
|
x1 = 0
|
|
y1 = 0
|
|
x2 = x1 + eval(attrs['strip_length'], self.config.constants)
|
|
y2 = y1 + 3
|
|
else:
|
|
if direction == 'east':
|
|
x1 = 0
|
|
y1 = 0
|
|
else: # west
|
|
x1 = -3
|
|
y1 = 1
|
|
x2 = x1 + 3
|
|
y2 = y1 + eval(attrs['strip_length'], self.config.constants)
|
|
|
|
self.connections[direction].init_canvas(self.map_frame)
|
|
self.connections[direction].canvas.pack(side={'north':TOP, 'south':BOTTOM, 'west':LEFT,'east':RIGHT}[direction])
|
|
self.connections[direction].map.crop(x1, y1, x2, y2)
|
|
self.connections[direction].draw()
|
|
|
|
|
|
class MapRenderer:
|
|
def __init__(self, config=config, **kwargs):
|
|
self.config = config
|
|
self.__dict__.update(kwargs)
|
|
self.map = Map(**kwargs)
|
|
|
|
@property
|
|
def canvas_width(self):
|
|
return self.map.width * self.map.block_width
|
|
|
|
@property
|
|
def canvas_height(self):
|
|
return self.map.height * self.map.block_height
|
|
|
|
def init_canvas(self, parent=None):
|
|
if parent == None:
|
|
parent = self.parent
|
|
if hasattr(self, 'canvas'):
|
|
pass
|
|
else:
|
|
self.canvas = Canvas(parent)
|
|
self.canvas.xview_moveto(0)
|
|
self.canvas.yview_moveto(0)
|
|
|
|
def kill_canvas(self):
|
|
if hasattr(self, 'canvas'):
|
|
self.canvas.destroy()
|
|
|
|
def draw(self):
|
|
self.canvas.configure(width=self.canvas_width, height=self.canvas_height)
|
|
for i in xrange(len(self.map.blockdata)):
|
|
block_x = i % self.map.width
|
|
block_y = i / self.map.width
|
|
self.draw_block(block_x, block_y)
|
|
|
|
def draw_block(self, block_x, block_y):
|
|
# the canvas starts at 4, 4 for some reason
|
|
# probably something to do with a border
|
|
index, indey = 4, 4
|
|
|
|
# Draw one block (4x4 tiles)
|
|
block = self.map.blockdata[block_y * self.map.width + block_x]
|
|
|
|
# Ignore nonexistent blocks.
|
|
if block >= len(self.map.tileset.blocks): return
|
|
|
|
for j, tile in enumerate(self.map.tileset.blocks[block]):
|
|
try:
|
|
# Tile gfx are split in half to make vram mapping easier
|
|
if tile >= 0x80:
|
|
tile -= 0x20
|
|
tile_x = block_x * self.map.block_width + (j % 4) * 8
|
|
tile_y = block_y * self.map.block_height + (j / 4) * 8
|
|
self.canvas.create_image(index + tile_x, indey + tile_y, image=self.map.tileset.tiles[tile])
|
|
except:
|
|
pass
|
|
|
|
def crop(self, *args, **kwargs):
|
|
self.map.crop(*args, **kwargs)
|
|
self.draw()
|
|
|
|
|
|
class Map:
|
|
width = 20
|
|
height = 20
|
|
block_width = 32
|
|
block_height = 32
|
|
|
|
def __init__(self, config=config, **kwargs):
|
|
self.parent = None
|
|
self.name = ''
|
|
self.blk_path = ''
|
|
self.tileset = Tileset(config=config)
|
|
self.blockdata = []
|
|
self.connections = {'north': {}, 'south': {}, 'west': {}, 'east': {}}
|
|
|
|
self.__dict__.update(kwargs)
|
|
self.config = config
|
|
|
|
self.log = logging.getLogger("{0}.{1}".format(self.__class__.__name__, id(self)))
|
|
|
|
if not self.blk_path and self.name:
|
|
self.blk_path = os.path.join(self.config.map_dir, self.name + '.blk')
|
|
if os.path.exists(self.blk_path) and self.blockdata == []:
|
|
self.blockdata = bytearray(open(self.blk_path).read())
|
|
|
|
if self.config.version == 'red':
|
|
if self.name:
|
|
attrs = map_header(self.name, config=self.config)
|
|
self.tileset = Tileset(id=attrs['tileset_id'], config=self.config)
|
|
self.height = eval(attrs['height'], self.config.constants)
|
|
self.width = eval(attrs['width'], self.config.constants)
|
|
self.connections = attrs['connections']
|
|
|
|
elif self.config.version == 'crystal':
|
|
|
|
asm_filename = ''
|
|
if self.name:
|
|
asm_filename = os.path.join(self.config.asm_dir, self.name + '.asm')
|
|
|
|
if os.path.exists(asm_filename):
|
|
for props in [
|
|
map_header(self.name, config=self.config),
|
|
second_map_header(self.name, config=self.config)
|
|
]:
|
|
self.__dict__.update(props)
|
|
|
|
self.asm = open(asm_filename, 'r').read()
|
|
self.events = event_header(self.asm, self.name)
|
|
self.scripts = script_header(self.asm, self.name)
|
|
|
|
self.tileset = Tileset(id=self.tileset_id, config=self.config)
|
|
|
|
self.width = eval(self.width, self.config.constants)
|
|
self.height = eval(self.height, self.config.constants)
|
|
|
|
def crop(self, x1=0, y1=0, x2=None, y2=None):
|
|
if x2 is None: x2 = self.width
|
|
if y2 is None: y2 = self.height
|
|
start = y1 * self.width + x1
|
|
width = x2 - x1
|
|
height = y2 - y1
|
|
blockdata = []
|
|
for y in xrange(height):
|
|
index = start + y * self.width
|
|
blockdata.extend( self.blockdata[index : index + width] )
|
|
self.blockdata = bytearray(blockdata)
|
|
self.width = width
|
|
self.height = height
|
|
|
|
|
|
class Tileset:
|
|
def __init__(self, config=config, **kwargs):
|
|
if config.version == 'red':
|
|
self.id = 0
|
|
elif config.version == 'crystal':
|
|
self.id = 2
|
|
|
|
self.tile_width = 8
|
|
self.tile_height = 8
|
|
self.block_width = 4
|
|
self.block_height = 4
|
|
|
|
self.alpha = 255
|
|
|
|
self.__dict__.update(kwargs)
|
|
self.id = eval(str(self.id), config.constants)
|
|
|
|
self.config = config
|
|
self.log = logging.getLogger("{0}.{1}".format(self.__class__.__name__, id(self)))
|
|
|
|
if self.config.palettes_on:
|
|
self.get_palettes()
|
|
self.get_palette_map()
|
|
|
|
self.get_blocks()
|
|
self.get_tiles()
|
|
|
|
def read_header(self):
|
|
if self.config.version == 'red':
|
|
tileset_headers = self.config.open('data/tileset_headers.asm').readlines()
|
|
tileset_header = map(str.strip, tileset_headers[self.id + 1].split('\ttileset')[1].split(','))
|
|
return tileset_header
|
|
|
|
def get_tileset_gfx_filename(self):
|
|
filename = None
|
|
|
|
if self.config.version == 'red':
|
|
gfx_label = self.read_header()[1]
|
|
filename = read_incbin_in_file(gfx_label, filename='main.asm', config=self.config)
|
|
filename = filename.replace('.2bpp','.png')
|
|
filename = os.path.join(self.config.path, filename)
|
|
|
|
if not filename: # last resort
|
|
filename = os.path.join(
|
|
self.config.gfx_dir,
|
|
self.config.to_gfx_name(self.id) + '.png'
|
|
)
|
|
|
|
return filename
|
|
|
|
def get_tiles(self):
|
|
filename = self.get_tileset_gfx_filename()
|
|
if not os.path.exists(filename):
|
|
gfx.export_2bpp_to_png(filename.replace('.png','.2bpp'))
|
|
self.img = Image.open(filename)
|
|
self.img.width, self.img.height = self.img.size
|
|
self.tiles = []
|
|
cur_tile = 0
|
|
for y in xrange(0, self.img.height, self.tile_height):
|
|
for x in xrange(0, self.img.width, self.tile_width):
|
|
tile = self.img.crop((x, y, x + self.tile_width, y + self.tile_height))
|
|
|
|
if hasattr(self, 'palette_map') and hasattr(self, 'palettes'):
|
|
# Palette maps are padded to make vram mapping easier.
|
|
pal = self.palette_map[cur_tile + 0x20 if cur_tile >= 0x60 else cur_tile] & 0x7
|
|
tile = self.colorize_tile(tile, self.palettes[pal])
|
|
|
|
self.tiles += [ImageTk.PhotoImage(tile)]
|
|
cur_tile += 1
|
|
|
|
def colorize_tile(self, tile, palette):
|
|
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
|
|
|
|
def get_blocks(self):
|
|
if self.config.version == 'crystal':
|
|
filename = os.path.join(
|
|
self.config.block_dir,
|
|
self.config.to_gfx_name(self.id) + self.config.block_ext
|
|
)
|
|
|
|
elif self.config.version == 'red':
|
|
block_label = self.read_header()[0]
|
|
filename = read_incbin_in_file(block_label, 'main.asm', config=self.config)
|
|
|
|
self.blocks = []
|
|
block_length = self.block_width * self.block_height
|
|
blocks = bytearray(open(filename, 'rb').read())
|
|
for block in xrange(len(blocks) / (block_length)):
|
|
i = block * block_length
|
|
self.blocks += [blocks[i : i + block_length]]
|
|
|
|
def get_palette_map(self):
|
|
filename = os.path.join(
|
|
self.config.palmap_dir,
|
|
str(self.id).zfill(2) + '_palette_map.bin'
|
|
)
|
|
self.palette_map = []
|
|
palmap = bytearray(open(filename, 'rb').read())
|
|
for i in xrange(len(palmap)):
|
|
self.palette_map += [palmap[i] & 0xf]
|
|
self.palette_map += [(palmap[i] >> 4) & 0xf]
|
|
|
|
def get_palettes(self):
|
|
filename = os.path.join(
|
|
self.config.palette_dir,
|
|
['morn', 'day', 'nite'][self.config.time_of_day] + '.pal'
|
|
)
|
|
self.palettes = get_palettes(filename)
|
|
|
|
def get_palettes(filename):
|
|
lines = open(filename, 'r').readlines()
|
|
colors = gfx.read_rgb_macros(lines)
|
|
palettes = [colors[i:i+4] for i in xrange(0, len(colors), 4)]
|
|
return palettes
|
|
|
|
def get_available_maps(config=config):
|
|
for root, dirs, files in os.walk(config.map_dir):
|
|
for filename in files:
|
|
base_name, ext = os.path.splitext(filename)
|
|
if ext == '.blk':
|
|
yield base_name
|
|
|
|
def map_header(name, config=config):
|
|
if config.version == 'crystal':
|
|
headers = open(os.path.join(config.header_dir, 'map_headers.asm'), 'r').read()
|
|
label = name
|
|
header = asm_at_label(headers, '\tmap_header ' + label, colon=',')
|
|
attributes = [
|
|
('label', 'map_header'),
|
|
('tileset_id', 'db'),
|
|
('permission', 'db'),
|
|
('world_map_location', 'db'),
|
|
('music', 'db'),
|
|
('time_of_day', 'db'),
|
|
('fishing_group', 'db'),
|
|
]
|
|
attrs, l = read_header_macros_2(header, attributes)
|
|
return attrs
|
|
|
|
elif config.version == 'red':
|
|
header = config.open('data/mapHeaders/{0}.asm'.format(name)).read()
|
|
header = split_comments(header.split('\n'))
|
|
attributes = [
|
|
('tileset_id', 'db'),
|
|
('height', 'db'),
|
|
('width', 'db'),
|
|
('blockdata_label', 'dw'),
|
|
('text_label', 'dw'),
|
|
('script_label', 'dw'),
|
|
('which_connections', 'db'),
|
|
]
|
|
|
|
attrs, l = read_header_macros_2(header, attributes)
|
|
|
|
attrs['connections'], l = connections(attrs['which_connections'], header, l, config=config)
|
|
|
|
attributes = [('object_label', 'dw')]
|
|
more_attrs, l = read_header_macros_2(header[l:], attributes)
|
|
attrs.update(more_attrs)
|
|
|
|
return attrs
|
|
|
|
return {}
|
|
|
|
def second_map_header(name, config=config):
|
|
if config.version == 'crystal':
|
|
headers = open(os.path.join(config.header_dir, 'second_map_headers.asm'), 'r').read()
|
|
label = '\tmap_header_2 ' + name
|
|
header = asm_at_label(headers, label, colon=',')
|
|
|
|
attributes = [
|
|
('second_label', 'map_header_2'),
|
|
('dimension_base', 'db'),
|
|
('border_block', 'db'),
|
|
('which_connections', 'db'),
|
|
]
|
|
|
|
attrs, l = read_header_macros_2(header, attributes)
|
|
|
|
# hack to use dimension constants, eventually dimensions will be here for real
|
|
attrs['height'] = attrs['dimension_base'] + '_HEIGHT'
|
|
attrs['width'] = attrs['dimension_base'] + '_WIDTH'
|
|
|
|
attrs['connections'], l = connections(attrs['which_connections'], header, l, config=config)
|
|
return attrs
|
|
|
|
return {}
|
|
|
|
def connections(which_connections, header, l=0, config=config):
|
|
directions = { 'north': {}, 'south': {}, 'west': {}, 'east': {} }
|
|
|
|
if config.version == 'crystal':
|
|
attributes = [
|
|
('map', 'map'),
|
|
('strip_pointer', 'dw'),
|
|
('strip_destination', 'dw'),
|
|
('strip_length', 'db'),
|
|
('map_width', 'db'),
|
|
('y_offset', 'db'),
|
|
('x_offset', 'db'),
|
|
('window', 'dw'),
|
|
]
|
|
|
|
elif config.version == 'red':
|
|
conn_attrs = {
|
|
'north': ['map_id', 'other_width', 'other_height', 'x_offset', 'strip_offset', 'strip_length', 'other_blocks'],
|
|
'south': ['map_id', 'other_width', 'x_offset', 'strip_offset', 'strip_length', 'other_blocks', 'width', 'height'],
|
|
'east': ['map_id', 'other_width', 'y_offset', 'strip_offset', 'strip_length', 'other_blocks', 'width'],
|
|
'west': ['map_id', 'other_width', 'y_offset', 'strip_offset', 'strip_length', 'other_blocks', 'width'],
|
|
}
|
|
|
|
for d in ['north', 'south', 'west', 'east']:
|
|
if d.upper() in which_connections:
|
|
|
|
if config.version == 'crystal':
|
|
attrs, l2 = read_header_macros_2(header[l:], attributes)
|
|
l += l2
|
|
directions[d] = attrs
|
|
directions[d]['map_name'] = directions[d]['map'].title().replace('_','')
|
|
|
|
elif config.version == 'red':
|
|
attrs, l2 = read_header_macros_2(header[l:], zip(conn_attrs[d], [d.upper() + '_MAP_CONNECTION'] * len(conn_attrs[d])))
|
|
l += l2
|
|
directions[d] = attrs
|
|
directions[d]['map_name'] = directions[d]['map_id'].lower().replace('_','')
|
|
|
|
return directions, l
|
|
|
|
def read_header_macros_2(header, attributes):
|
|
values, l = read_header_macros(header, [x[0] for x in attributes], [x[1] for x in attributes])
|
|
return dict(zip([x[0] for x in attributes], values)), l
|
|
|
|
def read_header_macros(header, attributes, macros):
|
|
values = []
|
|
i = 0
|
|
l = 0
|
|
for l, (asm, comment) in enumerate(header):
|
|
if asm.strip() != '':
|
|
mvalues = macro_values(asm, macros[i])
|
|
values += mvalues
|
|
i += len(mvalues)
|
|
if len(values) >= len(attributes):
|
|
l += 1
|
|
break
|
|
return values, l
|
|
|
|
def event_header(asm, name):
|
|
return {}
|
|
|
|
def script_header(asm, name):
|
|
return {}
|
|
|
|
def macro_values(line, macro):
|
|
values = macro.join(line.split(macro)[1:]).split(',')
|
|
#values = line[line.find(macro) + len(macro):].split(',')
|
|
values = [v.replace('$','0x').strip() for v in values]
|
|
if values[0] == 'w': # dbw
|
|
values = values[1:]
|
|
return values
|
|
|
|
def asm_at_label(asm, label, colon=':'):
|
|
label_def = label + colon
|
|
lines = asm.split('\n')
|
|
for i, line in enumerate(lines):
|
|
if label_def in line:
|
|
lines = lines[i:]
|
|
break
|
|
return split_comments(lines)
|
|
|
|
def split_comments(lines):
|
|
content = []
|
|
for line in lines:
|
|
l, comment = preprocessor.separate_comment(line + '\n')
|
|
# skip over labels? this should be in macro_values
|
|
while ':' in l:
|
|
l = l[l.index(':') + 1:]
|
|
content += [[l, comment]]
|
|
return content
|
|
|
|
|
|
def main(config=config):
|
|
"""
|
|
Creates an application instance.
|
|
"""
|
|
root = Tk()
|
|
root.columnconfigure(0, weight=1)
|
|
root.wm_title("ayy lmap")
|
|
app = Application(master=root, config=config)
|
|
return app
|
|
|
|
def init(config=config, version='crystal'):
|
|
"""
|
|
Launches a map editor instance.
|
|
"""
|
|
setup_logging()
|
|
configure_for_version(version, config)
|
|
get_constants(config=config)
|
|
return main(config=config)
|
|
|
|
if __name__ == "__main__":
|
|
ap = argparse.ArgumentParser()
|
|
ap.add_argument('version', nargs='?', default='crystal')
|
|
args = ap.parse_args()
|
|
app = init(config=config, version=args.version)
|
|
app.mainloop()
|