pyffi.spells.nif.fix

Last Built: Mar 06, 2020

Source code for pyffi.spells.nif.fix

"""
: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 SpellApplySkinDeformation(NifSpell): """Apply skin deformation to nif.""" # TODO pass
[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