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