Example #1
0
    def import_bhkcapsule_shape(self, bhk_shape):
        """Import a BhkCapsule block as a simple cylinder collision object"""
        NifLog.debug(f"Importing {bhk_shape.__class__.__name__}")

        radius = bhk_shape.radius * self.HAVOK_SCALE
        p_1 = bhk_shape.first_point
        p_2 = bhk_shape.second_point
        length = (p_1 - p_2).norm() * self.HAVOK_SCALE
        first_point = p_1 * self.HAVOK_SCALE
        second_point = p_2 * self.HAVOK_SCALE
        minx = miny = -radius
        maxx = maxy = +radius
        minz = -radius - length / 2
        maxz = length / 2 + radius

        # create blender object
        b_obj = Object.box_from_extents("capsule", minx, maxx, miny, maxy,
                                        minz, maxz)
        # here, these are not encoded as a direction so we must first calculate the direction
        b_obj.matrix_local = self.center_origin_to_matrix(
            (first_point + second_point) / 2, first_point - second_point)
        self.set_b_collider(b_obj,
                            bounds_type="CAPSULE",
                            display_type="CAPSULE",
                            radius=radius,
                            n_obj=bhk_shape)
        return [b_obj]
Example #2
0
    def import_bhksphere_shape(self, bhk_shape):
        """Import a BhkSphere block as a simple sphere collision object"""
        NifLog.debug(f"Importing {bhk_shape.__class__.__name__}")

        r = bhk_shape.radius * self.HAVOK_SCALE
        b_obj = Object.box_from_extents("sphere", -r, r, -r, r, -r, r)
        self.set_b_collider(b_obj, display_type="SPHERE", bounds_type='SPHERE', radius=r, n_obj=bhk_shape)
        return [b_obj]
Example #3
0
 def import_nitristrips(self, bhk_shape):
     """Import a NiTriStrips block as a Triangle-Mesh collision object"""
     # no factor 7 correction!!!
     verts = [(v.x, v.y, v.z) for v in bhk_shape.vertices]
     faces = list(bhk_shape.get_triangles())
     b_obj = Object.mesh_from_data("poly", verts, faces)
     # TODO [collision] self.havok_mat!
     self.set_b_collider(b_obj, bounds_type="MESH", radius=bhk_shape.radius)
     return [b_obj]
Example #4
0
 def import_boxbv(self, box):
     offset = box.center
     # ignore for now, seems to be a unity 3x3 matrix
     axes = box.axis
     x, y, z = box.extent
     b_obj = Object.box_from_extents("box", -x, x, -y, y, -z, z)
     b_obj.location = (offset.x, offset.y, offset.z)
     self.set_b_collider(b_obj, radius=(x + y + z) / 3)
     return [b_obj]
Example #5
0
 def import_spherebv(self, sphere):
     r = sphere.radius
     c = sphere.center
     b_obj = Object.box_from_extents("sphere", -r, r, -r, r, -r, r)
     b_obj.location = (c.x, c.y, c.z)
     self.set_b_collider(b_obj,
                         bounds_type="SPHERE",
                         display_type='SPHERE',
                         radius=r)
     return [b_obj]
    def import_armature(self, n_armature):
        """Scans an armature hierarchy, and returns a whole armature.
        This is done outside the normal node tree scan to allow for positioning
        of the bones before skins are attached."""

        armature_name = block_store.import_name(n_armature)
        b_armature_data = bpy.data.armatures.new(armature_name)
        b_armature_data.display_type = 'STICK'

        # use heuristics to determine a suitable orientation
        forward, up = self.guess_orientation(n_armature)
        # pass them to the matrix utility
        math.set_bone_orientation(forward, up)
        # store axis orientation for export
        b_armature_data.niftools.axis_forward = forward
        b_armature_data.niftools.axis_up = up
        b_armature_obj = Object.create_b_obj(n_armature, b_armature_data)
        b_armature_obj.show_in_front = True

        armature_space_bind_store, armature_space_pose_store = self.import_pose(
            n_armature)

        # make armature editable and create bones
        bpy.ops.object.mode_set(mode='EDIT', toggle=False)
        for n_child in n_armature.children:
            self.import_bone_bind(n_child, armature_space_bind_store,
                                  b_armature_data, n_armature)
        self.fix_bone_lengths(b_armature_data)
        bpy.ops.object.mode_set(mode='OBJECT', toggle=False)

        # The armature has been created in editmode,
        # now we are ready to set the bone keyframes and store the bones' long names.
        if NifOp.props.animation:
            self.transform_anim.create_action(b_armature_obj,
                                              armature_name + "-Anim")

        for bone_name, b_bone in b_armature_obj.data.bones.items():
            n_block = self.name_to_block[bone_name]
            # the property is only available from object mode!
            block_store.store_longname(b_bone, safe_decode(n_block.name))
            if NifOp.props.animation:
                self.transform_anim.import_transforms(n_block, b_armature_obj,
                                                      bone_name)

        # import pose
        for b_name, n_block in self.name_to_block.items():
            n_pose = armature_space_pose_store[n_block]
            b_pose_bone = b_armature_obj.pose.bones[b_name]
            n_bind = mathutils.Matrix(n_pose.as_list()).transposed()
            b_pose_bone.matrix = math.nif_bind_to_blender_bind(n_bind)
            # force update is required to ensure the transforms are set properly in blender
            bpy.context.view_layer.update()

        return b_armature_obj
