v50 Steam/Premium information for editors
  • v50 information can now be added to pages in the main namespace. v0.47 information can still be found in the DF2014 namespace. See here for more details on the new versioning policy.
  • Use this page to report any issues related to the migration.
This notice may be cached—the current version can be found here.

Difference between revisions of "User:Button/BAMM"

From Dwarf Fortress Wiki
Jump to navigation Jump to search
 
(3 intermediate revisions by the same user not shown)
Line 19: Line 19:
 
Sometimes I'm on a computer I can't put a git client on.
 
Sometimes I'm on a computer I can't put a git client on.
  
<pre>
+
<pre># Modders' guide to BAMM! graphics references
_is_logging_initialized = False
+
# Place your conversions in a file named bamm_gfx_<identifier here>.txt . The file should be encoded in codepage 437, the same as the Dwarf Fortress raws.
  
# TODO Allow file to be passed in, or possibly even a dict?
+
# Start by defining names for your target graphics elements.
def load_run_config():
+
# These names must always start with @.
"""Load config information from the default file.
+
# So if you want to reference the background color being used for the Lion creature token, you can do that with this definition:
+
# CREATURE:LION|COLOR::@lionbgcolor
Also initializes loggers if they haven't been already.
+
# or
"""
+
# CREATURE:LION|COLOR::@lionbgcolor:
    print("Loading run configuration...")
+
# If you want to reference the *whole* color of the lion tile, which is a 3-argument value, you can add a number after the @, like this:
    global runconfig
+
# CREATURE:LION|COLOR:@3lionfullcolor
    runconfig_file = open(runconfig, 'r')
+
# Or you can declare references for each argument of the lion color individually:
    global properties
