diff --git a/nml/actions/real_sprite.py b/nml/actions/real_sprite.py --- a/nml/actions/real_sprite.py +++ b/nml/actions/real_sprite.py @@ -160,9 +160,12 @@ class RealSprite(object): @ivar flags: Cropping/warning flags. @type flags: L{expression.ConstantNumeric} + + @ivar poslist: Position of creation of the sprite, if available. + @type poslist: C{list} of L{Position} """ - def __init__(self, param_list = None, label = None): + def __init__(self, param_list = None, label = None, poslist = None): self.param_list = param_list self.label = label self.is_empty = False @@ -170,6 +173,10 @@ class RealSprite(object): self.ypos = None self.xsize = None self.ysize = None + if poslist is None: + self.poslist = [] + else: + self.poslist = poslist def debug_print(self, indentation): generic.print_dbg(indentation, 'Real sprite, parameters:') @@ -182,8 +189,8 @@ class RealSprite(object): labels[self.label.value] = 0 return labels, 1 - def expand(self, default_file, default_mask_file, id_dict): - return [parse_real_sprite(self, default_file, default_mask_file, id_dict)] + def expand(self, default_file, default_mask_file, poslist, id_dict): + return [parse_real_sprite(self, default_file, default_mask_file, poslist, id_dict)] def check_sprite_size(self): generic.check_range(self.xpos.value, 0, 0x7fffFFFF, "Real sprite paramater 'xpos'", self.xpos.pos) @@ -249,6 +256,13 @@ class RealSprite(object): return (rgb_file, rgb_rect, mask_file, mask_rect, do_crop) class SpriteAction(base_action.BaseAction): + """ + @ivar sprite_num: Number of the sprite, or C{None} if not decided yet. + @type sprite_num: C{int} or C{None} + + @ivar last: Whether this sprite action is the last of a series. + @type last: C{bool} + """ def __init__(self): self.sprite_num = None self.last = False @@ -275,9 +289,13 @@ class RealSpriteAction(SpriteAction): if self.last: file.newline() class RecolourSprite(object): - def __init__(self, mapping, label = None): + def __init__(self, mapping, label = None, poslist = None): self.mapping = mapping self.label = label + if poslist is None: + self.poslist = [] + else: + self.poslist = poslist def debug_print(self, indentation): generic.print_dbg(indentation, 'Recolour sprite, mapping:') @@ -290,7 +308,7 @@ class RecolourSprite(object): labels[self.label.value] = 0 return labels, 1 - def expand(self, default_file, default_mask_file, id_dict): + def expand(self, default_file, default_mask_file, poslist, id_dict): # create new struct, needed for template expansion new_mapping = [] for old_assignment in self.mapping: @@ -299,7 +317,7 @@ class RecolourSprite(object): to_min_value = old_assignment.value.min.reduce_constant([id_dict]) to_max_value = None if old_assignment.value.max is None else old_assignment.value.max.reduce_constant([id_dict]) new_mapping.append(assignment.Assignment(assignment.Range(from_min_value, from_max_value), assignment.Range(to_min_value, to_max_value), old_assignment.pos)) - return [RecolourSprite(new_mapping)] + return [RecolourSprite(new_mapping, poslist = poslist)] def __str__(self): ret = "" if self.label is None else str(self.label) + ": " @@ -372,7 +390,7 @@ class TemplateUsage(object): labels[self.label.value] = 0 return labels, offset - def expand(self, default_file, default_mask_file, parameters): + def expand(self, default_file, default_mask_file, poslist, parameters): if self.name.value not in sprite_template_map: raise generic.ScriptError("Encountered unknown template identifier: " + self.name.value, self.name.pos) template = sprite_template_map[self.name.value] @@ -385,13 +403,13 @@ class TemplateUsage(object): raise generic.ScriptError("Template parameters should be compile-time constants", param.pos) param_dict[template.param_list[i].value] = param.value - return parse_sprite_list(template.sprite_list, default_file, default_mask_file, param_dict) + return parse_sprite_list(template.sprite_list, default_file, default_mask_file, poslist + [self.pos], param_dict) def __str__(self): return "{}({})".format(self.name, ", ".join(str(param) for param in self.param_list)) -def parse_real_sprite(sprite, default_file, default_mask_file, id_dict): +def parse_real_sprite(sprite, default_file, default_mask_file, poslist, id_dict): # check the number of parameters num_param = len(sprite.param_list) if num_param == 0: @@ -401,7 +419,7 @@ def parse_real_sprite(sprite, default_fi raise generic.ScriptError("Invalid number of arguments for real sprite. Expected 2..9.", sprite.param_list[0].pos) # create new sprite struct, needed for template expansion - new_sprite = RealSprite() + new_sprite = RealSprite(poslist = poslist + sprite.poslist) param_offset = 0 @@ -474,10 +492,10 @@ def parse_real_sprite(sprite, default_fi sprite_template_map = {} -def parse_sprite_list(sprite_list, default_file, default_mask_file, parameters = {}): +def parse_sprite_list(sprite_list, default_file, default_mask_file, poslist, parameters = {}): real_sprite_list = [] for sprite in sprite_list: - real_sprite_list.extend(sprite.expand(default_file, default_mask_file, parameters)) + real_sprite_list.extend(sprite.expand(default_file, default_mask_file, poslist, parameters)) return real_sprite_list def parse_sprite_data(sprite_container): @@ -493,8 +511,8 @@ def parse_sprite_data(sprite_container): first = True for sprite_data in all_sprite_data: - sprite_list, default_file, default_mask_file, zoom_level, bit_depth = sprite_data - new_sprite_list = parse_sprite_list(sprite_list, default_file, default_mask_file) + sprite_list, default_file, default_mask_file, pos, zoom_level, bit_depth = sprite_data + new_sprite_list = parse_sprite_list(sprite_list, default_file, default_mask_file, [pos]) if not first and len(new_sprite_list) != len(action_list): msg = "Expected {:d} alternative sprites for {} '{}', got {:d}." msg = msg.format(len(action_list), sprite_container.block_type, sprite_container.block_name.value, len(new_sprite_list)) diff --git a/nml/ast/sprite_container.py b/nml/ast/sprite_container.py --- a/nml/ast/sprite_container.py +++ b/nml/ast/sprite_container.py @@ -28,7 +28,7 @@ class SpriteContainer(object): @type block_name: L{Identifier}, or C{None} if N/A @ivar sprite_data: Mapping of (zoom level, bit-depth) to (sprite list, default file) - @type sprite_data: C{dict} that maps (C{tuple} of (C{int}, C{int})) to (C{tuple} of (C{list} of (L{RealSprite}, L{RecolourSprite} or L{TemplateUsage}), L{StringLiteral} or C{None})) + @type sprite_data: C{dict} that maps (C{tuple} of (C{int}, C{int})) to (C{tuple} of (C{list} of (L{RealSprite}, L{RecolourSprite} or L{TemplateUsage}), L{StringLiteral} or C{None}, L{Position})) """ sprite_blocks = {} @@ -50,14 +50,17 @@ class SpriteContainer(object): "level / bit depth combination. This data will be overridden.") msg = msg.format(self.block_type, self.block_name.value) generic.print_warning(msg, pos) - self.sprite_data[key] = (sprite_list, default_file, default_mask_file) + self.sprite_data[key] = (sprite_list, default_file, default_mask_file, pos) def get_all_sprite_data(self): """ - Get all sprite data as a list of 5-tuples (sprite_list, default_file, default_mask_file, zoom_level, bit_depth) - Sorting makes sure that the order is consistent, and that the normal zoom, 8bpp sprites appear first + Get all sprite data. + Sorting makes sure that the order is consistent, and that the normal zoom, 8bpp sprites appear first. + + @return: List of 6-tuples (sprite_list, default_file, default_mask_file, position, zoom_level, bit_depth). + @rtype: C{list} of C{tuple} of (C{list} of (L{RealSprite}, L{RecolourSprite} or L{TemplateUsage}), L{StringLiteral} or C{None}, L{Position}, C{int}, C{int}) """ - return [(self.sprite_data[key][0], self.sprite_data[key][1], self.sprite_data[key][2], key[0], key[1]) for key in sorted(self.sprite_data)] + return [val + key for key, val in sorted(self.sprite_data.items())] @classmethod def resolve_sprite_block(cls, block_name): diff --git a/nml/generic.py b/nml/generic.py --- a/nml/generic.py +++ b/nml/generic.py @@ -139,8 +139,10 @@ class ImageFilePosition(Position): """ Generic (not position-dependant) error with an image file """ - def __init__(self, filename): - Position.__init__(self, filename, []) + def __init__(self, filename, pos = None): + poslist = [] + if pos is not None: poslist.append(pos) + Position.__init__(self, filename, poslist) def __str__(self): return 'Image file "{}"'.format(self.filename) @@ -181,8 +183,8 @@ class RangeError(ScriptError): ScriptError.__init__(self, name + " out of range " + str(min_value) + ".." + str(max_value) + ", encountered " + str(value), pos) class ImageError(ScriptError): - def __init__(self, value, filename): - ScriptError.__init__(self, value, ImageFilePosition(filename)) + def __init__(self, value, filename, pos = None): + ScriptError.__init__(self, value, ImageFilePosition(filename, pos)) class OnlyOnceError(ScriptError): """ diff --git a/nml/parser.py b/nml/parser.py --- a/nml/parser.py +++ b/nml/parser.py @@ -451,9 +451,9 @@ class NMLParser(object): '''real_sprite : LBRACKET expression_list RBRACKET | ID COLON LBRACKET expression_list RBRACKET''' if len(t) == 4: - t[0] = real_sprite.RealSprite(t[2]) + t[0] = real_sprite.RealSprite(param_list = t[2], poslist = [t.lineno(1)]) else: - t[0] = real_sprite.RealSprite(t[4], t[1]) + t[0] = real_sprite.RealSprite(param_list = t[4], label = t[1], poslist = [t.lineno(1)]) def p_recolour_assignment_list(self, t): '''recolour_assignment_list : diff --git a/nml/spriteencoder.py b/nml/spriteencoder.py --- a/nml/spriteencoder.py +++ b/nml/spriteencoder.py @@ -263,14 +263,16 @@ class SpriteEncoder(object): if filename_32bpp is not None: im = self.open_image_file(filename_32bpp.value) if im.mode not in ("RGB", "RGBA"): - raise generic.ImageError("32bpp image is not a full colour RGB(A) image.", filename_32bpp.value) + pos = build_position(sprite_info.poslist) + raise generic.ImageError("32bpp image is not a full colour RGB(A) image.", filename_32bpp.value, pos) info_byte |= INFO_RGB if im.mode == "RGBA": info_byte |= INFO_ALPHA (im_width, im_height) = im.size if x < 0 or y < 0 or x + size_x > im_width or y + size_y > im_height: - raise generic.ScriptError("Read beyond bounds of image file '{}'".format(filename_32bpp.value), filename_32bpp.pos) + pos = build_position(sprite_info.poslist) + raise generic.ScriptError("Read beyond bounds of image file '{}'".format(filename_32bpp.value), pos) sprite = im.crop((x, y, x + size_x, y + size_y)) rgb_sprite_data = sprite.tostring() @@ -281,13 +283,15 @@ class SpriteEncoder(object): if filename_8bpp is not None: mask_im = self.open_image_file(filename_8bpp.value) if mask_im.mode != "P": - raise generic.ImageError("8bpp image does not have a palette", filename_8bpp.value) + pos = build_position(sprite_info.poslist) + raise generic.ImageError("8bpp image does not have a palette", filename_8bpp.value, pos) im_mask_pal = palette.validate_palette(mask_im, filename_8bpp.value) info_byte |= INFO_PAL (im_width, im_height) = mask_im.size if mask_x < 0 or mask_y < 0 or mask_x + size_x > im_width or mask_y + size_y > im_height: - raise generic.ScriptError("Read beyond bounds of image file '{}'".format(filename_8bpp.value), filename_8bpp.pos) + pos = build_position(sprite_info.poslist) + raise generic.ScriptError("Read beyond bounds of image file '{}'".format(filename_8bpp.value), pos) mask_sprite = mask_im.crop((mask_x, mask_y, mask_x + size_x, mask_y + size_y)) mask_sprite_data = self.palconvert(mask_sprite.tostring(), im_mask_pal) @@ -497,3 +501,21 @@ class SpriteEncoder(object): else: return sprite_str +def build_position(poslist): + """ + Construct a L{Position} object that expands to positions in the template instantiation stack. + + @param poslist: Sequence of positions to report. First entry is the innermost template, + last entry is the nml statement that started it all. + @type poslist: C{list} of L{Position} + + @return: Position to attach to an error. + @rtype: L{Position} + """ + if poslist is None or len(poslist) == 0: + return None + if len(poslist) == 1: + return poslist[0] + pos = poslist[-1] + pos.includes = pos.includes + poslist[:-1] + return pos