Return True if chunk_id is a valid psk / psa ( file_ext ) magick number .

def util_is_header_valid(filename, file_ext, chunk_id, error_callback):
    '''Return True if chunk_id is a valid psk/psa (file_ext) 'magick number'.'''
    if chunk_id != PSKPSA_FILE_HEADER[file_ext]:
        error_callback(
            "File %s is not a %s file. (header mismach)\nExpected: %s \nPresent %s"  % (
                filename, file_ext,
                PSKPSA_FILE_HEADER[file_ext], chunk_id)
        )
        return False
    return True


def util_gen_name_part(filepath):
    '''Return file name without extension'''
    return re.match(r'.*[/\\]([^/\\]+?)(\..{2,5})?$', filepath).group(1)

def util_gen_name_part ( filepath ) : return re . match ( r'.*[/\\]([^/\\]+?)(\..{2,5})?$' , filepath ) . group ( 1 )


Make ** vec_out ** to be an axis - aligned unit vector that is closest to vec_in . ( basis? )

def vec_to_axis_vec(vec_in, vec_out):
    '''Make **vec_out** to be an axis-aligned unit vector that is closest to vec_in. (basis?)'''
    x, y, z = vec_in
    if abs(x) > abs(y):
        if abs(x) > abs(z):
            vec_out.x = 1 if x >= 0 else -1
        else:
            vec_out.z = 1 if z >= 0 else -1
    else:
        if abs(y) > abs(z):
            vec_out.y = 1 if y >= 0 else -1
        else:
            vec_out.z = 1 if z >= 0 else -1

def vec_to_axis_vec ( vec_in , vec_out ) : x , y , z = vec_in if abs ( x ) > abs ( y ) : if abs ( x ) > abs ( z ) : vec_out . x = 1 if x >= 0 else - 1 else : vec_out . z = 1 if z >= 0 else - 1 else : if abs ( y ) > abs ( z ) : vec_out . y = 1 if y >= 0 else - 1 else : vec_out . z = 1 if z >= 0 else - 1


