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