Example #7
0
    def import_bounding_box(self, n_block):
        """Import a NiNode's bounding box or attached BSBound extra data."""
        if not n_block or not isinstance(n_block, NifFormat.NiNode):
            return []
        # we have a ninode with bounding box
        if n_block.has_bounding_box:
            b_name = 'Bounding Box'

            # Ninode's bbox behaves like a seperate mesh.
            # bounding_box center(n_block.bounding_box.translation) is relative to the bound_box
            n_bl_trans = n_block.translation
            n_bbox = n_block.bounding_box
            n_b_trans = n_bbox.translation
            minx = n_b_trans.x - n_bl_trans.x - n_bbox.radius.x
            miny = n_b_trans.y - n_bl_trans.y - n_bbox.radius.y
            minz = n_b_trans.z - n_bl_trans.z - n_bbox.radius.z
            maxx = n_b_trans.x - n_bl_trans.x + n_bbox.radius.x
            maxy = n_b_trans.y - n_bl_trans.y + n_bbox.radius.y
            maxz = n_b_trans.z - n_bl_trans.z + n_bbox.radius.z
            bbox_center = n_b_trans.as_list()

        # we may still have a BSBound extra data attached to this node
        else:
            for n_extra in n_block.get_extra_datas():
                # TODO [extra][data] Move to property processor
                if isinstance(n_extra, NifFormat.BSBound):
                    b_name = 'BSBound'
                    center = n_extra.center
                    dims = n_extra.dimensions
                    minx = -dims.x
                    miny = -dims.y
                    minz = -dims.z
                    maxx = +dims.x
                    maxy = +dims.y
                    maxz = +dims.z
                    bbox_center = center.as_list()
                    break
            # none was found
            else:
                return []

        # create blender object
        b_obj = Object.box_from_extents(b_name, minx, maxx, miny, maxy, minz,
                                        maxz)
        # probably only on NiNodes with BB
        if hasattr(n_block, "flags"):
            b_obj.niftools.flags = n_block.flags
        b_obj.location = bbox_center
        self.set_b_collider(b_obj, radius=max(maxx, maxy, maxz))
        return [
            b_obj,
        ]
Example #8
0
    def import_bhkconvex_vertices_shape(self, bhk_shape):
        """Import a BhkConvexVertex block as a convex hull collision object"""
        NifLog.debug(f"Importing {bhk_shape.__class__.__name__}")

        # find vertices (and fix scale)
        scaled_verts = [(self.HAVOK_SCALE * n_vert.x, self.HAVOK_SCALE * n_vert.y, self.HAVOK_SCALE * n_vert.z)
                        for n_vert in bhk_shape.vertices]
        verts, faces = qhull3d(scaled_verts)

        b_obj = Object.mesh_from_data("convexpoly", verts, faces)
        radius = bhk_shape.radius * self.HAVOK_SCALE
        self.set_b_collider(b_obj, bounds_type="CONVEX_HULL", radius=radius, n_obj=bhk_shape)
        return [b_obj]
