"""
:mod:`pyffi.spells.nif.fix` --- spells to fix errors
=====================================================
Module which contains all spells that fix something in a nif.
Implementation
--------------
.. autoclass:: SpellDelTangentSpace
:show-inheritance:
:members:
.. autoclass:: SpellAddTangentSpace
:show-inheritance:
:members:
.. autoclass:: SpellFFVT3RSkinPartition
:show-inheritance:
:members:
.. autoclass:: SpellFixTexturePath
:show-inheritance:
:members:
.. autoclass:: SpellDetachHavokTriStripsData
:show-inheritance:
:members:
.. autoclass:: SpellClampMaterialAlpha
:show-inheritance:
:members:
.. autoclass:: SpellSendGeometriesToBindPosition
:show-inheritance:
:members:
.. autoclass:: SpellSendDetachedGeometriesToNodePosition
:show-inheritance:
:members:
.. autoclass:: SpellSendBonesToBindPosition
:show-inheritance:
:members:
.. autoclass:: SpellMergeSkeletonRoots
:show-inheritance:
:members:
.. autoclass:: SpellApplySkinDeformation
:show-inheritance:
:members:
.. autoclass:: SpellScale
:show-inheritance:
:members:
.. autoclass:: SpellFixCenterRadius
:show-inheritance:
:members:
.. autoclass:: SpellFixSkinCenterRadius
:show-inheritance:
:members:
.. autoclass:: SpellFixMopp
:show-inheritance:
:members:
.. autoclass:: SpellCleanStringPalette
:show-inheritance:
:members:
.. autoclass:: SpellDelUnusedRoots
:show-inheritance:
:members:
.. autoclass:: SpellFixEmptySkeletonRoots
:show-inheritance:
:members:
Regression tests
----------------
"""
# --------------------------------------------------------------------------
# ***** 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.spells.nif import NifSpell
import pyffi.spells.nif
import pyffi.spells.nif.check # recycle checking spells for update spells
[docs]class SpellDelTangentSpace(NifSpell):
"""Delete tangentspace if it is present."""
SPELLNAME = "fix_deltangentspace"
READONLY = False
[docs] def datainspect(self):
return self.inspectblocktype(NifFormat.NiBinaryExtraData)
[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 tangent space data?
for extra in branch.get_extra_datas():
if isinstance(extra, NifFormat.NiBinaryExtraData):
if (extra.name ==
b'Tangent space (binormal & tangent vectors)'):
self.toaster.msg("removing tangent space block")
branch.remove_extra_data(extra)
self.changed = True
# all extra blocks here done; no need to recurse further
return False
# recurse further
return True
[docs]class SpellAddTangentSpace(NifSpell):
"""Add tangentspace if none is present."""
SPELLNAME = "fix_addtangentspace"
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 tangent space data?
for extra in branch.get_extra_datas():
if isinstance(extra, NifFormat.NiBinaryExtraData):
if (extra.name ==
b'Tangent space (binormal & tangent vectors)'):
# tangent space found, done!
return False
# no tangent space found
self.toaster.msg("adding tangent space")
branch.update_tangent_space()
self.changed = True
# all extra blocks here done; no need to recurse further
return False
else:
# recurse further
return True
[docs]class SpellFFVT3RSkinPartition(NifSpell):
"""Create or update skin partition, with settings that work for Freedom
Force vs. The 3rd Reich."""
SPELLNAME = "fix_ffvt3rskinpartition"
READONLY = False
[docs] def datainspect(self):
return self.inspectblocktype(NifFormat.NiSkinInstance)
[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):
# if the branch has skinning info
if branch.skin_instance:
# then update the skin partition
self.toaster.msg("updating skin partition")
branch.update_skin_partition(
maxbonesperpartition=4, maxbonespervertex=4,
stripify=False, verbose=0, padbones=True)
self.changed = True
return False
# done; no need to recurse further in this branch
else:
# recurse further
return True
class SpellParseTexturePath(NifSpell):
"""Base class for spells which must parse all texture paths, with
hook for texture path substitution.
"""
# abstract spell, so no spell name
READONLY = False
def substitute(self, old_path):
"""Helper function to allow subclasses of this spell to
change part of the path with minimum of code.
This implementation returns path unmodified.
"""
return old_path
def datainspect(self):
# only run the spell if contains
# NiSourceTexture or BSShaderTextureSet blocks
if self.inspectblocktype(NifFormat.BSShaderTextureSet):
return True
elif self.inspectblocktype(NifFormat.NiSourceTexture):
return True
else:
return False
def branchinspect(self, branch):
# only inspect the NiAVObject branch, texturing properties and source
# textures
return isinstance(branch, (NifFormat.NiAVObject,
NifFormat.NiTexturingProperty,
NifFormat.NiSourceTexture,
NifFormat.BSLightingShaderProperty,
NifFormat.BSShaderTextureSet))
def branchentry(self, branch):
if isinstance(branch, NifFormat.NiSourceTexture):
branch.file_name = self.substitute(branch.file_name)
return False
elif isinstance(branch, NifFormat.BSShaderTextureSet):
for n, tex in enumerate (branch.textures):
branch.textures[n] = self.substitute(tex)
return False
else:
return True
[docs]class SpellFixTexturePath(SpellParseTexturePath):
r"""Fix the texture path. Transforms 0x0a into \n and 0x0d into
\r. This fixes a bug in nifs saved with older versions of
nifskope. Also transforms / into \. This fixes problems when
packing files into a bsa archive. Also if the version is 20.0.0.4
or higher it will check for bad texture path form of e.g.
c:\program files\bethsoft\ob\textures\file\path.dds and replace it
with e.g. textures\file\path.dds.
"""
SPELLNAME = "fix_texturepath"
[docs] def substitute(self, old_path):
new_path = old_path
new_path = new_path.replace(b'\n', b'\\n')
new_path = new_path.replace(b'\r', b'\\r')
new_path = new_path.replace(b'/', b'\\')
# baphometal found some nifs that use double slashes
# this causes textures not to show, so here we convert them
# back to single slashes
new_path = new_path.replace(b'\\\\', b'\\')
textures_index = new_path.lower().find(b'textures\\')
if textures_index > 0:
# path contains textures\ at position other than starting
# position
new_path = new_path[textures_index:]
if new_path != old_path:
self.toaster.msg("fixed file name '%s'"
% new_path.decode("utf8", "ignore"))
self.changed = True
return new_path
# the next spell solves issue #2065018, MiddleWolfRug01.NIF
[docs]class SpellDetachHavokTriStripsData(NifSpell):
"""For NiTriStrips if their NiTriStripsData also occurs in a
bhkNiTriStripsShape, make deep copy of data in havok. This is
mainly useful as a preperation for other spells that act on
NiTriStripsData, to ensure that the havok data remains untouched."""
SPELLNAME = "fix_detachhavoktristripsdata"
READONLY = False
def __init__(self, *args, **kwargs):
NifSpell.__init__(self, *args, **kwargs)
# provides the bhknitristripsshapes within the current NiTriStrips
self.bhknitristripsshapes = None
[docs] def datainspect(self):
# only run the spell if there are bhkNiTriStripsShape blocks
return self.inspectblocktype(NifFormat.bhkNiTriStripsShape)
[docs] def dataentry(self):
# build list of all NiTriStrips blocks
self.nitristrips = [branch for branch in self.data.get_global_iterator()
if isinstance(branch, NifFormat.NiTriStrips)]
if self.nitristrips:
return True
else:
return False
[docs] def branchinspect(self, branch):
# only inspect the NiAVObject branch and collision branch
return isinstance(branch, (NifFormat.NiAVObject,
NifFormat.bhkCollisionObject,
NifFormat.bhkRefObject))
[docs] def branchentry(self, branch):
if isinstance(branch, NifFormat.bhkNiTriStripsShape):
for i, data in enumerate(branch.strips_data):
if data in [otherbranch.data
for otherbranch in self.nitristrips]:
# detach!
self.toaster.msg("detaching havok data")
branch.strips_data[i] = NifFormat.NiTriStripsData().deepcopy(data)
self.changed = True
return False
else:
return True
[docs]class SpellClampMaterialAlpha(NifSpell):
"""Clamp corrupted material alpha values."""
SPELLNAME = "fix_clampmaterialalpha"
READONLY = False
[docs] def datainspect(self):
# only run the spell if there are material property blocks
return self.inspectblocktype(NifFormat.NiMaterialProperty)
[docs] def branchinspect(self, branch):
# only inspect the NiAVObject branch, and material properties
return isinstance(branch, (NifFormat.NiAVObject,
NifFormat.NiMaterialProperty))
[docs] def branchentry(self, branch):
if isinstance(branch, NifFormat.NiMaterialProperty):
# check if alpha exceeds usual values
if branch.alpha > 1:
# too large
self.toaster.msg(
"clamping alpha value (%f -> 1.0)" % branch.alpha)
branch.alpha = 1.0
self.changed = True
elif branch.alpha < 0:
# too small
self.toaster.msg(
"clamping alpha value (%f -> 0.0)" % branch.alpha)
branch.alpha = 0.0
self.changed = True
# stop recursion
return False
else:
# keep recursing into children
return True
[docs]class SpellSendGeometriesToBindPosition(pyffi.spells.nif.SpellVisitSkeletonRoots):
"""Transform skinned geometries so similar bones have the same bone data,
and hence, the same bind position, over all geometries.
"""
SPELLNAME = "fix_sendgeometriestobindposition"
READONLY = False
[docs] def skelrootentry(self, branch):
self.toaster.msg("sending geometries to bind position")
branch.send_geometries_to_bind_position()
self.changed = True
[docs]class SpellSendDetachedGeometriesToNodePosition(pyffi.spells.nif.SpellVisitSkeletonRoots):
"""Transform geometries so each set of geometries that shares bones
is aligned with the transform of the root bone of that set.
"""
SPELLNAME = "fix_senddetachedgeometriestonodeposition"
READONLY = False
[docs] def skelrootentry(self, branch):
self.toaster.msg("sending detached geometries to node position")
branch.send_detached_geometries_to_node_position()
self.changed = True
[docs]class SpellSendBonesToBindPosition(pyffi.spells.nif.SpellVisitSkeletonRoots):
"""Transform bones so bone data agrees with bone transforms,
and hence, all bones are in bind position.
"""
SPELLNAME = "fix_sendbonestobindposition"
READONLY = False
[docs] def skelrootentry(self, branch):
self.toaster.msg("sending bones to bind position")
branch.send_bones_to_bind_position()
self.changed = True
[docs]class SpellMergeSkeletonRoots(NifSpell):
"""Merges skeleton roots in the NIF file so that no skeleton root has
another skeleton root as child. Warns if merge is impossible (this happens
if the global skin data of the geometry is not the unit transform).
"""
SPELLNAME = "fix_mergeskeletonroots"
READONLY = False
[docs] def datainspect(self):
# only run the spell if there are skinned geometries
return self.inspectblocktype(NifFormat.NiSkinInstance)
[docs] def dataentry(self):
# make list of skeleton roots
skelroots = []
for branch in self.data.get_global_iterator():
if isinstance(branch, NifFormat.NiGeometry):
if branch.skin_instance:
skelroot = branch.skin_instance.skeleton_root
if skelroot and not skelroot in skelroots:
skelroots.append(skelroot)
# find the 'root' skeleton roots (those that have no other skeleton
# roots as child)
self.skelrootlist = set()
for skelroot in skelroots:
for skelroot_other in skelroots:
if skelroot_other is skelroot:
continue
if skelroot_other.find_chain(skelroot):
# skelroot_other has skelroot as child
# so skelroot is no longer an option
break
else:
# no skeleton root children!
self.skelrootlist.add(skelroot)
# only apply spell if there are skeleton roots
if self.skelrootlist:
return True
else:
return False
[docs] def branchinspect(self, branch):
# only inspect the NiNode branch
return isinstance(branch, NifFormat.NiNode)
[docs] def branchentry(self, branch):
if branch in self.skelrootlist:
result, failed = branch.merge_skeleton_roots()
self.changed = True
for geom in result:
self.toaster.msg("reassigned skeleton root of %s" % geom.name)
self.skelrootlist.remove(branch)
# continue recursion only if there is still more to come
if self.skelrootlist:
return True
else:
return False
[docs]class SpellScale(NifSpell):
"""Scale a model."""
SPELLNAME = "fix_scale"
READONLY = False
[docs] @classmethod
def toastentry(cls, toaster):
if not toaster.options["arg"]:
toaster.logger.warn(
"must specify scale as argument (e.g. -a 10) "
"to apply spell")
return False
else:
toaster.scale = float(toaster.options["arg"])
return True
[docs] def dataentry(self):
# initialize list of blocks that have been scaled
self.toaster.msg("scaling by factor %f" % self.toaster.scale)
self.scaled_branches = []
return True
[docs] def branchinspect(self, branch):
# only do every branch once
return (branch not in self.scaled_branches)
[docs] def branchentry(self, branch):
branch.apply_scale(self.toaster.scale)
self.changed = True
self.scaled_branches.append(branch)
# continue recursion
return True
[docs]class SpellFixCenterRadius(pyffi.spells.nif.check.SpellCheckCenterRadius):
"""Recalculate geometry centers and radii."""
SPELLNAME = "fix_centerradius"
READONLY = False
[docs]class SpellFixSkinCenterRadius(pyffi.spells.nif.check.SpellCheckSkinCenterRadius):
"""Recalculate skin centers and radii."""
SPELLNAME = "fix_skincenterradius"
READONLY = False
[docs]class SpellFixMopp(pyffi.spells.nif.check.SpellCheckMopp):
"""Recalculate mopp data from collision geometry."""
SPELLNAME = "fix_mopp"
READONLY = False
[docs] def branchentry(self, branch):
# we don't recycle the check mopp code here
# that spell does not actually recalculate the mopp at all
# it only parses the existing mopp...
if not isinstance(branch, NifFormat.bhkMoppBvTreeShape):
# keep recursing
return True
else:
self.toaster.msg("updating mopp")
branch.update_mopp()
self.changed = True
[docs]class SpellCleanStringPalette(NifSpell):
"""Remove unused strings from string palette."""
SPELLNAME = "fix_cleanstringpalette"
READONLY = False
[docs] def substitute(self, old_string):
"""Helper function to substitute strings in the string palette,
to allow subclasses of this spell can modify the strings.
This implementation returns string unmodified.
"""
return old_string
[docs] def datainspect(self):
# only run the spell if there is a string palette block
return self.inspectblocktype(NifFormat.NiStringPalette)
[docs] def branchinspect(self, branch):
# only inspect branches where NiControllerSequence can occur
return isinstance(branch, (NifFormat.NiAVObject,
NifFormat.NiControllerManager,
NifFormat.NiControllerSequence))
[docs] def branchentry(self, branch):
"""Parses string palette of either a single controller sequence,
or of all controller sequences in a controller manager.
>>> seq = NifFormat.NiControllerSequence()
>>> seq.string_palette = NifFormat.NiStringPalette()
>>> block = seq.add_controlled_block()
>>> block.string_palette = seq.string_palette
>>> block.set_variable_1("there")
>>> block.set_node_name("hello")
>>> block.string_palette.palette.add_string("test")
12
>>> seq.string_palette.palette.get_all_strings()
[b'there', b'hello', b'test']
>>> SpellCleanStringPalette().branchentry(seq)
pyffi.toaster:INFO:parsing string palette
False
>>> seq.string_palette.palette.get_all_strings()
[b'hello', b'there']
>>> block.get_variable_1()
b'there'
>>> block.get_node_name()
b'hello'
"""
if isinstance(branch, (NifFormat.NiControllerManager,
NifFormat.NiControllerSequence)):
# get list of controller sequences
if isinstance(branch, NifFormat.NiControllerManager):
# multiple controller sequences sharing a single
# string palette
if not branch.controller_sequences:
# no controller sequences: nothing to do
return False
controller_sequences = branch.controller_sequences
else:
# unmanaged controller sequence
controller_sequences = [branch]
# and clean their string palettes
self.toaster.msg("parsing string palette")
# use the first string palette as reference
string_palette = controller_sequences[0].string_palette
palette = string_palette.palette
# 1) calculate number of strings, for reporting
# (this assumes that all blocks already use the same
# string palette!)
num_strings = len(palette.get_all_strings())
# 2) substitute strings
# first convert the controlled block strings to the old style
# (storing the actual string, and not just an offset into the
# string palette)
for controller_sequence in controller_sequences:
for block in controller_sequence.controlled_blocks:
# set old style strings from string palette strings
block.node_name = self.substitute(block.get_node_name())
block.property_type = self.substitute(block.get_property_type())
block.controller_type = self.substitute(block.get_controller_type())
block.variable_1 = self.substitute(block.get_variable_1())
block.variable_2 = self.substitute(block.get_variable_2())
# ensure single string palette for all controlled blocks
block.string_palette = string_palette
# ensure single string palette for all controller sequences
controller_sequence.string_palette = string_palette
# clear the palette
palette.clear()
# and then convert old style back to new style
for controller_sequence in controller_sequences:
for block in controller_sequence.controlled_blocks:
block.set_node_name(block.node_name)
block.set_property_type(block.property_type)
block.set_controller_type(block.controller_type)
block.set_variable_1(block.variable_1)
block.set_variable_2(block.variable_2)
self.changed = True
# do not recurse further
return False
else:
# keep looking for managers or sequences
return True
class SpellFixFallout3StringOffsets(NifSpell):
"""Fix Oblivion style kf files to work with Fallout 3, by
replacing empty string offsets to point to a null byte.
"""
SPELLNAME = "fix_fallout3stringoffsets"
READONLY = False
def datainspect(self):
# only run the spell if it looks like an Oblivion kf
return (
self.data.version == 0x14000005
and self.inspectblocktype(NifFormat.NiStringPalette)
and self.inspectblocktype(NifFormat.NiControllerSequence)
)
def branchinspect(self, branch):
# only inspect branches where NiControllerSequence can occur
return isinstance(branch, (NifFormat.NiAVObject,
NifFormat.NiControllerManager,
NifFormat.NiControllerSequence))
def branchentry(self, branch):
"""Parses string palette of either a single controller sequence,
or of all controller sequences in a controller manager.
>>> seq = NifFormat.NiControllerSequence()
>>> seq.string_palette = NifFormat.NiStringPalette()
>>> block = seq.add_controlled_block()
>>> block.string_palette = seq.string_palette
>>> block.set_variable_1("there")
>>> block.set_node_name("hello")
>>> block.string_palette.palette.add_string("test")
12
>>> block.node_name_offset
6
>>> block.property_type_offset
-1
>>> block.controller_type_offset
-1
>>> block.variable_1_offset
0
>>> block.variable_2_offset
-1
>>> block.get_node_name()
b'hello'
>>> block.get_property_type()
b''
>>> block.get_controller_type()
b''
>>> block.get_variable_1()
b'there'
>>> block.get_variable_2()
b''
>>> SpellFixFallout3StringOffsets().branchentry(seq)
pyffi.toaster:INFO:updating empty links
pyffi.toaster:INFO:updated 'property_type_offset' for b'hello' node
pyffi.toaster:INFO:updated 'controller_type_offset' for b'hello' node
pyffi.toaster:INFO:updated 'variable_2_offset' for b'hello' node
False
>>> block.node_name_offset
6
>>> block.property_type_offset
16
>>> block.controller_type_offset
16
>>> block.variable_1_offset
0
>>> block.variable_2_offset
16
>>> block.get_node_name()
b'hello'
>>> block.get_property_type()
pyffi.nif.stringpalette:WARNING:StringPalette: no string starts at offset 16 (string is b'', preceeding character is b't')
b''
>>> block.get_controller_type()
pyffi.nif.stringpalette:WARNING:StringPalette: no string starts at offset 16 (string is b'', preceeding character is b't')
b''
>>> block.get_variable_1()
b'there'
>>> block.get_variable_2()
pyffi.nif.stringpalette:WARNING:StringPalette: no string starts at offset 16 (string is b'', preceeding character is b't')
b''
"""
if isinstance(branch,NifFormat.NiControllerSequence):
self.toaster.msg("updating empty links")
# use the first string palette as reference
string_palette = branch.string_palette
if not string_palette:
self.toaster.logger.warn("empty string palette, skipped")
return False
palette = string_palette.palette.palette
b00_offset = palette.rfind(b'\x00')
if b00_offset == -1:
self.toaster.logger.error(
"string palette has no null bytes, skipped")
return False
for block in branch.controlled_blocks:
for attr in (
"node_name", "property_type", "controller_type",
"variable_1", "variable_2"):
attr_offset = attr + "_offset"
offset = getattr(block, attr_offset)
if offset == -1:
self.toaster.msg(
"updated %r for %r node"
% (attr_offset, block.get_node_name()))
setattr(block, attr_offset, b00_offset)
self.changed = True
return False
else:
return True
[docs]class SpellDelUnusedRoots(pyffi.spells.nif.NifSpell):
"""Remove root branches that shouldn't be root branches and are
unused in the file such as NiProperty branches that are not
properly parented.
"""
SPELLNAME = "fix_delunusedroots"
READONLY = False
[docs] def datainspect(self):
if self.inspectblocktype(NifFormat.NiAVObject):
# check last 8 bytes
pos = self.stream.tell()
try:
self.stream.seek(-8, 2)
if self.stream.read(8) == '\x01\x00\x00\x00\x00\x00\x00\x00':
# standard nif with single root: do not remove anything
# and quit early without reading the full file
return False
else:
return True
finally:
self.stream.seek(pos)
else:
return False
[docs] def dataentry(self):
# make list of good roots
good_roots = [
root for root in self.data.roots
if isinstance(root, (NifFormat.NiAVObject,
NifFormat.NiSequence,
NifFormat.NiPixelData,
NifFormat.NiPhysXProp,
NifFormat.NiSequenceStreamHelper))]
# if actual roots differ from good roots set roots to good
# roots and report
if self.data.roots != good_roots:
self.toaster.msg("removing %i bad roots"
% (len(self.data.roots) - len(good_roots)))
self.data.roots = good_roots
self.changed = True
return False
class SpellFixBhkSubShapes(NifSpell):
"""Fix bad subshape vertex counts in bhkPackedNiTriStripsShape blocks."""
SPELLNAME = "fix_bhksubshapes"
READONLY = False
def datainspect(self):
return self.inspectblocktype(NifFormat.bhkPackedNiTriStripsShape)
def branchinspect(self, branch):
# only inspect the NiAVObject branch and collision branch
return isinstance(branch, (
NifFormat.NiAVObject,
NifFormat.bhkCollisionObject,
NifFormat.bhkRefObject))
def branchentry(self, branch):
if isinstance(branch, NifFormat.bhkPackedNiTriStripsShape):
if not branch.data:
# no data... this is weird, but let's just ignore it
return False
# calculate number of vertices in subshapes
num_verts_in_sub_shapes = sum(
(sub_shape.num_vertices
for sub_shape in branch.get_sub_shapes()), 0)
if num_verts_in_sub_shapes != branch.data.num_vertices:
self.toaster.logger.warn(
"bad subshape vertex count (expected %i, got %i)"
% (branch.data.num_vertices, num_verts_in_sub_shapes))
# remove or add vertices from subshapes (start with the last)
for sub_shape in reversed(branch.get_sub_shapes()):
self.toaster.msg("fixing count in subshape")
# calculate new number of vertices
# if everything were to be fixed with this shape
sub_shape_num_vertices = (
sub_shape.num_vertices
+ branch.data.num_vertices
- num_verts_in_sub_shapes)
if sub_shape_num_vertices > 0:
# we can do everything in the last shape
# so do it
sub_shape.num_vertices = sub_shape_num_vertices
break
else:
# too many to remove...
# first remove everything from this shape
# the remainder will come from the following shapes
num_verts_in_sub_shapes -= sub_shape.num_vertices
sub_shape.num_vertices = 0
# no need to recurse further
return False
# recurse further
return True
[docs]class SpellFixEmptySkeletonRoots(NifSpell):
"""Fix empty skeleton roots in an as sane as possible way."""
SPELLNAME = "fix_emptyskeletonroots"
READONLY = False
[docs] def datainspect(self):
# only run the spell if there is a skin instance block
return self.inspectblocktype(NifFormat.NiSkinInstance)
[docs] def dataentry(self):
# set skeleton root: first block of data
# this is what the engine usually seems to assume if it is not present
if not self.data.roots:
return False
self.skeleton_root = self.data.roots[0]
# sanity check
if not isinstance(self.skeleton_root, NifFormat.NiAVObject):
# we'll fail in this case...
self.skeleton_root = None
self.toaster.logger.info("no skeleton root candidate")
return False
return True
[docs] def branchinspect(self, branch):
# only inspect branches where NiSkinInstance can occur
return isinstance(branch, (NifFormat.NiAVObject,
NifFormat.NiSkinInstance))
[docs] def branchentry(self, branch):
if isinstance(branch, NifFormat.NiSkinInstance):
if not branch.skeleton_root:
if self.skeleton_root:
self.toaster.logger.warn(
"fixed missing skeleton root")
branch.skeleton_root = self.skeleton_root
self.changed = True
else:
self.toaster.logger.error(
"missing skeleton root, "
"but no skeleton root candidate!")
# do not recurse further
return False
else:
# keep looking for managers or sequences
return True