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
Line 187: Line 187:
 
This initializes the scaffolding for all future raw parsing, which is stored in graphics.template_tree .
 
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
 +
# 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.
 +
 +
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. """
 
</pre>
 
</pre>

Revision as of 21:50, 5 June 2015

About

Features

Planned Features

Usage Instructions

Download

You can download the script from GitHub.

Python 3

Configuration

Button's Workspace

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

_is_logging_initialized = False

# TODO Allow file to be passed in, or possibly even a dict?
def load_run_config():
	"""Load config information from the default file.
	
	Also initializes loggers if they haven't been already.
	"""
    print("Loading run configuration...")
    global runconfig
    runconfig_file = open(runconfig, 'r')
    global properties
    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()

	initialize_logging()
	userlog.info("**********")
    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)
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
    fmt = logging.Formatter('%(message)s')

    userhandler = logging.FileHandler(properties[USERSLOG][1])
    userhandler.setFormatter(fmt)
    userlog.addHandler(userhandler)

    if properties[DEBUG][1]:
        userlog.setLevel(logging.DEBUG)
    else:
        userlog.setLevel(logging.INFO)

    modderhandler = logging.FileHandler(properties[MODDERSLOG][1])
    modderhandler.setFormatter(fmt)
    modderslog.addHandler(modderhandler)
    modderslog.setLevel(logging.INFO)

def _property_has_format_error(propkey, value):
	"""Returns True if the property is formatted incorrectly.
	
	* 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):
	""" 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):
	""" 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
    # Literal colons are going to require some special processing, because of
    # the following case:  GROWTH:'r':'x': etc. That's why we can't just use
    # a blind replaceAll.

    # If odd, we are inside a tag. If even, we are outside a tag.
    bracketscount = 0
    count = 0                # Where we are in the string
    quotescount = 0
    while count < len(line)-2:
        # Going from inside a tag to outside or vice versa
        if (((bracketscount % 2 == 0 and line[count] == "[") or
             (bracketscount % 2 == 1 and line[count] == "]"))):
            bracketscount += 1
        # We are inside a tag and we have discovered a ' character beginning a
        # literal value, with another 2 characters down on the other side.
        elif (quotescount % 2 == 0 and bracketscount % 2 == 1 and
              line[count:count+3] in ascii_codes.keys()):
            # If the character could be a problem for later processing, replace
            #  it with its ascii code.
            line = line[:count] + ascii_codes[line[count:count+3]] + \
                line[count+3:]
        elif line[count] == "'":
            quotescount += 1
        elif bracketscount % 2 == 1 and line[count] == ':':
            quotescount = 0
        count += 1
    # line has now had its literal "use this tile" versions of its special
    # characters replaced with their numbers.
    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
# 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.

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. """