Example #9
0
    def import_bhkpackednitristrips_shape(self, bhk_shape):
        """Import a BhkPackedNiTriStrips block as a Triangle-Mesh collision object"""
        NifLog.debug(f"Importing {bhk_shape.__class__.__name__}")

        # create mesh for each sub shape
        hk_objects = []
        vertex_offset = 0
        subshapes = bhk_shape.sub_shapes

        if not subshapes:
            # fallout 3 stores them in the data
            subshapes = bhk_shape.data.sub_shapes

        for subshape_num, subshape in enumerate(subshapes):
            verts = []
            faces = []
            for vert_index in range(vertex_offset,
                                    vertex_offset + subshape.num_vertices):
                n_vert = bhk_shape.data.vertices[vert_index]
                verts.append(
                    (n_vert.x * self.HAVOK_SCALE, n_vert.y * self.HAVOK_SCALE,
                     n_vert.z * self.HAVOK_SCALE))

            for bhk_triangle in bhk_shape.data.triangles:
                bhk_tri = bhk_triangle.triangle
                if (vertex_offset <= bhk_tri.v_1) and (
                        bhk_tri.v_1 < vertex_offset + subshape.num_vertices):
                    faces.append((bhk_tri.v_1 - vertex_offset,
                                  bhk_tri.v_2 - vertex_offset,
                                  bhk_tri.v_3 - vertex_offset))
                else:
                    continue

            b_obj = Object.mesh_from_data(f'poly{subshape_num:d}', verts,
                                          faces)
            radius = min(vert.co.length for vert in b_obj.data.vertices)
            self.set_b_collider(b_obj,
                                bounds_type="MESH",
                                radius=radius,
                                n_obj=subshape)

            vertex_offset += subshape.num_vertices
            hk_objects.append(b_obj)

        return hk_objects
Example #10
0
 def import_billboard(n_node, b_obj):
     """ Import a NiBillboardNode """
     if isinstance(n_node, NifFormat.NiBillboardNode) and not isinstance(
             b_obj, bpy.types.Bone):
         # find camera object
         for obj in bpy.context.scene.objects:
             if obj.type == 'CAMERA':
                 b_obj_camera = obj
                 break
         # none exists, create one
         else:
             b_obj_camera_data = bpy.data.cameras.new("Camera")
             b_obj_camera = Object.create_b_obj(None, b_obj_camera_data)
         # make b_obj track camera object
         constr = b_obj.constraints.new('TRACK_TO')
         constr.target = b_obj_camera
         constr.track_axis = 'TRACK_Z'
         constr.up_axis = 'UP_Y'
Example #11
0
    def import_bhkbox_shape(self, bhk_shape):
        """Import a BhkBox block as a simple Box collision object"""
        NifLog.debug(f"Importing {bhk_shape.__class__.__name__}")

        # create box
        r = bhk_shape.radius * self.HAVOK_SCALE
        dims = bhk_shape.dimensions
        minx = -dims.x * self.HAVOK_SCALE
        maxx = +dims.x * self.HAVOK_SCALE
        miny = -dims.y * self.HAVOK_SCALE
        maxy = +dims.y * self.HAVOK_SCALE
        minz = -dims.z * self.HAVOK_SCALE
        maxz = +dims.z * self.HAVOK_SCALE

        # create blender object
        b_obj = Object.box_from_extents("box", minx, maxx, miny, maxy, minz, maxz)
        self.set_b_collider(b_obj, radius=r, n_obj=bhk_shape)
        return [b_obj]
Example #12
0
    def import_capsulebv(self, capsule):
        offset = capsule.center
        # always a normalized vector
        direction = capsule.origin
        # nb properly named in newer nif.xmls
        extent = capsule.unknown_float_1
        radius = capsule.unknown_float_2

        # positions of the box verts
        minx = miny = -radius
        maxx = maxy = +radius
        minz = -(extent + 2 * radius) / 2
        maxz = +(extent + 2 * radius) / 2

        # create blender object
        b_obj = Object.box_from_extents("capsule", minx, maxx, miny, maxy,
                                        minz, maxz)
        # apply transform in local space
        b_obj.matrix_local = self.center_origin_to_matrix(offset, direction)
        self.set_b_collider(b_obj,
                            bounds_type="CAPSULE",
                            display_type="CAPSULE",
                            radius=radius)
        return [b_obj]
Example #13
0
 def import_empty(n_block):
     """Creates and returns a grouping empty."""
     b_empty = Object.create_b_obj(n_block, None)
     # TODO [flags] Move out to generic processing
     b_empty.niftools.flags = n_block.flags
     return b_empty
