From fbbab0be6950ce56e6263a72b64779f0b28ec3b1 Mon Sep 17 00:00:00 2001 From: Bryan Bishop Date: Sun, 17 Nov 2013 01:53:00 -0600 Subject: groundwork for path planning implementation --- pokemontools/vba/path.py | 588 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 588 insertions(+) create mode 100644 pokemontools/vba/path.py diff --git a/pokemontools/vba/path.py b/pokemontools/vba/path.py new file mode 100644 index 0000000..e861a39 --- /dev/null +++ b/pokemontools/vba/path.py @@ -0,0 +1,588 @@ +""" +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). +""" + +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, height, width): + """ + :param cry: pokemon crystal emulation interface + :type cry: crystal + """ + self.cry = cry + + self.threat_zones = set() + self.obstacles = set() + + 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 broken_main(): + """ + An attempt at an entry point. This hasn't been sufficiently considered yet. + """ + current_map = Map.from_wram(cry) + + # make a graph based on the map + nodes = create_graph(current_map) + + current_map.travel_to(destination_location) + + return current_map -- cgit v1.2.3 From 8767c40b04fb97364cbcacb4dea8a985e4829866 Mon Sep 17 00:00:00 2001 From: Bryan Bishop Date: Fri, 22 Nov 2013 21:04:10 -0600 Subject: move the map offsets out of main() This is a function that is necessary outside of the context of a main method. --- pokemontools/crystal.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/pokemontools/crystal.py b/pokemontools/crystal.py index cdab01f..c195ad9 100644 --- a/pokemontools/crystal.py +++ b/pokemontools/crystal.py @@ -6044,7 +6044,7 @@ def find_incbin_to_replace_for(address, debug=False, rom_file=None): incbin = processed_incbins[incbin_key] start = incbin["start"] end = incbin["end"] - if debug: + nf debug: argstuff = { "start": start, "end": end, @@ -6953,6 +6953,13 @@ Command.trainer_group_maximums = trainer_group_maximums SingleByteParam.map_internal_ids = map_internal_ids MultiByteParam.map_internal_ids = map_internal_ids +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)] + def main(rom=None): if not rom: # read the rom and figure out the offsets for maps @@ -6961,8 +6968,7 @@ def main(rom=None): # 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)] + 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) -- cgit v1.2.3 From 8fe3f149d0fd665fc70a0646dad8dfc7aa55ef89 Mon Sep 17 00:00:00 2001 From: Bryan Bishop Date: Fri, 22 Nov 2013 21:10:58 -0600 Subject: comment add_map_offsets_into_map_names --- pokemontools/crystal.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pokemontools/crystal.py b/pokemontools/crystal.py index c195ad9..e763b55 100644 --- a/pokemontools/crystal.py +++ b/pokemontools/crystal.py @@ -6968,6 +6968,7 @@ def main(rom=None): # 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) + # 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 -- cgit v1.2.3 From 7413d97cd85b15131883a9cb694ce69116af59f3 Mon Sep 17 00:00:00 2001 From: Bryan Bishop Date: Fri, 22 Nov 2013 21:12:30 -0600 Subject: rename main -> parse_rom --- pokemontools/crystal.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/pokemontools/crystal.py b/pokemontools/crystal.py index e763b55..0565a1c 100644 --- a/pokemontools/crystal.py +++ b/pokemontools/crystal.py @@ -6596,7 +6596,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 +6614,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 +6633,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 +6653,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 +6669,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] @@ -6960,7 +6960,7 @@ def add_map_offsets_into_map_names(map_group_offsets, map_names=None): # 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)] -def main(rom=None): +def parse_rom(rom=None): if not rom: # read the rom and figure out the offsets for maps rom = direct_load_rom() -- cgit v1.2.3 From 80588db61ce02cbc9128eea1f2522c4b09f799d1 Mon Sep 17 00:00:00 2001 From: Bryan Bishop Date: Fri, 22 Nov 2013 21:16:50 -0600 Subject: fix typo in crystal.py --- pokemontools/crystal.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pokemontools/crystal.py b/pokemontools/crystal.py index 0565a1c..b1e252f 100644 --- a/pokemontools/crystal.py +++ b/pokemontools/crystal.py @@ -6044,7 +6044,7 @@ def find_incbin_to_replace_for(address, debug=False, rom_file=None): incbin = processed_incbins[incbin_key] start = incbin["start"] end = incbin["end"] - nf debug: + if debug: argstuff = { "start": start, "end": end, -- cgit v1.2.3 From 504d9cc896db356012f6bf16894355f19f9469a4 Mon Sep 17 00:00:00 2001 From: Bryan Bishop Date: Fri, 22 Nov 2013 21:28:19 -0600 Subject: fixup TextScript to store script_parse_table ref --- pokemontools/crystal.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/pokemontools/crystal.py b/pokemontools/crystal.py index b1e252f..83ee7dd 100644 --- a/pokemontools/crystal.py +++ b/pokemontools/crystal.py @@ -174,7 +174,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 +249,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 +428,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 +563,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: -- cgit v1.2.3 From bbebec611dbcfe0ea8e3f71188f6c7821878260b Mon Sep 17 00:00:00 2001 From: Bryan Bishop Date: Fri, 22 Nov 2013 21:28:50 -0600 Subject: fix call to item_label_by_id --- pokemontools/crystal.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pokemontools/crystal.py b/pokemontools/crystal.py index 83ee7dd..61142a4 100644 --- a/pokemontools/crystal.py +++ b/pokemontools/crystal.py @@ -778,7 +778,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: -- cgit v1.2.3 From a38a86806b6a72402d07482f7c259c8a4e522762 Mon Sep 17 00:00:00 2001 From: Bryan Bishop Date: Fri, 22 Nov 2013 21:41:11 -0600 Subject: use default config in WRAMProcessor if available Don't override the given configuration. --- pokemontools/wram.py | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/pokemontools/wram.py b/pokemontools/wram.py index e1b9212..1f61ff5 100644 --- a/pokemontools/wram.py +++ b/pokemontools/wram.py @@ -99,9 +99,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): """ -- cgit v1.2.3 From 6292c574f6ed7757d6c43f6b1772fc41f687ccad Mon Sep 17 00:00:00 2001 From: Bryan Bishop Date: Fri, 22 Nov 2013 21:56:24 -0600 Subject: parse wram labels in crystal.py --- pokemontools/crystal.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/pokemontools/crystal.py b/pokemontools/crystal.py index 61142a4..e15d777 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 ---- @@ -6709,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 @@ -6968,6 +6981,9 @@ def parse_rom(rom=None): # 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) -- cgit v1.2.3 From f77f2bd9b801aa18c4e2134bdc0618de4513144e Mon Sep 17 00:00:00 2001 From: Bryan Bishop Date: Fri, 22 Nov 2013 21:56:54 -0600 Subject: include pokecrystal gbhw.asm and hram.asm These files are used when parsing wram and other constants. --- pokemontools/data/pokecrystal/gbhw.asm | 102 +++++++++++++++++++++++++++++++++ pokemontools/data/pokecrystal/hram.asm | 71 +++++++++++++++++++++++ 2 files changed, 173 insertions(+) create mode 100644 pokemontools/data/pokecrystal/gbhw.asm create mode 100644 pokemontools/data/pokecrystal/hram.asm 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 + -- cgit v1.2.3 From 54f5fa07d88d5c3d3abb580ff2ed59c5c2801547 Mon Sep 17 00:00:00 2001 From: Bryan Bishop Date: Fri, 22 Nov 2013 22:02:02 -0600 Subject: fix log message with wrong variable in template --- pokemontools/crystal.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pokemontools/crystal.py b/pokemontools/crystal.py index e15d777..7a5b65e 100644 --- a/pokemontools/crystal.py +++ b/pokemontools/crystal.py @@ -2933,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), -- cgit v1.2.3 From 6e1eb898b872d3abe4997975efcb99cd7506d935 Mon Sep 17 00:00:00 2001 From: Bryan Bishop Date: Fri, 22 Nov 2013 22:10:09 -0600 Subject: make parse_rom return map_names The map_names object is the entry point to the parsed rom data. --- pokemontools/crystal.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pokemontools/crystal.py b/pokemontools/crystal.py index 7a5b65e..5e16056 100644 --- a/pokemontools/crystal.py +++ b/pokemontools/crystal.py @@ -7004,5 +7004,7 @@ def parse_rom(rom=None): # improve duplicate trainer names make_trainer_group_name_trainer_ids(trainer_group_table) + return map_names + if __name__ == "crystal": pass -- cgit v1.2.3 From 0099da43b0ff1d7453715a771b0769b58d4c1b5b Mon Sep 17 00:00:00 2001 From: Bryan Bishop Date: Sat, 23 Nov 2013 01:16:14 -0600 Subject: make a way to remember if parse_rom called --- pokemontools/crystal.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/pokemontools/crystal.py b/pokemontools/crystal.py index 5e16056..2b16284 100644 --- a/pokemontools/crystal.py +++ b/pokemontools/crystal.py @@ -6976,6 +6976,8 @@ def add_map_offsets_into_map_names(map_group_offsets, map_names=None): # 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 @@ -7004,7 +7006,18 @@ def parse_rom(rom=None): # improve duplicate trainer names make_trainer_group_name_trainer_ids(trainer_group_table) + rom_parsed = True + return map_names +def cachably_parse_rom(rom=None): + """ + Calls parse_rom if it hasn't been called and completed yet. + """ + if not rom_parsed: + return parse_rom(rom=rom) + else: + return map_names + if __name__ == "crystal": pass -- cgit v1.2.3 From feef50505e8ff78ca68a09d1f6f535e002db6c13 Mon Sep 17 00:00:00 2001 From: Bryan Bishop Date: Sat, 23 Nov 2013 02:11:07 -0600 Subject: generate images of maps --- pokemontools/map_gfx.py | 246 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 246 insertions(+) create mode 100644 pokemontools/map_gfx.py diff --git a/pokemontools/map_gfx.py b/pokemontools/map_gfx.py new file mode 100644 index 0000000..f851f92 --- /dev/null +++ b/pokemontools/map_gfx.py @@ -0,0 +1,246 @@ +""" +Map-related graphic functions. +""" + +import os + +from PIL import ( + Image, +) + +import crystal +import gfx + +tile_width = 8 +tile_height = 8 +block_width = 4 +block_height = 4 + +# 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 + +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) + +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. + """ + 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] + + 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 + +def read_tiles(tileset_id, palette_map, palettes, config=config): + """ + Opens the tileset png file and reads bytes for each tile in the tileset. + """ + 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): + tile = image.crop((x, y, x + tile_width, y + tile_height)) + + # 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 + +def read_palette_map(tileset_id, config=config): + """ + Loads a palette map. + """ + 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] + + 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 draw_map(map_group_id, map_id, 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) + palettes = read_palettes(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)) + + 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)) + + return map_image + +def save_map(map_group_id, map_id, savedir, 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) + + print "Drawing {}".format(map_name) + map_image = draw_map(map_group_id, map_id, config) + map_image.save(filepath) + + return map_image + +def save_maps(savedir, 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(): + image = save_map(map_group_id, map_id, savedir, config) -- cgit v1.2.3 From a02dd03581baba728b68a20d84b007edd04cf310 Mon Sep 17 00:00:00 2001 From: Bryan Bishop Date: Sat, 23 Nov 2013 02:33:03 -0600 Subject: draw more than one map group --- pokemontools/map_gfx.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pokemontools/map_gfx.py b/pokemontools/map_gfx.py index f851f92..568969f 100644 --- a/pokemontools/map_gfx.py +++ b/pokemontools/map_gfx.py @@ -243,4 +243,5 @@ def save_maps(savedir, config=config): for map_group_id in crystal.map_names.keys(): for map_id in crystal.map_names[map_group_id].keys(): - image = save_map(map_group_id, map_id, savedir, config) + if isinstance(map_id, int): + image = save_map(map_group_id, map_id, savedir, config) -- cgit v1.2.3 From b7c36a0c072390ccb8e5dc2d8ee0605601da8508 Mon Sep 17 00:00:00 2001 From: Bryan Bishop Date: Sat, 23 Nov 2013 03:27:24 -0600 Subject: attempt to improve performance by caching --- pokemontools/map_gfx.py | 30 ++++++++++++++++++++++++++---- 1 file changed, 26 insertions(+), 4 deletions(-) diff --git a/pokemontools/map_gfx.py b/pokemontools/map_gfx.py index 568969f..e1cfd90 100644 --- a/pokemontools/map_gfx.py +++ b/pokemontools/map_gfx.py @@ -51,11 +51,15 @@ def load_png(filepath): """ 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 @@ -72,6 +76,8 @@ def read_blocks(tileset_id, config=config): block = blocksetdata[block_num : block_num + block_length] blocks += [block] + all_blocks[tileset_id] = blocks + return blocks def colorize_tile(tile, palette): @@ -91,10 +97,15 @@ def colorize_tile(tile, palette): 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 @@ -110,7 +121,11 @@ def read_tiles(tileset_id, palette_map, palettes, config=config): for y in xrange(0, image.height, tile_height): for x in xrange(0, image.width, tile_width): - tile = image.crop((x, y, x + tile_width, y + tile_height)) + 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 @@ -122,10 +137,14 @@ def read_tiles(tileset_id, palette_map, palettes, config=config): 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) @@ -137,6 +156,8 @@ def read_palette_map(tileset_id, config=config): 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): @@ -174,7 +195,7 @@ def read_palettes(time_of_day=1, config=config): return palettes -def draw_map(map_group_id, map_id, config=config): +def draw_map(map_group_id, map_id, palettes, config=config): """ Makes a picture of a map. """ @@ -191,7 +212,6 @@ def draw_map(map_group_id, map_id, config=config): blockdata = read_map_blockdata(map_header) palette_map = read_palette_map(tileset_id, config=config) - palettes = read_palettes(config=config) tileset_blocks = read_blocks(tileset_id, config=config) tileset_images = read_tiles(tileset_id, palette_map, palettes, config=config) @@ -229,8 +249,10 @@ def save_map(map_group_id, map_id, savedir, config=config): 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, config) + map_image = draw_map(map_group_id, map_id, palettes, config) map_image.save(filepath) return map_image -- cgit v1.2.3 From de5d9bb1564177dd011398a5651c169c1728ee40 Mon Sep 17 00:00:00 2001 From: Bryan Bishop Date: Sat, 23 Nov 2013 03:40:29 -0600 Subject: sigh, use a global for caching parsed status --- pokemontools/crystal.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pokemontools/crystal.py b/pokemontools/crystal.py index 2b16284..eb88b6b 100644 --- a/pokemontools/crystal.py +++ b/pokemontools/crystal.py @@ -7006,6 +7006,7 @@ def parse_rom(rom=None): # improve duplicate trainer names make_trainer_group_name_trainer_ids(trainer_group_table) + global rom_parsed rom_parsed = True return map_names @@ -7014,6 +7015,7 @@ 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: -- cgit v1.2.3 From 1e2b7cc055e2ddd82f2a92ad9190324ceef56719 Mon Sep 17 00:00:00 2001 From: Bryan Bishop Date: Sat, 23 Nov 2013 18:56:51 -0600 Subject: draw sprites on each map --- pokemontools/map_gfx.py | 122 ++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 117 insertions(+), 5 deletions(-) diff --git a/pokemontools/map_gfx.py b/pokemontools/map_gfx.py index e1cfd90..8ba6926 100644 --- a/pokemontools/map_gfx.py +++ b/pokemontools/map_gfx.py @@ -16,6 +16,10 @@ 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 @@ -28,6 +32,7 @@ def add_pokecrystal_paths_to_configuration(config=config): 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) @@ -195,7 +200,110 @@ def read_palettes(time_of_day=1, config=config): return palettes -def draw_map(map_group_id, map_id, palettes, config=config): +def load_sprite_image(image_id, config=config): + """ + Make standard file path. + """ + overworld_path = os.path.join("./gfx", "overworld") + filepath = os.path.join(overworld_path, "{image_id}.png") + filepath = filepath.format(image_id=str(image_id).zfill(3)) + sprite_image = Image.open(filepath) + + #pal = bytearray(open(os.path.join(config.block_dir, "day.pal"), "rb").read()) + #sprite_image = colorize_tile(sprite_image, pal) + + 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(sprite_count): + rom_bytes = crystal.rom[current_address : current_address + sprite_header_size] + header = [ord(x) for x in rom_bytes] + + 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": {}, + } + + # store the actual metadata + sprites[sprite_id] = sprite + + if sprite_type == WALKING_SPRITE: + # down, up, left, move down, move up, move left + sprite["images"]["down"] = load_sprite_image(current_image_id, config=config) + sprite["images"]["up"] = load_sprite_image(current_image_id + 1, config=config) + sprite["images"]["left"] = load_sprite_image(current_image_id + 2, config=config) + + current_image_id += image_count * 2 + elif sprite_type == STANDING_SPRITE: + # down, up, left + sprite["images"]["down"] = load_sprite_image(current_image_id, config=config) + sprite["images"]["up"] = load_sprite_image(current_image_id + 1, config=config) + sprite["images"]["left"] = load_sprite_image(current_image_id + 2, config=config) + + current_image_id += image_count * 1 + elif sprite_type == STILL_SPRITE: + # just one image + sprite["images"]["still"] = load_sprite_image(current_image_id, config=config) + + current_image_id += image_count * 1 + + 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] + + if sprite_image_id not in sprites.keys(): + print "sprite_image_id {} is not in sprites".format(sprite_image_id) + continue + + 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)) + +def draw_map(map_group_id, map_id, palettes, show_sprites=True, config=config): """ Makes a picture of a map. """ @@ -218,6 +326,7 @@ def draw_map(map_group_id, map_id, 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 @@ -236,9 +345,12 @@ def draw_map(map_group_id, map_id, palettes, config=config): 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, config=config): +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. """ @@ -252,12 +364,12 @@ def save_map(map_group_id, map_id, savedir, config=config): palettes = read_palettes(config=config) print "Drawing {}".format(map_name) - map_image = draw_map(map_group_id, map_id, palettes, config) + 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, config=config): +def save_maps(savedir, show_sprites=True, config=config): """ Draw as many maps as possible. """ @@ -266,4 +378,4 @@ def save_maps(savedir, config=config): 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, config) + image = save_map(map_group_id, map_id, savedir, show_sprites=show_sprites, config=config) -- cgit v1.2.3 From 6b5f469df828e2413834ca556de1b133482bd753 Mon Sep 17 00:00:00 2001 From: Bryan Bishop Date: Sun, 24 Nov 2013 19:28:56 -0600 Subject: read sprite images from rom --- pokemontools/map_gfx.py | 81 ++++++++++++++++++++++++++++++------------------- 1 file changed, 50 insertions(+), 31 deletions(-) diff --git a/pokemontools/map_gfx.py b/pokemontools/map_gfx.py index 8ba6926..77e7d56 100644 --- a/pokemontools/map_gfx.py +++ b/pokemontools/map_gfx.py @@ -3,9 +3,12 @@ Map-related graphic functions. """ import os +import png +from io import BytesIO from PIL import ( Image, + ImageDraw, ) import crystal @@ -200,17 +203,22 @@ def read_palettes(time_of_day=1, config=config): return palettes -def load_sprite_image(image_id, config=config): +def load_sprite_image(address, config=config): """ Make standard file path. """ - overworld_path = os.path.join("./gfx", "overworld") - filepath = os.path.join(overworld_path, "{image_id}.png") - filepath = filepath.format(image_id=str(image_id).zfill(3)) - sprite_image = Image.open(filepath) + pal_file = os.path.join(config.block_dir, "day.pal") - #pal = bytearray(open(os.path.join(config.block_dir, "day.pal"), "rb").read()) - #sprite_image = colorize_tile(sprite_image, 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 @@ -230,10 +238,17 @@ def load_all_sprite_images(config=config): current_image_id = 0 - for sprite_id in xrange(sprite_count): + 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] @@ -247,29 +262,25 @@ def load_all_sprite_images(config=config): "images": {}, } - # store the actual metadata - sprites[sprite_id] = sprite - - if sprite_type == WALKING_SPRITE: + if sprite_type in [WALKING_SPRITE, STANDING_SPRITE]: # down, up, left, move down, move up, move left - sprite["images"]["down"] = load_sprite_image(current_image_id, config=config) - sprite["images"]["up"] = load_sprite_image(current_image_id + 1, config=config) - sprite["images"]["left"] = load_sprite_image(current_image_id + 2, config=config) - - current_image_id += image_count * 2 - elif sprite_type == STANDING_SPRITE: - # down, up, left - sprite["images"]["down"] = load_sprite_image(current_image_id, config=config) - sprite["images"]["up"] = load_sprite_image(current_image_id + 1, config=config) - sprite["images"]["left"] = load_sprite_image(current_image_id + 2, config=config) - - current_image_id += image_count * 1 + 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(current_image_id, config=config) + 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 @@ -291,17 +302,25 @@ def draw_map_sprites(map_header, map_image, config=config): some_pointer = event.params[9] bit_table_bit_number = event.params[10] - if sprite_image_id not in sprites.keys(): + 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) - continue - sprite = sprites[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: 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)) + 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): """ -- cgit v1.2.3 From 80f32f5b379877be7ee58a5961024adcf6ea67b1 Mon Sep 17 00:00:00 2001 From: Bryan Bishop Date: Fri, 29 Nov 2013 18:49:56 -0600 Subject: some code to prep for path planning --- pokemontools/vba/path.py | 47 ++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 44 insertions(+), 3 deletions(-) diff --git a/pokemontools/vba/path.py b/pokemontools/vba/path.py index e861a39..7596b86 100644 --- a/pokemontools/vba/path.py +++ b/pokemontools/vba/path.py @@ -6,6 +6,11 @@ path finding implementation (note that they can be members of multiple zones simultaneously). """ +import pokemontools.configuration +config = pokemontools.configuration.Config() + +import pokemontools.map_gfx + 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 @@ -536,11 +541,12 @@ class Map(object): map. """ - def __init__(self, cry, height, width): + def __init__(self, cry, 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() @@ -549,6 +555,9 @@ class Map(object): self.height = height self.width = width + self.map_group_id = map_group_id + self.map_id = map_id + def travel_to(self, destination_location): """ Does path planning and figures out the quickest way to get to the @@ -574,6 +583,36 @@ class Map(object): """ 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) + + # TODO: draw the given path on the map_image image object + raise NotImplementedError + + 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. + """ + raise NotImplementedError + def broken_main(): """ An attempt at an entry point. This hasn't been sufficiently considered yet. @@ -583,6 +622,8 @@ def broken_main(): # make a graph based on the map nodes = create_graph(current_map) - current_map.travel_to(destination_location) + planner = PathPlanner(current_map, (0, 0), (5, 5)) + path = planner.plan() - return current_map + drawn = current_map.draw_path(path) + return drawn -- cgit v1.2.3 From a077e470b2b24fc00db75aad220cc381f602bb3e Mon Sep 17 00:00:00 2001 From: Bryan Bishop Date: Fri, 29 Nov 2013 23:56:35 -0600 Subject: basic plan for drawing paths This doesn't work yet. --- pokemontools/vba/path.py | 57 ++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 46 insertions(+), 11 deletions(-) diff --git a/pokemontools/vba/path.py b/pokemontools/vba/path.py index 7596b86..2e50d1b 100644 --- a/pokemontools/vba/path.py +++ b/pokemontools/vba/path.py @@ -9,8 +9,14 @@ path finding implementation 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 @@ -541,7 +547,7 @@ class Map(object): map. """ - def __init__(self, cry, height, width, map_group_id, map_id, config=config): + def __init__(self, cry, parsed_map, height, width, map_group_id, map_id, config=config): """ :param cry: pokemon crystal emulation interface :type cry: crystal @@ -552,11 +558,11 @@ class Map(object): self.threat_zones = set() self.obstacles = set() - self.height = height - self.width = width - + 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): """ @@ -591,8 +597,17 @@ class Map(object): 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) - # TODO: draw the given path on the map_image image object - raise NotImplementedError + 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 @@ -611,19 +626,39 @@ class PathPlanner(object): Runs the path planner and returns a list of positions making up the path. """ - raise NotImplementedError + return [(0, 0), (1, 0), (1, 1), (1, 2), (1, 3)] -def broken_main(): +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. """ - current_map = Map.from_wram(cry) + 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) - # make a graph based on the map + # 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) - planner = PathPlanner(current_map, (0, 0), (5, 5)) + # 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 -- cgit v1.2.3 From 79630cd43b8e814afa517b473344d065217c851a Mon Sep 17 00:00:00 2001 From: yenatch Date: Mon, 2 Dec 2013 14:43:43 -0500 Subject: wram: more consistent rgbasm value conversion --- pokemontools/wram.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/pokemontools/wram.py b/pokemontools/wram.py index 1f61ff5..d34737e 100644 --- a/pokemontools/wram.py +++ b/pokemontools/wram.py @@ -9,6 +9,11 @@ import os NUM_OBJECTS = 0x10 OBJECT_LENGTH = 0x10 + +def rgbasm_to_py(text): + return text.replace('$', '0x').replace('%', '0b') + + def make_wram_labels(wram_sections): wram_labels = {} for section in wram_sections: @@ -30,7 +35,7 @@ def read_bss_sections(bss): if 'SECTION' in line: if section: sections.append(section) # last section - address = eval(line[line.find('[')+1:line.find(']')].replace('$','0x')) + address = eval(rgbasm_to_py(line[line.find('[')+1:line.find(']')])) section = { 'name': line.split('"')[1], #'type': line.split(',')[1].split('[')[0].strip(), @@ -49,7 +54,7 @@ def read_bss_sections(bss): }] elif line[:3] == 'ds ': - length = eval(line[3:line.find(';')].replace('$','0x')) + length = eval(rgbasm_to_py(line[3:line.find(';')])) address += length # adjacent labels use the same space for label in section['labels'][::-1]: @@ -61,14 +66,14 @@ def read_bss_sections(bss): elif 'EQU' in line: # some space is defined using constants name, value = line.split('EQU') - name, value = name.strip(), value.strip().replace('$','0x').replace('%','0b') + name, value = name.strip(), rgbasm_to_py(value) globals()[name] = eval(value) sections.append(section) return sections def constants_to_dict(constants): - return dict((eval(constant[constant.find('EQU')+3:constant.find(';')].replace('$','0x')), constant[:constant.find('EQU')].strip()) for constant in constants) + return dict((eval(rgbasm_to_py(constant[constant.find('EQU')+3:constant.find(';')])), constant[:constant.find('EQU')].strip()) for constant in constants) def scrape_constants(text): if type(text) is not list: -- cgit v1.2.3 From 765e846a9eac19008e00a15cf4dcf05653e614c9 Mon Sep 17 00:00:00 2001 From: yenatch Date: Mon, 2 Dec 2013 14:45:52 -0500 Subject: crystal: fix storetext and rework PointerLabel{Before,After}Bank - storetext only takes one param - the logic for PointerLabel*Bank was actually in preprocessor.py. theyve also been changed to take a single label --- pokemontools/crystal.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/pokemontools/crystal.py b/pokemontools/crystal.py index eb88b6b..3385945 100644 --- a/pokemontools/crystal.py +++ b/pokemontools/crystal.py @@ -945,12 +945,19 @@ class PointerLabelParam(MultiByteParam): class PointerLabelBeforeBank(PointerLabelParam): bank = True # bank appears first, see calculate_pointer_from_bytes_at - size = 3 - byte_type = "dw" + byte_type = 'db' + + @staticmethod + def from_asm(value): + return 'BANK({0})\n\tdw {0}'.format(value) class PointerLabelAfterBank(PointerLabelParam): bank = "reverse" # bank appears last, see calculate_pointer_from_bytes_at - size = 3 + byte_type = 'dw' + + @staticmethod + def from_asm(value): + return '{0}\n\tdb BANK({0})'.format(value) class ScriptPointerLabelParam(PointerLabelParam): pass @@ -2358,7 +2365,7 @@ pksv_crystal_more = { 0xA1: ["halloffame"], 0xA2: ["credits"], 0xA3: ["warpfacing", ["facing", SingleByteParam], ["map_group", MapGroupParam], ["map_id", MapIdParam], ["x", SingleByteParam], ["y", SingleByteParam]], - 0xA4: ["storetext", ["pointer", PointerLabelBeforeBank], ["memory", SingleByteParam]], + 0xA4: ["storetext", ["memory", SingleByteParam]], 0xA5: ["displaylocation", ["id", SingleByteParam], ["memory", SingleByteParam]], 0xA6: ["trainerclassname", ["id", SingleByteParam]], 0xA7: ["name", ["type", SingleByteParam], ["id", SingleByteParam]], -- cgit v1.2.3 From 07f7b1b892ac39597cb7e17fa95e3f52e617f5a2 Mon Sep 17 00:00:00 2001 From: yenatch Date: Mon, 2 Dec 2013 14:56:06 -0500 Subject: preprocessor: get rid of PointerLabel cruft now pretty much everything uses from_asm --- pokemontools/preprocessor.py | 46 +++----------------------------------------- 1 file changed, 3 insertions(+), 43 deletions(-) diff --git a/pokemontools/preprocessor.py b/pokemontools/preprocessor.py index f4e92b6..d6b7a43 100644 --- a/pokemontools/preprocessor.py +++ b/pokemontools/preprocessor.py @@ -604,64 +604,24 @@ class Preprocessor(object): if do_macro_sanity_check: self.check_macro_sanity(params, macro, original_line) - # used for storetext - correction = 0 output = "" index = 0 while index < len(params): - param_type = macro.param_types[index - correction] + param_type = macro.param_types[index] description = param_type["name"].strip() param_klass = param_type["class"] byte_type = param_klass.byte_type # db or dw - size = param_klass.size param = params[index].strip() - # param_klass.to_asm() won't work here because it doesn't - # include db/dw. - - # some parameters are really multiple types of bytes - if (byte_type == "dw" and size != 2) or \ - (byte_type == "db" and size != 1): - - output += ("; " + description + "\n") - - if size == 3 and is_based_on(param_klass, "PointerLabelBeforeBank"): - # write the bank first - output += ("db " + param + "\n") - # write the pointer second - output += ("dw " + params[index+1].strip() + "\n") - index += 2 - correction += 1 - elif size == 3 and is_based_on(param_klass, "PointerLabelAfterBank"): - # write the pointer first - output += ("dw " + param + "\n") - # write the bank second - output += ("db " + params[index+1].strip() + "\n") - index += 2 - correction += 1 - elif size == 3 and "from_asm" in dir(param_klass): - output += ("\t" + byte_type + " " + param_klass.from_asm(param) + "\n") - index += 1 - else: - raise exceptions.MacroException( - "dunno what to do with this macro param ({klass}) in line: {line}" - .format( - klass=param_klass, - line=original_line, - ) - ) - - elif "from_asm" in dir(param_klass): + if "from_asm" in dir(param_klass): output += ("\t" + byte_type + " " + param_klass.from_asm(param) + " ; " + description + "\n") - index += 1 - # or just print out the byte else: output += ("\t" + byte_type + " " + param + " ; " + description + "\n") - index += 1 + index += 1 sys.stdout.write(output) -- cgit v1.2.3 From a648e0a8cdc6c7d981a5641f4dc4d5b02e23e81e Mon Sep 17 00:00:00 2001 From: yenatch Date: Mon, 2 Dec 2013 15:08:19 -0500 Subject: preprocessor: remove some redundant code in macro_translator --- pokemontools/preprocessor.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/pokemontools/preprocessor.py b/pokemontools/preprocessor.py index d6b7a43..1fac44d 100644 --- a/pokemontools/preprocessor.py +++ b/pokemontools/preprocessor.py @@ -581,12 +581,6 @@ class Preprocessor(object): if show_original_lines: sys.stdout.write("; original_line: " + original_line) - # rgbasm can handle "db" so no preprocessing is required, plus this wont be - # reached because of earlier checks in macro_test. - if macro.macro_name in ["db", "dw"]: - sys.stdout.write(original_line) - return - # rgbasm can handle other macros too if "is_rgbasm_macro" in dir(macro): if macro.is_rgbasm_macro: -- cgit v1.2.3 From 3a586ef165b8022b0f5159dfdd586789f3166e70 Mon Sep 17 00:00:00 2001 From: yenatch Date: Mon, 2 Dec 2013 15:15:19 -0500 Subject: preprocessor: simplify macro_translator --- pokemontools/preprocessor.py | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/pokemontools/preprocessor.py b/pokemontools/preprocessor.py index 1fac44d..e032125 100644 --- a/pokemontools/preprocessor.py +++ b/pokemontools/preprocessor.py @@ -598,24 +598,18 @@ class Preprocessor(object): if do_macro_sanity_check: self.check_macro_sanity(params, macro, original_line) - output = "" - - index = 0 - while index < len(params): + for index in xrange(len(params)): param_type = macro.param_types[index] description = param_type["name"].strip() param_klass = param_type["class"] - byte_type = param_klass.byte_type # db or dw + byte_type = param_klass.byte_type param = params[index].strip() if "from_asm" in dir(param_klass): - output += ("\t" + byte_type + " " + param_klass.from_asm(param) + " ; " + description + "\n") - - else: - output += ("\t" + byte_type + " " + param + " ; " + description + "\n") + param = param_klass.from_asm(param) - index += 1 + output += ("\t" + byte_type + " " + param + " ; " + description + "\n") sys.stdout.write(output) -- cgit v1.2.3 From fef1cdd4f2d5760ce6742e40cb256296f597b836 Mon Sep 17 00:00:00 2001 From: yenatch Date: Mon, 2 Dec 2013 23:48:24 -0500 Subject: preprocessor: str.split() already removes preceding/trailing whitespace --- pokemontools/preprocessor.py | 20 ++++---------------- 1 file changed, 4 insertions(+), 16 deletions(-) diff --git a/pokemontools/preprocessor.py b/pokemontools/preprocessor.py index e032125..5ac7724 100644 --- a/pokemontools/preprocessor.py +++ b/pokemontools/preprocessor.py @@ -553,29 +553,17 @@ class Preprocessor(object): original_line = line - # remove trailing newline - if line[-1] == "\n": - line = line[:-1] + has_tab = line[0] == "\t" - # remove first tab - has_tab = False - if line[0] == "\t": - has_tab = True - line = line[1:] - - # remove duplicate whitespace (also trailing) + # remove whitespace line = " ".join(line.split()) - params = [] - # check if the line has params if " " in line: # split the line into separate parameters params = line.replace(token, "").split(",") - - # check if there are no params (redundant) - if len(params) == 1 and params[0] == "": - raise exceptions.MacroException("macro has no params?") + else: + params = [] # write out a comment showing the original line if show_original_lines: -- cgit v1.2.3 From a401ebc620fdca69aab2ed8feb91fa696647b17c Mon Sep 17 00:00:00 2001 From: yenatch Date: Wed, 4 Dec 2013 01:12:45 -0500 Subject: crystal: make PointerLabelParam output just a label to match the new from_asm handling --- pokemontools/crystal.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/pokemontools/crystal.py b/pokemontools/crystal.py index 3385945..eed7c32 100644 --- a/pokemontools/crystal.py +++ b/pokemontools/crystal.py @@ -853,8 +853,6 @@ class PointerLabelParam(MultiByteParam): # bank can be overriden if "bank" in kwargs.keys(): if kwargs["bank"] != False and kwargs["bank"] != None and kwargs["bank"] in [True, "reverse"]: - # not +=1 because child classes set size=3 already - self.size = self.default_size + 1 self.given_bank = kwargs["bank"] #if kwargs["bank"] not in [None, False, True, "reverse"]: # raise Exception("bank cannot be: " + str(kwargs["bank"])) @@ -927,8 +925,11 @@ class PointerLabelParam(MultiByteParam): bank_part = "$%.2x" % (pointers.calculate_bank(caddress)) else: bank_part = "BANK("+label+")" + # for labels, expand bank_part at build time + if bank in ["reverse", True] and label: + return pointer_part # return the asm based on the order the bytes were specified to be in - if bank == "reverse": # pointer, bank + elif bank == "reverse": # pointer, bank return pointer_part+", "+bank_part elif bank == True: # bank, pointer return bank_part+", "+pointer_part @@ -944,6 +945,7 @@ class PointerLabelParam(MultiByteParam): raise Exception("this should never happen") class PointerLabelBeforeBank(PointerLabelParam): + size = 3 bank = True # bank appears first, see calculate_pointer_from_bytes_at byte_type = 'db' @@ -952,6 +954,7 @@ class PointerLabelBeforeBank(PointerLabelParam): return 'BANK({0})\n\tdw {0}'.format(value) class PointerLabelAfterBank(PointerLabelParam): + size = 3 bank = "reverse" # bank appears last, see calculate_pointer_from_bytes_at byte_type = 'dw' -- cgit v1.2.3 From 0fd121a81e1cbbf1a42e72a5d7c9e8099ef59f97 Mon Sep 17 00:00:00 2001 From: yenatch Date: Thu, 5 Dec 2013 16:38:31 -0500 Subject: wram: try to determine addresses for section defs without any --- pokemontools/wram.py | 38 ++++++++++++++++++++++++++++++++------ 1 file changed, 32 insertions(+), 6 deletions(-) diff --git a/pokemontools/wram.py b/pokemontools/wram.py index 60001aa..e467a01 100644 --- a/pokemontools/wram.py +++ b/pokemontools/wram.py @@ -14,22 +14,48 @@ def make_wram_labels(wram_sections): wram_labels[label['address']] += [label['label']] return wram_labels +def bracket_value(string, i=0): + return string.split('[')[1 + i*2].split(']')[0] + def read_bss_sections(bss): sections = [] section = { - "labels": [], } address = None if type(bss) is not list: bss = bss.split('\n') for line in bss: line = line.lstrip() - if 'SECTION' in line: - if section: sections.append(section) # last section + if 'SECTION' == line[:7]: + if section: # previous + sections += [section] + + section_def = line.split(',') + name = section_def[0].split('"')[1] + type_ = section_def[1].strip() + if len(section_def) > 2: + bank = bracket_value(section_def[2]) + else: + bank = None + + if '[' in type_: + address = int(bracket_value(type_).replace('$','0x'), 16) + else: + if address == None or bank != section['bank']: + for type__, addr in [ + ('VRAM', 0x8000), + ('SRAM', 0xa000), + ('WRAM0', 0xc000), + ('WRAMX', 0xd000), + ('HRAM', 0xff80), + ]: + if type__ == type_ and section['type'] == type__: + address = addr + # else: keep going from this address - address = eval(line[line.find('[')+1:line.find(']')].replace('$','0x')) section = { - 'name': line.split('"')[1], - #'type': line.split(',')[1].split('[')[0].strip(), + 'name': name, + 'type': type_, + 'bank': bank, 'start': address, 'labels': [], } -- cgit v1.2.3 From 806fd657c2424a7328c21a4246da038a6dafed0e Mon Sep 17 00:00:00 2001 From: yenatch Date: Thu, 5 Dec 2013 20:33:43 -0500 Subject: use file includes in rgbasm objects to generate dependencies --- pokemontools/scan_includes.py | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 pokemontools/scan_includes.py diff --git a/pokemontools/scan_includes.py b/pokemontools/scan_includes.py new file mode 100644 index 0000000..138f011 --- /dev/null +++ b/pokemontools/scan_includes.py @@ -0,0 +1,34 @@ +# coding: utf-8 + +""" +Recursively scan an asm file for rgbasm INCLUDEs and INCBINs. +Used to generate dependencies for each rgbasm object. +""" + +import os +import sys + +import configuration +conf = configuration.Config() + +def recursive_scan(filename, includes = []): + if (filename[-4:] == '.asm' or filename[-3] == '.tx') and os.path.exists(filename): + lines = open(filename).readlines() + for line in lines: + for directive in ('INCLUDE', 'INCBIN'): + if directive in line: + line = line[:line.find(';')] + if directive in line: + include = line.split('"')[1] + if include not in includes: + includes += [include] + includes = recursive_scan(include, includes) + break + return includes + +if __name__ == '__main__': + filenames = sys.argv[1:] + for filename in filenames: + dependencies = recursive_scan(os.path.join(conf.path, filename)) + sys.stdout.write(' '.join(dependencies)) + -- cgit v1.2.3 From 1be104b8e8beb964783fdd7de82569c291f9cdfc Mon Sep 17 00:00:00 2001 From: yenatch Date: Fri, 6 Dec 2013 19:39:34 -0500 Subject: sym: rgbds does ram banks now --- pokemontools/sym.py | 33 ++++++++++++++------------------- 1 file changed, 14 insertions(+), 19 deletions(-) diff --git a/pokemontools/sym.py b/pokemontools/sym.py index ebd8532..b1e755f 100644 --- a/pokemontools/sym.py +++ b/pokemontools/sym.py @@ -4,7 +4,7 @@ import os import sys import json -def make_sym_from_json(filename = '../pokecrystal.sym', j = 'labels.json'): +def make_sym_from_json(filename = 'pokecrystal.sym', j = 'labels.json'): output = '' labels = json.load(open(j)) for label in labels: @@ -12,13 +12,13 @@ def make_sym_from_json(filename = '../pokecrystal.sym', j = 'labels.json'): with open(filename, 'w') as sym: sym.write(output) -def make_json_from_mapfile(filename='labels.json', mapfile='../pokecrystal.map'): +def make_json_from_mapfile(filename='labels.json', mapfile='pokecrystal.map'): output = [] labels = filter_wram_addresses(read_mapfile(mapfile)) with open(filename, 'w') as out: out.write(json.dumps(labels)) -def read_mapfile(filename='../pokecrystal.map'): +def read_mapfile(filename='pokecrystal.map'): """ Scrape label addresses from an rgbds mapfile. """ @@ -29,9 +29,15 @@ def read_mapfile(filename='../pokecrystal.map'): lines = mapfile.readlines() for line in lines: - # bank # - if 'Bank #' in line: - cur_bank = int(line.lstrip('Bank #').strip(';\n').strip(' (HOME)')) + if line[0].strip(): # section type def + section_type = line.split(' ')[0] + if section_type == 'Bank': # ROM + cur_bank = int(line.split(' ')[1].split(':')[0][1:]) + elif section_type in ['WRAM0', 'HRAM']: + cur_bank = 0 + elif section_type in ['WRAM, VRAM']: + cur_bank = int(line.split(' ')[2].split(':')[0][1:]) + cur_bank = int(line.split(' ')[2].split(':')[0][1:]) # label definition elif '=' in line: @@ -39,21 +45,10 @@ def read_mapfile(filename='../pokecrystal.map'): address = int(address.lstrip().replace('$', '0x'), 16) label = label.strip() - # rgbds doesn't support ram banks yet bank = cur_bank offset = address - - ranges = [ - 0x8000 <= address < 0xa000, - 0xa000 <= address < 0xc000, - 0xc000 <= address < 0xd000, - 0xd000 <= address < 0xe000, - ] - - if any(ranges): - bank = 0 - else: - offset += (bank * 0x4000 - 0x4000) if bank > 0 else 0 + if address < 0x8000 and bank: # ROM + offset += (bank - 1) * 0x4000 labels += [{ 'label': label, -- cgit v1.2.3 From 460171a2c52d6fcd591559ab1c5fff09e9bc49cb Mon Sep 17 00:00:00 2001 From: yenatch Date: Fri, 6 Dec 2013 19:42:58 -0500 Subject: wram: cleaner line parsing fixes 0fd121a8 --- pokemontools/wram.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/pokemontools/wram.py b/pokemontools/wram.py index e467a01..7bc017d 100644 --- a/pokemontools/wram.py +++ b/pokemontools/wram.py @@ -20,11 +20,19 @@ def bracket_value(string, i=0): def read_bss_sections(bss): sections = [] section = { + 'name': None, + 'type': None, + 'bank': None, + 'start': None, + 'labels': [], } address = None if type(bss) is not list: bss = bss.split('\n') for line in bss: - line = line.lstrip() + + comment_index = line.find(';') + line, comment = line[:comment_index].lstrip(), line[comment_index:] + if 'SECTION' == line[:7]: if section: # previous sections += [section] @@ -71,7 +79,7 @@ def read_bss_sections(bss): }] elif line[:3] == 'ds ': - length = eval(line[3:line.find(';')].replace('$','0x')) + length = eval(line[3:].replace('$','0x')) address += length # adjacent labels use the same space for label in section['labels'][::-1]: -- cgit v1.2.3 From 05b622e7c019a97bb69007a43de733f64076e59a Mon Sep 17 00:00:00 2001 From: yenatch Date: Fri, 6 Dec 2013 19:44:02 -0500 Subject: labels: grab labels from a mapfile instead of an old json dump labels.json had to be deleted manually to be updated, and rgbasm is better at scanning labels anyway --- pokemontools/labels.py | 26 ++++++++++++++++---------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/pokemontools/labels.py b/pokemontools/labels.py index 96e34b9..87e9990 100644 --- a/pokemontools/labels.py +++ b/pokemontools/labels.py @@ -8,33 +8,39 @@ import json import logging import pointers +import sym class Labels(object): """ Store all labels. """ - filename = "labels.json" - def __init__(self, config): + def __init__(self, config, filename="pokecrystal.map"): """ Setup the instance. """ self.config = config - self.path = os.path.join(self.config.path, Labels.filename) + self.filename = filename + self.path = os.path.join(self.config.path, self.filename) def initialize(self): """ Handle anything requiring file-loading and such. """ + # Look for a mapfile if it's not given if not os.path.exists(self.path): - logging.info( - "Running crystal.scan_for_predefined_labels to create \"{0}\". Trying.." - .format(Labels.filename) - ) - import crystal - crystal.scan_for_predefined_labels() + self.filename = find_mapfile_in_dir(self.config.path) + if self.filename == None: + raise Exception, "Couldn't find any mapfiles. Run rgblink -m to create a mapfile." + self.path = os.path.join(self.config.path, self.filename) - self.labels = json.read(open(self.path, "r").read()) + self.labels = sym.read_mapfile(self.path) + +def find_mapfile_in_dir(path): + for filename in os.listdir(path): + if os.path.splitext(filename)[1] == '.map': + return filename + return None def remove_quoted_text(line): """get rid of content inside quotes -- cgit v1.2.3 From f5ddc3370560a4b5f58ea79a513d4bf1207fa9f4 Mon Sep 17 00:00:00 2001 From: yenatch Date: Sat, 7 Dec 2013 05:05:54 -0500 Subject: preprocessor: dont write to globals.asm for each process --- pokemontools/preprocessor.py | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/pokemontools/preprocessor.py b/pokemontools/preprocessor.py index 5fe0851..24a74e1 100644 --- a/pokemontools/preprocessor.py +++ b/pokemontools/preprocessor.py @@ -483,22 +483,18 @@ class Preprocessor(object): for l in lines: self.read_line(l) - self.update_globals() - def update_globals(self): """ Add any labels not already in globals.asm. """ - # TODO: pokered needs to be fixed - try: - globes = open(os.path.join(self.config.path, 'globals.asm'), 'r+') + path = os.path.join(self.config.path, 'globals.asm') + if os.path.exists(path): + globes = open(path, 'r+') lines = globes.readlines() for globe in self.globes: line = 'GLOBAL ' + globe + '\n' if line not in lines: globes.write(line) - except Exception as exception: - pass # don't care if it's not there... def read_line(self, l): """ -- cgit v1.2.3 From f35bb2c5cc390ec0008cede2721104592dbcb29d Mon Sep 17 00:00:00 2001 From: yenatch Date: Sun, 8 Dec 2013 01:26:09 -0500 Subject: scan_includes: join names properly and actually use conf.path --- pokemontools/scan_includes.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/pokemontools/scan_includes.py b/pokemontools/scan_includes.py index 138f011..7f34e92 100644 --- a/pokemontools/scan_includes.py +++ b/pokemontools/scan_includes.py @@ -22,13 +22,15 @@ def recursive_scan(filename, includes = []): include = line.split('"')[1] if include not in includes: includes += [include] - includes = recursive_scan(include, includes) + includes = recursive_scan(os.path.join(conf.path, include), includes) break return includes if __name__ == '__main__': filenames = sys.argv[1:] + dependencies = [] for filename in filenames: - dependencies = recursive_scan(os.path.join(conf.path, filename)) - sys.stdout.write(' '.join(dependencies)) + dependencies += recursive_scan(os.path.join(conf.path, filename)) + dependencies = list(set(dependencies)) + sys.stdout.write(' '.join(dependencies)) -- cgit v1.2.3 From 697e2fa2ade2face0756f17858057548071bf506 Mon Sep 17 00:00:00 2001 From: yenatch Date: Sun, 8 Dec 2013 17:19:38 -0500 Subject: audio: sort output by content --- pokemontools/audio.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pokemontools/audio.py b/pokemontools/audio.py index 38fd65f..0e7d375 100644 --- a/pokemontools/audio.py +++ b/pokemontools/audio.py @@ -26,7 +26,7 @@ conf = configuration.Config() def sort_asms(asms): """sort and remove duplicates from a list of tuples format (address, asm, last_address)""" - return sorted(set(asms), key=lambda (x,y,z):(x,z,not y.startswith(';'), ':' not in y)) + return sorted(set(asms), key=lambda (x,y,z):(x,z,not y.startswith(';'), ':' not in y, y)) class NybbleParam: size = 0.5 -- cgit v1.2.3 From b0fa67a51191a9ddc2a4ce548ade0243b6be554c Mon Sep 17 00:00:00 2001 From: yenatch Date: Wed, 11 Dec 2013 20:28:25 -0500 Subject: wram: fix section address allocation who wrote this? --- pokemontools/wram.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/pokemontools/wram.py b/pokemontools/wram.py index 7bc017d..87af4a2 100644 --- a/pokemontools/wram.py +++ b/pokemontools/wram.py @@ -48,16 +48,16 @@ def read_bss_sections(bss): if '[' in type_: address = int(bracket_value(type_).replace('$','0x'), 16) else: - if address == None or bank != section['bank']: - for type__, addr in [ - ('VRAM', 0x8000), - ('SRAM', 0xa000), - ('WRAM0', 0xc000), - ('WRAMX', 0xd000), - ('HRAM', 0xff80), - ]: - if type__ == type_ and section['type'] == type__: - address = addr + types = { + 'VRAM': 0x8000, + 'SRAM': 0xa000, + 'WRAM0': 0xc000, + 'WRAMX': 0xd000, + 'HRAM': 0xff80, + } + if address == None or bank != section['bank'] or section['type'] != type_: + if type_ in types.keys(): + address = types[type_] # else: keep going from this address section = { -- cgit v1.2.3 From 049891c4edc4c78e649be3d925e28e057072e142 Mon Sep 17 00:00:00 2001 From: yenatch Date: Sun, 8 Dec 2013 22:16:20 -0500 Subject: battle animations --- pokemontools/battle_animations.py | 294 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 294 insertions(+) create mode 100644 pokemontools/battle_animations.py diff --git a/pokemontools/battle_animations.py b/pokemontools/battle_animations.py new file mode 100644 index 0000000..96f0090 --- /dev/null +++ b/pokemontools/battle_animations.py @@ -0,0 +1,294 @@ +# coding: utf-8 + +import os +from new import classobj + +import configuration +conf = configuration.Config() + +from crystal import ( + SingleByteParam, + PointerLabelParam, + DecimalParam, + BigEndianParam, + Command, + load_rom +) + +from gbz80disasm import get_local_address, get_global_address +from audio import sort_asms + + +from wram import read_constants + +rom = bytearray(load_rom()) + +sfx_constants = read_constants(os.path.join(conf.path, 'constants/sfx_constants.asm')) +class SoundEffectParam(SingleByteParam): + def to_asm(self): + if self.byte in sfx_constants.keys(): + sfx_constant = sfx_constants[self.byte] + return sfx_constant + return SingleByteParam.to_asm(self) + +anim_gfx_constants = read_constants(os.path.join(conf.path, 'constants/gfx_constants.asm')) +class AnimGFXParam(SingleByteParam): + def to_asm(self): + if self.byte in anim_gfx_constants.keys(): + return anim_gfx_constants[self.byte] + return SingleByteParam.to_asm(self) + +anims = read_constants(os.path.join(conf.path, 'constants/animation_constants.asm')) +objs = { k: v for k, v in anims.items() if 'ANIM_OBJ' in v } +bgs = { k: v for k, v in anims.items() if 'ANIM_BG' in v } +anims = { k: v.replace('ANIM_','') for k, v in anims.items() } +from move_constants import moves +anims.update(moves) + +class AnimObjParam(SingleByteParam): + def to_asm(self): + if self.byte in objs.keys(): + return objs[self.byte] + return SingleByteParam.to_asm(self) + +class BGEffectParam(SingleByteParam): + def to_asm(self): + if self.byte in bgs.keys(): + return bgs[self.byte] + return SingleByteParam.to_asm(self) + + +battle_animation_commands = { + 0xd0: ['anim_obj', ['obj', AnimObjParam], ['x', DecimalParam], ['y', DecimalParam], ['param', SingleByteParam]], + 0xd1: ['anim_1gfx', ['gfx1', AnimGFXParam]], + 0xd2: ['anim_2gfx', ['gfx1', AnimGFXParam], ['gfx2', AnimGFXParam]], + 0xd3: ['anim_3gfx', ['gfx1', AnimGFXParam], ['gfx2', AnimGFXParam], ['gfx3', AnimGFXParam]], + 0xd4: ['anim_4gfx', ['gfx1', AnimGFXParam], ['gfx2', AnimGFXParam], ['gfx3', AnimGFXParam], ['gfx4', AnimGFXParam]], + 0xd5: ['anim_5gfx', ['gfx1', AnimGFXParam], ['gfx2', AnimGFXParam], ['gfx3', AnimGFXParam], ['gfx4', AnimGFXParam], ['gfx5', AnimGFXParam]], + 0xd6: ['anim_incobj', ['id', SingleByteParam]], + 0xd7: ['anim_setobj', ['id', SingleByteParam], ['obj', AnimObjParam]], # bug: second param is interpreted as a command if not found in the object array + 0xd8: ['anim_incbgeffect', ['effect', BGEffectParam]], + 0xd9: ['anim_enemyfeetobj'], + 0xda: ['anim_playerheadobj'], + 0xdb: ['anim_checkpokeball'], + 0xdc: ['anim_transform'], + 0xdd: ['anim_raisesub'], + 0xde: ['anim_dropsub'], + 0xdf: ['anim_resetobp0'], + 0xe0: ['anim_sound', ['tracks', SingleByteParam], ['id', SoundEffectParam]], + 0xe1: ['anim_cry', ['pitch', SingleByteParam]], + 0xe2: ['anim_minimizeopp'], # unused + 0xe3: ['anim_oamon'], + 0xe4: ['anim_oamoff'], + 0xe5: ['anim_clearobjs'], + 0xe6: ['anim_beatup'], + 0xe7: ['anim_0xe7'], # nothing + 0xe8: ['anim_updateactorpic'], + 0xe9: ['anim_minimize'], + 0xea: ['anim_0xea'], # nothing + 0xeb: ['anim_0xeb'], # nothing + 0xec: ['anim_0xec'], # nothing + 0xed: ['anim_0xed'], # nothing + 0xee: ['anim_jumpand', ['value', SingleByteParam], ['address', PointerLabelParam]], + 0xef: ['anim_jumpuntil', ['address', PointerLabelParam]], + 0xf0: ['anim_bgeffect', ['effect', BGEffectParam], ['unknown', SingleByteParam], ['unknown', SingleByteParam], ['unknown', SingleByteParam]], + 0xf1: ['anim_bgp', ['colors', SingleByteParam]], + 0xf2: ['anim_obp0', ['colors', SingleByteParam]], + 0xf3: ['anim_obp1', ['colors', SingleByteParam]], + 0xf4: ['anim_clearsprites'], + 0xf5: ['anim_0xf5'], # nothing + 0xf6: ['anim_0xf6'], # nothing + 0xf7: ['anim_0xf7'], # nothing + 0xf8: ['anim_jumpif', ['value', SingleByteParam], ['address', PointerLabelParam]], + 0xf9: ['anim_setvar', ['value', SingleByteParam]], + 0xfa: ['anim_incvar'], + 0xfb: ['anim_jumpvar', ['value', SingleByteParam], ['address', PointerLabelParam]], + 0xfc: ['anim_jump', ['address', PointerLabelParam]], + 0xfd: ['anim_loop', ['count', SingleByteParam], ['address', PointerLabelParam]], + 0xfe: ['anim_call', ['address', PointerLabelParam]], + 0xff: ['anim_ret'], +} + +battle_animation_enders = [ + 'anim_jump', + 'anim_ret', +] + +def create_battle_animation_classes(): + classes = [] + for cmd, command in battle_animation_commands.items(): + cmd_name = command[0] + params = { + 'id': cmd, + 'size': 1, + 'end': cmd_name in battle_animation_enders, + 'macro_name': cmd_name, + 'param_types': {}, + } + for i, (name, class_) in enumerate(command[1:]): + params['param_types'][i] = {'name': name, 'class': class_} + params['size'] += class_.size + class_name = cmd_name + 'Command' + class_ = classobj(class_name, (Command,), params) + globals()[class_name] = class_ + classes += [class_] + return classes + +battle_animation_classes = create_battle_animation_classes() + + +class Wait(Command): + macro_name = 'anim_wait' + size = 1 + end = macro_name in battle_animation_enders + param_types = { + 0: {'name': 'duration', 'class': DecimalParam}, + } + override_byte_check = True + + +class BattleAnim: + + def __init__(self, address, base_label=None, label=None, used_labels=[]): + self.start_address = address + self.address = address + + self.base_label = base_label + if self.base_label == None: + self.base_label = 'BattleAnim_' + hex(self.start_address) + + self.label = label + if self.label == None: + self.label = self.base_label + + self.used_labels = used_labels + + self.output = [] + self.labels = [] + self.label_asm = ( + self.start_address, + '%s: ; %x' % (self.label, self.start_address), + self.start_address + ) + self.labels += [self.label_asm] + self.used_labels += [self.label_asm] + + self.parse() + + def parse(self): + + done = False + while not done: + cmd = rom[self.address] + class_ = self.get_command_class(cmd)(address=self.address) + asm = class_.to_asm() + + # label jumps/calls + for key, param in class_.param_types.items(): + if param['class'] == PointerLabelParam: + label_address = class_.params[key].parsed_address + label = '%s_branch_%x' % (self.base_label, label_address) + label_def = '%s: ; %x' % (label, label_address) + label_asm = (label_address, label_def, label_address) + if label_asm not in self.used_labels: + self.labels += [label_asm] + asm = asm.replace('$%x' % get_local_address(label_address), label) + + self.output += [(self.address, '\t' + asm, self.address + class_.size)] + self.address += class_.size + + done = class_.end + # infinite loops are enders + if class_.macro_name == 'anim_loop': + if class_.params[0].byte == 0: + done = True + + # last_address comment + self.output += [(self.address, '; %x\n' % self.address, self.address)] + + # parse any other branches too + self.labels = list(set(self.labels)) + for address, asm, last_address in self.labels: + if not (self.start_address <= address < self.address) and (address, asm, last_address) not in self.used_labels: + self.used_labels += [(address, asm, last_address)] + sub = BattleAnim(address=address, base_label=self.base_label, label=asm.split(':')[0], used_labels=self.used_labels) + self.output += sub.output + self.labels += sub.labels + + self.output = list(set(self.output)) + self.labels = list(set(self.labels)) + + def to_asm(self): + output = sorted(self.output + self.labels, key = lambda (x, y, z): (x, z)) + text = '' + for (address, asm, last_address) in output: + text += asm + '\n' + #text += '; %x\n' % last_address + return text + + def get_command_class(self, cmd): + if cmd < 0xd0: + return Wait + for class_ in battle_animation_classes: + if class_.id == cmd: + return class_ + return None + + +def battle_anim_label(i): + if i in anims.keys(): + base_label = 'BattleAnim_%s' % anims[i].title().replace('_','') + else: + base_label = 'BattleAnim_%d' % i + return base_label + +def dump_battle_anims(table_address=0xc906f, num_anims=278): + """ + Dump each battle animation from a pointer table. + """ + + asms = [] + + asms += [(table_address, 'BattleAnimations: ; %x' % table_address, table_address)] + + address = table_address + bank = address / 0x4000 + + for i in xrange(num_anims): + pointer_address = address + anim_address = rom[pointer_address] + rom[pointer_address + 1] * 0x100 + anim_address = get_global_address(anim_address, bank) + base_label = battle_anim_label(i) + address += 2 + + # anim pointer + asms += [(pointer_address, '\tdw %s' % base_label, address)] + + # anim script + anim = BattleAnim(address=anim_address, base_label=base_label) + asms += anim.output + anim.labels + + asms += [(address, '; %x\n' % address, address)] + + # jp sonicboom + anim = BattleAnim(address=0xc9c00, base_label='BattleAnim_Sonicboom_JP') + asms += anim.output + anim.labels + + asms = sort_asms(asms) + return asms + +def print_asm_list(asms): + # incbin any unknown areas + # not really needed since there are no gaps + last = asms[0][0] + for addr, asm, last_addr in asms: + if addr > last: + print '\nINCBIN "baserom.gbc", $%x, $%x - $%x\n\n' % (last, addr, last) + if addr >= last: + print asm + last = last_addr + +if __name__ == '__main__': + asms = dump_battle_anims() + print_asm_list(asms) + -- cgit v1.2.3 From 973a78167b104479bb6c8c4c528ae914106b1b53 Mon Sep 17 00:00:00 2001 From: yenatch Date: Thu, 19 Dec 2013 19:35:21 -0500 Subject: gfx: use RGB asm macros for .pal files simpler to edit by hand than bin dumps --- pokemontools/gfx.py | 108 ++++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 80 insertions(+), 28 deletions(-) diff --git a/pokemontools/gfx.py b/pokemontools/gfx.py index 8397337..e452da4 100644 --- a/pokemontools/gfx.py +++ b/pokemontools/gfx.py @@ -1012,7 +1012,7 @@ def get_uncompressed_gfx(start, num_tiles, filename): -def hex_to_rgb(word): +def bin_to_rgb(word): red = word & 0b11111 word >>= 5 green = word & 0b11111 @@ -1020,23 +1020,39 @@ def hex_to_rgb(word): blue = word & 0b11111 return (red, green, blue) -def grab_palettes(address, length=0x80): +def rgb_from_rom(address, length=0x80): + return convert_binary_pal_to_text(rom[address:address+length]) + +def convert_binary_pal_to_text_by_filename(filename): + with open(filename) as f: + pal = bytearray(f.read()) + return convert_binary_pal_to_text(pal) + +def convert_binary_pal_to_text(pal): output = '' - for word in range(length/2): - color = ord(rom[address+1])*0x100 + ord(rom[address]) - address += 2 - color = hex_to_rgb(color) - red = str(color[0]).zfill(2) - green = str(color[1]).zfill(2) - blue = str(color[2]).zfill(2) - output += '\tRGB '+red+', '+green+', '+blue + words = [hi * 0x100 + lo for lo, hi in zip(pal[::2], pal[1::2])] + for word in words: + red, green, blue = ['%.2d' % c for c in bin_to_rgb(word)] + output += '\tRGB ' + ', '.join((red, green, blue)) output += '\n' return output +def read_rgb_macros(lines): + colors = [] + for line in lines: + macro = line.split(" ")[0].strip() + if macro == 'RGB': + params = ' '.join(line.split(" ")[1:]).split(',') + red, green, blue = [int(v) for v in params] + colors += [[red, green, blue]] + return colors - - +def rewrite_binary_pals_to_text(filenames): + for filename in filenames: + pal_text = convert_binary_pal_to_text_by_filename(filename) + with open(filename, 'w') as out: + out.write(pal_text) def dump_monster_pals(): @@ -1147,6 +1163,9 @@ def to_lines(image, width): def dmg2rgb(word): + """ + For PNGs. + """ def shift(value): while True: yield value & (2**5 - 1) @@ -1159,27 +1178,49 @@ def dmg2rgb(word): def rgb_to_dmg(color): + """ + For PNGs. + """ word = (color['r'] / 8) word += (color['g'] / 8) << 5 word += (color['b'] / 8) << 10 return word -def png_pal(filename): - with open(filename, 'rb') as pal_data: - words = pal_data.read() - dmg_pals = [] - for word in range(len(words)/2): - dmg_pals.append(ord(words[word*2]) + ord(words[word*2+1])*0x100) +def pal_to_png(filename): + """ + Interpret a .pal file as a png palette. + """ + with open(filename) as rgbs: + colors = read_rgb_macros(rgbs.readlines()) + a = 255 palette = [] + for color in colors: + # even distribution over 000-255 + r, g, b = [int(hue * 8.25) for hue in color] + palette += [(r, g, b, a)] white = (255,255,255,255) black = (000,000,000,255) - for word in dmg_pals: palette += [dmg2rgb(word)] - if white not in dmg_pals and len(palette) < 4: palette = [white] + palette - if black not in dmg_pals and len(palette) < 4: palette += [black] + if white not in palette and len(palette) < 4: + palette = [white] + palette + if black not in palette and len(palette) < 4: + palette = palette + [black] return palette +def png_to_rgb(palette): + """ + Convert a png palette to rgb macros. + """ + output = '' + for color in palette: + r, g, b = [color[c] / 8 for c in 'rgb'] + output += '\tRGB ' + ', '.join(['%.2d' % hue for hue in (r, g, b)]) + output += '\n' + return output + + + def export_2bpp_to_png(filein, fileout=None, pal_file=None, height=0, width=0): if fileout == None: fileout = os.path.splitext(filein)[0] + '.png' @@ -1234,7 +1275,7 @@ def convert_2bpp_to_png(image, width=0, height=0, pal_file=None): px_map = [[3 - pixel for pixel in line] for line in lines] else: # gbc color - palette = png_pal(pal_file) + palette = pal_to_png(pal_file) greyscale = False bitdepth = 8 px_map = [[pixel for pixel in line] for line in lines] @@ -1371,13 +1412,22 @@ def png_to_2bpp(filein): def export_palette(palette, filename): + """ + Export a palette from png to rgb macros in a .pal file. + """ + if os.path.exists(filename): - output = [] - for color in palette: - word = rgb_to_dmg(color) - output += [word & 0xff] - output += [word >> 8] - to_file(filename, output) + + # Pic palettes are 2 colors (black/white are added later). + with open(filename) as rgbs: + colors = read_rgb_macros(rgbs.readlines()) + + if len(colors) == 2: + palette = palette[1:3] + + text = png_to_rgb(palette) + with open(filename, 'w') as out: + out.write(text) def png_to_lz(filein): @@ -1566,6 +1616,8 @@ def expand_pic_palettes(): with open(filename, 'wb') as out: out.write(w + palette + b) + + if __name__ == "__main__": debug = False -- cgit v1.2.3 From b5c3ef9b4fbbc14d9baa125b10ebf4d96a79de7e Mon Sep 17 00:00:00 2001 From: yenatch Date: Sat, 21 Dec 2013 00:38:11 -0500 Subject: gfx: 1bpp-to-png option --- pokemontools/gfx.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pokemontools/gfx.py b/pokemontools/gfx.py index e452da4..147621b 100644 --- a/pokemontools/gfx.py +++ b/pokemontools/gfx.py @@ -1679,6 +1679,9 @@ if __name__ == "__main__": elif argv[1] == 'png-to-1bpp': export_png_to_1bpp(argv[2]) + elif argv[1] == '1bpp-to-png': + export_1bpp_to_png(argv[2]) + elif argv[1] == '2bpp-to-lz': if argv[2] == '--vert': filein = argv[3] -- cgit v1.2.3 From d996bcf18e89826563e548014bbddddc677af229 Mon Sep 17 00:00:00 2001 From: yenatch Date: Mon, 23 Dec 2013 03:32:50 -0500 Subject: map_editor: read rgb macros instead of binary data for palettes also fix tileset graphics handling --- pokemontools/map_editor.py | 29 ++++------------------------- 1 file changed, 4 insertions(+), 25 deletions(-) diff --git a/pokemontools/map_editor.py b/pokemontools/map_editor.py index 43042cb..b9a6b61 100644 --- a/pokemontools/map_editor.py +++ b/pokemontools/map_editor.py @@ -445,7 +445,7 @@ class Tileset: def get_tiles(self): filename = self.get_tileset_gfx_filename() if not os.path.exists(filename): - gfx.to_png(filename.replace('.png','.2bpp'), filename) + gfx.export_lz_to_png(filename.replace('.png','.lz')) self.img = Image.open(filename) self.img.width, self.img.height = self.img.size self.tiles = [] @@ -505,30 +505,9 @@ class Tileset: self.palettes = get_palettes(filename) def get_palettes(filename): - pals = bytearray(open(filename, 'rb').read()) - - num_colors = 4 - color_length = 2 - - palette_length = num_colors * color_length - - num_pals = len(pals) / palette_length - - palettes = [] - 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, - ] - ]] + 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): -- cgit v1.2.3 From 9dc5b70cac00b23b8f61d485bd938a7adc30abf8 Mon Sep 17 00:00:00 2001 From: yenatch Date: Mon, 23 Dec 2013 03:40:04 -0500 Subject: map_gfx: read rgb macros for palettes --- pokemontools/map_gfx.py | 26 +++----------------------- 1 file changed, 3 insertions(+), 23 deletions(-) diff --git a/pokemontools/map_gfx.py b/pokemontools/map_gfx.py index 77e7d56..b06f0df 100644 --- a/pokemontools/map_gfx.py +++ b/pokemontools/map_gfx.py @@ -178,29 +178,9 @@ def read_palettes(time_of_day=1, config=config): 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, - ] - ]] - + lines = open(filepath, "r").readlines() + colors = gfx.read_rgb_macros(lines) + palettes = [colors[i:i+4] for i in xrange(0, len(colors), 4)] return palettes def load_sprite_image(address, config=config): -- cgit v1.2.3 From 12f0578a6c2dd0c75a4ef1aa9499b8137d094e1e Mon Sep 17 00:00:00 2001 From: yenatch Date: Wed, 25 Dec 2013 06:10:44 -0500 Subject: audio: more readable asm list sorting --- pokemontools/audio.py | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/pokemontools/audio.py b/pokemontools/audio.py index 0e7d375..9b0ff85 100644 --- a/pokemontools/audio.py +++ b/pokemontools/audio.py @@ -24,9 +24,22 @@ conf = configuration.Config() def sort_asms(asms): - """sort and remove duplicates from a list of tuples - format (address, asm, last_address)""" - return sorted(set(asms), key=lambda (x,y,z):(x,z,not y.startswith(';'), ':' not in y, y)) + """Sort and remove duplicates from a list of tuples. + Format (address, asm, last_address)""" + def is_comment(asm): + return asm.startswith(';') + def is_label(asm): + return ':' in asm + def sort_method(asm_list): + address, asm, last_address = asm_list + return ( + address, + last_address, + not is_comment(asm), + not is_label(asm), + asm + ) + return sorted(set(asms), key=sort_method) class NybbleParam: size = 0.5 -- cgit v1.2.3 From f0538721a3cc55a909462f42af077a749aca5e38 Mon Sep 17 00:00:00 2001 From: yenatch Date: Wed, 25 Dec 2013 06:11:03 -0500 Subject: battle_animations: dont combine print and text conversion --- pokemontools/battle_animations.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/pokemontools/battle_animations.py b/pokemontools/battle_animations.py index 96f0090..f32ed1b 100644 --- a/pokemontools/battle_animations.py +++ b/pokemontools/battle_animations.py @@ -277,18 +277,19 @@ def dump_battle_anims(table_address=0xc906f, num_anims=278): asms = sort_asms(asms) return asms -def print_asm_list(asms): - # incbin any unknown areas - # not really needed since there are no gaps +def asm_list_to_text(asms): + output = '' last = asms[0][0] for addr, asm, last_addr in asms: if addr > last: - print '\nINCBIN "baserom.gbc", $%x, $%x - $%x\n\n' % (last, addr, last) + # incbin any unknown areas + output += '\nINCBIN "baserom.gbc", $%x, $%x - $%x\n\n\n' % (last, addr, last) if addr >= last: - print asm + output += asm + '\n' last = last_addr + return output if __name__ == '__main__': asms = dump_battle_anims() - print_asm_list(asms) + print asm_list_to_text(asms) -- cgit v1.2.3 From dd471c6d5ddde53be5fffffa44ad323bde95ade9 Mon Sep 17 00:00:00 2001 From: yenatch Date: Wed, 25 Dec 2013 06:19:06 -0500 Subject: rename battle animation command class Wait to BattleAnimWait --- pokemontools/battle_animations.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pokemontools/battle_animations.py b/pokemontools/battle_animations.py index f32ed1b..349c6fa 100644 --- a/pokemontools/battle_animations.py +++ b/pokemontools/battle_animations.py @@ -137,7 +137,7 @@ def create_battle_animation_classes(): battle_animation_classes = create_battle_animation_classes() -class Wait(Command): +class BattleAnimWait(Command): macro_name = 'anim_wait' size = 1 end = macro_name in battle_animation_enders @@ -228,7 +228,7 @@ class BattleAnim: def get_command_class(self, cmd): if cmd < 0xd0: - return Wait + return BattleAnimWait for class_ in battle_animation_classes: if class_.id == cmd: return class_ -- cgit v1.2.3 From bddb346ca075e8cc666957b190207c68e1152a76 Mon Sep 17 00:00:00 2001 From: yenatch Date: Wed, 25 Dec 2013 06:31:58 -0500 Subject: battle_animations: dont use globals for macros in class BattleAnim --- pokemontools/battle_animations.py | 28 ++++++++++++++++++++++------ 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/pokemontools/battle_animations.py b/pokemontools/battle_animations.py index 349c6fa..4b641e7 100644 --- a/pokemontools/battle_animations.py +++ b/pokemontools/battle_animations.py @@ -149,7 +149,7 @@ class BattleAnimWait(Command): class BattleAnim: - def __init__(self, address, base_label=None, label=None, used_labels=[]): + def __init__(self, address, base_label=None, label=None, used_labels=[], macros=[]): self.start_address = address self.address = address @@ -173,6 +173,8 @@ class BattleAnim: self.labels += [self.label_asm] self.used_labels += [self.label_asm] + self.macros = macros + self.parse() def parse(self): @@ -211,7 +213,13 @@ class BattleAnim: for address, asm, last_address in self.labels: if not (self.start_address <= address < self.address) and (address, asm, last_address) not in self.used_labels: self.used_labels += [(address, asm, last_address)] - sub = BattleAnim(address=address, base_label=self.base_label, label=asm.split(':')[0], used_labels=self.used_labels) + sub = BattleAnim( + address=address, + base_label=self.base_label, + label=asm.split(':')[0], + used_labels=self.used_labels, + macros=self.macros + ) self.output += sub.output self.labels += sub.labels @@ -229,7 +237,7 @@ class BattleAnim: def get_command_class(self, cmd): if cmd < 0xd0: return BattleAnimWait - for class_ in battle_animation_classes: + for class_ in self.macros: if class_.id == cmd: return class_ return None @@ -242,7 +250,7 @@ def battle_anim_label(i): base_label = 'BattleAnim_%d' % i return base_label -def dump_battle_anims(table_address=0xc906f, num_anims=278): +def dump_battle_anims(table_address=0xc906f, num_anims=278, macros=battle_animation_classes): """ Dump each battle animation from a pointer table. """ @@ -265,13 +273,21 @@ def dump_battle_anims(table_address=0xc906f, num_anims=278): asms += [(pointer_address, '\tdw %s' % base_label, address)] # anim script - anim = BattleAnim(address=anim_address, base_label=base_label) + anim = BattleAnim( + address=anim_address, + base_label=base_label, + macros=macros + ) asms += anim.output + anim.labels asms += [(address, '; %x\n' % address, address)] # jp sonicboom - anim = BattleAnim(address=0xc9c00, base_label='BattleAnim_Sonicboom_JP') + anim = BattleAnim( + address=0xc9c00, + base_label='BattleAnim_Sonicboom_JP', + macros=macros + ) asms += anim.output + anim.labels asms = sort_asms(asms) -- cgit v1.2.3 From 906472a8f1758743a04beb1727f7d05b72174596 Mon Sep 17 00:00:00 2001 From: yenatch Date: Wed, 25 Dec 2013 06:44:31 -0500 Subject: battle_animations: docstring for BattleAnim, consistent sorting --- pokemontools/battle_animations.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/pokemontools/battle_animations.py b/pokemontools/battle_animations.py index 4b641e7..678e9ce 100644 --- a/pokemontools/battle_animations.py +++ b/pokemontools/battle_animations.py @@ -148,6 +148,17 @@ class BattleAnimWait(Command): class BattleAnim: + """ + A list of battle animation commands read from a given address. + + Results in a list of commands (self.output) and a list of labels (self.labels). + Format is (address, asm, last_address). Includes any subroutines and their output. + + To convert to text, use self.to_asm(). + + For combining multiple BattleAnims, take self.output + self.labels from each + and sort with sort_asms. + """ def __init__(self, address, base_label=None, label=None, used_labels=[], macros=[]): self.start_address = address @@ -227,11 +238,10 @@ class BattleAnim: self.labels = list(set(self.labels)) def to_asm(self): - output = sorted(self.output + self.labels, key = lambda (x, y, z): (x, z)) + output = sort_asms(self.output + self.labels) text = '' for (address, asm, last_address) in output: text += asm + '\n' - #text += '; %x\n' % last_address return text def get_command_class(self, cmd): -- cgit v1.2.3 From d983b8a02ad046ee9a671f2a5b7c330b4084b8ec Mon Sep 17 00:00:00 2001 From: yenatch Date: Wed, 25 Dec 2013 06:52:55 -0500 Subject: battle_animations: docstring for battle_anim_label --- pokemontools/battle_animations.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pokemontools/battle_animations.py b/pokemontools/battle_animations.py index 678e9ce..ffc89e4 100644 --- a/pokemontools/battle_animations.py +++ b/pokemontools/battle_animations.py @@ -254,6 +254,9 @@ class BattleAnim: def battle_anim_label(i): + """ + Return a label matching the name of a battle animation by id. + """ if i in anims.keys(): base_label = 'BattleAnim_%s' % anims[i].title().replace('_','') else: -- cgit v1.2.3 From 020ae38d348d895ccdd36f9c2bdce4c22505d627 Mon Sep 17 00:00:00 2001 From: yenatch Date: Wed, 25 Dec 2013 06:54:52 -0500 Subject: wram: return an empty dict for missing constants files --- pokemontools/wram.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pokemontools/wram.py b/pokemontools/wram.py index 1029c8e..2133444 100644 --- a/pokemontools/wram.py +++ b/pokemontools/wram.py @@ -117,10 +117,10 @@ def read_constants(filepath): """ Load lines from a file and call scrape_constants. """ - lines = None - - with open(filepath, "r") as file_handler: - lines = file_handler.readlines() + lines = [] + if os.path.exists(filepath): + with open(filepath, "r") as file_handler: + lines = file_handler.readlines() constants = scrape_constants(lines) return constants -- cgit v1.2.3 From 486483ef19dcfbb3041f6a9bcf20b4b7412cba57 Mon Sep 17 00:00:00 2001 From: yenatch Date: Fri, 27 Dec 2013 21:46:29 -0500 Subject: don't use children functions in sort_asms --- pokemontools/audio.py | 50 ++++++++++++++++++++++++++++++++------------------ 1 file changed, 32 insertions(+), 18 deletions(-) diff --git a/pokemontools/audio.py b/pokemontools/audio.py index 9b0ff85..c310a50 100644 --- a/pokemontools/audio.py +++ b/pokemontools/audio.py @@ -23,23 +23,37 @@ import configuration conf = configuration.Config() +def is_label(asm): + return ':' in asm + +def is_comment(asm): + return asm.startswith(';') + +def asm_sort(asm_def): + """ + Sort key for asm lists. + + Usage: + list.sort(key=asm_sort) + sorted(list, key=asm_sort) + """ + address, asm, last_address = asm_def + return ( + address, + last_address, + not is_comment(asm), + not is_label(asm), + asm + ) + def sort_asms(asms): - """Sort and remove duplicates from a list of tuples. - Format (address, asm, last_address)""" - def is_comment(asm): - return asm.startswith(';') - def is_label(asm): - return ':' in asm - def sort_method(asm_list): - address, asm, last_address = asm_list - return ( - address, - last_address, - not is_comment(asm), - not is_label(asm), - asm - ) - return sorted(set(asms), key=sort_method) + """ + Sort and remove duplicates from an asm list. + + Format: [(address, asm, last_address), ...] + """ + return sorted(set(asms), key=asm_sort) + class NybbleParam: size = 0.5 @@ -212,10 +226,10 @@ class Channel: output = sort_asms(self.output + self.labels) text = '' for i, (address, asm, last_address) in enumerate(output): - if ':' in asm: + if is_label(asm): # dont print labels for empty chunks for (address_, asm_, last_address_) in output[i:]: - if ':' not in asm_: + if not is_label(asm_): text += '\n' + asm + '\n' break else: -- cgit v1.2.3 From 981e0c634f6da555fb74e5e2c948c13a820ec6ef Mon Sep 17 00:00:00 2001 From: yenatch Date: Sat, 28 Dec 2013 18:32:35 -0500 Subject: audio: fix Noise and size handling for nybble params --- pokemontools/audio.py | 32 ++++++++++++++++++++------------ 1 file changed, 20 insertions(+), 12 deletions(-) diff --git a/pokemontools/audio.py b/pokemontools/audio.py index c310a50..699bebf 100644 --- a/pokemontools/audio.py +++ b/pokemontools/audio.py @@ -70,15 +70,18 @@ class NybbleParam: def parse(self): self.nybble = (rom[self.address] >> {'lo': 0, 'hi': 4}[self.which]) & 0xf -class HiNybbleParam(NybbleParam): - which = 'hi' def to_asm(self): return '%d' % self.nybble + @staticmethod + def from_asm(value): + return value + +class HiNybbleParam(NybbleParam): + which = 'hi' + class LoNybbleParam(NybbleParam): which = 'lo' - def to_asm(self): - return '%d' % self.nybble class PitchParam(HiNybbleParam): def to_asm(self): @@ -93,10 +96,9 @@ class PitchParam(HiNybbleParam): pitch += '_' return pitch - class Note(Command): macro_name = "note" - size = 1 + size = 0 end = False param_types = { 0: {"name": "pitch", "class": PitchParam}, @@ -110,6 +112,7 @@ class Note(Command): self.params = [] byte = rom[self.address] current_address = self.address + size = 0 for (key, param_type) in self.param_types.items(): name = param_type["name"] class_ = param_type["class"] @@ -119,12 +122,20 @@ class Note(Command): self.params += [obj] current_address += obj.size + size += obj.size + + # can't fit bytes into nybbles + if obj.size > 0.5: + if current_address % 1: + current_address = int(ceil(current_address)) + if size % 1: + size = int(ceil(size)) self.params = dict(enumerate(self.params)) - # obj sizes were 0.5, but were working with ints + # obj sizes were 0.5, but we're working with ints current_address = int(ceil(current_address)) - self.size = int(ceil(self.size)) + self.size += int(ceil(size)) self.last_address = current_address return True @@ -132,7 +143,6 @@ class Note(Command): class Noise(Note): macro_name = "noise" - size = 0 end = False param_types = { 0: {"name": "duration", "class": LoNybbleParam}, @@ -241,7 +251,7 @@ class Channel: for class_ in sound_classes: if class_.id == i: return class_ - if self.channel in [4, 8]: return Noise + if self.channel == 8: return Noise return Note @@ -347,7 +357,6 @@ def dump_sounds(origin, names, base_label='Sound_'): sound_at = read_bank_address_pointer(origin + i * 3) sound = Sound(sound_at, base_label + name) output = sound.to_asm(labels) + '\n' - # incbin trailing commands that didnt get picked up index = addresses.index((sound.start_address, sound.last_address)) if index + 1 < len(addresses): @@ -413,5 +422,4 @@ def generate_crystal_cry_pointers(): if __name__ == '__main__': dump_crystal_music() - dump_crystal_sfx() -- cgit v1.2.3 From 82389c999856e58b443c025228ecddd24dc6e4ab Mon Sep 17 00:00:00 2001 From: yenatch Date: Sat, 28 Dec 2013 19:53:32 -0500 Subject: audio: note duration from 0-15 to 1-16 to match pokered --- pokemontools/audio.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/pokemontools/audio.py b/pokemontools/audio.py index 699bebf..898c646 100644 --- a/pokemontools/audio.py +++ b/pokemontools/audio.py @@ -96,13 +96,23 @@ class PitchParam(HiNybbleParam): pitch += '_' return pitch +class NoteDurationParam(LoNybbleParam): + def to_asm(self): + self.nybble += 1 + return LoNybbleParam.to_asm(self) + + @staticmethod + def from_asm(value): + value = str(int(value) - 1) + return LoNybbleParam.from_asm(value) + class Note(Command): macro_name = "note" size = 0 end = False param_types = { 0: {"name": "pitch", "class": PitchParam}, - 1: {"name": "duration", "class": LoNybbleParam}, + 1: {"name": "duration", "class": NoteDurationParam}, } allowed_lengths = [2] override_byte_check = True -- cgit v1.2.3 From 70cd4f7c00b33a398ed7af071773c06ca335c105 Mon Sep 17 00:00:00 2001 From: yenatch Date: Sat, 28 Dec 2013 19:57:37 -0500 Subject: audio: use labels.line_has_label and clean up crystal imports --- pokemontools/audio.py | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/pokemontools/audio.py b/pokemontools/audio.py index 898c646..0cdfcbc 100644 --- a/pokemontools/audio.py +++ b/pokemontools/audio.py @@ -5,14 +5,13 @@ import os from math import ceil from gbz80disasm import get_global_address, get_local_address - -import crystal +from labels import line_has_label from crystal import music_classes as sound_classes - from crystal import ( Command, SingleByteParam, MultiByteParam, + PointerLabelParam, load_rom, ) @@ -23,9 +22,6 @@ import configuration conf = configuration.Config() -def is_label(asm): - return ':' in asm - def is_comment(asm): return asm.startswith(';') @@ -42,7 +38,7 @@ def asm_sort(asm_def): address, last_address, not is_comment(asm), - not is_label(asm), + not line_has_label(asm), asm ) @@ -202,7 +198,7 @@ class Channel: # label any jumps or calls for key, param in class_.param_types.items(): - if param['class'] == crystal.PointerLabelParam: + if param['class'] == PointerLabelParam: label_address = class_.params[key].parsed_address label = '%s_branch_%x' % ( self.base_label, @@ -246,10 +242,10 @@ class Channel: output = sort_asms(self.output + self.labels) text = '' for i, (address, asm, last_address) in enumerate(output): - if is_label(asm): + if line_has_label(asm): # dont print labels for empty chunks for (address_, asm_, last_address_) in output[i:]: - if not is_label(asm_): + if not line_has_label(asm_): text += '\n' + asm + '\n' break else: -- cgit v1.2.3 From 51d6f1edca87b9e6c92d92219d143449b32412e2 Mon Sep 17 00:00:00 2001 From: yenatch Date: Sun, 29 Dec 2013 03:49:31 -0500 Subject: map_editor: allow saving new maps + save file dialog saves to newmap.asm by default --- pokemontools/map_editor.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/pokemontools/map_editor.py b/pokemontools/map_editor.py index b9a6b61..a7aeda1 100644 --- a/pokemontools/map_editor.py +++ b/pokemontools/map_editor.py @@ -15,6 +15,7 @@ from Tkinter import ( X, TclError, ) +import tkFileDialog from ttk import ( Frame, @@ -178,7 +179,8 @@ class Application(Frame): def new_map(self): self.map_name = None self.init_map() - self.map.blockdata = [self.paint_tile] * 20 * 20 + self.map.blockdata_filename = os.path.join(self.config.map_dir, 'newmap.blk') + self.map.blockdata = bytearray([self.paint_tile] * 20 * 20) self.map.width = 20 self.map.height = 20 self.draw_map() @@ -193,7 +195,8 @@ class Application(Frame): def save_map(self): if hasattr(self, 'map'): if self.map.blockdata_filename: - with open(self.map.blockdata_filename, 'wb') as save: + filename = tkFileDialog.asksaveasfilename(initialfile=self.map.blockdata_filename) + with open(filename, 'wb') as save: save.write(self.map.blockdata) self.log.info('blockdata saved as {}'.format(self.map.blockdata_filename)) else: -- cgit v1.2.3 From c234cdd9036f8b6f55b9c26abcc75967e8485144 Mon Sep 17 00:00:00 2001 From: yenatch Date: Sun, 29 Dec 2013 04:13:56 -0500 Subject: gfx: touch exported lz files so they don't get remade --- pokemontools/gfx.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/pokemontools/gfx.py b/pokemontools/gfx.py index 147621b..04ccac5 100644 --- a/pokemontools/gfx.py +++ b/pokemontools/gfx.py @@ -1566,11 +1566,16 @@ def export_lz_to_png(filename): """ assert filename[-3:] == ".lz" lz_data = open(filename, "rb").read() + bpp = Decompressed(lz_data).output bpp_filename = filename.replace(".lz", ".2bpp") to_file(bpp_filename, bpp) + export_2bpp_to_png(bpp_filename) + # touch the lz file so it doesn't get remade + os.utime(filename, None) + def dump_tileset_pngs(): """ Convert .lz format tilesets into .png format tilesets. -- cgit v1.2.3 From 89ca24a93475cdb7fdbdc046cb3ac7a83fe9d9e9 Mon Sep 17 00:00:00 2001 From: yenatch Date: Sun, 19 Jan 2014 19:36:45 -0500 Subject: preprocessor: rgbasm "EQUS" takes string literals --- pokemontools/preprocessor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pokemontools/preprocessor.py b/pokemontools/preprocessor.py index bde5f70..a942223 100644 --- a/pokemontools/preprocessor.py +++ b/pokemontools/preprocessor.py @@ -523,7 +523,7 @@ class Preprocessor(object): sys.stdout.write(asm) # convert text to bytes when a quote appears (not in a comment) - elif "\"" in asm: + elif "\"" in asm and "EQUS" not in asm: sys.stdout.write(quote_translator(asm)) # check against other preprocessor features -- cgit v1.2.3