pyffi.spells.nif.modify

Last Built: Mar 06, 2020

Source code for pyffi.spells.nif.modify

"""
:mod:`pyffi.spells.nif.modify` ---  spells to make modifications
=================================================================
Module which contains all spells that modify a nif.

.. autoclass:: SpellTexturePath
   :show-inheritance:
   :members:

.. autoclass:: SpellSubstituteTexturePath
   :show-inheritance:
   :members:

.. autoclass:: SpellLowResTexturePath
   :show-inheritance:
   :members:

.. autoclass:: SpellCollisionType
   :show-inheritance:
   :members:

.. autoclass:: SpellCollisionMaterial
   :show-inheritance:
   :members:

.. autoclass:: SpellScaleAnimationTime
   :show-inheritance:
   :members:

.. autoclass:: SpellReverseAnimation
   :show-inheritance:
   :members:

.. autoclass:: SpellSubstituteStringPalette
   :show-inheritance:
   :members:

.. autoclass:: SpellChangeBonePriorities
   :show-inheritance:
   :members:

.. autoclass:: SpellSetInterpolatorTransRotScale
   :show-inheritance:
   :members:

.. autoclass:: SpellDelInterpolatorTransformData
   :show-inheritance:
   :members:

.. autoclass:: SpellDelBranches
   :show-inheritance:
   :members:

.. autoclass:: _SpellDelBranchClasses
   :show-inheritance:
   :members:

.. autoclass:: SpellDelSkinShapes
   :show-inheritance:
   :members:

.. autoclass:: SpellDisableParallax
   :show-inheritance:
   :members:

.. autoclass:: SpellAddStencilProperty
   :show-inheritance:
   :members:

.. autoclass:: SpellDelVertexColor
   :show-inheritance:
   :members:

.. autoclass:: SpellMakeSkinlessNif
   :show-inheritance:
   :members:

.. autoclass:: SpellCleanFarNif
   :show-inheritance:
   :members:

.. autoclass:: SpellMakeFarNif
   :show-inheritance:
   :members:

"""

# --------------------------------------------------------------------------
# ***** BEGIN LICENSE BLOCK *****
#
# Copyright (c) 2007-2012, NIF File Format Library and Tools.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
#
#    * Redistributions of source code must retain the above copyright
#      notice, this list of conditions and the following disclaimer.
#
#    * Redistributions in binary form must reproduce the above
#      copyright notice, this list of conditions and the following
#      disclaimer in the documentation and/or other materials provided
#      with the distribution.
#
#    * Neither the name of the NIF File Format Library and Tools
#      project nor the names of its contributors may be used to endorse
#      or promote products derived from this software without specific
#      prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
#
# ***** END LICENSE BLOCK *****
# --------------------------------------------------------------------------

from pyffi.formats.nif import NifFormat
from pyffi.object_models.common import _as_bytes
from pyffi.spells.nif import NifSpell
import pyffi.spells.nif
import pyffi.spells.nif.check # recycle checking spells for update spells
import pyffi.spells.nif.fix


import codecs
import os
import re