+
# CREATURE:LION|COLOR:@lionfgcolor:@lionbgcolor:@lionbrightness
    for line in runconfig_file:
 
        uncommented = line.strip().split('#')[0]
 
        props = uncommented.strip().split('=')
 
        if len(props) == 0 or (len(props) == 1 and len(props[0]) == 0):
 
            continue
 
        elif len(props) != 2:
 
            print('Line "', line, '" in ', runconfig,
 
                  ' is improperly configured. Please format properties thus: \
 
                  "propertyname=value" (without quotes).')
 
        elif not _property_has_format_error(props[0], props[1]):
 
            set_property(props[0], props[1])
 
        else:
 
            print ('Line "', line, '" in', runconfig,
 
                  'is improperly configured. Please format properties thus: \
 
                  "propertyname=value" (without quotes).')
 
  
    runconfig_file.close()
+
# The name is case-sensitive and can contain any character in the alphabet. Numbers are not allowed except immediately after the @. Special characters, including underscores, are not allowed.
  
initialize_logging()
+
# You can also create generic all-graphics-belonging-to-this-object referents this way:
userlog.info("**********")
+
# CREATURE:LION|@allLionTags
    modderslog.info("**********")
 
    userlog.info("Run configuration loaded.")
 
userlog.debug("Properties:")
 
    for propname in properties.keys():
 
        userlog.debug("Property %s:", propname)
 
        for item in properties[propname]:
 
            userlog.debug("\t%s", item)
 
  
# TODO implement parameters with defaults (and update docstring)
+
# BE AWARE: trying to define non-graphics or partially non-graphic referents won't work. Referent declarations which include non-graphic information will simply be ignored.
def initialize_logging()
 
"""Initialize loggers config.userlog and config.modderslog
 
 
Will not double-initialize.
 
"""
 
global _is_logging_initialized
 
 
if _is_logging_initialized:
 
return
 
else:
 
_is_logging_initialized = True
 
  
    # Logging
+
# OK, now that we have our referents, it's time to use them to modify raws.
    fmt = logging.Formatter('%(message)s')
 
  
    userhandler = logging.FileHandler(properties[USERSLOG][1])
+
# creature_modded.txt
    userhandler.setFormatter(fmt)
+
# [CREATURE:MODDED_LION]
    userlog.addHandler(userhandler)
+
# @allLionTags
  
    if properties[DEBUG][1]:
+
# This sets all of the modded lion's graphics equal to the lion's graphics, in any graphics set.
        userlog.setLevel(logging.DEBUG)
+
# However, this "copy all" functionality couldn't be used to set properties belonging to a non-creature object:
    else:
 
        userlog.setLevel(logging.INFO)
 
  
    modderhandler = logging.FileHandler(properties[MODDERSLOG][1])
+
# [INORGANIC:LIONSTONE] This won't work
    modderhandler.setFormatter(fmt)
+
# @allLionTags This won't work
    modderslog.addHandler(modderhandler)
 
    modderslog.setLevel(logging.INFO)
 
  
def _property_has_format_error(propkey, value):
+
# In order to make lionstone the color of a lion, you'll need to reference the colors instead:
"""Returns True if the property is formatted incorrectly.
+
# [INORGANIC:LIONSTONE]
+
# [DISPLAY_COLOR:@3lionfullcolor]
* propkey is the "name" of the property, and is expected to be one of the CONFIG.X module variables declared up above.
 
* value is the value you wish to check for compatibility with the property in question.
 
 
Returns True if:
 
 
* The property key is not recognized
 
* The property's type is IS_BOOL and the value is not 'True' or 'False
 
* The property's type is IS_DIR and the value is an existing (non-directory) file
 
* The property's type is IS_FILE and the value is an existing directory.
 
 
Otherwise, returns False.
 
"""
 
    return (propkey not in properties.keys() or
 
            (properties[propkey][0] == IS_DIR and
 
                os.path.exists(value) and not os.path.isdir(value)) or
 
            (properties[propkey][0] == IS_FILE and os.path.exists(value) and
 
                not os.path.isfile(value)) or
 
            (properties[propkey][0] == IS_BOOL and
 
                value not in ('True', 'False')))
 
  
def set_property(prop_id, value):
+
# NB. References are always made to graphics as declared in the graphics source raws. If you declare a lion to use a deer's graphics, and a deer to use a lion's graphics, they'll swap, not have the same graphics.
""" Sets property prop_id to value. """
 
    global properties
 
    if prop_id not in properties.keys():
 
        pass
 
    elif not _property_has_format_error(prop_id, value):
 
        properties[prop_id] = properties[prop_id][:1]
 
        if properties[prop_id][0] == IS_REGEX_LIST:
 
            properties[prop_id].extend(value.split(','))
 
        elif properties[prop_id][0] == IS_BOOL:
 
            if value == 'True':
 
                properties[prop_id].append(True)
 
            elif value == 'False':
 
                properties[prop_id].append(False)
 
        else:
 
            properties[prop_id].append(value)
 
  
def escape_problematic_literals(line):
+
# Eventually I intend to expand this to include the ability to apply these changes based on the properties of entries, but right now, this should be enough.
""" Returns line with its char literals replaced with their cp437 codes.
 
 
Char literals are usually used for defining tiles, and are two single quotes around a character, so: '*'. Since this is the only case in which the DF raw characters ']', '[' and ':' are allowed within a tag outside their uses, and since cp437 codes are equally valid, replacing these with their cp437 codes is harmless and streamlines lexing considerably.
 
"""
 
    global ascii_codes
 
  
    # Replace literal key characters with number codes
+
# TODO docstring
    # Literal colons are going to require some special processing, because of
+
def handle_graphics_references(graphics_root,def_file):
    # the following case:  GROWTH:'r':'x': etc. That's why we can't just use
+
# Runs after the graphics source has been pulled in
    # a blind replaceAll.
+
# Modifies same
 
+
global graphics_referents
    # If odd, we are inside a tag. If even, we are outside a tag.
+
if graphics_refs is None:
    bracketscount = 0
+
graphics_refs = [] # This is meant to be a dict, check syntax
    count = 0                # Where we are in the string
+
definitions_over_yet = False
    quotescount = 0
+
for line in def_file:
    while count < len(line)-2:
+
actline = line.split('#')[0].strip()
        # Going from inside a tag to outside or vice versa
+
if len(actline) == 0:
        if (((bracketscount % 2 == 0 and line[count] == "[") or
+
continue
            (bracketscount % 2 == 1 and line[count] == "]"))):
+
elif actline == defs_over # TODO declare this as a constant
            bracketscount += 1
+
definitions_over_yet = True
        # We are inside a tag and we have discovered a ' character beginning a
+
elif not definitions_over_yet:
        # literal value, with another 2 characters down on the other side.
+
# load referent definition
        elif (quotescount % 2 == 0 and bracketscount % 2 == 1 and
+
# If the referent name already exists in graphics_refs, write an error to the log and continue to the next line.
              line[count:count+3] in ascii_codes.keys()):
+
# Otherwise, match it against a template
            # If the character could be a problem for later processing, replace
+
# Then match it against the GRAPHICS NODES that meet that template
            #  it with its ascii code.
+
# Not the bound node or the target node.  
            line = line[:count] + ascii_codes[line[count:count+3]] + \
+
# (TODO make each template node hold lists of objects that refer to it, by function.)
                line[count+3:]
+
# If a partial definition matches more than one object, keep track of all of them so they can be narrowed down later
        elif line[count] == "'":
+
# If a full definition matches more than one object, write a warning to the logfile and choose one arbitrarily to go with.
            quotescount += 1
+
# If a full definition matches no graphics object, write a warning to the log and try it on target objects.  
        elif bracketscount % 2 == 1 and line[count] == ':':
+
# If a full definition matches neither graphics nor target, write an error to the log.
            quotescount = 0
+
# Put the mapping of referent name to an object storing the reference info into graphics_refs
        count += 1
+
else:
    # line has now had its literal "use this tile" versions of its special
+
# Get a collection of all the targets that meet the reference
    # characters replaced with their numbers.
+
</pre>
    return line
 
 
 
def path_compatible(full_path, allowed_paths):
 
    """Return True if full_path regex matches anything in allowed_paths, or
 
    False otherwise."""
 
    full_path = full_path.replace('\\', '/')
 
    for allowed_path in allowed_paths:
 
        allowed_path = allowed_path.replace('\\', '/')
 
        match = re.match(allowed_path, full_path)
 
        if match is not None:
 
            return True
 
    return False
 
 
 
# TODO overhaul.
 
def load_all_templates(templatefile):
 
""" Loads config information from templatefile.
 
 
* templatefile is a pipe-delimited, one-graphics-tag-per-line config file which should not be changed by users unless you REALLY know what you're doing.
 
 
This initializes the scaffolding for all future raw parsing, which is stored in graphics.template_tree .
 
"""
 
    userlog.info("Loading template configuration...")
 
    alltemplates = open(templatefile, 'r')
 
    global template_tree
 
    if template_tree is None:
 
        # initialize the template tree
 
        template_tree = TemplateNode(None)
 
    for line in alltemplates:
 
        real_line = line.strip()
 
        # Starting at the root of the tree with each newline
 
        curr_node = template_tree
 
        if len(real_line) > 0:
 
            tags = real_line.split('|')
 
            for tag in tags:
 
                if tag in curr_node._children.keys():
 
                    curr_node = curr_node._children[tag]
 
                else:
 
                    curr_node = TemplateNode(curr_node, tag)
 
 
 
            curr_node._is_graphics_tag = True
 
    alltemplates.close()
 
    userlog.info("Template configuration loaded.")
 
 
 
 
 
# TODO Maybe replace graphics_to_apply with curr_dict?
 
def _apply_graphics_to_file(graphics_to_apply, file, sourceroot, targetpath):
 
""" Writes merged raws belonging to a single file.
 
 
 
* graphics_to_apply is a collection of BoundNode trees formatted as { file:{topleveltag:BoundNode}}. This is the format returned by BoundNode.bind_graphics_tags.
 
* file is the name of the file to apply graphics to, without any information about its parent directories.
 
* sourceroot is the path to the directory containing the source raw version of 'file'.
 
* targetpath is the path to the output file, with the name of the file included.
 
 
 
For each line in the raw source file, the function searches BoundNode for a matching tag for each tag. If the tag is found, its contents are replaced in the line with a merged version from the appropriate BoundNode. Then the line - modified or unmodified - is written out to the output file.
 
"""
 
    userlog.info("Merging graphics into %s ...", file)
 
    curr_dict = graphics_to_apply[file]
 
    curr_node = None
 
    targetfile = open(targetpath, 'wt', encoding='cp437')
 
    sourcefile = open(os.path.join(sourceroot, file), 'rt', encoding='cp437')
 
    linecount = 0
 
    tags_to_reset_addl = []
 
    for line in sourcefile:
 
        linecount = linecount + 1
 
        modified_line = parsing.escape_problematic_literals(line)
 
        additional = []
 
        for tag in parsing.tags(line):
 
            matching_node = None
 
            if tag in curr_dict.keys():
 
                matching_node = curr_dict[tag]
 
            elif curr_node is not None:
 
                matching_node = curr_node.find_match(tag)
 
            if matching_node is not None:
 
                curr_node = matching_node
 
                matching_node.pop_self()
 
                if matching_node.is_there_a_difference():
 
                    merged_tag = matching_node.get_merged()
 
                    if merged_tag is not None:
 
                        replacement = matching_node.get_merged()
 
                        userlog.debug("Replacing %s with %s at line %i.", tag,
 
                                      replacement, linecount)
 
                        modified_line = modified_line.replace(tag, replacement)
 
                    else:
 
                        userlog.debug("Removing tag %s at line %i.", tag,
 
                                      linecount)
 
                        to_remove = "[" + tag + "]"
 
                        modified_line = modified_line.replace(to_remove, "")
 
                    # modified_line = modified_line[:-1] + " (BAMM)\n"
 
                additional.extend(matching_node.pop_addl())
 
                tags_to_reset_addl.append(matching_node)
 
            elif curr_node is not None:
 
                problem_parent = curr_node.find_targetsonly_owner(tag)
 
                if ((problem_parent is not None and
 
                    problem_parent._targets_only[tag].has_graphics_info())):
 
                    modderslog.info("Object missing graphics information in %s : %s",
 
                                    targetpath, tag)
 
                    # Targets without matching graphics
 
                    modified_line = "No tag corresponding to (" + tag + \
 
                        ") was found in graphics source. -BAMM\n" + modified_line
 
 
 
        targetfile.writelines(modified_line)
 
        for tag_node in additional:
 
            linecount = linecount + 1
 
            userlog.debug("Adding tag %s at line %i.", tag_node._tag,
 
                          linecount)
 
            line_to_write = "[" + tag_node._tag + "]\n"
 
            targetfile.writelines(line_to_write)
 
 
 
    targetfile.flush()
 
    userlog.info("Finished outputting %s .", file)
 
    targetfile.close()
 
    sourcefile.close()
 
# Resetting the additional tags for another
 
    for node in tags_to_reset_addl:
 
        node.reset_addl()
 
  
# TODO implement
+
<pre>class TemplateNode(TreeNode):
# TODO docstring (when method is finished)
 
def find_graphics_overrides(graphics_directory, graphics_overwrites):
 
"""Identifies all files to be overwritten from the graphics source."""
 
    to_return = []
 
    userlog.info("Locating graphics override files")
 
    for root, dirs, files in os.walk(graphics_directory):
 
        for file in files:
 
            filepath = os.path.join(root, file)
 
            if parsing.path_compatible(filepath, graphics_overwrites):
 
                to_return.append(filepath)
 
    return to_return
 
 
 
 
 
class TemplateNode(TreeNode):
 
 
"""A TreeNode tailored for holding graphics templates.
 
"""A TreeNode tailored for holding graphics templates.
  
Line 325: Line 119:
 
 
 
After creating itself, if the parent isn't None, it adds itself to its parent.
 
After creating itself, if the parent isn't None, it adds itself to its parent.
 
 
"""
 
"""
 
         TreeNode.__init__(self, parent)
 
         TreeNode.__init__(self, parent)
Line 348: Line 141:
 
     def is_standalone_tag(self):
 
     def is_standalone_tag(self):
 
""" Returns True if this is a tag without any non-graphical information. """
 
""" Returns True if this is a tag without any non-graphical information. """
</pre>
+
        return ('$' not in self._tag
 +
                ) and '&' not in self._tag and self._is_graphics_tag
 +
 
 +
    def add_child(self, node):
 +
""" Adds TemplateNode node as a child of this TemplateNode.
 +
 +
If a child with node's ._tag already exists, node will NOT be added.
 +
 +
Parameters:
 +
* node is an existing TemplateNode.
 +
 +
Returns:
 +
* The child now living at this TemplateNode._children[node._tag]. This may be the parameter
 +
node, or it may be a preexisting TemplateNode with the same ._tag as node._tag .
 +
"""
 +
        if node._tag in self._children.keys():
 +
            return self._children[node._tag]
 +
        else:
 +
            self._children[node._tag] = node
 +
            first_token = node._tag.split(':')[0]
 +
            if first_token not in self._childref.keys():
 +
                self._childref[first_token] = []
 +
            self._childref[first_token].append(node)
 +
            return node
 +
 
 +
    def get_child(self, tag):
 +
""" Returns the child node matching tag.
 +
 +
"Matching" in this case can mean two different things:
 +
* If there is a child of this node indexed as tag, that is a match and that node will be returned.
 +
* If not, we compare tag to the child templates of this template, using template-to-tag comparison logic as defined in TemplateNode.get_template_match.
 +
** If more than one child matches tag on this second criterion, the best match is chosen by TemplateNode._get_best_match.
 +
 +
Parameters:
 +
* tag is a a tag or template tag, which we want to compare to the children of this TemplateNode.
 +
 +
Returns:
 +
* The best-matching TemplateNode which is a child of self, or None if no matching children were found.
 +
"""
 +
        if tag in self._children.keys():
 +
            return self._children[tag]
 +
        else:
 +
            return_possibilities = []
 +
            first_token = tag.split(':')[0]
 +
            if first_token in self._childref:
 +
                for child in self._childref[first_token]:
 +
                    return_node = child.get_template_match(tag)
 +
                    if return_node is not None:
 +
                        return_possibilities.append((child, return_node))
 +
                if len(return_possibilities) == 1:
 +
                    return return_possibilities[0][0]
 +
                elif len(return_possibilities) == 0:
 +
                    return None
 +
                else:
 +
                    # TODO error handling
 +
                    userlog.debug("Found more than one matching child. \
 +
                                  Matching children are:")
 +
                    for poss in return_possibilities:
 +
                        userlog.debug(poss[1])
 +
                    possible_tags = [a[1] for a in return_possibilities]
 +
                    winner = TemplateNode._get_best_match(possible_tags)
 +
                    return return_possibilities[possible_tags.index(winner)][0]
 +
            else:
 +
                return None
 +
 
 +
    # This tells if a single tag matches a single tag; that is, it assumes
 +
    # we've got one element of the |-separated list
 +
    def get_template_match(self, tag_to_compare):
 +
""" Returns an expanded configuration of this template that describes/matches the given tag.
 +
 +
Parameters:
 +
* tag_to_compare can be a tag, a template, or a partially-templatized tag. If it is a template, it must be in expanded configuration.
 +
 +
Returns:
 +
* The fully-expanded configuration of self._tag which matches tag_to_compare, as a list of strings, each a token. If there are no matches, returns None. If there are multiple possible configurations, the configuration to return is chosen by _get_best_match.
 +
"""
 +
        if self._tag is None:
 +
            return None
 +
        template_token_bag = []
 +
        template_token_bag.append(self._tag.split(':'))
 +
        candidate_tokens = tag_to_compare.split(':')
 +
 
 +
        ii = 0
 +
        while (len(template_token_bag) > 0 and
 +
              (ii < len(candidate_tokens) or
 +
                ii < len(template_token_bag[0]))):
 +
            good_to_go = False
 +
            for var in template_token_bag:
 +
                if ii < len(candidate_tokens) and ii >= len(var):
 +
                    template_token_bag.remove(var)
 +
                elif ii >= len(var) and len(var) == len(candidate_tokens):
 +
                    good_to_go = True
 +
                elif (ii >= len(candidate_tokens) and
 +
                      (ii >= len(var) or
 +
                      (ii < len(var) and '(0,' not in var[ii]))):
 +
                    template_token_bag.remove(var)
 +
                elif ('&' == var[ii] or '?' == var[ii] or '$' == var[ii] or
 +
                      (ii < len(candidate_tokens) and
 +
                      var[ii] == candidate_tokens[ii])):
 +
                    # This case is an auto-pass
 +
                    good_to_go = True
 +
                else:
 +
                    if '&' in var[ii] or '?' in var[ii] or '$' in var[ii]:
 +
                        varii_type = var[ii][0]
 +
                        varii_range = var[ii][2:var[ii].index(')')].split(',')
 +
                        # If len(varii_range) == 1 then we have a range of
 +
                        # format (x,), indicating any number of :'s
 +
                        if len(varii_range[1]) == 0:
 +
                            varii_range[1] = len(candidate_tokens)-len(var) + 1
 +
                        # For every possible length (the +1 is because range is
 +
                        # exclusive-end and my notation is inclusive-end)
 +
                        for jj in range(int(varii_range[0]),
 +
                                        int(varii_range[1])+1):
 +
                            # Make a copy of var
 +
                            new_var = var[:]
 +
                            # Remove the range item
 +
                            del new_var[ii]
 +
                            # Replace it with (one of the possible lengths)
 +
                            # times the multiplied symbol
 +
                            # If jj is 0 the range item is just removed
 +
                            for kk in range(0, jj):
 +
                                new_var.insert(ii, varii_type)
 +
                            # Place the new variant in the token bag for
 +
                            # evaluation
 +
                            template_token_bag.append(new_var)
 +
                    # No counting, there is a new template_token_bag[ii]
 +
                    template_token_bag.remove(var)
 +
            if good_to_go:
 +
                ii += 1
 +
        for tag in template_token_bag:
 +
            if len(tag) != len(candidate_tokens):
 +
                template_token_bag.remove(tag)  # This isn't working properly?
 +
 
 +
        if len(template_token_bag) == 0:
 +
            return None
 +
        elif len(template_token_bag) == 1:
 +
            return template_token_bag[0]
 +
        else:
 +
            highest_priority = TemplateNode._get_best_match(template_token_bag)
 +
            userlog.debug("More than one template matched.\nTag: %s Matches:",
 +
                          tag_to_compare)
 +
            for template in template_token_bag:
 +
                userlog.debug("\t%s", template)
 +
            userlog.debug("The highest-priority match is %s", highest_priority)
 +
            # Technically this does in fact have a matching template
 +
            return highest_priority
 +
 
 +
    @staticmethod
 +
    def _get_best_match(template_tokens_bag):
 +
""" Chooses between different templates which both have been found to match a tag.
 +
 +
It chooses based on the number of tokens which are NOT wildcards. The principle is that a template which matches a tag with fewer wildcards is probably a better match, because matches on wildcards are cheap.
 +
 +
Parameters:
 +
* template_tokens_bag is a list of fully-expanded templates, with each template configured as a list of tokens.
 +
 +
Returns:
 +
* the list in template_tokens_bag with the least wildcards. If there is a tie on the number of wildcards, it returns the earliest one in the list.
 +
"""
 +
        if not template_tokens_bag:        # empty bag returns false
 +
            return None
 +
        elif len(template_tokens_bag) == 1:
 +
            return template_tokens_bag[0]
 +
        else:
 +
            best_currently = template_tokens_bag[0]
 +
            best_tokens = 0
 +
            for tag in template_tokens_bag:
 +
                challenger_tokens = 0
 +
                for token in tag:
 +
                    if token != '?' and token != '&' and token != '$':
 +
                        challenger_tokens = challenger_tokens + 1
 +
                if challenger_tokens > best_tokens:
 +
                    best_currently = tag
 +
                    best_tokens = challenger_tokens
 +
            return best_currently
 +
 
 +
    def how_many_generations(self):
 +
""" Returns how deep into the template tree this node lives."""
 +
# TODO consider moving this into TreeNode
 +
        temp_node = self
 +
        count = -1
 +
        global template_tree
 +
        while temp_node != template_tree:
 +
            temp_node = temp_node._parent
 +
            count = count + 1
 +
        return count</pre>

Latest revision as of 22:05, 31 July 2015

About[edit]

Features[edit]

Planned Features[edit]

Usage Instructions[edit]

Download[edit]

You can download the script from GitHub.

Python 3[edit]

Configuration[edit]

Button's Workspace[edit]

Sometimes I'm on a computer I can't put a git client on.

# Modders' guide to BAMM! graphics references
# Place your conversions in a file named bamm_gfx_<identifier here>.txt . The file should be encoded in codepage 437, the same as the Dwarf Fortress raws.

# Start by defining names for your target graphics elements. 
# These names must always start with @.
# So if you want to reference the background color being used for the Lion creature token, you can do that with this definition:
# CREATURE:LION|COLOR::@lionbgcolor
# or
# CREATURE:LION|COLOR::@lionbgcolor:
# If you want to reference the *whole* color of the lion tile, which is a 3-argument value, you can add a number after the @, like this:
# CREATURE:LION|COLOR:@3lionfullcolor
# Or you can declare references for each argument of the lion color individually:
# CREATURE:LION|COLOR:@lionfgcolor:@lionbgcolor:@lionbrightness

# The name is case-sensitive and can contain any character in the alphabet. Numbers are not allowed except immediately after the @. Special characters, including underscores, are not allowed.

# You can also create generic all-graphics-belonging-to-this-object referents this way:
# CREATURE:LION|@allLionTags

# BE AWARE: trying to define non-graphics or partially non-graphic referents won't work. Referent declarations which include non-graphic information will simply be ignored.

# OK, now that we have our referents, it's time to use them to modify raws.

# creature_modded.txt
# [CREATURE:MODDED_LION]
# @allLionTags

# This sets all of the modded lion's graphics equal to the lion's graphics, in any graphics set.
# However, this "copy all" functionality couldn't be used to set properties belonging to a non-creature object:

# [INORGANIC:LIONSTONE]	This won't work
# @allLionTags			This won't work

# In order to make lionstone the color of a lion, you'll need to reference the colors instead:
# [INORGANIC:LIONSTONE]
# [DISPLAY_COLOR:@3lionfullcolor]

# NB. References are always made to graphics as declared in the graphics source raws. If you declare a lion to use a deer's graphics, and a deer to use a lion's graphics, they'll swap, not have the same graphics.

# Eventually I intend to expand this to include the ability to apply these changes based on the properties of entries, but right now, this should be enough.

# TODO docstring		
def handle_graphics_references(graphics_root,def_file):
	# Runs after the graphics source has been pulled in
	# Modifies same
	global graphics_referents
	if graphics_refs is None:
		graphics_refs = []	# This is meant to be a dict, check syntax
	definitions_over_yet = False
	for line in def_file:
		actline = line.split('#')[0].strip()
		if len(actline) == 0:
			continue
		elif actline == defs_over	# TODO declare this as a constant
			definitions_over_yet = True
		elif not definitions_over_yet:
			# load referent definition
			# If the referent name already exists in graphics_refs, write an error to the log and continue to the next line.
			# Otherwise, match it against a template
			# Then match it against the GRAPHICS NODES that meet that template
				# Not the bound node or the target node. 
			# (TODO make each template node hold lists of objects that refer to it, by function.)
			# If a partial definition matches more than one object, keep track of all of them so they can be narrowed down later
			# If a full definition matches more than one object, write a warning to the logfile and choose one arbitrarily to go with.
			# If a full definition matches no graphics object, write a warning to the log and try it on target objects. 
				# If a full definition matches neither graphics nor target, write an error to the log.
			# Put the mapping of referent name to an object storing the reference info into graphics_refs
		else:
			# Get a collection of all the targets that meet the reference
class TemplateNode(TreeNode):
"""A TreeNode tailored for holding graphics templates.

TemplateNodes store the patterns which define what tags are graphics tags, and which of their tokens are immutable, identifying, graphical, or irrelevant. Each TemplateNode represents the template for a single type of graphics tag.

Members:
* _tag is the template string. It is a series of tokens separated by colons. Each token is one of the following:
    * LITERAL. This is a string, usually in all caps, which must be present in this exact form for a tag to match this template. The first token in a template is always a literal. Enums (e.g. the PLANT_GROWTH timing values of ALL and NONE) may simulated by splitting a template into multiple templates, one for each enum, with the enum in question present as a literal.
	* VARIABLE. This is a single character, '?' '$' or '&' (quotes not present in _tag). 
		* '?' indicates that this is a graphics token. When matching a tag to a template, this can match any value. When merging occurs, this token's value will come from the graphics source.
		* '&' indicates that this is an info token. When matching a tag to a template, this can match any value. When merging occurs, this token's value will come from the raw source.
		* '$' indicates that this is an identifier token. When matching a tag to a template, this can match any value; but for two tags to match each other, they must share the same values for all identifier tokens in their template.
	* VARIABLE RANGE. This is a variable character, followed by a range in parentheses, e.g. ?(0,3). This means that the tag can contain 0-3 (inclusive) tokens in this position, all of which hold graphical information. The second value is optional - if omitted, it means there is no upper bound on how many tokens can be in this series.
For examples, please see graphics_templates.config . The string between each pipe ( | ) is a valid TemplateNode._tag .
* _children is a dict containing the TemplateNodes representing the types of tags which are allowed to be children of the type of tag represented by this TemplateNode. The keys are the full _tags of the child TemplateNodes.
* _childref is the same as _children, but indexed by the first token (the "value") of the child's _tag, instead of the full _tag. For convenience/performance only.
* _parent is the TemplateNode representing the type of tag that the type of tag this TemplateNode represents can be a child of.
* _is_graphics_tag is a boolean that lets us know if this tag is a graphics tag, as opposed to a template included because it can have graphics children. This is necessary because some graphics tags are composed of only a single literal token.
"""

    # string does not contain the character '|'.
    def __init__(self, parent, string=""):
	""" Initializes a TemplateNode
	
	Parameters:
	* parent is the parent TemplateNode of this node, or None. If it is None, this TemplateNode will replace the current global template_tree.
	* string is the template-formatted string which will be this TemplateNode's _tag.
	
	After creating itself, if the parent isn't None, it adds itself to its parent.
	"""
        TreeNode.__init__(self, parent)
        self._is_graphics_tag = False
        self._childref = {}
        self._tag = None
        global template_tree
        if parent is None:
            self._parent = None
            template_tree = self
        else:
            if template_tree is None:
                self._parent = TemplateNode(None, "")
            else:
                self._parent = parent

            self._tag = string

            parent.add_child(self)


    def is_standalone_tag(self):
	""" Returns True if this is a tag without any non-graphical information. """
        return ('$' not in self._tag
                ) and '&' not in self._tag and self._is_graphics_tag

    def add_child(self, node):
	""" Adds TemplateNode node as a child of this TemplateNode. 
	
	If a child with node's ._tag already exists, node will NOT be added.
	
	Parameters:
	* node is an existing TemplateNode.
	
	Returns:
	* The child now living at this TemplateNode._children[node._tag]. This may be the parameter 
	node, or it may be a preexisting TemplateNode with the same ._tag as node._tag .
	"""
        if node._tag in self._children.keys():
            return self._children[node._tag]
        else:
            self._children[node._tag] = node
            first_token = node._tag.split(':')[0]
            if first_token not in self._childref.keys():
                self._childref[first_token] = []
            self._childref[first_token].append(node)
            return node

    def get_child(self, tag):
		""" Returns the child node matching tag.
		
		"Matching" in this case can mean two different things:
		* If there is a child of this node indexed as tag, that is a match and that node will be returned.
		* If not, we compare tag to the child templates of this template, using template-to-tag comparison logic as defined in TemplateNode.get_template_match. 
		** If more than one child matches tag on this second criterion, the best match is chosen by TemplateNode._get_best_match.
		
		Parameters:
		* tag is a a tag or template tag, which we want to compare to the children of this TemplateNode.
		
		Returns:
		* The best-matching TemplateNode which is a child of self, or None if no matching children were found.
		"""
        if tag in self._children.keys():
            return self._children[tag]
        else:
            return_possibilities = []
            first_token = tag.split(':')[0]
            if first_token in self._childref:
                for child in self._childref[first_token]:
                    return_node = child.get_template_match(tag)
                    if return_node is not None:
                        return_possibilities.append((child, return_node))
                if len(return_possibilities) == 1:
                    return return_possibilities[0][0]
                elif len(return_possibilities) == 0:
                    return None
                else:
                    # TODO error handling
                    userlog.debug("Found more than one matching child. \
                                  Matching children are:")
                    for poss in return_possibilities:
                        userlog.debug(poss[1])
                    possible_tags = [a[1] for a in return_possibilities]
                    winner = TemplateNode._get_best_match(possible_tags)
                    return return_possibilities[possible_tags.index(winner)][0]
            else:
                return None

    # This tells if a single tag matches a single tag; that is, it assumes
    # we've got one element of the |-separated list
    def get_template_match(self, tag_to_compare):
		""" Returns an expanded configuration of this template that describes/matches the given tag.
		
		Parameters:
		* tag_to_compare can be a tag, a template, or a partially-templatized tag. If it is a template, it must be in expanded configuration.
		
		Returns:
		* The fully-expanded configuration of self._tag which matches tag_to_compare, as a list of strings, each a token. If there are no matches, returns None. If there are multiple possible configurations, the configuration to return is chosen by _get_best_match.
		"""
        if self._tag is None:
            return None
        template_token_bag = []
        template_token_bag.append(self._tag.split(':'))
        candidate_tokens = tag_to_compare.split(':')

        ii = 0
        while (len(template_token_bag) > 0 and
               (ii < len(candidate_tokens) or
                ii < len(template_token_bag[0]))):
            good_to_go = False
            for var in template_token_bag:
                if ii < len(candidate_tokens) and ii >= len(var):
                    template_token_bag.remove(var)
                elif ii >= len(var) and len(var) == len(candidate_tokens):
                    good_to_go = True
                elif (ii >= len(candidate_tokens) and
                      (ii >= len(var) or
                       (ii < len(var) and '(0,' not in var[ii]))):
                    template_token_bag.remove(var)
                elif ('&' == var[ii] or '?' == var[ii] or '$' == var[ii] or
                      (ii < len(candidate_tokens) and
                       var[ii] == candidate_tokens[ii])):
                    # This case is an auto-pass
                    good_to_go = True
                else:
                    if '&' in var[ii] or '?' in var[ii] or '$' in var[ii]:
                        varii_type = var[ii][0]
                        varii_range = var[ii][2:var[ii].index(')')].split(',')
                        # If len(varii_range) == 1 then we have a range of
                        # format (x,), indicating any number of :'s
                        if len(varii_range[1]) == 0:
                            varii_range[1] = len(candidate_tokens)-len(var) + 1
                        # For every possible length (the +1 is because range is
                        # exclusive-end and my notation is inclusive-end)
                        for jj in range(int(varii_range[0]),
                                        int(varii_range[1])+1):
                            # Make a copy of var
                            new_var = var[:]
                            # Remove the range item
                            del new_var[ii]
                            # Replace it with (one of the possible lengths)
                            # times the multiplied symbol
                            # If jj is 0 the range item is just removed
                            for kk in range(0, jj):
                                new_var.insert(ii, varii_type)
                            # Place the new variant in the token bag for
                            # evaluation
                            template_token_bag.append(new_var)
                    # No counting, there is a new template_token_bag[ii]
                    template_token_bag.remove(var)
            if good_to_go:
                ii += 1
        for tag in template_token_bag:
            if len(tag) != len(candidate_tokens):
                template_token_bag.remove(tag)  # This isn't working properly?

        if len(template_token_bag) == 0:
            return None
        elif len(template_token_bag) == 1:
            return template_token_bag[0]
        else:
            highest_priority = TemplateNode._get_best_match(template_token_bag)
            userlog.debug("More than one template matched.\nTag: %s Matches:",
                          tag_to_compare)
            for template in template_token_bag:
                userlog.debug("\t%s", template)
            userlog.debug("The highest-priority match is %s", highest_priority)
            # Technically this does in fact have a matching template
            return highest_priority

    @staticmethod
    def _get_best_match(template_tokens_bag):
		""" Chooses between different templates which both have been found to match a tag.
		
		It chooses based on the number of tokens which are NOT wildcards. The principle is that a template which matches a tag with fewer wildcards is probably a better match, because matches on wildcards are cheap.
		
		Parameters:
		* template_tokens_bag is a list of fully-expanded templates, with each template configured as a list of tokens.
		
		Returns:
		* the list in template_tokens_bag with the least wildcards. If there is a tie on the number of wildcards, it returns the earliest one in the list.
		"""
        if not template_tokens_bag:        # empty bag returns false
            return None
        elif len(template_tokens_bag) == 1:
            return template_tokens_bag[0]
        else:
            best_currently = template_tokens_bag[0]
            best_tokens = 0
            for tag in template_tokens_bag:
                challenger_tokens = 0
                for token in tag:
                    if token != '?' and token != '&' and token != '$':
                        challenger_tokens = challenger_tokens + 1
                if challenger_tokens > best_tokens:
                    best_currently = tag
                    best_tokens = challenger_tokens
            return best_currently

    def how_many_generations(self):
	""" Returns how deep into the template tree this node lives."""
		# TODO consider moving this into TreeNode
        temp_node = self
        count = -1
        global template_tree
        while temp_node != template_tree:
            temp_node = temp_node._parent
            count = count + 1
        return count