Example #14
0
    def execute(self):
        """Main import function."""
        self.load_files()  # needs to be first to provide version info.

        self.armaturehelper = Armature()
        self.boundhelper = Bound()
        self.bhkhelper = BhkCollision()
        self.constrainthelper = Constraint()
        self.objecthelper = Object()
        self.object_anim = ObjectAnimation()
        self.transform_anim = TransformAnimation()

        # find and store this list now of selected objects as creating new objects adds them to the selection list
        self.SELECTED_OBJECTS = bpy.context.selected_objects[:]

        # catch nif import errors
        try:
            # check that one armature is selected in 'import geometry + parent
            # to armature' mode
            if NifOp.props.process == "GEOMETRY_ONLY":
                if len(self.SELECTED_OBJECTS
                       ) != 1 or self.SELECTED_OBJECTS[0].type != 'ARMATURE':
                    raise io_scene_niftools.utils.logging.NifError(
                        "You must select exactly one armature in 'Import Geometry Only' mode."
                    )

            NifLog.info("Importing data")
            # calculate and set frames per second
            if NifOp.props.animation:
                Animation.set_frames_per_second(NifData.data.roots)

            # merge skeleton roots and transform geometry into the rest pose
            if NifOp.props.merge_skeleton_roots:
                pyffi.spells.nif.fix.SpellMergeSkeletonRoots(
                    data=NifData.data).recurse()
            if NifOp.props.send_geoms_to_bind_pos:
                pyffi.spells.nif.fix.SpellSendGeometriesToBindPosition(
                    data=NifData.data).recurse()
            if NifOp.props.send_detached_geoms_to_node_pos:
                pyffi.spells.nif.fix.SpellSendDetachedGeometriesToNodePosition(
                    data=NifData.data).recurse()
            if NifOp.props.apply_skin_deformation:
                VertexGroup.apply_skin_deformation(NifData.data)

            # store scale correction
            bpy.context.scene.niftools_scene.scale_correction = NifOp.props.scale_correction
            self.apply_scale(NifData.data, NifOp.props.scale_correction)

            # import all root blocks
            for block in NifData.data.roots:
                root = block
                # root hack for corrupt better bodies meshes and remove geometry from better bodies on skeleton import
                for b in (b for b in block.tree() if
                          isinstance(b, NifFormat.NiGeometry) and b.is_skin()):
                    # check if root belongs to the children list of the skeleton root (can only happen for better bodies meshes)
                    if root in [
                            c for c in b.skin_instance.skeleton_root.children
                    ]:
                        # fix parenting and update transform accordingly
                        b.skin_instance.data.set_transform(
                            root.get_transform() *
                            b.skin_instance.data.get_transform())
                        b.skin_instance.skeleton_root = root
                        # delete non-skeleton nodes if we're importing skeleton only
                        if NifOp.props.process == "SKELETON_ONLY":
                            nonbip_children = (child for child in root.children
                                               if child.name[:6] != 'Bip01 ')
                            for child in nonbip_children:
                                root.remove_child(child)

                # import this root block
                NifLog.debug(f"Root block: {root.get_global_display()}")
                self.import_root(root)

        except NifError:
            return {'CANCELLED'}

        NifLog.info("Finished")
        return {'FINISHED'}