Import mesh and skeleton from . psk / . pskx files Args : bReorientBones : Axis based bone orientation to children error_callback : Called when importing is failed . __name__ ( ? error_callback = lambda msg : print ( reason : msg )

def pskimport(filepath,
        context = bpy.context,
        bImportmesh = True,
        bImportbone = True,
        bSpltiUVdata = False,
        fBonesize = 5.0,
        fBonesizeRatio = 0.6,
        bDontInvertRoot = False,
        bReorientBones = False,
        bReorientDirectly = False,
        error_callback = None):
    '''
    Import mesh and skeleton from .psk/.pskx files

    Args:
        bReorientBones:
            Axis based bone orientation to children

        error_callback:
            Called when importing is failed.

            __name__('?', error_callback = lambda msg: print('reason:',msg)

    '''
    if not hasattr( error_callback, '__call__'):
        error_callback = __pass

    # ref_time = time.process_time()
    if not bImportbone and not bImportmesh:
        error_callback("Nothing to do.\nSet something for import.")
        return False
    file_ext = 'psk'

    print ("-----------------------------------------------")
    print ("---------EXECUTING PSK PYTHON IMPORTER---------")
    print ("-----------------------------------------------")

    #file may not exist
    try:
        file = open(filepath,'rb')
    except IOError:
        error_callback('Error while opening file for reading:\n  "'+filepath+'"')
        return False

    if not util_check_file_header(file, 'psk'):
        error_callback('Not psk file:\n  "'+filepath+'"')
        return False

    Vertices = None
    Wedges = None
    Faces = None
    UV_by_face = None
    Materials = None
    Bones = None
    Weights = None
    Extrauvs = []
    Normals = None

    #==================================================================================================
    # Materials   MaterialNameRaw | TextureIndex | PolyFlags | AuxMaterial | AuxFlags |  LodBias | LodStyle
    # Only Name is usable.
    def read_materials():

        nonlocal Materials

        Materials = []

        for counter in range(chunk_datacount):

            (MaterialNameRaw,) = unpack_from('64s24x', chunk_data, chunk_datasize * counter)

            Materials.append( util_bytes_to_str( MaterialNameRaw ) )

    #==================================================================================================
    # Faces WdgIdx1 | WdgIdx2 | WdgIdx3 | MatIdx | AuxMatIdx | SmthGrp
    def read_faces():
        nonlocal Faces, UV_by_face

        if not bImportmesh:
            return True

        UV_by_face = [None] * chunk_datacount
        Faces = [None] * chunk_datacount

        if len(Wedges) > 65536:
            unpack_format = '=IIIBBI'
        else:
            unpack_format = '=HHHBBI'

        unpack_data = Struct(unpack_format).unpack_from

        for counter in range(chunk_datacount):
            (WdgIdx1, WdgIdx2, WdgIdx3,
             MatIndex,
             AuxMatIndex, #unused
             SmoothingGroup # Umodel is not exporting SmoothingGroups
             ) = unpack_data(chunk_data, counter * chunk_datasize)

            # looks ugly
            # Wedges is (point_index, u, v, MatIdx)
            ((vertid0, u0, v0, matid0), (vertid1, u1, v1, matid1), (vertid2, u2, v2, matid2)) = Wedges[WdgIdx1], Wedges[WdgIdx2], Wedges[WdgIdx3]

            # note order: C,B,A
            Faces[counter] = (vertid2,  vertid1, vertid0)

            uv = ( ( u2, 1.0 - v2 ), ( u1, 1.0 - v1 ), ( u0, 1.0 - v0 ) )

            # Mapping: FaceIndex <=> UV data <=> FaceMatIndex
            UV_by_face[counter] = (uv, MatIndex, (matid2, matid1, matid0))

    #==================================================================================================
    # Vertices X | Y | Z
    def read_vertices():
        nonlocal bImportbone, bImportmesh

        if not bImportmesh:
            return True

        nonlocal Vertices

        Vertices = [None] * chunk_datacount

        unpack_data = Struct('3f').unpack_from

        for counter in range( chunk_datacount ):
            (vec_x, vec_y, vec_z) = unpack_data(chunk_data, counter * chunk_datasize)
            Vertices[counter]  = (vec_x, vec_y, vec_z)

    #==================================================================================================
    # Wedges (UV)   VertexId |  U |  V | MatIdx
    def read_wedges():

        nonlocal bImportbone, bImportmesh
        if not bImportmesh:
            return True

        nonlocal Wedges

        Wedges = [None] * chunk_datacount

        unpack_data = Struct('=IffBxxx').unpack_from

        for counter in range( chunk_datacount ):
            (vertex_id,
             u, v,
             material_index) = unpack_data( chunk_data, counter * chunk_datasize )

            # print(vertex_id, u, v, material_index)
            # Wedges[counter] = (vertex_id, u, v, material_index)
            Wedges[counter] = [vertex_id, u, v, material_index]

    #==================================================================================================
    # Bones (VBone .. VJointPos ) Name|Flgs|NumChld|PrntIdx|Qw|Qx|Qy|Qz|LocX|LocY|LocZ|Lngth|XSize|YSize|ZSize
    def read_bones():

        nonlocal Bones, bImportbone

        if chunk_datacount == 0:
            bImportbone = False

        if bImportbone:
            unpack_data = Struct('64s3i11f').unpack_from
        else:
            unpack_data = Struct('64s56x').unpack_from

        Bones = [None] * chunk_datacount

        for counter in range( chunk_datacount ):
            Bones[counter] = unpack_data( chunk_data, chunk_datasize * counter)

    #==================================================================================================
    # Influences (Bone Weight) (VRawBoneInfluence) ( Weight | PntIdx | BoneIdx)
    def read_weights():
        # nonlocal Weights, bImportmesh
        nonlocal Weights

        if not bImportmesh:
            return True

        Weights = [None] * chunk_datacount

        unpack_data = Struct('fii').unpack_from

        for counter in range(chunk_datacount):
            Weights[counter] = unpack_data(chunk_data, chunk_datasize * counter)

    #==================================================================================================
    # Extra UV. U | V
    def read_extrauvs():
        unpack_data = Struct("=2f").unpack_from

        uvdata = [None] * chunk_datacount

        for counter in range( chunk_datacount ):
            uvdata[counter] = unpack_data(chunk_data, chunk_datasize * counter)

        Extrauvs.append(uvdata)

    #==================================================================================================
    # Vertex Normals NX | NY | NZ
    def read_normals():
        if not bImportmesh:
            return True

        nonlocal Normals
        Normals = [None] * chunk_datacount

        unpack_data = Struct('3f').unpack_from

        for counter in range(chunk_datacount):
            Normals[counter] = unpack_data(chunk_data, counter * chunk_datasize)

    CHUNKS_HANDLERS = {
        'PNTS0000': read_vertices,
        'VTXW0000': read_wedges,
        'VTXW3200': read_wedges,#?
        'FACE0000': read_faces,
        'FACE3200': read_faces,
        'MATT0000': read_materials,
        'REFSKELT': read_bones,
        'REFSKEL0': read_bones, #?
        'RAWW0000': read_weights,
        'RAWWEIGH': read_weights,
        'EXTRAUVS': read_extrauvs,
        'VTXNORMS': read_normals
    }

    #===================================================================================================
    # File. Read all needed data.
    #         VChunkHeader Struct
    # ChunkID|TypeFlag|DataSize|DataCount
    # 0      |1       |2       |3

    while True:

        header_bytes = file.read(32)

        if len(header_bytes) < 32:

            if len(header_bytes) != 0:
                error_callback("Unexpected end of file.(%s/32 bytes)" % len(header_bytes))
            break

        (chunk_id, chunk_type, chunk_datasize, chunk_datacount) = unpack('20s3i', header_bytes)

        chunk_id_str = util_bytes_to_str(chunk_id)
        chunk_id_str = chunk_id_str[:8]

        if chunk_id_str in CHUNKS_HANDLERS:

            chunk_data = file.read( chunk_datasize * chunk_datacount)

            if len(chunk_data) < chunk_datasize * chunk_datacount:
                error_callback('Psk chunk %s is broken.' % chunk_id_str)
                return False

            CHUNKS_HANDLERS[chunk_id_str]()

        else:

            print('Unknown chunk: ', chunk_id_str)
            file.seek(chunk_datasize * chunk_datacount, 1)

        # print(chunk_id_str, chunk_datacount)

    file.close()

    print(" Importing file:", filepath)

    if not bImportmesh and (Bones is None or len(Bones) == 0):
        error_callback("Psk: no skeleton data.")
        return False

    MAX_UVS = 8
    NAME_UV_PREFIX = "UV"

    # file name w/out extension
    gen_name_part = util_gen_name_part(filepath)
    gen_names = {
        'armature_object':  gen_name_part + '.ao',
        'armature_data':    gen_name_part + '.ad',
            'mesh_object':  gen_name_part + '.mo',
            'mesh_data':    gen_name_part + '.md'
    }

    if bImportmesh:
        mesh_data = bpy.data.meshes.new(gen_names['mesh_data'])
        mesh_obj = bpy.data.objects.new(gen_names['mesh_object'], mesh_data)

    #==================================================================================================
    # UV. Prepare
    if bImportmesh:
        if bSpltiUVdata:
        # store how much each "matrial index" have vertices

            uv_mat_ids = {}

            for (_, _, _, material_index) in Wedges:

                if not (material_index in uv_mat_ids):
                    uv_mat_ids[material_index] = 1
                else:
                    uv_mat_ids[material_index] += 1

            # if we have more UV material indexes than blender UV maps, then...
            if bSpltiUVdata and len(uv_mat_ids) > MAX_UVS :

                uv_mat_ids_len = len(uv_mat_ids)

                print('UVs: %s out of %s is combined in a first UV map(%s0)' % (uv_mat_ids_len - 8, uv_mat_ids_len, NAME_UV_PREFIX))

                mat_idx_proxy = [0] * len(uv_mat_ids)

                counts_sorted = sorted(uv_mat_ids.values(), reverse = True)

                new_mat_index = MAX_UVS - 1

                for c in counts_sorted:
                    for mat_idx, counts in uv_mat_ids.items():
                        if c == counts:
                            mat_idx_proxy[mat_idx] = new_mat_index
                            if new_mat_index > 0:
                                new_mat_index -= 1
                            # print('MatIdx remap: %s > %s' % (mat_idx,new_mat_index))

                for i in range(len(Wedges)):
                    Wedges[i][3] = mat_idx_proxy[Wedges[i][3]]

        # print('Wedges:', chunk_datacount)
        # print('uv_mat_ids', uv_mat_ids)
        # print('uv_mat_ids', uv_mat_ids)
        # for w in Wedges:

    if bImportmesh:
        # print("-- Materials -- (index, name, faces)")
        blen_materials = []
        for materialname in Materials:
            matdata = bpy.data.materials.get(materialname)

            if matdata is None:
                matdata = bpy.data.materials.new( materialname )
            # matdata = bpy.data.materials.new( materialname )

            blen_materials.append( matdata )
            mesh_data.materials.append( matdata )
            # print(counter,materialname,TextureIndex)
            # if mat_groups.get(counter) is not None:
                # print("%i: %s" % (counter, materialname), len(mat_groups[counter]))

    #==================================================================================================
    # Prepare bone data
    psk_bone_name_toolong = False

    def init_psk_bone(i, psk_bones, name_raw):
        psk_bone = class_psk_bone()
        psk_bone.children = []
        psk_bone.name = util_bytes_to_str(name_raw)
        psk_bones[i] = psk_bone
        return psk_bone

    # indexed by bone index. array of psk_bone
    psk_bones = [None] * len(Bones)

    if not bImportbone: #data needed for mesh-only import

        for counter,(name_raw,) in enumerate(Bones):
            init_psk_bone(counter, psk_bones, name_raw)

    if bImportbone:  #else?

        # average bone length
        sum_bone_pos = 0

        for counter, (name_raw, flags, NumChildren, ParentIndex, #0 1 2 3
             quat_x, quat_y, quat_z, quat_w,            #4 5 6 7
             vec_x, vec_y, vec_z,                       #8 9 10
             joint_length,                              #11
             scale_x, scale_y, scale_z) in enumerate(Bones):

            psk_bone = init_psk_bone(counter, psk_bones, name_raw)

            psk_bone.bone_index = counter
            psk_bone.parent_index = ParentIndex

            if len(psk_bone.name) > 60:
                psk_bone_name_toolong = True

            # print(psk_bone.bone_index, psk_bone.parent_index, psk_bone.name)
            # make sure we have valid parent_index
            if psk_bone.parent_index < 0:
                psk_bone.parent_index = 0

            # psk_bone.scale = (scale_x, scale_y, scale_z)

            # store bind pose to make it available for psa-import via CustomProperty of the Blender bone
            psk_bone.orig_quat = Quaternion((quat_w, quat_x, quat_y, quat_z))
            psk_bone.orig_loc = Vector((vec_x, vec_y, vec_z))

            # root bone must have parent_index = 0 and selfindex = 0
            if psk_bone.parent_index == 0 and psk_bone.bone_index == psk_bone.parent_index:
                if bDontInvertRoot:
                    psk_bone.mat_world_rot = psk_bone.orig_quat.to_matrix()
                else:
                    psk_bone.mat_world_rot = psk_bone.orig_quat.conjugated().to_matrix()
                psk_bone.mat_world = Matrix.Translation(psk_bone.orig_loc)

            sum_bone_pos += psk_bone.orig_loc.length

    #==================================================================================================
    # Bones. Calc World-space matrix

        # TODO optimize math.
        for psk_bone in psk_bones:

            if psk_bone.parent_index == 0:
                if psk_bone.bone_index == 0:
                    psk_bone.parent = None
                    continue

            parent = psk_bones[psk_bone.parent_index]

            psk_bone.parent = parent

            parent.children.append(psk_bone)

            # mat_world -     world space bone matrix WITHOUT own rotation
            # mat_world_rot - world space bone rotation WITH own rotation
            psk_bone.mat_world = parent.mat_world_rot.to_4x4()
            psk_bone.mat_world.translation = parent.mat_world.translation + parent.mat_world_rot * psk_bone.orig_loc
            psk_bone.mat_world_rot = parent.mat_world_rot * psk_bone.orig_quat.conjugated().to_matrix()

            # psk_bone.mat_world =  ( parent.mat_world_rot.to_4x4() * psk_bone.trans)
            # psk_bone.mat_world.translation += parent.mat_world.translation
            # psk_bone.mat_world_rot = parent.mat_world_rot * psk_bone.orig_quat.conjugated().to_matrix()

    #==================================================================================================
    # Skeleton. Prepare.

        armature_data = bpy.data.armatures.new(gen_names['armature_data'])
        armature_obj = bpy.data.objects.new(gen_names['armature_object'], armature_data)
        # TODO: options for axes and x_ray?
        armature_data.show_axes = False
        armature_data.draw_type = 'STICK'
        armature_obj.show_x_ray = True

        util_obj_link(context, armature_obj)

        util_select_all(False)
        util_obj_select(context, armature_obj)
        util_obj_set_active(context, armature_obj)

        utils_set_mode('EDIT')

        sum_bone_pos /= len(Bones) # average
        sum_bone_pos *= fBonesizeRatio # corrected

        bone_size_choosen = max(0.01, round((min(sum_bone_pos, fBonesize))))

        if not bReorientBones:
            new_bone_size = bone_size_choosen
    #==================================================================================================
    # Skeleton. Build.
        if psk_bone_name_toolong:
            for psk_bone in psk_bones:

                # TODO too long name cutting options?
                long_name = psk_bone.name
                psk_bone.name = psk_bone.name[-60:]

                edit_bone = armature_obj.data.edit_bones.new(psk_bone.name)
                edit_bone["long_name"] = long_name

                psk_bone.name = edit_bone.name

                # print(psk_bone.name)
                # print(edit_bone.name)
        else:
            for psk_bone in psk_bones:
                edit_bone = armature_obj.data.edit_bones.new(psk_bone.name)
                psk_bone.name = edit_bone.name

        for psk_bone in psk_bones:
            edit_bone = armature_obj.data.edit_bones[psk_bone.name]

            armature_obj.data.edit_bones.active = edit_bone

            if psk_bone.parent is not None:
                edit_bone.parent = armature_obj.data.edit_bones[psk_bone.parent.name]
            else:
                if bDontInvertRoot:
                    psk_bone.orig_quat.conjugate()

            if bReorientBones:
                (new_bone_size, quat_orient_diff) = calc_bone_rotation(psk_bone, bone_size_choosen, bReorientDirectly, sum_bone_pos)
                post_quat = psk_bone.orig_quat.conjugated() * quat_orient_diff
            else:
                post_quat = psk_bone.orig_quat.conjugated()

            # only length of this vector is matter?
            edit_bone.tail = Vector(( 0.0, new_bone_size, 0.0))
            # edit_bone.tail = Vector((0.0, 0.0, new_bone_size))

            # edit_bone.matrix = psk_bone.mat_world * quat_diff.to_matrix().to_4x4()
            edit_bone.matrix = psk_bone.mat_world * post_quat.to_matrix().to_4x4()

            # some dev code...
            #### FINAL
            # post_quat = psk_bone.orig_quat.conjugated() * quat_diff
            # edit_bone.matrix = psk_bone.mat_world * test_quat.to_matrix().to_4x4()
            # edit_bone["post_quat"] = test_quat
            ####

            # edit_bone["post_quat"] = Quaternion((1,0,0,0))
            # edit_bone.matrix = psk_bone.mat_world* psk_bone.rot

            # if edit_bone.parent:
              # edit_bone.matrix = edit_bone.parent.matrix * psk_bone.trans * (psk_bone.orig_quat.conjugated().to_matrix().to_4x4())
              # edit_bone.matrix = edit_bone.parent.matrix * psk_bone.trans * (test_quat.to_matrix().to_4x4())
            # else:
              # edit_bone.matrix = psk_bone.orig_quat.to_matrix().to_4x4()

            # save bindPose information for .psa import
            edit_bone["orig_quat"] = psk_bone.orig_quat
            edit_bone["orig_loc"]  = psk_bone.orig_loc
            edit_bone["post_quat"] = post_quat

    utils_set_mode('OBJECT')

    #==================================================================================================
    # Weights
    if bImportmesh:

        vertices_total = len(Vertices)

        for ( _, PointIndex, BoneIndex ) in Weights:
            if PointIndex < vertices_total: # can it be not?
                psk_bones[BoneIndex].have_weight_data = True
            # else:
                # print(psk_bones[BoneIndex].name, 'for other mesh',PointIndex ,vertices_total)

            #print("weight:", PointIndex, BoneIndex, Weight)
        # Weights.append(None)
        # print(Weights.count(None))

    # Original vertex colorization code
    '''
    # Weights.sort( key = lambda wgh: wgh[0])
    if bImportmesh:
        VtxCol = []
        bones_count = len(psk_bones)
        for x in range(bones_count):
            #change the overall darkness of each material in a range between 0.1 and 0.9
            tmpVal = ((float(x) + 1.0) / bones_count * 0.7) + 0.1
            tmpVal = int(tmpVal * 256)
            tmpCol = [tmpVal, tmpVal, tmpVal, 0]
            #Change the color of each material slightly
            if x % 3 == 0:
                if tmpCol[0] < 128:
                    tmpCol[0] += 60
                else:
                    tmpCol[0] -= 60
            if x % 3 == 1:
                if tmpCol[1] < 128:
                    tmpCol[1] += 60
                else:
                    tmpCol[1] -= 60
            if x % 3 == 2:
                if tmpCol[2] < 128:
                    tmpCol[2] += 60
                else:
                    tmpCol[2] -= 60
            #Add the material to the mesh
            VtxCol.append(tmpCol)

    for x in range(len(Tmsh.faces)):
        for y in range(len(Tmsh.faces[x].v)):
            #find v in Weights[n][0]
            findVal = Tmsh.faces[x].v[y].index
            n = 0
            while findVal != Weights[n][0]:
                n = n + 1
            TmpCol = VtxCol[Weights[n][1]]
            #check if a vertex has more than one influence
            if n != len(Weights) - 1:
                if Weights[n][0] == Weights[n + 1][0]:
                    #if there is more than one influence, use the one with the greater influence
                    #for simplicity only 2 influences are checked, 2nd and 3rd influences are usually very small
                    if Weights[n][2] < Weights[n + 1][2]:
                        TmpCol = VtxCol[Weights[n + 1][1]]
        Tmsh.faces[x].col.append(NMesh.Col(TmpCol[0], TmpCol[1], TmpCol[2], 0))
    '''

    #===================================================================================================
    # UV. Setup.

    if bImportmesh:
        # Trick! Create UV maps BEFORE mesh and get (0,0) coordinates for free!
        #   ...otherwise UV coords will be copied from active, or calculated from mesh...

        if bSpltiUVdata:

            for i in range(len(uv_mat_ids)):
                get_uv_layers(mesh_data).new(name = NAME_UV_PREFIX + str(i))

        else:

            get_uv_layers(mesh_data).new(name = NAME_UV_PREFIX+"_SINGLE")

        for counter, uv_data in enumerate(Extrauvs):

            if len(mesh_data.uv_layers) < MAX_UVS:

                get_uv_layers(mesh_data).new(name = "EXTRAUVS"+str(counter))

            else:

                Extrauvs.remove(uv_data)
                print('Extra UV layer %s is ignored. Re-import without "Split UV data".' % counter)

    #==================================================================================================
    # Mesh. Build.

        mesh_data.from_pydata(Vertices,[],Faces)

    #==================================================================================================
    # Vertex Normal. Set.

        if Normals is not None:
            mesh_data.polygons.foreach_set("use_smooth", [True] * len(mesh_data.polygons))
            mesh_data.normals_split_custom_set_from_vertices(Normals)
            mesh_data.use_auto_smooth = True

    #===================================================================================================
    # UV. Set.

    if bImportmesh:

        for face in mesh_data.polygons:
            face.material_index = UV_by_face[face.index][1]

        uv_layers = mesh_data.uv_layers

        if not bSpltiUVdata:
           uvLayer = uv_layers[0]

        # per face
        # for faceIdx, (faceUVs, faceMatIdx, _, _, wmidx) in enumerate(UV_by_face):
        for faceIdx, (faceUVs, faceMatIdx, WedgeMatIds) in enumerate(UV_by_face):

            # per vertex
            for vertN, uv in enumerate(faceUVs):
                loopId = faceIdx * 3 + vertN

                if bSpltiUVdata:
                    uvLayer = uv_layers[WedgeMatIds[vertN]]

                uvLayer.data[loopId].uv = uv

    #===================================================================================================
    # Extra UVs. Set.

        for counter, uv_data in enumerate(Extrauvs):

            uvLayer = mesh_data.uv_layers[ counter - len(Extrauvs) ]

            for uv_index, uv_coords in enumerate(uv_data):

                uvLayer.data[uv_index].uv = (uv_coords[0], 1.0 - uv_coords[1])

    #===================================================================================================
    # Mesh. Vertex Groups. Bone Weights.

        for psk_bone in psk_bones:
            if psk_bone.have_weight_data:
                psk_bone.vertex_group = mesh_obj.vertex_groups.new(psk_bone.name)
            # else:
                # print(psk_bone.name, 'have no influence on this mesh')

        for weight, vertex_id, bone_index_w in filter(None, Weights):
            psk_bones[bone_index_w].vertex_group.add((vertex_id,), weight, 'ADD')

    #===================================================================================================
    # Skeleton. Colorize.

    if bImportbone:

        bone_group_unused = armature_obj.pose.bone_groups.new("Unused bones")
        bone_group_unused.color_set = 'THEME14'

        bone_group_nochild = armature_obj.pose.bone_groups.new("No children")
        bone_group_nochild.color_set = 'THEME03'

        armature_data.show_group_colors = True

        for psk_bone in psk_bones:

            pose_bone = armature_obj.pose.bones[psk_bone.name]

            if psk_bone.have_weight_data:

                if len(psk_bone.children) == 0:
                    pose_bone.bone_group = bone_group_nochild

            else:
                pose_bone.bone_group = bone_group_unused

    #===================================================================================================
    # Final

    if bImportmesh:

        util_obj_link(context, mesh_obj)
        util_select_all(False)

        if not bImportbone:

            util_obj_select(context, mesh_obj)
            util_obj_set_active(context, mesh_obj)

        else:
            # select_all(False)
            util_obj_select(context, armature_obj)

            # parenting mesh to armature object
            mesh_obj.parent = armature_obj
            mesh_obj.parent_type = 'OBJECT'

            # add armature modifier
            blender_modifier = mesh_obj.modifiers.new( armature_obj.data.name, type = 'ARMATURE')
            blender_modifier.show_expanded = False
            blender_modifier.use_vertex_groups = True
            blender_modifier.use_bone_envelopes = False
            blender_modifier.object = armature_obj

            # utils_set_mode('OBJECT')
            # select_all(False)
            util_obj_select(context, armature_obj)
            util_obj_set_active(context, armature_obj)

    # print("Done: %f sec." % (time.process_time() - ref_time))
    utils_set_mode('OBJECT')
    return True

# Wedges (UV)   VertexId |  U |  V | MatIdx


Import animation data from filepath using oArmature Args : first_frames : ( 0 - import all ) Import only first_frames from each action bActionsToTrack : Put all imported actions in one NLAtrack . oArmature : Skeleton used to calculate keyframes

def psaimport(filepath,
        context = bpy.context,
        oArmature = None,
        bFilenameAsPrefix = False,
        bActionsToTrack = False,
        first_frames = 0,
        bDontInvertRoot = False,
        bUpdateTimelineRange = False,
        fcurve_interpolation = 'LINEAR',
        error_callback = __pass
        ):
    """Import animation data from 'filepath' using 'oArmature'

    Args:
        first_frames: (0 - import all)
            Import only 'first_frames' from each action

        bActionsToTrack:
            Put all imported actions in one NLAtrack.

        oArmature:
            Skeleton used to calculate keyframes
    """
    print ("-----------------------------------------------")
    print ("---------EXECUTING PSA PYTHON IMPORTER---------")
    print ("-----------------------------------------------")

    file_ext = 'psa'
    try:
        psafile = open(filepath, 'rb')
    except IOError:
        error_callback('Error while opening file for reading:\n  "'+filepath+'"')
        return False

    print ("Importing file: ", filepath)

    armature_obj = oArmature

    if armature_obj is None:
        armature_obj = blen_get_armature_from_selection()
        if armature_obj is None:
            error_callback("No armature selected.")
            return False

    chunk_id = None
    chunk_type = None
    chunk_datasize = None
    chunk_datacount = None
    chunk_data = None

    def read_chunk():
        nonlocal chunk_id, chunk_type,\
                 chunk_datasize, chunk_datacount,\
                 chunk_data

        (chunk_id, chunk_type,
         chunk_datasize, chunk_datacount) = unpack('20s3i', psafile.read(32))

        chunk_data = psafile.read(chunk_datacount * chunk_datasize)
    #==============================================================================================
    # General Header
    #==============================================================================================
    read_chunk()

    if not util_is_header_valid(filepath, file_ext, chunk_id, error_callback):
        return False

    #==============================================================================================
    # Bones (FNamedBoneBinary)
    #==============================================================================================
    read_chunk()

    psa_bones = {}

    def new_psa_bone(bone, pose_bone):
        psa_bone = class_psa_bone()

        psa_bones[pose_bone.name] = psa_bone

        psa_bone.name = pose_bone.name

        psa_bone.pose_bone = pose_bone

        if bone.parent != None:
            # does needed parent bone was added from psa file
            if bone.parent.name in psa_bones:
                psa_bone.parent = psa_bones[bone.parent.name]
            # no. armature doesnt match
            else:
                psa_bone.parent = None
        # else:
            # psa_bone.parent = None

        psa_bone.orig_quat = Quaternion(bone['orig_quat'])
        psa_bone.orig_loc  =     Vector(bone['orig_loc'])
        psa_bone.post_quat = Quaternion(bone['post_quat'])
        return psa_bone

    #Bones Data
    BoneIndex2Name = [None] * chunk_datacount
    BoneNotFoundList = []
    BonesWithoutAnimation = []
    PsaBonesToProcess = [None] * chunk_datacount

    # printlog("Name\tFlgs\tNumChld\tPrntIdx\tQx\tQy\tQz\tQw\tLocX\tLocY\tLocZ\tLength\tXSize\tYSize\tZSize\n")

    # for case insensetive comparison
    # key = lowered name
    # value = orignal name
    skeleton_bones_lowered = {}

    for blender_bone_name in armature_obj.data.bones.keys():
      skeleton_bones_lowered[blender_bone_name.lower()] = blender_bone_name

    for counter in range(chunk_datacount):

        # tPrntIdx is -1 for parent; and 0 for other; no more useful data
        # indata = unpack_from('64s3i11f', chunk_data, chunk_datasize * counter)
        (indata) = unpack_from('64s56x', chunk_data, chunk_datasize * counter)
        in_name = util_bytes_to_str(indata[0])
        # bonename = util_bytes_to_str(indata[0]).upper()

        in_name_lowered = in_name.lower()
        if in_name_lowered in skeleton_bones_lowered:
            orig_name = skeleton_bones_lowered[in_name_lowered]

            # use a skeleton bone name
            BoneIndex2Name[counter] = orig_name
            PsaBonesToProcess[counter] = new_psa_bone(armature_obj.data.bones[orig_name],
                                                      armature_obj.pose.bones[orig_name])
        else:
            # print("Can't find the bone:", bonename)
            BoneNotFoundList.append(counter)

    if len(psa_bones) == 0:
        error_callback('No bone was match!\nSkip import!')
        return False

    # does anyone care?
    for blender_bone_name in armature_obj.data.bones.keys():
        if BoneIndex2Name.count(blender_bone_name) == 0:
            BonesWithoutAnimation.append(blender_bone_name)

    if len(BoneNotFoundList) > 0:
      print('Not found bones: %i.' % len(BoneNotFoundList));

    if len(BonesWithoutAnimation) > 0:
      print('Bones(%i) without animation data:\n' % len(BonesWithoutAnimation), ', '.join(BonesWithoutAnimation))
    #==============================================================================================
    # Animations (AniminfoBinary)
    #==============================================================================================
    read_chunk()

    Raw_Key_Nums = 0
    Action_List = [None] * chunk_datacount

    for counter in range(chunk_datacount):
        (action_name_raw,        #0
         group_name_raw,         #1
         Totalbones,             #2
         RootInclude,            #3
         KeyCompressionStyle,    #4
         KeyQuotum,              #5
         KeyReduction,           #6
         TrackTime,              #7
         AnimRate,               #8
         StartBone,              #9
         FirstRawFrame,          #10
         NumRawFrames            #11
        ) = unpack_from('64s64s4i3f3i', chunk_data, chunk_datasize * counter)

        action_name = util_bytes_to_str( action_name_raw )
        group_name = util_bytes_to_str( group_name_raw  )

        Raw_Key_Nums += Totalbones * NumRawFrames
        Action_List[counter] = ( action_name, group_name, Totalbones, NumRawFrames)

    #==============================================================================================
    # Raw keys (VQuatAnimKey) 3f vec, 4f quat, 1f time
    #==============================================================================================
    read_chunk()

    if(Raw_Key_Nums != chunk_datacount):
        error_callback(
                'Raw_Key_Nums Inconsistent.'
                '\nData count found: '+chunk_datacount+
                '\nRaw_Key_Nums:' + Raw_Key_Nums
                )
        return False

    Raw_Key_List = [None] * chunk_datacount

    unpack_data = Struct('3f4f4x').unpack_from

    for counter in range(chunk_datacount):
        pos = Vector()
        quat = Quaternion()

        ( pos.x,  pos.y,  pos.z,
         quat.x, quat.y, quat.z, quat.w
        ) = unpack_data( chunk_data, chunk_datasize * counter)

        Raw_Key_List[counter] = (pos, quat)

    psafile.close()

    utils_set_mode('OBJECT')

    # index of current frame in raw input data
    raw_key_index = 0

    util_obj_set_active(context, armature_obj)

    gen_name_part = util_gen_name_part(filepath)

    armature_obj.animation_data_create()

    if bActionsToTrack:
        nla_track = armature_obj.animation_data.nla_tracks.new()
        nla_track.name = gen_name_part
        nla_stripes = nla_track.strips
        nla_track_last_frame = 0
    else:
        is_first_action = True
        first_action = None

    for counter, (Name, Group, Totalbones, NumRawFrames) in enumerate(Action_List):
        ref_time = time.process_time()

        if Group != 'None':
            Name = "(%s) %s" % (Group,Name)
        if bFilenameAsPrefix:
            Name = "(%s) %s" % (gen_name_part, Name)

        action = bpy.data.actions.new(name = Name)

        # force print usefull information to console(due to possible long execution)
        print("Action {0:>3d}/{1:<3d} frames: {2:>4d} {3}".format(
                counter+1, len(Action_List), NumRawFrames, Name)
              )

        if first_frames > 0:
            maxframes = first_frames
            keyframes = min(first_frames, NumRawFrames)
            #dev
            # keyframes += 1
        else:
            maxframes = 99999999
            keyframes = NumRawFrames

        # create all fcurves(for all bones) for an action
        # for pose_bone in armature_obj.pose.bones:
        for psa_bone in PsaBonesToProcess:
            if psa_bone is None:
                continue
            pose_bone = psa_bone.pose_bone

            data_path = pose_bone.path_from_id("rotation_quaternion")
            psa_bone.fcurve_quat_w = action.fcurves.new(data_path, index = 0)
            psa_bone.fcurve_quat_x = action.fcurves.new(data_path, index = 1)
            psa_bone.fcurve_quat_y = action.fcurves.new(data_path, index = 2)
            psa_bone.fcurve_quat_z = action.fcurves.new(data_path, index = 3)

            data_path = pose_bone.path_from_id("location")
            psa_bone.fcurve_loc_x = action.fcurves.new(data_path, index = 0)
            psa_bone.fcurve_loc_y = action.fcurves.new(data_path, index = 1)
            psa_bone.fcurve_loc_z = action.fcurves.new(data_path, index = 2)

            # 1. Pre-add keyframes! \0/
            # 2. Set data: keyframe_points[].co[0..1]
            # 3. If 2 is not done, do 4: (important!!!)
            # 4. "Apply" data: fcurve.update()
            # added keyframes points by default is breaking fcurve somehow
            # bcs they are all at the same position?
            psa_bone.fcurve_quat_w.keyframe_points.add(keyframes)
            psa_bone.fcurve_quat_x.keyframe_points.add(keyframes)
            psa_bone.fcurve_quat_y.keyframe_points.add(keyframes)
            psa_bone.fcurve_quat_z.keyframe_points.add(keyframes)

            psa_bone.fcurve_loc_x.keyframe_points.add(keyframes)
            psa_bone.fcurve_loc_y.keyframe_points.add(keyframes)
            psa_bone.fcurve_loc_z.keyframe_points.add(keyframes)

        for i in range(0,min(maxframes, NumRawFrames)):
            # raw_key_index+= Totalbones * 5 #55
            for j in range(Totalbones):
                if j in BoneNotFoundList:
                    raw_key_index += 1
                    continue

                psa_bone = PsaBonesToProcess[j]
                pose_bone = psa_bone.pose_bone

                p_pos = Raw_Key_List[raw_key_index][0]
                p_quat = Raw_Key_List[raw_key_index][1]

                ##### Worked with no bone rotation
                # quat = p_quat.conjugated() * psa_bone.orig_quat
                # loc = p_pos - psa_bone.orig_loc
                #####

                if psa_bone.parent:
                    ##### Correct
                    # orig_prot = pose_bone.bone.parent.matrix_local.to_3x3().to_quaternion()
                    # orig_rot = pose_bone.bone.matrix_local.to_3x3().to_quaternion()
                    # orig_rot = (orig_prot.conjugated() * orig_rot)
                    ######

                    #### FINAL
                    quat = (p_quat * psa_bone.post_quat).conjugated() * (psa_bone.orig_quat * psa_bone.post_quat)
                    # loc = psa_bone.post_quat.conjugated() * p_pos -  psa_bone.post_quat.conjugated() * psa_bone.orig_loc
                    ####
                else:
                    if bDontInvertRoot:
                        quat = (p_quat.conjugated() * psa_bone.post_quat).conjugated() * (psa_bone.orig_quat * psa_bone.post_quat)
                    else:
                        quat = (p_quat * psa_bone.post_quat).conjugated() * (psa_bone.orig_quat * psa_bone.post_quat)

                loc = psa_bone.post_quat.conjugated() * p_pos -  psa_bone.post_quat.conjugated() * psa_bone.orig_loc

                pose_bone.rotation_quaternion = quat
                pose_bone.location = loc
                    # pose_bone.rotation_quaternion = orig_rot.conjugated()
                    # pose_bone.location = p_pos - (pose_bone.bone.matrix_local.translation - pose_bone.bone.parent.matrix_local.translation)

                ##### Works + post_quat (without location works)
                # quat = (p_quat * psa_bone.post_quat).conjugated() * (psa_bone.orig_quat * psa_bone.post_quat)
                # loc = psa_bone.post_quat.conjugated() * (p_pos - psa_bone.orig_loc)

                psa_bone.fcurve_quat_w.keyframe_points[i].co = i, quat.w
                psa_bone.fcurve_quat_x.keyframe_points[i].co = i, quat.x
                psa_bone.fcurve_quat_y.keyframe_points[i].co = i, quat.y
                psa_bone.fcurve_quat_z.keyframe_points[i].co = i, quat.z

                psa_bone.fcurve_quat_w.keyframe_points[i].interpolation = fcurve_interpolation
                psa_bone.fcurve_quat_x.keyframe_points[i].interpolation = fcurve_interpolation
                psa_bone.fcurve_quat_y.keyframe_points[i].interpolation = fcurve_interpolation
                psa_bone.fcurve_quat_z.keyframe_points[i].interpolation = fcurve_interpolation

                psa_bone.fcurve_loc_x.keyframe_points[i].co = i, loc.x
                psa_bone.fcurve_loc_y.keyframe_points[i].co = i, loc.y
                psa_bone.fcurve_loc_z.keyframe_points[i].co = i, loc.z

                psa_bone.fcurve_loc_x.keyframe_points[i].interpolation = fcurve_interpolation
                psa_bone.fcurve_loc_y.keyframe_points[i].interpolation = fcurve_interpolation
                psa_bone.fcurve_loc_z.keyframe_points[i].interpolation = fcurve_interpolation

                # Old path. Slower.
                # psa_bone.fcurve_quat_w.keyframe_points.insert(i,quat.w,{'NEEDED','FAST'}).interpolation = fcurve_interpolation
                # psa_bone.fcurve_quat_x.keyframe_points.insert(i,quat.x,{'NEEDED','FAST'}).interpolation = fcurve_interpolation
                # psa_bone.fcurve_quat_y.keyframe_points.insert(i,quat.y,{'NEEDED','FAST'}).interpolation = fcurve_interpolation
                # psa_bone.fcurve_quat_z.keyframe_points.insert(i,quat.z,{'NEEDED','FAST'}).interpolation = fcurve_interpolation

                # psa_bone.fcurve_loc_x.keyframe_points.insert(i,loc.x,{'NEEDED','FAST'}).interpolation = fcurve_interpolation
                # psa_bone.fcurve_loc_y.keyframe_points.insert(i,loc.y,{'NEEDED','FAST'}).interpolation = fcurve_interpolation
                # psa_bone.fcurve_loc_z.keyframe_points.insert(i,loc.z,{'NEEDED','FAST'}).interpolation = fcurve_interpolation
                raw_key_index += 1

            # on first frame
            # break
        raw_key_index += (NumRawFrames-min(maxframes,NumRawFrames)) * Totalbones

        # Add action to tail of the nla track
        if bActionsToTrack:
            if nla_track_last_frame == 0:
                nla_stripes.new(Name, 0, action)
            else:
                nla_stripes.new(Name, nla_stripes[-1].frame_end, action)

            nla_track_last_frame += NumRawFrames
        elif is_first_action:
            first_action = action
            is_first_action = False

        print("Done: %f sec." % (time.process_time() - ref_time))
        # break on first animation set
        # break

    scene = util_get_scene(context)

    if not bActionsToTrack:
        if not scene.is_nla_tweakmode:
            armature_obj.animation_data.action = first_action

    if bUpdateTimelineRange:

        scene.frame_start = 0

        if bActionsToTrack:
            scene.frame_end = sum(frames for _, _, _, frames in Action_List) - 1
        else:
            scene.frame_end = max(frames for _, _, _, frames in Action_List) - 1

    util_select_all(False)
    util_obj_select(context, armature_obj)
    util_obj_set_active(context, armature_obj)

    # 2.8 crashes
    if not is_blen_280:
        scene.frame_set(0)


"File %s is not a %s file. (header mismach)\nExpected: %s \nPresent %s"


def util_gen_name_part ( filepath ) : return re . match ( r'.*[/\\]([^/\\]+?)(\..{2,5})?$' , filepath ) . group ( 1 )


def vec_to_axis_vec ( vec_in , vec_out ) : x , y , z = vec_in if abs ( x ) > abs ( y ) : if abs ( x ) > abs ( z ) : vec_out . x = 1 if x >= 0 else - 1 else : vec_out . z = 1 if z >= 0 else - 1 else : if abs ( y ) > abs ( z ) : vec_out . y = 1 if y >= 0 else - 1 else : vec_out . z = 1 if z >= 0 else - 1


Convert from linear to sRGB color space . Source : Cycles addon implementation node_color . h .

def color_linear_to_srgb(c):
    """
    Convert from linear to sRGB color space.
    Source: Cycles addon implementation, node_color.h.
    """
    if c < 0.0031308:
        return 0.0 if c < 0.0 else c * 12.92
    else:
        return 1.055 * pow(c, 1.0 / 2.4) - 0.055


Import mesh and skeleton from . psk / . pskx files Args : bReorientBones : Axis based bone orientation to children error_callback : Called when importing is failed . error_callback = lambda msg : print ( reason : msg )

def pskimport(filepath,
        context = None,
        bImportmesh = True,
        bImportbone = True,
        bSpltiUVdata = False,
        fBonesize = 5.0,
        fBonesizeRatio = 0.6,
        bDontInvertRoot = True,
        bReorientBones = False,
        bReorientDirectly = False,
        bScaleDown = True,
        bToSRGB = True,
        error_callback = None):
    '''
    Import mesh and skeleton from .psk/.pskx files

    Args:
        bReorientBones:
            Axis based bone orientation to children

        error_callback:
            Called when importing is failed.

            error_callback = lambda msg: print('reason:', msg)

    '''
    if not hasattr( error_callback, '__call__'):
        # error_callback = __pass
        error_callback = print

    # ref_time = time.process_time()
    if not bImportbone and not bImportmesh:
        error_callback("Nothing to do.\nSet something for import.")
        return False

    print ("-----------------------------------------------")
    print ("---------EXECUTING PSK PYTHON IMPORTER---------")
    print ("-----------------------------------------------")

    #file may not exist
    try:
        file = open(filepath,'rb')
    except IOError:
        error_callback('Error while opening file for reading:\n  "'+filepath+'"')
        return False

    if not util_check_file_header(file, 'psk'):
        error_callback('Not psk file:\n  "'+filepath+'"')
        return False

    Vertices = None
    Wedges = None
    Faces = None
    UV_by_face = None
    Materials = None
    Bones = None
    Weights = None
    VertexColors = None
    Extrauvs = []
    Normals = None
    WedgeIdx_by_faceIdx = None

    if not context:
        context = bpy.context
    #==================================================================================================
    # Materials   MaterialNameRaw | TextureIndex | PolyFlags | AuxMaterial | AuxFlags |  LodBias | LodStyle
    # Only Name is usable.
    def read_materials():

        nonlocal Materials

        Materials = []

        for counter in range(chunk_datacount):

            (MaterialNameRaw,) = unpack_from('64s24x', chunk_data, chunk_datasize * counter)

            Materials.append( util_bytes_to_str( MaterialNameRaw ) )

    #==================================================================================================
    # Faces WdgIdx1 | WdgIdx2 | WdgIdx3 | MatIdx | AuxMatIdx | SmthGrp
    def read_faces():

        if not bImportmesh:
            return True

        nonlocal Faces, UV_by_face, WedgeIdx_by_faceIdx

        UV_by_face = [None] * chunk_datacount
        Faces = [None] * chunk_datacount
        WedgeIdx_by_faceIdx = [None] * chunk_datacount

        if len(Wedges) > 65536:
            unpack_format = '=IIIBBI'
        else:
            unpack_format = '=HHHBBI'

        unpack_data = Struct(unpack_format).unpack_from

        for counter in range(chunk_datacount):
            (WdgIdx1, WdgIdx2, WdgIdx3,
             MatIndex,
             AuxMatIndex, #unused
             SmoothingGroup # Umodel is not exporting SmoothingGroups
             ) = unpack_data(chunk_data, counter * chunk_datasize)

            # looks ugly
            # Wedges is (point_index, u, v, MatIdx)
            ((vertid0, u0, v0, matid0), (vertid1, u1, v1, matid1), (vertid2, u2, v2, matid2)) = Wedges[WdgIdx1], Wedges[WdgIdx2], Wedges[WdgIdx3]

            # note order: C,B,A
            # Faces[counter] = (vertid2,  vertid1, vertid0)

            Faces[counter] = (vertid1,  vertid0, vertid2)
            # Faces[counter] = (vertid1,  vertid2, vertid0)
            # Faces[counter] = (vertid0,  vertid1, vertid2)

            # uv = ( ( u2, 1.0 - v2 ), ( u1, 1.0 - v1 ), ( u0, 1.0 - v0 ) )
            uv = ( ( u1, 1.0 - v1 ), ( u0, 1.0 - v0 ), ( u2, 1.0 - v2 ) )

            # Mapping: FaceIndex <=> UV data <=> FaceMatIndex
            UV_by_face[counter] = (uv, MatIndex, (matid2, matid1, matid0))

            # We need this for EXTRA UVs
            WedgeIdx_by_faceIdx[counter] = (WdgIdx3, WdgIdx2, WdgIdx1)

    #==================================================================================================
    # Vertices X | Y | Z
    def read_vertices():

        if not bImportmesh:
            return True

        nonlocal Vertices

        Vertices = [None] * chunk_datacount

        unpack_data = Struct('3f').unpack_from

        if bScaleDown:
            for counter in range( chunk_datacount ):
                (vec_x, vec_y, vec_z) = unpack_data(chunk_data, counter * chunk_datasize)
                Vertices[counter]  = (vec_x*0.01, vec_y*0.01, vec_z*0.01)
                # equal to gltf
                # Vertices[counter]  = (vec_x*0.01, vec_z*0.01, -vec_y*0.01)
        else:
            for counter in range( chunk_datacount ):
                Vertices[counter]  =  unpack_data(chunk_data, counter * chunk_datasize)

    #==================================================================================================
    # Wedges (UV)   VertexId |  U |  V | MatIdx
    def read_wedges():

        if not bImportmesh:
            return True

        nonlocal Wedges

        Wedges = [None] * chunk_datacount

        unpack_data = Struct('=IffBxxx').unpack_from

        for counter in range( chunk_datacount ):
            (vertex_id,
             u, v,
             material_index) = unpack_data( chunk_data, counter * chunk_datasize )

            # print(vertex_id, u, v, material_index)
            # Wedges[counter] = (vertex_id, u, v, material_index)
            Wedges[counter] = [vertex_id, u, v, material_index]

    #==================================================================================================
    # Bones (VBone .. VJointPos ) Name|Flgs|NumChld|PrntIdx|Qw|Qx|Qy|Qz|LocX|LocY|LocZ|Lngth|XSize|YSize|ZSize
    def read_bones():

        nonlocal Bones, bImportbone

        if chunk_datacount == 0:
            bImportbone = False

        if bImportbone:
            # unpack_data = Struct('64s3i11f').unpack_from
            unpack_data = Struct('64s3i7f16x').unpack_from
        else:
            unpack_data = Struct('64s56x').unpack_from

        Bones = [None] * chunk_datacount

        for counter in range( chunk_datacount ):
            Bones[counter] = unpack_data( chunk_data, chunk_datasize * counter)

    #==================================================================================================
    # Influences (Bone Weight) (VRawBoneInfluence) ( Weight | PntIdx | BoneIdx)
    def read_weights():

        nonlocal Weights

        if not bImportmesh:
            return True

        Weights = [None] * chunk_datacount

        unpack_data = Struct('fii').unpack_from

        for counter in range(chunk_datacount):
            Weights[counter] = unpack_data(chunk_data, chunk_datasize * counter)

    #==================================================================================================
    # Vertex colors. R G B A bytes. NOTE: it is Wedge color.(uses Wedges index)
    def read_vertex_colors():

        nonlocal VertexColors

        unpack_data = Struct("=4B").unpack_from

        VertexColors = [None] * chunk_datacount

        for counter in range( chunk_datacount ):
            VertexColors[counter] = unpack_data(chunk_data, chunk_datasize * counter)

    #==================================================================================================
    # Extra UV. U | V
    def read_extrauvs():

        unpack_data = Struct("=2f").unpack_from

        uvdata = [None] * chunk_datacount

        for counter in range( chunk_datacount ):
            uvdata[counter] = unpack_data(chunk_data, chunk_datasize * counter)

        Extrauvs.append(uvdata)

    #==================================================================================================
    # Vertex Normals NX | NY | NZ
    def read_normals():
        if not bImportmesh:
            return True

        nonlocal Normals
        Normals = [None] * chunk_datacount

        unpack_data = Struct('3f').unpack_from

        for counter in range(chunk_datacount):
            Normals[counter] = unpack_data(chunk_data, counter * chunk_datasize)

    CHUNKS_HANDLERS = {
        'PNTS0000': read_vertices,
        'VTXW0000': read_wedges,
        'VTXW3200': read_wedges,#?
        'FACE0000': read_faces,
        'FACE3200': read_faces,
        'MATT0000': read_materials,
        'REFSKELT': read_bones,
        'REFSKEL0': read_bones, #?
        'RAWW0000': read_weights,
        'RAWWEIGH': read_weights,
        'VERTEXCO': read_vertex_colors, # VERTEXCOLOR
        'EXTRAUVS': read_extrauvs,
        'VTXNORMS': read_normals
    }

    #===================================================================================================
    # File. Read all needed data.
    #         VChunkHeader Struct
    # ChunkID|TypeFlag|DataSize|DataCount
    # 0      |1       |2       |3

    while True:

        header_bytes = file.read(32)

        if len(header_bytes) < 32:

            if len(header_bytes) != 0:
                error_callback("Unexpected end of file.(%s/32 bytes)" % len(header_bytes))
            break

        (chunk_id, chunk_type, chunk_datasize, chunk_datacount) = unpack('20s3i', header_bytes)

        chunk_id_str = util_bytes_to_str(chunk_id)
        chunk_id_str = chunk_id_str[:8]

        if chunk_id_str in CHUNKS_HANDLERS:

            chunk_data = file.read( chunk_datasize * chunk_datacount)

            if len(chunk_data) < chunk_datasize * chunk_datacount:
                error_callback('Psk chunk %s is broken.' % chunk_id_str)
                return False

            CHUNKS_HANDLERS[chunk_id_str]()

        else:

            print('Unknown chunk: ', chunk_id_str)
            file.seek(chunk_datasize * chunk_datacount, 1)

        # print(chunk_id_str, chunk_datacount)

    file.close()

    print(" Importing file:", filepath)

    if not bImportmesh and (Bones is None or len(Bones) == 0):
        error_callback("Psk: no skeleton data.")
        return False

    MAX_UVS = 8
    NAME_UV_PREFIX = "UV"

    # file name w/out extension
    gen_name_part = util_gen_name_part(filepath)
    gen_names = {
        'armature_object':  gen_name_part + '.ao',
        'armature_data':    gen_name_part + '.ad',
            'mesh_object':  gen_name_part + '.mo',
            'mesh_data':    gen_name_part + '.md'
    }

    if bImportmesh:
        mesh_data = bpy.data.meshes.new(gen_names['mesh_data'])
        mesh_obj = bpy.data.objects.new(gen_names['mesh_object'], mesh_data)

    #==================================================================================================
    # UV. Prepare
    if bImportmesh:
        if bSpltiUVdata:
        # store how much each "matrial index" have vertices

            uv_mat_ids = {}

            for (_, _, _, material_index) in Wedges:

                if not (material_index in uv_mat_ids):
                    uv_mat_ids[material_index] = 1
                else:
                    uv_mat_ids[material_index] += 1

            # if we have more UV material indexes than blender UV maps, then...
            if bSpltiUVdata and len(uv_mat_ids) > MAX_UVS :

                uv_mat_ids_len = len(uv_mat_ids)

                print('UVs: %s out of %s is combined in a first UV map(%s0)' % (uv_mat_ids_len - 8, uv_mat_ids_len, NAME_UV_PREFIX))

                mat_idx_proxy = [0] * len(uv_mat_ids)

                counts_sorted = sorted(uv_mat_ids.values(), reverse = True)

                new_mat_index = MAX_UVS - 1

                for c in counts_sorted:
                    for mat_idx, counts in uv_mat_ids.items():
                        if c == counts:
                            mat_idx_proxy[mat_idx] = new_mat_index
                            if new_mat_index > 0:
                                new_mat_index -= 1
                            # print('MatIdx remap: %s > %s' % (mat_idx,new_mat_index))

                for i in range(len(Wedges)):
                    Wedges[i][3] = mat_idx_proxy[Wedges[i][3]]

        # print('Wedges:', chunk_datacount)
        # print('uv_mat_ids', uv_mat_ids)
        # print('uv_mat_ids', uv_mat_ids)
        # for w in Wedges:

    if bImportmesh:
        # print("-- Materials -- (index, name, faces)")
        blen_materials = []
        for materialname in Materials:
            matdata = bpy.data.materials.get(materialname)

            if matdata is None:
                matdata = bpy.data.materials.new( materialname )
            # matdata = bpy.data.materials.new( materialname )

            blen_materials.append( matdata )
            mesh_data.materials.append( matdata )
            # print(counter,materialname,TextureIndex)
            # if mat_groups.get(counter) is not None:
                # print("%i: %s" % (counter, materialname), len(mat_groups[counter]))

    #==================================================================================================
    # Prepare bone data
    def init_psk_bone(i, psk_bones, name_raw):
        psk_bone = class_psk_bone()
        psk_bone.children = []
        psk_bone.name = util_bytes_to_str(name_raw)
        psk_bones[i] = psk_bone
        return psk_bone

    psk_bone_name_toolong = False

    # indexed by bone index. array of psk_bone
    psk_bones = [None] * len(Bones)

    if not bImportbone: #data needed for mesh-only import

        for counter,(name_raw,) in enumerate(Bones):
            init_psk_bone(counter, psk_bones, name_raw)

    if bImportbone:  #else?

        # average bone length
        sum_bone_pos = 0

        for counter, (name_raw, flags, NumChildren, ParentIndex, #0 1 2 3
             quat_x, quat_y, quat_z, quat_w,            #4 5 6 7
             vec_x, vec_y, vec_z
            #  ,                       #8 9 10
            #  joint_length,                              #11
            #  scale_x, scale_y, scale_z
             ) in enumerate(Bones):

            psk_bone = init_psk_bone(counter, psk_bones, name_raw)

            psk_bone.bone_index = counter
            psk_bone.parent_index = ParentIndex

            # Tested. 64 is getting cut to 63
            if len(psk_bone.name) > 63:
                psk_bone_name_toolong = True
                # print('Warning. Bone name is too long:', psk_bone.name)

            # make sure we have valid parent_index
            if psk_bone.parent_index < 0:
                psk_bone.parent_index = 0

            # psk_bone.scale = (scale_x, scale_y, scale_z)
            # print("%s: %03f %03f | %f" % (psk_bone.name, scale_x, scale_y, joint_length),scale_x)
            # print("%s:" % (psk_bone.name), vec_x, quat_x)

            # store bind pose to make it available for psa-import via CustomProperty of the Blender bone
            psk_bone.orig_quat = Quaternion((quat_w, quat_x, quat_y, quat_z))

            if bScaleDown:
                psk_bone.orig_loc = Vector((vec_x * 0.01, vec_y * 0.01, vec_z * 0.01))
            else:
                psk_bone.orig_loc = Vector((vec_x, vec_y, vec_z))

            # root bone must have parent_index = 0 and selfindex = 0
            if psk_bone.parent_index == 0 and psk_bone.bone_index == psk_bone.parent_index:
                if bDontInvertRoot:
                    psk_bone.mat_world_rot = psk_bone.orig_quat.to_matrix()
                else:
                    psk_bone.mat_world_rot = psk_bone.orig_quat.conjugated().to_matrix()
                psk_bone.mat_world = Matrix.Translation(psk_bone.orig_loc)

            sum_bone_pos += psk_bone.orig_loc.length

    #==================================================================================================
    # Bones. Calc World-space matrix

        # TODO optimize math.
        for psk_bone in psk_bones:

            if psk_bone.parent_index == 0:
                if psk_bone.bone_index == 0:
                    psk_bone.parent = None
                    continue

            parent = psk_bones[psk_bone.parent_index]

            psk_bone.parent = parent

            parent.children.append(psk_bone)

            # mat_world -     world space bone matrix WITHOUT own rotation
            # mat_world_rot - world space bone rotation WITH own rotation

            # psk_bone.mat_world = parent.mat_world_rot.to_4x4()
            # psk_bone.mat_world.translation = parent.mat_world.translation + parent.mat_world_rot * psk_bone.orig_loc
            # psk_bone.mat_world_rot = parent.mat_world_rot * psk_bone.orig_quat.conjugated().to_matrix()

            psk_bone.mat_world = parent.mat_world_rot.to_4x4()

            v = psk_bone.orig_loc.copy()
            v.rotate( parent.mat_world_rot )
            psk_bone.mat_world.translation = parent.mat_world.translation + v

            psk_bone.mat_world_rot = psk_bone.orig_quat.conjugated().to_matrix()
            psk_bone.mat_world_rot.rotate( parent.mat_world_rot )

            # psk_bone.mat_world =  ( parent.mat_world_rot.to_4x4() * psk_bone.trans)
            # psk_bone.mat_world.translation += parent.mat_world.translation
            # psk_bone.mat_world_rot = parent.mat_world_rot * psk_bone.orig_quat.conjugated().to_matrix()

    #==================================================================================================
    # Skeleton. Prepare.

        armature_data = bpy.data.armatures.new(gen_names['armature_data'])
        armature_obj = bpy.data.objects.new(gen_names['armature_object'], armature_data)
        # TODO: options for axes and x_ray?
        armature_data.show_axes = False

        armature_data.display_type = 'STICK'
        armature_obj.show_in_front = True

        util_obj_link(context, armature_obj)

        util_select_all(False)
        util_obj_select(context, armature_obj)
        util_obj_set_active(context, armature_obj)

        utils_set_mode('EDIT')

        sum_bone_pos /= len(Bones) # average
        sum_bone_pos *= fBonesizeRatio # corrected

        # bone_size_choosen = max(0.01, round((min(sum_bone_pos, fBonesize))))
        bone_size_choosen = max(0.01, round((min(sum_bone_pos, fBonesize))*100)/100)
        # bone_size_choosen = max(0.01, min(sum_bone_pos, fBonesize))
        # print("Bonesize %f | old: %f round: %f" % (bone_size_choosen, max(0.01, min(sum_bone_pos, fBonesize)),max(0.01, round((min(sum_bone_pos, fBonesize))*100)/100)))

        if not bReorientBones:
            new_bone_size = bone_size_choosen

    #==================================================================================================
    # Skeleton. Build.
        if psk_bone_name_toolong:
            print('Warning. Some bones will be renamed(names are too long). Animation import may be broken.')
            for psk_bone in psk_bones:

                # TODO too long name cutting options?
                orig_long_name = psk_bone.name

                # Blender will cut the name here (>63 chars)
                edit_bone = armature_obj.data.edit_bones.new(psk_bone.name)
                edit_bone["orig_long_name"] = orig_long_name

                # if orig_long_name != edit_bone.name:
                #     print('--')
                #     print(len(orig_long_name),orig_long_name)
                #     print(len(edit_bone.name),edit_bone.name)

                # Use the bone name made by blender (.001 , .002 etc.)
                psk_bone.name = edit_bone.name

        else:
            for psk_bone in psk_bones:
                edit_bone = armature_obj.data.edit_bones.new(psk_bone.name)
                psk_bone.name = edit_bone.name

        for psk_bone in psk_bones:
            edit_bone = armature_obj.data.edit_bones[psk_bone.name]

            armature_obj.data.edit_bones.active = edit_bone

            if psk_bone.parent is not None:
                edit_bone.parent = armature_obj.data.edit_bones[psk_bone.parent.name]
            else:
                if bDontInvertRoot:
                    psk_bone.orig_quat.conjugate()

            if bReorientBones:
                (new_bone_size, quat_orient_diff) = calc_bone_rotation(psk_bone, bone_size_choosen, bReorientDirectly, sum_bone_pos)
                # @
                # post_quat = psk_bone.orig_quat.conjugated() * quat_orient_diff

                post_quat = quat_orient_diff
                post_quat.rotate( psk_bone.orig_quat.conjugated() )
            else:
                post_quat = psk_bone.orig_quat.conjugated()

            # only length of this vector is matter?
            edit_bone.tail = Vector(( 0.0, new_bone_size, 0.0))

            # @
            # edit_bone.matrix = psk_bone.mat_world * post_quat.to_matrix().to_4x4()

            m = post_quat.copy()
            m.rotate( psk_bone.mat_world )

            m = m.to_matrix().to_4x4()
            m.translation = psk_bone.mat_world.translation

            edit_bone.matrix = m

            # some dev code...
            #### FINAL
            # post_quat = psk_bone.orig_quat.conjugated() * quat_diff
            # edit_bone.matrix = psk_bone.mat_world * test_quat.to_matrix().to_4x4()
            # edit_bone["post_quat"] = test_quat
            ####

            # edit_bone["post_quat"] = Quaternion((1,0,0,0))
            # edit_bone.matrix = psk_bone.mat_world* psk_bone.rot

            # if edit_bone.parent:
              # edit_bone.matrix = edit_bone.parent.matrix * psk_bone.trans * (psk_bone.orig_quat.conjugated().to_matrix().to_4x4())
              # edit_bone.matrix = edit_bone.parent.matrix * psk_bone.trans * (test_quat.to_matrix().to_4x4())
            # else:
              # edit_bone.matrix = psk_bone.orig_quat.to_matrix().to_4x4()

            # save bindPose information for .psa import
            # dev
            edit_bone["orig_quat"] = psk_bone.orig_quat
            edit_bone["orig_loc"]  = psk_bone.orig_loc
            edit_bone["post_quat"] = post_quat

            '''
            bone = edit_bone
            if psk_bone.parent is not None:
                orig_loc  =  bone.matrix.translation - bone.parent.matrix.translation
                orig_loc.rotate( bone.parent.matrix.to_quaternion().conjugated() )

                orig_quat = bone.matrix.to_quaternion()
                orig_quat.rotate( bone.parent.matrix.to_quaternion().conjugated()  )
                orig_quat.conjugate()

                if orig_quat.dot( psk_bone.orig_quat ) < 0.95:
                    print(bone.name, psk_bone.orig_quat, orig_quat, orig_quat.dot( psk_bone.orig_quat ))
                    print('parent:', bone.parent.matrix.to_quaternion(), bone.parent.matrix.to_quaternion().rotation_difference(bone.matrix.to_quaternion()) )

                if (psk_bone.orig_loc - orig_loc).length > 0.02:
                    print(bone.name, psk_bone.orig_loc, orig_loc, (psk_bone.orig_loc - orig_loc).length)
            '''
    utils_set_mode('OBJECT')

    #==================================================================================================
    # Weights
    if bImportmesh:

        vertices_total = len(Vertices)

        for ( _, PointIndex, BoneIndex ) in Weights:
            if PointIndex < vertices_total: # can it be not?
                psk_bones[BoneIndex].have_weight_data = True
            # else:
                # print(psk_bones[BoneIndex].name, 'for other mesh',PointIndex ,vertices_total)

            #print("weight:", PointIndex, BoneIndex, Weight)
        # Weights.append(None)
        # print(Weights.count(None))

    # Original vertex colorization code
    '''
    # Weights.sort( key = lambda wgh: wgh[0])
    if bImportmesh:
        VtxCol = []
        bones_count = len(psk_bones)
        for x in range(bones_count):
            #change the overall darkness of each material in a range between 0.1 and 0.9
            tmpVal = ((float(x) + 1.0) / bones_count * 0.7) + 0.1
            tmpVal = int(tmpVal * 256)
            tmpCol = [tmpVal, tmpVal, tmpVal, 0]
            #Change the color of each material slightly
            if x % 3 == 0:
                if tmpCol[0] < 128:
                    tmpCol[0] += 60
                else:
                    tmpCol[0] -= 60
            if x % 3 == 1:
                if tmpCol[1] < 128:
                    tmpCol[1] += 60
                else:
                    tmpCol[1] -= 60
            if x % 3 == 2:
                if tmpCol[2] < 128:
                    tmpCol[2] += 60
                else:
                    tmpCol[2] -= 60
            #Add the material to the mesh
            VtxCol.append(tmpCol)

    for x in range(len(Tmsh.faces)):
        for y in range(len(Tmsh.faces[x].v)):
            #find v in Weights[n][0]
            findVal = Tmsh.faces[x].v[y].index
            n = 0
            while findVal != Weights[n][0]:
                n = n + 1
            TmpCol = VtxCol[Weights[n][1]]
            #check if a vertex has more than one influence
            if n != len(Weights) - 1:
                if Weights[n][0] == Weights[n + 1][0]:
                    #if there is more than one influence, use the one with the greater influence
                    #for simplicity only 2 influences are checked, 2nd and 3rd influences are usually very small
                    if Weights[n][2] < Weights[n + 1][2]:
                        TmpCol = VtxCol[Weights[n + 1][1]]
        Tmsh.faces[x].col.append(NMesh.Col(TmpCol[0], TmpCol[1], TmpCol[2], 0))
    '''

    #===================================================================================================
    # UV. Setup.

    if bImportmesh:
        # Trick! Create UV maps BEFORE mesh and get (0,0) coordinates for free!
        #   ...otherwise UV coords will be copied from active, or calculated from mesh...

        if bSpltiUVdata:

            for i in range(len(uv_mat_ids)):
                get_uv_layers(mesh_data).new(name = NAME_UV_PREFIX + str(i))

        else:

            get_uv_layers(mesh_data).new(name = NAME_UV_PREFIX+"_SINGLE")

        for counter, uv_data in enumerate(Extrauvs):

            if len(mesh_data.uv_layers) < MAX_UVS:

                get_uv_layers(mesh_data).new(name = "EXTRAUVS"+str(counter))

            else:

                Extrauvs.remove(uv_data)
                print('Extra UV layer %s is ignored. Re-import without "Split UV data".' % counter)

    #==================================================================================================
    # Mesh. Build.

        mesh_data.from_pydata(Vertices,[],Faces)

    #==================================================================================================
    # Vertex Normal. Set.

        if Normals is not None:
            mesh_data.polygons.foreach_set("use_smooth", [True] * len(mesh_data.polygons))
            mesh_data.normals_split_custom_set_from_vertices(Normals)
            mesh_data.use_auto_smooth = True

    #===================================================================================================
    # UV. Set.

    if bImportmesh:

        for face in mesh_data.polygons:
            face.material_index = UV_by_face[face.index][1]

        uv_layers = mesh_data.uv_layers

        if not bSpltiUVdata:
           uvLayer = uv_layers[0]

        # per face
        # for faceIdx, (faceUVs, faceMatIdx, _, _, wmidx) in enumerate(UV_by_face):
        for faceIdx, (faceUVs, faceMatIdx, WedgeMatIds) in enumerate(UV_by_face):

            # per vertex
            for vertN, uv in enumerate(faceUVs):
                loopId = faceIdx * 3 + vertN

                if bSpltiUVdata:
                    uvLayer = uv_layers[WedgeMatIds[vertN]]

                uvLayer.data[loopId].uv = uv

    #==================================================================================================
    # VertexColors

        if VertexColors is not None:

            vtx_color_layer = mesh_data.vertex_colors.new(name = "PSKVTXCOL_0", do_init = False)

            pervertex = [None] * len(Vertices)

            for counter, (vertexid,_,_,_) in enumerate(Wedges):

                # Is it possible ?
                if (pervertex[vertexid] is not None) and (pervertex[vertexid] != VertexColors[counter]):
                    print('Not equal vertex colors. ', vertexid, pervertex[vertexid], VertexColors[counter])

                pervertex[vertexid] = VertexColors[counter]

            for counter, loop in enumerate(mesh_data.loops):

                color = pervertex[ loop.vertex_index ]

                if color is None:
                    vtx_color_layer.data[ counter ].color = (1.,1.,1.,1.)
                else:
                    if bToSRGB:
                        vtx_color_layer.data[ counter ].color = (
                            color_linear_to_srgb(color[0] / 255),
                            color_linear_to_srgb(color[1] / 255),
                            color_linear_to_srgb(color[2] / 255),
                            color[3] / 255
                        )
                    else:
                        vtx_color_layer.data[ counter ].color = (
                            color[0] / 255,
                            color[1] / 255,
                            color[2] / 255,
                            color[3] / 255
                        )

    #===================================================================================================
    # Extra UVs. Set.

        # for counter, uv_data in enumerate(Extrauvs):

        #     uvLayer = mesh_data.uv_layers[ counter - len(Extrauvs) ]

        #     for uv_index, uv_coords in enumerate(uv_data):

        #         uvLayer.data[uv_index].uv = (uv_coords[0], 1.0 - uv_coords[1])

        for counter, uv_data in enumerate(Extrauvs):

            uvLayer = mesh_data.uv_layers[ counter - len(Extrauvs) ]

            for faceIdx, (WedgeIdx3,WedgeIdx2,WedgeIdx1) in enumerate(WedgeIdx_by_faceIdx):

                # equal to gltf
                uvLayer.data[faceIdx*3  ].uv = (uv_data[WedgeIdx2][0], 1.0 - uv_data[WedgeIdx2][1])
                uvLayer.data[faceIdx*3+1].uv = (uv_data[WedgeIdx1][0], 1.0 - uv_data[WedgeIdx1][1])
                uvLayer.data[faceIdx*3+2].uv = (uv_data[WedgeIdx3][0], 1.0 - uv_data[WedgeIdx3][1])
                # uvLayer.data[faceIdx*3  ].uv = (uv_data[WedgeIdx3][0], 1.0 - uv_data[WedgeIdx3][1])
                # uvLayer.data[faceIdx*3+1].uv = (uv_data[WedgeIdx2][0], 1.0 - uv_data[WedgeIdx2][1])
                # uvLayer.data[faceIdx*3+2].uv = (uv_data[WedgeIdx1][0], 1.0 - uv_data[WedgeIdx1][1])

    #===================================================================================================
    # Mesh. Vertex Groups. Bone Weights.

        for psk_bone in psk_bones:
            if psk_bone.have_weight_data:
                psk_bone.vertex_group = mesh_obj.vertex_groups.new(name = psk_bone.name)
            # else:
                # print(psk_bone.name, 'have no influence on this mesh')

        for weight, vertex_id, bone_index_w in filter(None, Weights):
            psk_bones[bone_index_w].vertex_group.add((vertex_id,), weight, 'ADD')

    #===================================================================================================
    # Skeleton. Colorize.

    if bImportbone:

        bone_group_unused = armature_obj.pose.bone_groups.new(name = "Unused bones")
        bone_group_unused.color_set = 'THEME14'

        bone_group_nochild = armature_obj.pose.bone_groups.new(name = "No children")
        bone_group_nochild.color_set = 'THEME03'

        armature_data.show_group_colors = True

        for psk_bone in psk_bones:

            pose_bone = armature_obj.pose.bones[psk_bone.name]

            if psk_bone.have_weight_data:

                if len(psk_bone.children) == 0:
                    pose_bone.bone_group = bone_group_nochild

            else:
                pose_bone.bone_group = bone_group_unused

    #===================================================================================================
    # Final

    if bImportmesh:

        util_obj_link(context, mesh_obj)
        util_select_all(False)

        if not bImportbone:

            util_obj_select(context, mesh_obj)
            util_obj_set_active(context, mesh_obj)

        else:
            # select_all(False)
            util_obj_select(context, armature_obj)

            # parenting mesh to armature object
            mesh_obj.parent = armature_obj
            mesh_obj.parent_type = 'OBJECT'

            # add armature modifier
            blender_modifier = mesh_obj.modifiers.new( armature_obj.data.name, type = 'ARMATURE')
            blender_modifier.show_expanded = False
            blender_modifier.use_vertex_groups = True
            blender_modifier.use_bone_envelopes = False
            blender_modifier.object = armature_obj

            # utils_set_mode('OBJECT')
            # select_all(False)
            util_obj_select(context, armature_obj)
            util_obj_set_active(context, armature_obj)

    # print("Done: %f sec." % (time.process_time() - ref_time))
    utils_set_mode('OBJECT')
    return True

"Nothing to do.\nSet something for import."

'Error while opening file for reading:\n  "'

# Umodel is not exporting SmoothingGroups

# Faces[counter] = (vertid1,  vertid2, vertid0)

# Faces[counter] = (vertid0,  vertid1, vertid2)

# Wedges (UV)   VertexId |  U |  V | MatIdx

# print(vertex_id, u, v, material_index)

# unpack_data = Struct('64s3i11f').unpack_from

# if mat_groups.get(counter) is not None:

# indexed by bone index. array of psk_bone

#  joint_length,                              #11

# psk_bone.scale = (scale_x, scale_y, scale_z)

# Blender will cut the name here (>63 chars)

#     print(len(orig_long_name),orig_long_name)

#     print(len(edit_bone.name),edit_bone.name)

# save bindPose information for .psa import

# for counter, uv_data in enumerate(Extrauvs):


def psaimport(filepath,
        context = None,
        oArmature = None,
        bFilenameAsPrefix = False,
        bActionsToTrack = False,
        first_frames = 0,
        bDontInvertRoot = True,
        bUpdateTimelineRange = False,
        bRotationOnly = False,
        bScaleDown = True,
        fcurve_interpolation = 'LINEAR',
        # error_callback = __pass
        error_callback = print
        ):
    """Import animation data from 'filepath' using 'oArmature'

    Args:
        first_frames: (0 - import all)
            Import only 'first_frames' from each action

        bActionsToTrack:
            Put all imported actions in one NLAtrack.

        oArmature:
            Skeleton used to calculate keyframes
    """
    print ("-----------------------------------------------")
    print ("---------EXECUTING PSA PYTHON IMPORTER---------")
    print ("-----------------------------------------------")

    file_ext = 'psa'
    try:
        psafile = open(filepath, 'rb')
    except IOError:
        error_callback('Error while opening file for reading:\n  "'+filepath+'"')
        return False

    print ("Importing file: ", filepath)

    if not context:
        context = bpy.context

    armature_obj = oArmature

    if armature_obj is None:
        armature_obj = blen_get_armature_from_selection()
        if armature_obj is None:
            error_callback("No armature selected.")
            return False

    chunk_id = None
    chunk_type = None
    chunk_datasize = None
    chunk_datacount = None
    chunk_data = None

    def read_chunk():
        nonlocal chunk_id, chunk_type,\
                 chunk_datasize, chunk_datacount,\
                 chunk_data

        (chunk_id, chunk_type,
         chunk_datasize, chunk_datacount) = unpack('20s3i', psafile.read(32))

        chunk_data = psafile.read(chunk_datacount * chunk_datasize)
    #==============================================================================================
    # General Header
    #==============================================================================================
    read_chunk()

    if not util_is_header_valid(filepath, file_ext, chunk_id, error_callback):
        return False

    #==============================================================================================
    # Bones (FNamedBoneBinary)
    #==============================================================================================
    read_chunk()

    psa_bones = {}

    def new_psa_bone(bone, pose_bone):
        psa_bone = class_psa_bone()

        psa_bones[pose_bone.name] = psa_bone

        psa_bone.name = pose_bone.name

        psa_bone.pose_bone = pose_bone

        if bone.parent != None:
            # does needed parent bone was added from psa file
            if bone.parent.name in psa_bones:
                psa_bone.parent = psa_bones[bone.parent.name]
            # no. armature doesnt match
            else:
                psa_bone.parent = None
        # else:
            # psa_bone.parent = None

        # brute fix for non psk skeletons
        if bone.get('orig_quat') is None:

            if bone.parent != None:

                psa_bone.orig_loc  =  bone.matrix_local.translation - bone.parent.matrix_local.translation
                psa_bone.orig_loc.rotate( bone.parent.matrix_local.to_quaternion().conjugated() )

                psa_bone.orig_quat = bone.matrix_local.to_quaternion()
                psa_bone.orig_quat.rotate( bone.parent.matrix_local.to_quaternion().conjugated()  )
                psa_bone.orig_quat.conjugate()
            else:
                psa_bone.orig_loc  = bone.matrix_local.translation.copy()
                psa_bone.orig_quat = bone.matrix_local.to_quaternion()

            psa_bone.post_quat = psa_bone.orig_quat.conjugated()
        else:
            psa_bone.orig_quat = Quaternion(bone['orig_quat'])
            psa_bone.orig_loc  =     Vector(bone['orig_loc'])
            psa_bone.post_quat = Quaternion(bone['post_quat'])

        return psa_bone

    #Bones Data
    BoneIndex2Name = [None] * chunk_datacount
    BoneNotFoundList = []
    BonesWithoutAnimation = []
    PsaBonesToProcess = [None] * chunk_datacount
    BonePsaImportedNames = []

    # printlog("Name\tFlgs\tNumChld\tPrntIdx\tQx\tQy\tQz\tQw\tLocX\tLocY\tLocZ\tLength\tXSize\tYSize\tZSize\n")

    # for case insensetive comparison
    # key = lowered name
    # value = orignal name
    skeleton_bones_lowered = {}

    for blender_bone_name in armature_obj.data.bones.keys():
      skeleton_bones_lowered[blender_bone_name.lower()] = blender_bone_name

    for counter in range(chunk_datacount):

        # tPrntIdx is -1 for parent; and 0 for other; no more useful data
        # indata = unpack_from('64s3i11f', chunk_data, chunk_datasize * counter)
        (indata) = unpack_from('64s56x', chunk_data, chunk_datasize * counter)
        in_name = util_bytes_to_str(indata[0])
        # bonename = util_bytes_to_str(indata[0]).upper()

        in_name_lowered = in_name.lower()
        if in_name_lowered in skeleton_bones_lowered:
            orig_name = skeleton_bones_lowered[in_name_lowered]

            count_duplicates = BonePsaImportedNames.count( in_name_lowered )

            if count_duplicates > 0:

                duplicate_name_numbered = in_name_lowered + ('.%03d' % count_duplicates)

                # print('Dup:', in_name_lowered, '~',duplicate_name_numbered)

                # Skeleton have duplicate name too?
                if duplicate_name_numbered in skeleton_bones_lowered:
                    orig_name = orig_name + ('.%03d' % count_duplicates)
                else:
                    # Skip animation import for that bone
                    print(" PSK do not have numbered duplicate name(but PSA have!):", duplicate_name_numbered)
                    BonePsaImportedNames.append(in_name_lowered)
                    continue

            # use a skeleton bone name
            BoneIndex2Name[counter] = orig_name
            PsaBonesToProcess[counter] = new_psa_bone(armature_obj.data.bones[orig_name],
                                                    armature_obj.pose.bones[orig_name])
            BonePsaImportedNames.append(in_name_lowered)
        else:
            # print("Can't find the bone:", orig_name, in_name_lowered)
            BoneNotFoundList.append(counter)

    if len(psa_bones) == 0:
        error_callback('No bone was match!\nSkip import!')
        return False

    # does anyone care?
    for blender_bone_name in armature_obj.data.bones.keys():
        if BoneIndex2Name.count(blender_bone_name) == 0:
            BonesWithoutAnimation.append(blender_bone_name)

    if len(BoneNotFoundList) > 0:
        print('PSA have data for more bones: %i.' % len(BoneNotFoundList))

    if len(BonesWithoutAnimation) > 0:
        print('PSA do not have data for %i bones:\n' % len(BonesWithoutAnimation), ', '.join(BonesWithoutAnimation))
    #==============================================================================================
    # Animations (AniminfoBinary)
    #==============================================================================================
    read_chunk()

    Raw_Key_Nums = 0
    Action_List = [None] * chunk_datacount

    for counter in range(chunk_datacount):
        (action_name_raw,        #0
         group_name_raw,         #1
         Totalbones,             #2
         RootInclude,            #3
         KeyCompressionStyle,    #4
         KeyQuotum,              #5
         KeyReduction,           #6
         TrackTime,              #7
         AnimRate,               #8
         StartBone,              #9
         FirstRawFrame,          #10
         NumRawFrames            #11
        ) = unpack_from('64s64s4i3f3i', chunk_data, chunk_datasize * counter)

        action_name = util_bytes_to_str( action_name_raw )
        group_name = util_bytes_to_str( group_name_raw  )

        Raw_Key_Nums += Totalbones * NumRawFrames
        Action_List[counter] = ( action_name, group_name, Totalbones, NumRawFrames)

    #==============================================================================================
    # Raw keys (VQuatAnimKey) 3f vec, 4f quat, 1f time
    #==============================================================================================
    read_chunk()

    if(Raw_Key_Nums != chunk_datacount):
        error_callback(
                'Raw_Key_Nums Inconsistent.'
                '\nData count found: '+chunk_datacount+
                '\nRaw_Key_Nums:' + Raw_Key_Nums
                )
        return False

    Raw_Key_List = [None] * chunk_datacount

    unpack_data = Struct('3f4f4x').unpack_from

    for counter in range(chunk_datacount):
        pos = Vector()
        quat = Quaternion()

        ( pos.x,  pos.y,  pos.z,
         quat.x, quat.y, quat.z, quat.w
        ) = unpack_data( chunk_data, chunk_datasize * counter)

        if bScaleDown:
            Raw_Key_List[counter] = (pos * 0.01, quat)
        else:
            Raw_Key_List[counter] = (pos, quat)

    psafile.close()

    utils_set_mode('OBJECT')

    # index of current frame in raw input data
    raw_key_index = 0

    util_obj_set_active(context, armature_obj)

    gen_name_part = util_gen_name_part(filepath)

    armature_obj.animation_data_create()

    if bActionsToTrack:
        nla_track = armature_obj.animation_data.nla_tracks.new()
        nla_track.name = gen_name_part
        nla_stripes = nla_track.strips

        nla_track_last_frame = 0

        if len(armature_obj.animation_data.nla_tracks) > 0:
            for track in armature_obj.animation_data.nla_tracks:
                if len(track.strips) > 0:
                    if track.strips[-1].frame_end > nla_track_last_frame:
                        nla_track_last_frame = track.strips[-1].frame_end

    is_first_action = True
    first_action = None

    for counter, (Name, Group, Totalbones, NumRawFrames) in enumerate(Action_List):
        ref_time = time.process_time()

        if Group != 'None':
            Name = "(%s) %s" % (Group,Name)
        if bFilenameAsPrefix:
            Name = "(%s) %s" % (gen_name_part, Name)

        action = bpy.data.actions.new(name = Name)

        # force print usefull information to console(due to possible long execution)
        print("Action {0:>3d}/{1:<3d} frames: {2:>4d} {3}".format(
                counter+1, len(Action_List), NumRawFrames, Name)
              )

        if first_frames > 0:
            maxframes = first_frames
            keyframes = min(first_frames, NumRawFrames)
            #dev
            # keyframes += 1
        else:
            maxframes = 99999999
            keyframes = NumRawFrames

        # create all fcurves(for all bones) for an action
        # for pose_bone in armature_obj.pose.bones:
        for psa_bone in PsaBonesToProcess:
            if psa_bone is None:
                continue
            pose_bone = psa_bone.pose_bone

            data_path = pose_bone.path_from_id("rotation_quaternion")
            psa_bone.fcurve_quat_w = action.fcurves.new(data_path, index = 0)
            psa_bone.fcurve_quat_x = action.fcurves.new(data_path, index = 1)
            psa_bone.fcurve_quat_y = action.fcurves.new(data_path, index = 2)
            psa_bone.fcurve_quat_z = action.fcurves.new(data_path, index = 3)

            if not bRotationOnly:
                data_path = pose_bone.path_from_id("location")
                psa_bone.fcurve_loc_x = action.fcurves.new(data_path, index = 0)
                psa_bone.fcurve_loc_y = action.fcurves.new(data_path, index = 1)
                psa_bone.fcurve_loc_z = action.fcurves.new(data_path, index = 2)

            # 1. Pre-add keyframes! \0/
            # 2. Set data: keyframe_points[].co[0..1]
            # 3. If 2 is not done, do 4: (important!!!)
            # 4. "Apply" data: fcurve.update()
            #      # added keyframes points by default is breaking fcurve somehow
            #      # bcs they are all at the same position?
            psa_bone.fcurve_quat_w.keyframe_points.add(keyframes)
            psa_bone.fcurve_quat_x.keyframe_points.add(keyframes)
            psa_bone.fcurve_quat_y.keyframe_points.add(keyframes)
            psa_bone.fcurve_quat_z.keyframe_points.add(keyframes)

            if not bRotationOnly:
                psa_bone.fcurve_loc_x.keyframe_points.add(keyframes)
                psa_bone.fcurve_loc_y.keyframe_points.add(keyframes)
                psa_bone.fcurve_loc_z.keyframe_points.add(keyframes)

        for i in range(0,min(maxframes, NumRawFrames)):
            # raw_key_index+= Totalbones * 5 #55
            for j in range(Totalbones):
                if j in BoneNotFoundList:
                    raw_key_index += 1
                    continue

                psa_bone = PsaBonesToProcess[j]
                # pose_bone = psa_bone.pose_bone

                p_pos = Raw_Key_List[raw_key_index][0]
                p_quat = Raw_Key_List[raw_key_index][1]

                # @
                # if psa_bone.parent:
                    # quat = (p_quat * psa_bone.post_quat).conjugated() * (psa_bone.orig_quat * psa_bone.post_quat)
                # else:
                #     if bDontInvertRoot:
                #         quat = (p_quat.conjugated() * psa_bone.post_quat).conjugated() * (psa_bone.orig_quat * psa_bone.post_quat)
                #     else:
                        # quat = (p_quat * psa_bone.post_quat).conjugated() * (psa_bone.orig_quat * psa_bone.post_quat)

                q = psa_bone.post_quat.copy()
                q.rotate( psa_bone.orig_quat )

                quat = q

                q = psa_bone.post_quat.copy()

                if psa_bone.parent == None and bDontInvertRoot:
                    q.rotate( p_quat.conjugated() )
                else:
                    q.rotate( p_quat )

                quat.rotate( q.conjugated() )

                # @
                # loc = psa_bone.post_quat.conjugated() * p_pos -  psa_bone.post_quat.conjugated() * psa_bone.orig_loc

                if not bRotationOnly:
                    loc = (p_pos - psa_bone.orig_loc)
                    # "edit bone" location is in "parent space"
                    # but "pose bone" location is in "local space(bone)"
                    # so we need to transform from parent(edit_bone) to local space (pose_bone)
                    loc.rotate( psa_bone.post_quat.conjugated() )

                # if not bRotationOnly:
                    # loc = (p_pos - psa_bone.orig_loc)
                    # if psa_bone.parent is not None:
                        # q = psa_bone.parent.post_quat.copy()
                        # q.rotate( psa_bone.parent.orig_quat )
                        # print(q)
                        # loc.rotate( psa_bone.parent.post_quat.conjugated() )
                        # loc.rotate( q.conjugated() )
                        # loc.rotate( q )
                        # pass

                # quat = p_quat.conjugated()
                # quat = p_quat
                # quat.rotate( psa_bone.orig_quat.conjugated() )
                # quat = Quaternion()
                # loc = -p_pos
                # loc = (p_pos - psa_bone.orig_loc)
                # loc = Vector()
                # loc.rotate( psa_bone.post_quat.conjugated() )

                # Set it?
                # pose_bone.rotation_quaternion = quat
                # pose_bone.location = loc

                    # pose_bone.rotation_quaternion = orig_rot.conjugated()
                    # pose_bone.location = p_pos - (pose_bone.bone.matrix_local.translation - pose_bone.bone.parent.matrix_local.translation)

                ##### Works + post_quat (without location works)
                # quat = (p_quat * psa_bone.post_quat).conjugated() * (psa_bone.orig_quat * psa_bone.post_quat)
                # loc = psa_bone.post_quat.conjugated() * (p_pos - psa_bone.orig_loc)

                psa_bone.fcurve_quat_w.keyframe_points[i].co = i, quat.w
                psa_bone.fcurve_quat_x.keyframe_points[i].co = i, quat.x
                psa_bone.fcurve_quat_y.keyframe_points[i].co = i, quat.y
                psa_bone.fcurve_quat_z.keyframe_points[i].co = i, quat.z

                psa_bone.fcurve_quat_w.keyframe_points[i].interpolation = fcurve_interpolation
                psa_bone.fcurve_quat_x.keyframe_points[i].interpolation = fcurve_interpolation
                psa_bone.fcurve_quat_y.keyframe_points[i].interpolation = fcurve_interpolation
                psa_bone.fcurve_quat_z.keyframe_points[i].interpolation = fcurve_interpolation

                if not bRotationOnly:
                    psa_bone.fcurve_loc_x.keyframe_points[i].co = i, loc.x
                    psa_bone.fcurve_loc_y.keyframe_points[i].co = i, loc.y
                    psa_bone.fcurve_loc_z.keyframe_points[i].co = i, loc.z

                    psa_bone.fcurve_loc_x.keyframe_points[i].interpolation = fcurve_interpolation
                    psa_bone.fcurve_loc_y.keyframe_points[i].interpolation = fcurve_interpolation
                    psa_bone.fcurve_loc_z.keyframe_points[i].interpolation = fcurve_interpolation

                # Old path. Slower.
                # psa_bone.fcurve_quat_w.keyframe_points.insert(i,quat.w,{'NEEDED','FAST'}).interpolation = fcurve_interpolation
                # psa_bone.fcurve_quat_x.keyframe_points.insert(i,quat.x,{'NEEDED','FAST'}).interpolation = fcurve_interpolation
                # psa_bone.fcurve_quat_y.keyframe_points.insert(i,quat.y,{'NEEDED','FAST'}).interpolation = fcurve_interpolation
                # psa_bone.fcurve_quat_z.keyframe_points.insert(i,quat.z,{'NEEDED','FAST'}).interpolation = fcurve_interpolation

                # psa_bone.fcurve_loc_x.keyframe_points.insert(i,loc.x,{'NEEDED','FAST'}).interpolation = fcurve_interpolation
                # psa_bone.fcurve_loc_y.keyframe_points.insert(i,loc.y,{'NEEDED','FAST'}).interpolation = fcurve_interpolation
                # psa_bone.fcurve_loc_z.keyframe_points.insert(i,loc.z,{'NEEDED','FAST'}).interpolation = fcurve_interpolation
                raw_key_index += 1

            # on first frame
            # break
        raw_key_index += (NumRawFrames-min(maxframes,NumRawFrames)) * Totalbones

        # Add action to tail of the nla track
        if bActionsToTrack:

            if len(nla_track.strips) == 0:
                strip = nla_stripes.new(Name, nla_track_last_frame, action)
            else:
                strip = nla_stripes.new(Name, nla_stripes[-1].frame_end, action)

            # Do not pollute track. Makes other tracks 'visible' through 'empty space'.
            strip.extrapolation = 'NOTHING'

            nla_track_last_frame += NumRawFrames

        if is_first_action:
            first_action = action
            is_first_action = False

        print("Done: %f sec." % (time.process_time() - ref_time))
        # break on first animation set
        # break

    scene = util_get_scene(context)

    if not bActionsToTrack:
        if not scene.is_nla_tweakmode:
            armature_obj.animation_data.action = first_action

    if bUpdateTimelineRange:

        scene.frame_start = 0

        if bActionsToTrack:
            scene.frame_end = sum(frames for _, _, _, frames in Action_List) - 1
        else:
            scene.frame_end = max(frames for _, _, _, frames in Action_List) - 1

    util_select_all(False)
    util_obj_select(context, armature_obj)
    util_obj_set_active(context, armature_obj)