[docs]class SpellTexturePath( pyffi.spells.nif.fix.SpellParseTexturePath): """Changes the texture path while keeping the texture names.""" SPELLNAME = "modify_texturepath" READONLY = False
[docs] @classmethod def toastentry(cls, toaster): if not toaster.options["arg"]: toaster.logger.warn( "must specify path as argument " "(e.g. -a textures\\pm\\dungeons\\bloodyayleid\\interior) " "to apply spell") return False else: toaster.texture_path = str(toaster.options["arg"]) # standardize the path toaster.texture_path = toaster.texture_path.replace("/", os.sep) toaster.texture_path = toaster.texture_path.replace("\\", os.sep) return True
[docs] def substitute(self, old_path): # note: replace backslashes by os.sep in filename, and # when joined, revert them back, for linux new_path = os.path.join( self.toaster.texture_path, os.path.basename(old_path.replace("\\", os.sep)) ).replace(os.sep, "\\") if new_path != old_path: self.changed = True self.toaster.msg("%s -> %s" % (old_path, new_path)) return new_path
[docs]class SpellSubstituteTexturePath( pyffi.spells.nif.fix.SpellFixTexturePath): """Runs a regex replacement on texture paths.""" SPELLNAME = "modify_substitutetexturepath"
[docs] @classmethod def toastentry(cls, toaster): arg = toaster.options["arg"] if not arg: # missing arg toaster.logger.warn( "must specify regular expression and substitution as argument " "(e.g. -a /architecture/city) to apply spell") return False dummy, toaster.regex, toaster.sub = arg.split(arg[0]) toaster.sub = _as_bytes(toaster.sub) toaster.regex = re.compile(_as_bytes(toaster.regex)) return True
[docs] def substitute(self, old_path): """Returns modified texture path, and reports if path was modified. """ if not old_path: # leave empty path be return old_path new_path = self.toaster.regex.sub(self.toaster.sub, old_path) if old_path != new_path: self.changed = True self.toaster.msg("%s -> %s" % (old_path, new_path)) return new_path
[docs]class SpellLowResTexturePath(SpellSubstituteTexturePath): """Changes the texture path by replacing 'textures\\*' with 'textures\\lowres\\*' - used mainly for making _far.nifs """ SPELLNAME = "modify_texturepathlowres"
[docs] @classmethod def toastentry(cls, toaster): toaster.sub = _as_bytes("textures\\\\lowres\\\\") toaster.regex = re.compile(_as_bytes("^textures\\\\"), re.IGNORECASE) return True
[docs] def substitute(self, old_path): if (_as_bytes('\\lowres\\') not in old_path.lower()): return SpellSubstituteTexturePath.substitute(self, old_path) else: return old_path
[docs]class SpellCollisionType(NifSpell): """Sets the object collision to be a different type""" SPELLNAME = "modify_collisiontype" READONLY = False class CollisionTypeStatic: layer = 1 motion_system = 7 unknown_byte1 = 1 unknown_byte2 = 1 quality_type = 1 wind = 0 solid = True mass = 0 class CollisionTypeAnimStatic(CollisionTypeStatic): layer = 2 motion_system = 6 unknown_byte1 = 2 unknown_byte2 = 2 quality_type = 2 class CollisionTypeTerrain(CollisionTypeStatic): layer = 14 motion_system = 7 class CollisionTypeClutter(CollisionTypeAnimStatic): layer = 4 motion_system = 4 quality_type = 3 mass = 10 class CollisionTypeWeapon(CollisionTypeClutter): layer = 5 mass = 25 class CollisionTypeNonCollidable(CollisionTypeStatic): layer = 15 motion_system = 7 COLLISION_TYPE_DICT = { "static": CollisionTypeStatic, "anim_static": CollisionTypeAnimStatic, "clutter": CollisionTypeClutter, "weapon": CollisionTypeWeapon, "terrain": CollisionTypeTerrain, "non_collidable": CollisionTypeNonCollidable }
[docs] @classmethod def toastentry(cls, toaster): try: toaster.col_type = cls.COLLISION_TYPE_DICT[toaster.options["arg"]] except KeyError: # incorrect arg toaster.logger.warn( "must specify collision type to change to as argument " "(e.g. -a static (accepted names: %s) " "to apply spell" % ", ".join(iter(cls.COLLISION_TYPE_DICT.keys()))) return False else: return True
[docs] def datainspect(self): return self.inspectblocktype(NifFormat.bhkRigidBody)
[docs] def branchinspect(self, branch): # only inspect the NiAVObject branch return isinstance(branch, (NifFormat.NiAVObject, NifFormat.bhkCollisionObject, NifFormat.bhkRigidBody, NifFormat.bhkMoppBvTreeShape, NifFormat.bhkPackedNiTriStripsShape))
[docs] def branchentry(self, branch): if isinstance(branch, NifFormat.bhkRigidBody): self.changed = True branch.layer = self.toaster.col_type.layer branch.layer_copy = self.toaster.col_type.layer branch.mass = self.toaster.col_type.mass branch.motion_system = self.toaster.col_type.motion_system branch.unknown_byte_1 = self.toaster.col_type.unknown_byte1 branch.unknown_byte_2 = self.toaster.col_type.unknown_byte2 branch.quality_type = self.toaster.col_type.quality_type branch.wind = self.toaster.col_type.wind branch.solid = self.toaster.col_type.solid self.toaster.msg("collision set to %s" % self.toaster.options["arg"]) # bhkPackedNiTriStripsShape could be further down, so keep looking return True elif isinstance(branch, NifFormat.bhkPackedNiTriStripsShape): self.changed = True for subshape in branch.get_sub_shapes(): subshape.layer = self.toaster.col_type.layer self.toaster.msg("collision set to %s" % self.toaster.options["arg"]) # all extra blocks here done; no need to recurse further return False else: # recurse further return True
[docs]class SpellScaleAnimationTime(NifSpell): """Scales the animation time.""" SPELLNAME = "modify_scaleanimationtime" READONLY = False
[docs] @classmethod def toastentry(cls, toaster): if not toaster.options["arg"]: toaster.logger.warn( "must specify scaling number as argument " "(e.g. -a 0.6) to apply spell") return False else: toaster.animation_scale = float(toaster.options["arg"]) return True
[docs] def datainspect(self): # returns more than needed but easiest way to ensure it catches all # types of animations return True
[docs] def branchinspect(self, branch): # inspect the NiAVObject branch, and NiControllerSequence # branch (for kf files) return isinstance(branch, (NifFormat.NiAVObject, NifFormat.NiTimeController, NifFormat.NiInterpolator, NifFormat.NiControllerManager, NifFormat.NiControllerSequence, NifFormat.NiKeyframeData, NifFormat.NiTextKeyExtraData, NifFormat.NiFloatData))
[docs] def branchentry(self, branch): def scale_key_times(keys): """Helper function to scale key times.""" for key in keys: key.time *= self.toaster.animation_scale if isinstance(branch, NifFormat.NiKeyframeData): self.changed = True if branch.rotation_type == 4: scale_key_times(branch.xyz_rotations[0].keys) scale_key_times(branch.xyz_rotations[1].keys) scale_key_times(branch.xyz_rotations[2].keys) else: scale_key_times(branch.quaternion_keys) scale_key_times(branch.translations.keys) scale_key_times(branch.scales.keys) # no children of NiKeyframeData so no need to recurse further return False elif isinstance(branch, NifFormat.NiControllerSequence): self.changed = True branch.stop_time *= self.toaster.animation_scale # recurse further into children of NiControllerSequence return True elif isinstance(branch, NifFormat.NiTextKeyExtraData): self.changed = True scale_key_times(branch.text_keys) # no children of NiTextKeyExtraData so no need to recurse further return False elif isinstance(branch, NifFormat.NiTimeController): self.changed = True branch.stop_time *= self.toaster.animation_scale # recurse further into children of NiTimeController return True elif isinstance(branch, NifFormat.NiFloatData): self.changed = True scale_key_times(branch.data.keys) # no children of NiFloatData so no need to recurse further return False else: # recurse further return True
[docs]class SpellReverseAnimation(NifSpell): """Reverses the animation by reversing datas in relation to the time.""" SPELLNAME = "modify_reverseanimation" READONLY = False
[docs] def datainspect(self): # returns more than needed but easiest way to ensure it catches all # types of animations return True
[docs] def branchinspect(self, branch): # inspect the NiAVObject branch, and NiControllerSequence # branch (for kf files) return isinstance(branch, (NifFormat.NiAVObject, NifFormat.NiTimeController, NifFormat.NiInterpolator, NifFormat.NiControllerManager, NifFormat.NiControllerSequence, NifFormat.NiKeyframeData, NifFormat.NiTextKeyExtraData, NifFormat.NiFloatData))
[docs] def branchentry(self, branch): def reverse_keys(keys): """Helper function to reverse keys.""" # copy the values key_values = [key.value for key in keys] # reverse them for key, new_value in zip(keys, reversed(key_values)): key.value = new_value if isinstance(branch, NifFormat.NiKeyframeData): self.changed = True # (this also covers NiTransformData) if branch.rotation_type == 4: reverse_keys(branch.xyz_rotations[0].keys) reverse_keys(branch.xyz_rotations[1].keys) reverse_keys(branch.xyz_rotations[2].keys) else: reverse_keys(branch.quaternion_keys) reverse_keys(branch.translations.keys) reverse_keys(branch.scales.keys) # no children of NiTransformData so no need to recurse further return False elif isinstance(branch, NifFormat.NiTextKeyExtraData): self.changed = True reverse_keys(branch.text_keys) # no children of NiTextKeyExtraData so no need to recurse further return False elif isinstance(branch, NifFormat.NiFloatData): self.changed = True reverse_keys(branch.data.keys) # no children of NiFloatData so no need to recurse further return False else: # recurse further return True
[docs]class SpellCollisionMaterial(NifSpell): """Sets the object's collision material to be a different type""" SPELLNAME = "modify_collisionmaterial" READONLY = False class CollisionMaterialStone: material = 0 class CollisionMaterialCloth: material = 1 class CollisionMaterialMetal: material = 5 COLLISION_MATERIAL_DICT = { "stone": CollisionMaterialStone, "cloth": CollisionMaterialCloth, "metal": CollisionMaterialMetal }
[docs] @classmethod def toastentry(cls, toaster): try: toaster.col_material = cls.COLLISION_MATERIAL_DICT[toaster.options["arg"]] except KeyError: # incorrect arg toaster.logger.warn( "must specify collision material to change to as argument " "(e.g. -a stone (accepted names: %s) " "to apply spell" % ", ".join(iter(cls.COLLISION_MATERIAL_DICT.keys()))) return False else: return True
[docs] def datainspect(self): return self.inspectblocktype(NifFormat.bhkShape)
[docs] def branchinspect(self, branch): # only inspect the NiAVObject branch return isinstance(branch, (NifFormat.NiAVObject, NifFormat.bhkCollisionObject, NifFormat.bhkRigidBody, NifFormat.bhkShape))
[docs] def branchentry(self, branch): if isinstance(branch, NifFormat.bhkShape): self.changed = True branch.material = self.toaster.col_material.material self.toaster.msg("collision material set to %s" % self.toaster.options["arg"]) # bhkPackedNiTriStripsShape could be further down, so keep looking return True elif isinstance(branch, NifFormat.bhkPackedNiTriStripsShape): self.changed = True for subshape in branch.get_sub_shapes(): subshape.material = self.toaster.col_type.material self.toaster.msg("collision material set to %s" % self.toaster.options["arg"]) # all extra blocks here done; no need to recurse further return False else: # recurse further return True
[docs]class SpellDelBranches(NifSpell): """Delete blocks that match the exclude list.""" SPELLNAME = "modify_delbranches" READONLY = False
[docs] def is_branch_to_be_deleted(self, branch): """Returns ``True`` for those branches that must be deleted. The default implementation returns ``True`` for branches that are not admissible as specified by include/exclude options of the toaster. Override in subclasses that must delete specific branches. """ # check if it is excluded or not return not self.toaster.is_admissible_branch_class(branch.__class__)
def _branchinspect(self, branch): """This spell inspects every branch, also the non-admissible ones, therefore we must override this method. """ return True
[docs] def branchentry(self, branch): """Strip branch if it is flagged for deletion. """ # check if it is to be deleted or not if self.is_branch_to_be_deleted(branch): # it is, wipe it out self.toaster.msg("stripping this branch") self.data.replace_global_node(branch, None) self.changed = True # do not recurse further return False else: # this one was not excluded, keep recursing return True
[docs]class _SpellDelBranchClasses(SpellDelBranches): """Delete blocks that match a given list. Only useful as base class for other spells. """ BRANCH_CLASSES_TO_BE_DELETED = () """List of branch classes that have to be deleted."""
[docs] def datainspect(self): return any( self.inspectblocktype(branch_class) for branch_class in self.BRANCH_CLASSES_TO_BE_DELETED)
[docs] def is_branch_to_be_deleted(self, branch): return isinstance(branch, self.BRANCH_CLASSES_TO_BE_DELETED)
[docs]class SpellDelVertexColor(SpellDelBranches): """Delete vertex color properties and vertex color data.""" SPELLNAME = "modify_delvertexcolor"
[docs] def is_branch_to_be_deleted(self, branch): return isinstance(branch, NifFormat.NiVertexColorProperty)
[docs] def datainspect(self): return self.inspectblocktype(NifFormat.NiTriBasedGeom)
[docs] def branchinspect(self, branch): # only inspect the NiAVObject branch return isinstance(branch, (NifFormat.NiAVObject, NifFormat.NiTriBasedGeomData, NifFormat.NiVertexColorProperty))
[docs] def branchentry(self, branch): # delete vertex color property SpellDelBranches.branchentry(self, branch) # reset vertex color flags if isinstance(branch, NifFormat.NiTriBasedGeomData): if branch.has_vertex_colors: self.toaster.msg("removing vertex colors") branch.has_vertex_colors = False self.changed = True # no children; no need to recurse further return False # recurse further return True
# identical to niftoaster.py modify_delbranches -x NiVertexColorProperty # delete? class SpellDelVertexColorProperty(_SpellDelBranchClasses): """Delete vertex color property if it is present.""" SPELLNAME = "modify_delvertexcolorprop" BRANCH_CLASSES_TO_BE_DELETED = (NifFormat.NiVertexColorProperty,) # identical to niftoaster.py modify_delbranches -x NiAlphaProperty # delete? class SpellDelAlphaProperty(_SpellDelBranchClasses): """Delete alpha property if it is present.""" SPELLNAME = "modify_delalphaprop" BRANCH_CLASSES_TO_BE_DELETED = (NifFormat.NiAlphaProperty,) # identical to niftoaster.py modify_delbranches -x NiSpecularProperty # delete? class SpellDelSpecularProperty(_SpellDelBranchClasses): """Delete specular property if it is present.""" SPELLNAME = "modify_delspecularprop" BRANCH_CLASSES_TO_BE_DELETED = (NifFormat.NiSpecularProperty,) # identical to niftoaster.py modify_delbranches -x BSXFlags # delete? class SpellDelBSXFlags(_SpellDelBranchClasses): """Delete BSXFlags if any are present.""" SPELLNAME = "modify_delbsxflags" BRANCH_CLASSES_TO_BE_DELETED = (NifFormat.BSXFlags,) # identical to niftoaster.py modify_delbranches -x NiStringExtraData # delete? class SpellDelStringExtraDatas(_SpellDelBranchClasses): """Delete NiSringExtraDatas if they are present.""" SPELLNAME = "modify_delstringextradatas" BRANCH_CLASSES_TO_BE_DELETED = (NifFormat.NiStringExtraData,)
[docs]class SpellDelSkinShapes(SpellDelBranches): """Delete any geometries with a material name of 'skin'""" SPELLNAME = "modify_delskinshapes"
[docs] def is_branch_to_be_deleted(self, branch): if isinstance(branch, NifFormat.NiTriBasedGeom): for prop in branch.get_properties(): if isinstance(prop, NifFormat.NiMaterialProperty): if prop.name.lower() == "skin": # skin material, tag for deletion return True # do not delete anything else return False
[docs] def branchinspect(self, branch): # only inspect the NiAVObject branch return isinstance(branch, NifFormat.NiAVObject)
# identical to niftoaster.py modify_delbranches -x NiCollisionObject # delete? class SpellDelCollisionData(_SpellDelBranchClasses): """Deletes any Collision data present.""" SPELLNAME = "modify_delcollision" BRANCH_CLASSES_TO_BE_DELETED = (NifFormat.NiCollisionObject,) # identical to niftoaster.py modify_delbranches -x NiTimeController # delete? class SpellDelAnimation(_SpellDelBranchClasses): """Deletes any animation data present.""" SPELLNAME = "modify_delanimation" BRANCH_CLASSES_TO_BE_DELETED = (NifFormat.NiTimeController,)
[docs]class SpellDisableParallax(NifSpell): """Disable parallax shader (for Oblivion, but may work on other nifs too). """ SPELLNAME = "modify_disableparallax" READONLY = False
[docs] def datainspect(self): # XXX should we check that the nif is Oblivion version? # only run the spell if there are textures return self.inspectblocktype(NifFormat.NiTexturingProperty)
[docs] def branchinspect(self, branch): return isinstance(branch, (NifFormat.NiAVObject, NifFormat.NiTexturingProperty))
[docs] def branchentry(self, branch): if isinstance(branch, NifFormat.NiTexturingProperty): # is parallax enabled? if branch.apply_mode == 4: # yes! self.toaster.msg("disabling parallax shader") branch.apply_mode = 2 self.changed = True # stop recursing return False else: # keep recursing return True
[docs]class SpellAddStencilProperty(NifSpell): """Adds a NiStencilProperty to each geometry if it is not present.""" SPELLNAME = "modify_addstencilprop" READONLY = False
[docs] def datainspect(self): return self.inspectblocktype(NifFormat.NiTriBasedGeom)
[docs] def branchinspect(self, branch): # only inspect the NiAVObject branch return isinstance(branch, NifFormat.NiAVObject)
[docs] def branchentry(self, branch): if isinstance(branch, NifFormat.NiTriBasedGeom): # does this block have an stencil property? for prop in branch.get_properties(): if isinstance(prop, NifFormat.NiStencilProperty): return False # no stencil property found self.toaster.msg("adding NiStencilProperty") branch.add_property(NifFormat.NiStencilProperty()) self.changed = True # no geometry children, no need to recurse further return False # recurse further return True
# note: this should go into the optimize module # but we have to put it here to avoid circular dependencies
[docs]class SpellCleanFarNif( pyffi.spells.SpellGroupParallel( SpellDelVertexColorProperty, SpellDelAlphaProperty, SpellDelSpecularProperty, SpellDelBSXFlags, SpellDelStringExtraDatas, pyffi.spells.nif.fix.SpellDelTangentSpace, SpellDelCollisionData, SpellDelAnimation, SpellDisableParallax)): """Spell to clean _far type nifs (for even more optimizations, combine this with the optimize spell). """ SPELLNAME = "modify_cleanfarnif" # only apply spell on _far files
[docs] def datainspect(self): return self.stream.name.endswith('_far.nif')
# TODO: implement via modify_delbranches? # this is like SpellCleanFarNif but with changing the texture path # and optimizing the geometry
[docs]class SpellMakeFarNif( pyffi.spells.SpellGroupParallel( SpellDelVertexColorProperty, SpellDelAlphaProperty, SpellDelSpecularProperty, SpellDelBSXFlags, SpellDelStringExtraDatas, pyffi.spells.nif.fix.SpellDelTangentSpace, SpellDelCollisionData, SpellDelAnimation, SpellDisableParallax, SpellLowResTexturePath)): #TODO: implement vert decreaser. """Spell to make _far type nifs (for even more optimizations, combine this with the optimize spell). """ SPELLNAME = "modify_makefarnif"
[docs]class SpellMakeSkinlessNif( pyffi.spells.SpellGroupSeries( pyffi.spells.SpellGroupParallel( SpellDelSkinShapes, SpellAddStencilProperty) )): """Spell to make fleshless CMR (Custom Model Races) clothing/armour type nifs. """ SPELLNAME = "modify_makeskinlessnif"
[docs]class SpellSubstituteStringPalette( pyffi.spells.nif.fix.SpellCleanStringPalette): """Substitute strings in a string palette.""" SPELLNAME = "modify_substitutestringpalette"
[docs] @classmethod def toastentry(cls, toaster): arg = toaster.options["arg"] if not arg: # missing arg toaster.logger.warn( "must specify regular expression and substitution as argument " "(e.g. -a /Bip01/Bip02) to apply spell") return False dummy, toaster.regex, toaster.sub = arg.split(arg[0]) toaster.sub = _as_bytes(toaster.sub) toaster.regex = re.compile(_as_bytes(toaster.regex)) return True
[docs] def substitute(self, old_string): """Returns modified string, and reports if string was modified. """ if not old_string: # leave empty strings be return old_string new_string = self.toaster.regex.sub(self.toaster.sub, old_string) if old_string != new_string: self.changed = True self.toaster.msg("%s -> %s" % (old_string, new_string)) return new_string
[docs]class SpellChangeBonePriorities(NifSpell): """Changes controlled block priorities based on controlled block name.""" SPELLNAME = "modify_bonepriorities" READONLY = False
[docs] @classmethod def toastentry(cls, toaster): if not toaster.options["arg"]: toaster.logger.warn( "must specify bone(s) and priority(ies) as argument " "(e.g. -a 'bip01:50|bip01 spine:10') to apply spell " "make sure all bone names in lowercase") return False else: toaster.bone_priorities = dict( (name.lower(), int(priority)) for (name, priority) in ( namepriority.split(":") for namepriority in toaster.options["arg"].split("|"))) return True
[docs] def datainspect(self): # returns only if nif/kf contains NiSequence return self.inspectblocktype(NifFormat.NiSequence)
[docs] def branchinspect(self, branch): # inspect the NiAVObject and NiSequence branches return isinstance(branch, (NifFormat.NiAVObject, NifFormat.NiControllerManager, NifFormat.NiSequence))
[docs] def branchentry(self, branch): if isinstance(branch, NifFormat.NiSequence): for controlled_block in branch.controlled_blocks: try: controlled_block.priority = self.toaster.bone_priorities[ controlled_block.get_node_name().lower()] except KeyError: # node name not in bone priority list continue self.changed = True self.toaster.msg("%s priority changed to %d" % (controlled_block.get_node_name(), controlled_block.priority)) return True
class SpellChangeAllBonePriorities(SpellChangeBonePriorities): """Changes all controlled block priorities to supplied argument.""" SPELLNAME = "modify_allbonepriorities" @classmethod def toastentry(cls, toaster): if not toaster.options["arg"]: toaster.logger.warn( "must specify priority as argument (e.g. -a 20)") return False else: toaster.bone_priority = int(toaster.options["arg"]) return True def branchentry(self, branch): if isinstance(branch, NifFormat.NiSequence): for controlled_block in branch.controlled_blocks: if controlled_block.priority == self.toaster.bone_priority: self.toaster.msg("%s priority is already %d" % (controlled_block.get_node_name(), controlled_block.priority)) else: controlled_block.priority = self.toaster.bone_priority self.changed = True self.toaster.msg("%s priority changed to %d" % (controlled_block.get_node_name(), controlled_block.priority)) return True # should go in dump, but is the counterpart of modify_setbonepriorities # therefore maintained here class SpellGetBonePriorities(NifSpell): """For each file.nif, dump bone priorites to file_bonepriorities.txt. """ SPELLNAME = "modify_getbonepriorities" def datainspect(self): # continue only if nif/kf contains NiSequence return self.inspectblocktype(NifFormat.NiSequence) def dataentry(self): # maps squence name and block name to priority self.bonepriorities = {} return True def branchinspect(self, branch): return isinstance(branch, (NifFormat.NiAVObject, NifFormat.NiControllerManager, NifFormat.NiSequence)) def branchentry(self, branch): if isinstance(branch, NifFormat.NiSequence): bonepriorities = {} for controlled_block in branch.controlled_blocks: name = controlled_block.get_node_name().decode() priority = controlled_block.priority if name not in bonepriorities: bonepriorities[name] = priority #self.toaster.msg("noted %r priority %i" % (name, priority)) elif bonepriorities[name] != priority: self.toaster.logger.warn( "multiple priorities for %r" % name) self.toaster.logger.warn( "(using %i, ignoring %i)" % (self.bonepriorities[name], priority)) sequence = branch.name.decode() if sequence not in self.bonepriorities: self.bonepriorities[sequence] = bonepriorities else: self.toaster.logger.warn( "multiple sequences named %r," " only the first will be recorded" % sequence) return True @staticmethod def key(value): """Strip ' R ' and ' L ' from name so they occur together in list.""" name, priority = value return re.sub("( R )|( L )", "", name) def dataexit(self): filename, ext = os.path.splitext(self.stream.name) filename = filename + "_bonepriorities.txt" self.toaster.msg("writing %s" % filename) with codecs.open(filename, "wb", encoding="ascii") as stream: for sequence, bonepriorities in self.bonepriorities.items(): print("[%s]" % sequence, file=stream, end="\r\n") for name, priority in sorted(bonepriorities.items(), key=self.key): print("%s=%i" % (name, priority), file=stream, end="\r\n") self.bonepriorities = {} class SpellSetBonePriorities(NifSpell): """For each file.nif, restore bone priorites from file_bonepriorities.txt. """ SPELLNAME = "modify_setbonepriorities" READONLY = False def datainspect(self): # returns only if nif/kf contains NiSequence return self.inspectblocktype(NifFormat.NiSequence) def dataentry(self): filename, ext = os.path.splitext(self.stream.name) filename = filename + "_bonepriorities.txt" if os.path.exists(filename): self.toaster.msg("reading %s" % filename) with codecs.open(filename, "rb", encoding="ascii") as stream: self.bonepriorities = {} # priorities for all sequences sequence = "" # current sequence bonepriorities = {} # priorities for current sequence for line in stream: line = line.rstrip('\r\n') m = re.match("\\[(.*)\\]$", line) if m: if sequence: self.bonepriorities[sequence] = bonepriorities sequence = m.group(1) bonepriorities = {} else: m = re.match("(.*)=([0-9]+)$", line) if not m: self.toaster.logger.warn("syntax error in %r" % line) bonepriorities[m.group(1)] = int(m.group(2)) if sequence: self.bonepriorities[sequence] = bonepriorities return True else: self.toaster.msg("%s not found, skipping" % filename) return False def branchinspect(self, branch): # inspect the NiAVObject and NiSequence branches return isinstance(branch, (NifFormat.NiAVObject, NifFormat.NiControllerManager, NifFormat.NiSequence)) def branchentry(self, branch): if isinstance(branch, NifFormat.NiSequence): sequence = branch.name.decode() if sequence not in self.bonepriorities: self.toaster.logger.warn( "sequence %r not listed, skipped" % sequence) return False bonepriorities = self.bonepriorities[sequence] for controlled_block in branch.controlled_blocks: name = controlled_block.get_node_name().decode() if name in bonepriorities: priority = bonepriorities[name] if priority != controlled_block.priority: self.toaster.msg("setting %r priority to %i (was %i)" % (name, priority, controlled_block.priority)) controlled_block.priority = priority self.changed = True else: self.toaster.msg("%r priority already at %i" % (name, priority)) else: self.toaster.logger.warn( "%r in NIF file but not in priority file" % name) return True
[docs]class SpellSetInterpolatorTransRotScale(NifSpell): """Changes specified bone(s) translations/rotations in their NiTransformInterpolator. """ SPELLNAME = "modify_interpolatortransrotscale" READONLY = False
[docs] @classmethod def toastentry(cls, toaster): if not toaster.options["arg"]: toaster.logger.warn( "must specify bone(s), translation and rotation for each" " bone as argument (e.g." " -a 'bip01:1,2,3;0,0,0,1;1|bip01 spine2:0,0,0;1,0,0,0.5;1')" " to apply spell; make sure all bone names are lowercase," " first three numbers being translation," " next three being rotation," " last being scale;" " enter X to leave existing value for that value") return False else: def _float(x): if x == "X": return None else: return float(x) toaster.interp_transforms = dict( (name.lower(), ([_float(x) for x in trans.split(",")], [_float(x) for x in rot.split(",")], _float(scale))) for (name, (trans, rot, scale)) in ( (name, transrotscale.split(";")) for (name, transrotscale) in ( name_transrotscale.split(":") for name_transrotscale in toaster.options["arg"].split("|")))) return True
[docs] def datainspect(self): # returns only if nif/kf contains NiSequence return self.inspectblocktype(NifFormat.NiSequence)
[docs] def branchinspect(self, branch): # inspect the NiAVObject and NiSequence branches return isinstance(branch, (NifFormat.NiAVObject, NifFormat.NiSequence))
[docs] def branchentry(self, branch): if isinstance(branch, NifFormat.NiSequence): for controlled_block in branch.controlled_blocks: try: (transx, transy, transz), (quatx, quaty, quatz, quatw), scale = self.toaster.interp_transforms[controlled_block.get_node_name().lower()] except KeyError: # node name not in change list continue interp = controlled_block.interpolator if transx is not None: interp.translation.x = transx if transy is not None: interp.translation.y = transy if transz is not None: interp.translation.z = transz if quatx is not None: interp.rotation.x = quatx if quaty is not None: interp.rotation.y = quaty if quatz is not None: interp.rotation.z = quatz if quatw is not None: interp.rotation.w = quatw if scale is not None: interp.scale = scale self.changed = True self.toaster.msg( "%s rotated/translated/scaled as per argument" % (controlled_block.get_node_name())) return True
[docs]class SpellDelInterpolatorTransformData(NifSpell): """Deletes the specified bone(s) NiTransformData(s).""" SPELLNAME = "modify_delinterpolatortransformdata" READONLY = False
[docs] @classmethod def toastentry(cls, toaster): if not toaster.options["arg"]: toaster.logger.warn( "must specify bone name(s) as argument " "(e.g. -a 'bip01|bip01 pelvis') to apply spell " "make sure all bone name(s) in lowercase") return False else: toaster.change_blocks = toaster.options["arg"].split('|') return True
[docs] def datainspect(self): # returns only if nif/kf contains NiSequence return self.inspectblocktype(NifFormat.NiSequence)
[docs] def branchinspect(self, branch): # inspect the NiAVObject and NiSequence branches return isinstance(branch, (NifFormat.NiAVObject, NifFormat.NiSequence))
[docs] def branchentry(self, branch): if isinstance(branch, NifFormat.NiSequence): for controlled_block in branch.controlled_blocks: if controlled_block.get_node_name().lower() in self.toaster.change_blocks: self.data.replace_global_node(controlled_block.interpolator.data, None) self.toaster.msg("NiTransformData removed from interpolator for %s" % (controlled_block.get_node_name())) self.changed = True return True
class SpellCollisionToMopp(NifSpell): """Transforms non-mopp triangle collisions to the more efficient mopps.""" SPELLNAME = "modify_collisiontomopp" READONLY = False def datainspect(self): return self.inspectblocktype(NifFormat.bhkRigidBody) def branchinspect(self, branch): # only inspect the NiAVObject branch return isinstance(branch, (NifFormat.NiAVObject, NifFormat.bhkCollisionObject, NifFormat.bhkRigidBody)) def branchentry(self, branch): if isinstance(branch, NifFormat.bhkRigidBody): if isinstance(branch.shape, (NifFormat.bhkNiTriStripsShape, NifFormat.bhkPackedNiTriStripsShape)): colmopp = NifFormat.bhkMoppBvTreeShape() colmopp.material = branch.shape.material colmopp.unknown_8_bytes[0] = 160 colmopp.unknown_8_bytes[1] = 13 colmopp.unknown_8_bytes[2] = 75 colmopp.unknown_8_bytes[3] = 1 colmopp.unknown_8_bytes[4] = 192 colmopp.unknown_8_bytes[5] = 207 colmopp.unknown_8_bytes[6] = 144 colmopp.unknown_8_bytes[7] = 11 colmopp.unknown_float = 1.0 if isinstance(branch.shape, NifFormat.bhkNiTriStripsShape): branch.shape = branch.shape.get_interchangeable_packed_shape() colmopp.shape = branch.shape branch.shape = colmopp self.changed = True branch.shape.update_mopp() self.toaster.msg("collision set to MOPP") # Don't need to recurse further return False else: # recurse further return True class SpellMirrorAnimation(NifSpell): """Mirrors the animation by switching bones and mirroring their x values. Only useable on creature/character animations (well any animations as long as they have bones in the form of bip01/2 L ...). """ SPELLNAME = "modify_mirroranimation" READONLY = False def datainspect(self): # returns more than needed but easiest way to ensure it catches all # types of animations return True def dataentry(self): # make list of used bones self.old_bone_data = {} for branch in self.data.get_global_iterator(): if isinstance(branch, NifFormat.NiControllerSequence): for block in branch.controlled_blocks: name = block.get_node_name().lower() if ' r ' in name or ' l ' in name: self.old_bone_data[name] = [block.interpolator, block.controller, block.priority, block.string_palette, block.node_name_offset, block.controller_type_offset] if self.old_bone_data: return True def branchinspect(self, branch): # inspect the NiAVObject branch, and NiControllerSequence # branch (for kf files) return isinstance(branch, (NifFormat.NiAVObject, NifFormat.NiTimeController, NifFormat.NiInterpolator, NifFormat.NiControllerManager, NifFormat.NiControllerSequence)) def branchentry(self, branch): old_bone_data = self.old_bone_data if isinstance(branch, NifFormat.NiControllerSequence): for block in branch.controlled_blocks: node_name = block.get_node_name().lower() if ' l ' in node_name: node_name = node_name.replace(' l ', ' r ') elif ' r ' in node_name: node_name = node_name.replace(' r ', ' l ') if node_name in old_bone_data: self.changed = True block.interpolator, block.controller, block.priority, block.string_palette, block.node_name_offset, block.controller_type_offset = old_bone_data[node_name] # and then reverse x movements (since otherwise the movement of f.e. an arm towards the center of the body will be still in the same direction but away from the body if not block.interpolator: continue ip = block.interpolator ip.translation.x = -ip.translation.x ip.rotation.x = -ip.rotation.x if ip.data: data = ip.data if data.translations.num_keys: for key in data.translations.keys: key.value.x = -key.value.x if data.rotation_type == 4: if data.xyz_rotations[1].num_keys != 0: for key in data.xyz_rotations[1].keys: key.value = -key.value elif data.num_rotation_keys != 0: for key in data.quaternion_keys: key.value.x = -key.value.x else: # recurse further return True