Example #15
0
class NifImport(NifCommon):
    def __init__(self, operator, context):
        NifCommon.__init__(self, operator, context)

    def execute(self):
        """Main import function."""
        self.load_files()  # needs to be first to provide version info.

        self.armaturehelper = Armature()
        self.boundhelper = Bound()
        self.bhkhelper = BhkCollision()
        self.constrainthelper = Constraint()
        self.objecthelper = Object()
        self.object_anim = ObjectAnimation()
        self.transform_anim = TransformAnimation()

        # find and store this list now of selected objects as creating new objects adds them to the selection list
        self.SELECTED_OBJECTS = bpy.context.selected_objects[:]

        # catch nif import errors
        try:
            # check that one armature is selected in 'import geometry + parent
            # to armature' mode
            if NifOp.props.process == "GEOMETRY_ONLY":
                if len(self.SELECTED_OBJECTS
                       ) != 1 or self.SELECTED_OBJECTS[0].type != 'ARMATURE':
                    raise io_scene_niftools.utils.logging.NifError(
                        "You must select exactly one armature in 'Import Geometry Only' mode."
                    )

            NifLog.info("Importing data")
            # calculate and set frames per second
            if NifOp.props.animation:
                Animation.set_frames_per_second(NifData.data.roots)

            # merge skeleton roots and transform geometry into the rest pose
            if NifOp.props.merge_skeleton_roots:
                pyffi.spells.nif.fix.SpellMergeSkeletonRoots(
                    data=NifData.data).recurse()
            if NifOp.props.send_geoms_to_bind_pos:
                pyffi.spells.nif.fix.SpellSendGeometriesToBindPosition(
                    data=NifData.data).recurse()
            if NifOp.props.send_detached_geoms_to_node_pos:
                pyffi.spells.nif.fix.SpellSendDetachedGeometriesToNodePosition(
                    data=NifData.data).recurse()
            if NifOp.props.apply_skin_deformation:
                VertexGroup.apply_skin_deformation(NifData.data)

            # store scale correction
            bpy.context.scene.niftools_scene.scale_correction = NifOp.props.scale_correction
            self.apply_scale(NifData.data, NifOp.props.scale_correction)

            # import all root blocks
            for block in NifData.data.roots:
                root = block
                # root hack for corrupt better bodies meshes and remove geometry from better bodies on skeleton import
                for b in (b for b in block.tree() if
                          isinstance(b, NifFormat.NiGeometry) and b.is_skin()):
                    # check if root belongs to the children list of the skeleton root (can only happen for better bodies meshes)
                    if root in [
                            c for c in b.skin_instance.skeleton_root.children
                    ]:
                        # fix parenting and update transform accordingly
                        b.skin_instance.data.set_transform(
                            root.get_transform() *
                            b.skin_instance.data.get_transform())
                        b.skin_instance.skeleton_root = root
                        # delete non-skeleton nodes if we're importing skeleton only
                        if NifOp.props.process == "SKELETON_ONLY":
                            nonbip_children = (child for child in root.children
                                               if child.name[:6] != 'Bip01 ')
                            for child in nonbip_children:
                                root.remove_child(child)

                # import this root block
                NifLog.debug(f"Root block: {root.get_global_display()}")
                self.import_root(root)

        except NifError:
            return {'CANCELLED'}

        NifLog.info("Finished")
        return {'FINISHED'}

    def load_files(self):
        NifData.init(NifFile.load_nif(NifOp.props.filepath))
        if NifOp.props.override_scene_info:
            scene.import_version_info(NifData.data)

    def import_root(self, root_block):
        """Main import function."""
        # check that this is not a kf file
        if isinstance(
                root_block,
            (NifFormat.NiSequence, NifFormat.NiSequenceStreamHelper)):
            raise io_scene_niftools.utils.logging.NifError(
                "Use the KF import operator to load KF files.")

        # divinity 2: handle CStreamableAssetData
        if isinstance(root_block, NifFormat.CStreamableAssetData):
            root_block = root_block.root

        # sets the root block parent to None, so that when crawling back the script won't barf
        root_block._parent = None

        # set the block parent through the tree, to ensure I can always move backward
        self.set_parents(root_block)

        # mark armature nodes and bones
        self.armaturehelper.mark_armatures_bones(root_block)

        # import the keyframe notes
        # if NifOp.props.animation:
        #     self.animationhelper.import_text_keys(root_block)

        # read the NIF tree
        if isinstance(root_block,
                      (NifFormat.NiNode, NifFormat.NiTriBasedGeom)):
            b_obj = self.import_branch(root_block)
            ObjectProperty().import_extra_datas(root_block, b_obj)

            # now all havok objects are imported, so we are ready to import the havok constraints
            self.constrainthelper.import_bhk_constraints()

            # parent selected meshes to imported skeleton
            if NifOp.props.process == "SKELETON_ONLY":
                # update parenting & armature modifier
                for child in bpy.context.selected_objects:
                    if isinstance(child, bpy.types.Object) and not isinstance(
                            child.data, bpy.types.Armature):
                        child.parent = b_obj
                        for mod in child.modifiers:
                            if mod.type == "ARMATURE":
                                mod.object = b_obj

        elif isinstance(root_block, NifFormat.NiCamera):
            NifLog.warn('Skipped NiCamera root')

        elif isinstance(root_block, NifFormat.NiPhysXProp):
            NifLog.warn('Skipped NiPhysXProp root')

        else:
            NifLog.warn(
                f"Skipped unsupported root block type '{root_block.__class__}' (corrupted nif?)."
            )

    def import_collision(self, n_node):
        """ Imports a NiNode's collision_object, if present"""
        if n_node.collision_object:
            if isinstance(n_node.collision_object,
                          NifFormat.bhkNiCollisionObject):
                return self.bhkhelper.import_bhk_shape(
                    n_node.collision_object.body)
            elif isinstance(n_node.collision_object,
                            NifFormat.NiCollisionData):
                return self.boundhelper.import_bounding_volume(
                    n_node.collision_object.bounding_volume)
        return []

    def import_branch(self, n_block, b_armature=None, n_armature=None):
        """Read the content of the current NIF tree branch to Blender recursively.

        :param n_block: The nif block to import.
        :param b_armature: The blender armature for the current branch.
        :param n_armature: The corresponding nif block for the armature for  the current branch.
        """
        if not n_block:
            return None

        NifLog.info(f"Importing data for block '{n_block.name.decode()}'")
        if isinstance(n_block, NifFormat.NiTriBasedGeom
                      ) and NifOp.props.process != "SKELETON_ONLY":
            return self.objecthelper.import_geometry_object(
                b_armature, n_block)

        elif isinstance(n_block, NifFormat.NiNode):
            # import object
            if self.armaturehelper.is_armature_root(n_block):
                # all bones in the tree are also imported by import_armature
                if NifOp.props.process != "GEOMETRY_ONLY":
                    b_obj = self.armaturehelper.import_armature(n_block)
                else:
                    n_name = block_store.import_name(n_block)
                    b_obj = math.get_armature()
                    NifLog.info(
                        f"Merging nif tree '{n_name}' with armature '{b_obj.name}'"
                    )
                    if n_name != b_obj.name:
                        NifLog.warn(
                            f"Using Nif block '{n_name}' as armature '{b_obj.name}' but names do not match"
                        )
                b_armature = b_obj
                n_armature = n_block

            elif self.armaturehelper.is_bone(n_block):
                # bones have already been imported during import_armature
                n_name = block_store.import_name(n_block)
                if n_name in b_armature.data.bones:
                    b_obj = b_armature.data.bones[n_name]
                else:
                    # this is a fallback for a weird bug, when a node is child of a NiLodNode in a skeletal nif
                    b_obj = self.objecthelper.create_b_obj(n_block,
                                                           None,
                                                           name=n_name)
                b_obj.niftools.flags = n_block.flags

            else:
                # import as an empty
                b_obj = NiTypes.import_empty(n_block)

            # find children
            b_children = []
            n_children = [child for child in n_block.children]
            for n_child in n_children:
                b_child = self.import_branch(n_child,
                                             b_armature=b_armature,
                                             n_armature=n_armature)
                if b_child and isinstance(b_child, bpy.types.Object):
                    b_children.append(b_child)

            # import collision objects & bounding box
            if NifOp.props.process != "SKELETON_ONLY":
                b_children.extend(self.import_collision(n_block))
                b_children.extend(
                    self.boundhelper.import_bounding_box(n_block))

            # set bind pose for children
            self.objecthelper.set_object_bind(b_obj, b_children, b_armature)

            # import extra node data, such as node type
            NiTypes.import_root_collision(n_block, b_obj)
            NiTypes.import_billboard(n_block, b_obj)
            NiTypes.import_range_lod_data(n_block, b_obj, b_children)

            # set object transform, this must be done after all children objects have been parented to b_obj
            if isinstance(b_obj, bpy.types.Object):
                # note: bones and this object's children already have their matrix set
                b_obj.matrix_local = math.import_matrix(n_block)

                # import object level animations (non-skeletal)
                if NifOp.props.animation:
                    # self.animationhelper.import_text_keys(n_block)
                    self.transform_anim.import_transforms(n_block, b_obj)
                    self.object_anim.import_visibility(n_block, b_obj)

            return b_obj

        # all else is currently discarded
        return None

    def set_parents(self, n_block):
        """Set the parent block recursively through the tree, to allow
        crawling back as needed."""
        if isinstance(n_block, NifFormat.NiNode):
            # list of non-null children
            children = [child for child in n_block.children if child]
            for child in children:
                child._parent = n_block
                self.set_parents(child)