#RE Engine [PC] - ".mesh" plugin for Rich Whitehouse's Noesis
#Authors: alphaZomega, Gh0stblade 
#Special thanks: Chrrox, SilverEzredes, Enaium 
Version = "v3.28 (September 21, 2024)"

#Changelog:
#- Fixed motlist.854 reading



#Options: These are global options that change or enable/disable certain features

#Var												Effect
#Export Extensions
bRayTracingExport			= True					#Enable or disable the export of RE2R, RE3R and RE7 RayTracing meshes and textures
bRE2Export 					= True					#Enable or disable export of mesh.1808312334 and tex.10 from the export list			
bRE3Export 					= True					#Enable or disable export of mesh.1902042334 and tex.190820018 from the export list
bDMCExport 					= True					#Enable or disable export of mesh.1808282334 and tex.11 from the export list
bRE7Export 					= True					#Enable or disable export of mesh.32 and tex.8 from the export list
bREVExport 					= True					#Enable or disable export of mesh.2102020001 from the export list (and tex.31)
bRE8Export 					= True					#Enable or disable export of mesh.2101050001 from the export list (and tex.30)
bMHRiseExport 				= False					#Enable or disable export of mesh.2008058288 from the export list (and tex.28) 
bMHRiseSunbreakExport 		= True					#Enable or disable export of mesh.2109148288 from the export list (and tex.28)
bSF6Export					= True					#Enable or disable export of mesh.230110883 from the export list (and tex.143230113)
bRE4Export					= True					#Enable or disable export of mesh.221108797 from the export list (and tex.143221013)
bExoExport					= True					#Enable or disable export of mesh.220907984 from the export list (and tex.40)
bApolloExport				= True					#Enable or disable export of mesh.230612127 from the export list (and tex.719230324)
bDD2Export					= True					#Enable or disable export of mesh.231011879 from the export list (and tex.760230703)
bDRDRExport					= True					#Enable or disable export of mesh.240424828 from the export list (and tex.240606151)



#Mesh Global
fDefaultMeshScale 			= 100.0 				#Override mesh scale (default is 1.0)
bMaterialsEnabled 			= True					#Load MDF Materials
bRenderAsPoints 			= False					#Render mesh as points without triangles drawn (1 = on, 0 = off)
bImportAllLODs 				= False					#Imports all LODGroups (as separate models)

#Vertex Components (Import)
bNORMsEnabled 				= True					#Normals
bTANGsEnabled 				= True					#Tangents
bUVsEnabled 				= True					#UVs
bSkinningEnabled 			= True					#Enable skin weights
bColorsEnabled				= True					#Enable Vertex Colors
bDebugNormals 				= False					#Debug normals as RGBA
bDebugTangents 				= False					#Debug tangents as RGBA

#Import Options
bPrintMDF 					= False					#Prints debug info for MDF files
bDebugMESH 					= False					#Prints debug info for MESH files
bPopupDebug 				= False					#Pops up debug window on opening MESH with MDF
bPrintFileList 				= True					#Prints a list of files used by the MDF
bColorize 					= False					#Colors the materials of the model and lists which material is which color
bUseOldNamingScheme 		= False					#Names submeshes by their material ID (like in the MaxScript) rather than by their order in the file 
bRenameMeshesToFilenames 	= False					#For use with Noesis Model Merger. Renames submeshes to have their filenames in the mesh names
bImportMaterialNames		= True					#Imports material name data for each mesh, appending it to the end of each Submesh's name
bShorterNames				= True					#Imports meshes named like "LOD_1_Main_1_Sub_1" instead of "LODGroup_1_MainMesh_1_SubMesh_1"
bImportMips 				= False					#Imports texture mip maps as separate images
texOutputExt				= ".tga"				#File format used when linking FBX materials to images
doConvertMatsForBlender		= False					#Load alpha maps as reflection maps, metallic maps as specular maps and roughness maps as bump maps for use with modified Blender FBX importer
bNoImportMenu				= False					#Hide the import menu on loading a mesh

#Export Options
bNewExportMenu				= False					#Show a custom Noesis window on mesh export
bAlwaysRewrite				= False					#Always try to rewrite the meshfile on export
bAlwaysWriteBones			= False					#Always write new skeleton to mesh
bNormalizeWeights 			= False					#Makes sure that the weights of every vertex add up to 1.0, giving the remainder to the bone with the least influence
bCalculateBoundingBoxes		= True					#Calculates the bounding box for each bone
BoundingBoxSize				= 1.0					#With bCalculateBoundingBoxes False, change the size of the bounding boxes created for each rigged bone when exporting with -bones or -rewrite
bRigToCoreBones				= False					#Assign non-matching bones to the hips and spine, when exporting a mesh without -bones or -rewrite
bSetNumModels				= True					#Sets the header byte "NumModels" to defaults when exporting without -rewrite, preventing crashes
bForceRootBoneToBone0		= True					#If the root bone is detected as the last bone in the bones list, this will move it to be the first bone in the list

#Import/Export:
bAddBoneNumbers 			= 2						#Adds bone numbers and colons before bone names to indicate if they are active. 0 = Off, 1 = On, 2 = Auto
bRotateBonesUpright			= False					#Rotates bones to be upright for editing and then back to normal for exporting
bReadGroupIds				= True					#Import/Export with the GroupID as the MainMesh number

#Plugin GUI
iListboxSize  				= 280					#The height of the list box in the plugin's import menu


from inc_noesis import *
from collections import namedtuple
import noewin
import math
import os
import re
import copy
import time

def registerNoesisTypes():

	def addOptions(handle):
		noesis.setTypeExportOptions(handle, "-noanims -notex")
		noesis.addOption(handle, "-bones", "Write new skeleton on export", 0)
		noesis.addOption(handle, "-rewrite", "Rewrite submeshes and materials structure", 0)
		noesis.addOption(handle, "-flip", "Reverse handedness from DirectX to OpenGL", 0)
		noesis.addOption(handle, "-bonenumbers", "Add bone numbers to imported bones", 0)
		noesis.addOption(handle, "-meshfile", "Export using a given source mesh filename", noesis.OPTFLAG_WANTARG)
		noesis.addOption(handle, "-b", "Run as a batch process", 0)
		noesis.addOption(handle, "-adv", "Show Advanced Export Options window", 0)
		noesis.addOption(handle, "-vfx", "Export as VFX mesh", 0)
		return handle
		
	handle = noesis.register("RE Engine MESH [PC]", ".1902042334;.1808312334;.1808282334;.2008058288;.2102020001;.2101050001;.2109108288;.2109148288;.220128762;.220301866;.220721329;.221108797;.220907984;.230110883;.230612127;.231011879;.240424828;.NewMesh")
	noesis.setHandlerTypeCheck(handle, meshCheckType)
	noesis.setHandlerLoadModel(handle, meshLoadModel)
	noesis.addOption(handle, "-noprompt", "Do not prompt for MDF file", 0)
	noesis.setTypeSharedModelFlags(handle, (noesis.NMSHAREDFL_WANTGLOBALARRAY))

	handle = noesis.register("RE Engine Texture [PC]", ".10;.190820018;.11;.8;.28;.stm;.30;.31;.34;.35;.36;.40;.143221013;.143230113;.719230324;.760230703;.240606151")
	noesis.setHandlerTypeCheck(handle, texCheckType)
	noesis.setHandlerLoadRGBA(handle, texLoadDDS)

	handle = noesis.register("RE Engine UVS [PC]", ".5;.7;.8")
	noesis.setHandlerTypeCheck(handle, UVSCheckType)
	noesis.setHandlerLoadModel(handle, UVSLoadModel)
	
	handle = noesis.register("RE Engine SCN [PC]", ".19;.20")
	noesis.setHandlerTypeCheck(handle, SCNCheckType)
	noesis.setHandlerLoadModel(handle, SCNLoadModel)
	
	handle = noesis.register("RE Engine MOTLIST [PC]", ".60;.85;.99;.484;.486;.500;.524;.528;.643;.653;.663;.750;.751;.851;.854")
	noesis.setHandlerTypeCheck(handle, motlistCheckType)
	noesis.setHandlerLoadModel(handle, motlistLoadModel)

	if bRE2Export:
		handle = noesis.register("RE2 Remake Texture [PC]", ".10")
		noesis.setHandlerWriteRGBA(handle, texWriteRGBA)
		handle = noesis.register("RE2 MESH", (".1808312334"))
		noesis.setHandlerTypeCheck(handle, meshCheckType)
		noesis.setHandlerWriteModel(handle, meshWriteModel)
		addOptions(handle)
		
	if bRE3Export:
		handle = noesis.register("RE3 Remake Texture [PC]", ".190820018")
		noesis.setHandlerWriteRGBA(handle, texWriteRGBA)
		handle = noesis.register("RE3 MESH", (".1902042334"))
		noesis.setHandlerTypeCheck(handle, meshCheckType)
		noesis.setHandlerWriteModel(handle, meshWriteModel)
		addOptions(handle)
	
	#fbxskel export is disabled
	#handle = noesis.register("fbxskel", (".fbxskel.3")) 
	#noesis.setHandlerWriteModel(handle, skelWriteFbxskel)
	#noesis.setTypeExportOptions(handle, "-noanims -notex")
		
	if bDMCExport:
		handle = noesis.register("Devil May Cry 5 Texture [PC]", ".11")
		noesis.setHandlerTypeCheck(handle, texCheckType)
		noesis.setHandlerWriteRGBA(handle, texWriteRGBA)
		handle = noesis.register("DMC5 MESH", (".1808282334"))
		noesis.setHandlerTypeCheck(handle, meshCheckType)
		noesis.setHandlerWriteModel(handle, meshWriteModel)
		addOptions(handle)
		
	if bREVExport or bRE8Export:
		handle = noesis.register("RE8 / ReVerse Texture [PC]", ".30")
		noesis.setHandlerTypeCheck(handle, texCheckType)
		noesis.setHandlerWriteRGBA(handle, texWriteRGBA);
		
	if bREVExport:
		handle = noesis.register("ReVerse MESH", (".2102020001"))
		noesis.setHandlerTypeCheck(handle, meshCheckType)
		noesis.setHandlerWriteModel(handle, meshWriteModel)
		addOptions(handle)
		
	if bRE8Export:
		handle = noesis.register("RE8 MESH", (".2101050001"))
		noesis.setHandlerTypeCheck(handle, meshCheckType)
		noesis.setHandlerWriteModel(handle, meshWriteModel)
		addOptions(handle)
		
	if bMHRiseExport or bMHRiseSunbreakExport:
		handle = noesis.register("MHRise Texture [PC]", ".28;.stm")
		noesis.setHandlerTypeCheck(handle, texCheckType)
		noesis.setHandlerWriteRGBA(handle, texWriteRGBA);
		if bMHRiseExport:
			handle = noesis.register("MHRise MESH", (".2008058288"))
			noesis.setHandlerTypeCheck(handle, meshCheckType)
			noesis.setHandlerWriteModel(handle, meshWriteModel)
			addOptions(handle)
		if bMHRiseSunbreakExport:
			handle = noesis.register("MHRise Sunbreak MESH", (".2109148288"))
			noesis.setHandlerTypeCheck(handle, meshCheckType)
			noesis.setHandlerWriteModel(handle, meshWriteModel)
			addOptions(handle)
		
	if bRE7Export:
		handle = noesis.register("Resident Evil 7 Texture [PC]", ".8")
		noesis.setHandlerTypeCheck(handle, texCheckType)
		noesis.setHandlerWriteRGBA(handle, texWriteRGBA)
		#RE7 MESH support is disabled for this version
		
	if bRayTracingExport:
		handle = noesis.register("RE2,3 RayTracing Texture [PC]", ".34")
		noesis.setHandlerTypeCheck(handle, texCheckType)
		noesis.setHandlerWriteRGBA(handle, texWriteRGBA);
		handle = noesis.register("RE7 RayTracing Texture [PC]", ".35")
		noesis.setHandlerTypeCheck(handle, texCheckType)
		noesis.setHandlerWriteRGBA(handle, texWriteRGBA);
		handle = noesis.register("RE2+3 RayTracing MESH", (".2109108288"))
		noesis.setHandlerTypeCheck(handle, meshCheckType)
		noesis.setHandlerWriteModel(handle, meshWriteModel)
		addOptions(handle)
		handle = noesis.register("RE7 RayTracing MESH", (".220128762"))
		noesis.setHandlerTypeCheck(handle, meshCheckType)
		noesis.setHandlerWriteModel(handle, meshWriteModel)
		addOptions(handle)
	
	if bSF6Export:
		handle = noesis.register("Street Fighter 6 Texture [PC]", ".143230113;")
		noesis.setHandlerTypeCheck(handle, texCheckType)
		noesis.setHandlerWriteRGBA(handle, texWriteRGBA);
		handle = noesis.register("Street Fighter 6 Mesh", (".230110883"))
		noesis.setHandlerTypeCheck(handle, meshCheckType)
		noesis.setHandlerWriteModel(handle, meshWriteModel)
		addOptions(handle)
		
	if bRE4Export:
		handle = noesis.register("RE4 Remake Texture [PC]", ".143221013")
		noesis.setHandlerTypeCheck(handle, texCheckType)
		noesis.setHandlerWriteRGBA(handle, texWriteRGBA);
		handle = noesis.register("RE4 Remake Mesh", (".221108797"))
		noesis.setHandlerTypeCheck(handle, meshCheckType)
		noesis.setHandlerWriteModel(handle, meshWriteModel)
		addOptions(handle)

	if bExoExport:
		handle = noesis.register("ExoPrimal Texture [PC]", ".40")
		noesis.setHandlerTypeCheck(handle, texCheckType)
		noesis.setHandlerWriteRGBA(handle, texWriteRGBA);
		handle = noesis.register("ExoPrimal Mesh", (".220907984"))
		noesis.setHandlerTypeCheck(handle, meshCheckType)
		noesis.setHandlerWriteModel(handle, meshWriteModel)
		addOptions(handle)

	if bApolloExport:
		handle = noesis.register("Apollo Justice: Ace Attorney Trilogy Texture [PC]", ".719230324")
		noesis.setHandlerTypeCheck(handle, texCheckType)
		noesis.setHandlerWriteRGBA(handle, texWriteRGBA);
		handle = noesis.register("Apollo Justice: Ace Attorney Trilogy Mesh", (".230612127"))
		noesis.setHandlerTypeCheck(handle, meshCheckType)
		noesis.setHandlerWriteModel(handle, meshWriteModel)
		addOptions(handle)
		
	if bDD2Export:
		handle = noesis.register("Dragon's Dogma 2 Texture [PC]", ".760230703")
		noesis.setHandlerTypeCheck(handle, texCheckType)
		noesis.setHandlerWriteRGBA(handle, texWriteRGBA);
		handle = noesis.register("Dragon's Dogma 2 Mesh", (".231011879"))
		noesis.setHandlerTypeCheck(handle, meshCheckType)
		noesis.setHandlerWriteModel(handle, meshWriteModel)
		addOptions(handle)
		
	if bDRDRExport:
		handle = noesis.register("Dead Rising DR Texture [PC]", ".240606151")
		noesis.setHandlerTypeCheck(handle, texCheckType)
		noesis.setHandlerWriteRGBA(handle, texWriteRGBA);
		#handle = noesis.register("Dead Rising DR Mesh", (".240424828"))
		#noesis.setHandlerTypeCheck(handle, meshCheckType)
		#noesis.setHandlerWriteModel(handle, meshWriteModel)
		#addOptions(handle)
		
		
	#noesis.logPopup()
	return 1
		
#Default global variables for internal use:
sGameName = "RE2"
extractedNativesPath = ""
sExportExtension = ".1808312334"
bWriteBones = False
bDoVFX = False
bReWrite = False
openOptionsDialog = None
w1 = 127
w2 = -128

formats = {
	"RE7":			{ "modelExt": ".-1", 		 "texExt": ".8", 		 "mmtrExt": ".69", 		   "nDir": "x64", "mdfExt": ".mdf2.6",  "meshVersion": 0, "mdfVersion": 0, "mlistExt": ".60", "meshMagic":352921600 },
	"RE2":			{ "modelExt": ".1808312334", "texExt": ".10",		 "mmtrExt": ".1808160001", "nDir": "x64", "mdfExt": ".mdf2.10", "meshVersion": 1, "mdfVersion": 1, "mlistExt": ".85", "meshMagic":386270720, "motionIDsData":[24,8] },
	"DMC5":			{ "modelExt": ".1808282334", "texExt": ".11", 		 "mmtrExt": ".1808168797", "nDir": "x64", "mdfExt": ".mdf2.10", "meshVersion": 1, "mdfVersion": 1, "mlistExt": ".85", "meshMagic":386270720, "motionIDsData":[24,8]  },
	"RE3": 			{ "modelExt": ".1902042334", "texExt": ".190820018", "mmtrExt": ".1905100741", "nDir": "stm", "mdfExt": ".mdf2.13", "meshVersion": 1, "mdfVersion": 2, "mlistExt": ".99", "meshMagic":386270720, "motionIDsData":[24,8] },
	"RE8": 			{ "modelExt": ".2101050001", "texExt": ".30", 		 "mmtrExt": ".2102188797", "nDir": "stm", "mdfExt": ".mdf2.19", "meshVersion": 2, "mdfVersion": 3, "mlistExt": ".486", "meshMagic":2020091500, "motionIDsData":[24,8] },
	"MHRise":		{ "modelExt": ".2008058288", "texExt": ".28", 		 "mmtrExt": ".2109301553", "nDir": "stm", "mdfExt": ".mdf2.19", "meshVersion": 2, "mdfVersion": 3, "mlistExt": ".484", "meshMagic":21091000, "motionIDsData":[72,8] },
	"MHRSunbreak":	{ "modelExt": ".2109148288", "texExt": ".28", 		 "mmtrExt": ".220427553",  "nDir": "stm", "mdfExt": ".mdf2.23", "meshVersion": 2, "mdfVersion": 3, "mlistExt": ".528", "meshMagic":21091000, "motionIDsData":[72,8] },
	"ReVerse":		{ "modelExt": ".2102020001", "texExt": ".31", 		 "mmtrExt": ".2108110001", "nDir": "stm", "mdfExt": ".mdf2.20", "meshVersion": 2, "mdfVersion": 3, "mlistExt": ".500", "meshMagic":2020091500, "motionIDsData":[24,8] },
	"RERT": 		{ "modelExt": ".2109108288", "texExt": ".34", 		 "mmtrExt": ".2109101635", "nDir": "stm", "mdfExt": ".mdf2.21", "meshVersion": 2, "mdfVersion": 3, "mlistExt": ".524", "meshMagic":21041600, "motionIDsData":[72,8] },
	"RE7RT": 		{ "modelExt": ".220128762",  "texExt": ".35", 		 "mmtrExt": ".2109101635", "nDir": "stm", "mdfExt": ".mdf2.21", "meshVersion": 2, "mdfVersion": 3, "mlistExt": ".524", "meshMagic":21041600, "motionIDsData":[72,8] },
	"SF6": 			{ "modelExt": ".230110883",  "texExt": ".143230113", "mmtrExt": ".221102761",  "nDir": "stm", "mdfExt": ".mdf2.31", "meshVersion": 3, "mdfVersion": 4, "mlistExt": ".653", "meshMagic":230403828, "motionIDsData":[72,8] },
	"ExoPrimal": 	{ "modelExt": ".220907984",  "texExt": ".40", 		 "mmtrExt": ".221007878",  "nDir": "stm", "mdfExt": ".mdf2.31", "meshVersion": 3, "mdfVersion": 4, "mlistExt": ".643", "meshMagic":220705151, "motionIDsData":[72,8] },
	"RE4": 			{ "modelExt": ".221108797",  "texExt": ".143221013", "mmtrExt": ".221007879",  "nDir": "stm", "mdfExt": ".mdf2.32", "meshVersion": 3, "mdfVersion": 4, "mlistExt": ".663", "meshMagic":220822879, "motionIDsData":[72,8] },
	"AJ_AAT": 		{ "modelExt": ".230612127",  "texExt": ".719230324", "mmtrExt": ".230815080",  "nDir": "stm", "mdfExt": ".mdf2.37", "meshVersion": 3, "mdfVersion": 4, "mlistExt": ".750", "meshMagic":230406984, "motionIDsData":[72,8] },
	"DD2": 			{ "modelExt": ".231011879",  "texExt": ".760230703", "mmtrExt": ".230815080",  "nDir": "stm", "mdfExt": ".mdf2.40", "meshVersion": 3, "mdfVersion": 4, "mlistExt": ".751", "meshMagic":230517984, "motionIDsData":[72,8] },
	"DRDR": 		{ "modelExt": ".240424828",  "texExt": ".240606151", "mmtrExt": ".240405143",  "nDir": "stm", "mdfExt": ".mdf2.40", "meshVersion": 3, "mdfVersion": 4, "mlistExt": ".854", "meshMagic":240423829, "motionIDsData":[72,8] },
}

extToFormat = { #incomplete, just testing
	"10": {
		"albm":   [99,5],
		"albmsc": [99,5],
		"alba":	  [99,1],
		"alb":    [72,0],
		"nrmr":   [98,6],
		"nrm":	  [98,1],
		"iam":	  [99,8],
		"atos":   [71,4],
		"msk1":   [80,3],
	},
	"34": {
		"albm":   [99,5],
		"albmsc": [99,5],
		"alba":	  [99,1],
		"alb":    [72,0],
		"nrmr":   [98,6],
		"nrm":	  [98,1],
		"iam":	  [99,8],
		"atos":   [71,4],
		"msk1":   [80,3],
	},
}
extToFormat["8"] = extToFormat["10"]
extToFormat["11"] = extToFormat["10"]
extToFormat["190820018"] = extToFormat["10"]
extToFormat["30"] = extToFormat["34"]
extToFormat["35"] = extToFormat["34"]
extToFormat["28"] = extToFormat["34"]
extToFormat["143221013"] = extToFormat["34"]
extToFormat["28.stm"] = extToFormat["28"]


texFormatNames = {
	0: "UNKNOWN",
	1: "R32G32B32A32_TYPELESS",
	2: "R32G32B32A32_FLOAT",
	3: "R32G32B32A32_UINT",
	4: "R32G32B32A32_SINT",
	5: "R32G32B32_TYPELESS",
	6: "R32G32B32_FLOAT",
	7: "R32G32B32_UINT",
	8: "R32G32B32_SINT",
	9: "R16G16B16A16_TYPELESS",
	10: "R16G16B16A16_FLOAT",
	11: "R16G16B16A16_UNORM",
	12: "R16G16B16A16_UINT",
	13: "R16G16B16A16_SNORM",
	14: "R16G16B16A16_SINT",
	15: "R32G32_TYPELESS",
	16: "R32G32_FLOAT",
	17: "R32G32_UINT",
	18: "R32G32_SINT",
	19: "R32G8X24_TYPELESS",
	20: "D32_FLOAT_S8X24_UINT",
	21: "R32_FLOAT_X8X24_TYPELESS",
	22: "X32_TYPELESS_G8X24_UINT",
	23: "R10G10B10A2_TYPELESS",
	24: "R10G10B10A2_UNORM",
	25: "R10G10B10A2_UINT",
	26: "R11G11B10_FLOAT",
	27: "R8G8B8A8_TYPELESS",
	28: "R8G8B8A8_UNORM",
	29: "R8G8B8A8_UNORM_SRGB",
	30: "R8G8B8A8_UINT",
	31: "R8G8B8A8_SNORM",
	32: "R8G8B8A8_SINT",
	33: "R16G16_TYPELESS",
	34: "R16G16_FLOAT",
	35: "R16G16_UNORM",
	36: "R16G16_UINT",
	37: "R16G16_SNORM",
	38: "R16G16_SINT",
	39: "R32_TYPELESS",
	40: "D32_FLOAT",
	41: "R32_FLOAT",
	42: "R32_UINT",
	43: "R32_SINT",
	44: "R24G8_TYPELESS",
	45: "D24_UNORM_S8_UINT",
	46: "R24_UNORM_X8_TYPELESS",
	47: "X24_TYPELESS_G8_UINT",
	48: "R8G8_TYPELESS",
	49: "R8G8_UNORM",
	50: "R8G8_UINT",
	51: "R8G8_SNORM",
	52: "R8G8_SINT",
	53: "R16_TYPELESS",
	54: "R16_FLOAT",
	55: "D16_UNORM",
	56: "R16_UNORM",
	57: "R16_UINT",
	58: "R16_SNORM",
	59: "R16_SINT",
	60: "R8_TYPELESS",
	61: "R8_UNORM",
	62: "R8_UINT",
	63: "R8_SNORM",
	64: "R8_SINT",
	65: "A8_UNORM",
	66: "R1_UNORM",
	67: "R9G9B9E5_SHAREDEXP",
	68: "R8G8_B8G8_UNORM",
	69: "G8R8_G8B8_UNORM",
	70: "BC1_TYPELESS",
	71: "BC1_UNORM",
	72: "BC1_UNORM_SRGB",
	73: "BC2_TYPELESS",
	74: "BC2_UNORM",
	75: "BC2_UNORM_SRGB",
	76: "BC3_TYPELESS",
	77: "BC3_UNORM",
	78: "BC3_UNORM_SRGB",
	79: "BC4_TYPELESS",
	80: "BC4_UNORM",
	81: "BC4_SNORM",
	82: "BC5_TYPELESS",
	83: "BC5_UNORM",
	84: "BC5_SNORM",
	85: "B5G6R5_UNORM",
	86: "B5G5R5A1_UNORM",
	87: "B8G8R8A8_UNORM",
	88: "B8G8R8X8_UNORM",
	89: "R10G10B10_XR_BIAS_A2_UNORM",
	90: "B8G8R8A8_TYPELESS",
	91: "B8G8R8A8_UNORM_SRGB",
	92: "B8G8R8X8_TYPELESS",
	93: "B8G8R8X8_UNORM_SRGB",
	94: "BC6H_TYPELESS",
	95: "BC6H_UF16",
	96: "BC6H_SF16",
	97: "BC7_TYPELESS",
	98: "BC7_UNORM",
	99: "BC7_UNORM_SRGB",
	100: "AYUV",
	101: "Y410",
	102: "Y416",
	103: "NV12",
	104: "P010",
	105: "P016",
	106: "_420_OPAQUE",
	107: "YUY2",
	108: "Y210",
	109: "Y216",
	110: "NV11",
	111: "AI44",
	112: "IA44",
	113: "P8",
	114: "A8P8",
	115: "B4G4R4A4_UNORM",
	130: "P208",
	131: "V208",
	132: "V408",
	0xffffffff:  "FORCE_UINT" 
}

fmtNameToBpp = {
    "R32G32B32A32_TYPELESS": 128,
    "R32G32B32A32_FLOAT": 128,
    "R32G32B32A32_UINT": 128,
    "R32G32B32A32_SINT": 128,
    "R32G32B32_TYPELESS": 96,
    "R32G32B32_FLOAT": 96,
    "R32G32B32_UINT": 96,
    "R32G32B32_SINT": 96,
    "R16G16B16A16_TYPELESS": 64,
    "R16G16B16A16_FLOAT": 64,
    "R16G16B16A16_UNORM": 64,
    "R16G16B16A16_UINT": 64,
    "R16G16B16A16_SNORM": 64,
    "R16G16B16A16_SINT": 64,
    "R32G32_TYPELESS": 64,
    "R32G32_FLOAT": 64,
    "R32G32_UINT": 64,
    "R32G32_SINT": 64,
    "R32G8X24_TYPELESS": 64,
    "D32_FLOAT_S8X24_UINT": 64,
    "R32_FLOAT_X8X24_TYPELESS": 64,
    "X32_TYPELESS_G8X24_UINT": 64,
    "Y416": 64,
    "Y210": 64,
    "Y216": 64,
    "R10G10B10A2_TYPELESS": 32,
    "R10G10B10A2_UNORM": 32,
    "R10G10B10A2_UINT": 32,
    "R11G11B10_FLOAT": 32,
    "R8G8B8A8_TYPELESS": 32,
    "R8G8B8A8_UNORM": 32,
    "R8G8B8A8_UNORM_SRGB": 32,
    "R8G8B8A8_UINT": 32,
    "R8G8B8A8_SNORM": 32,
    "R8G8B8A8_SINT": 32,
    "R16G16_TYPELESS": 32,
    "R16G16_FLOAT": 32,
    "R16G16_UNORM": 32,
    "R16G16_UINT": 32,
    "R16G16_SNORM": 32,
    "R16G16_SINT": 32,
    "R32_TYPELESS": 32,
    "D32_FLOAT": 32,
    "R32_FLOAT": 32,
    "R32_UINT": 32,
    "R32_SINT": 32,
    "R24G8_TYPELESS": 32,
    "D24_UNORM_S8_UINT": 32,
    "R24_UNORM_X8_TYPELESS": 32,
    "X24_TYPELESS_G8_UINT": 32,
    "R9G9B9E5_SHAREDEXP": 32,
    "R8G8_B8G8_UNORM": 32,
    "G8R8_G8B8_UNORM": 32,
    "B8G8R8A8_UNORM": 32,
    "B8G8R8X8_UNORM": 32,
    "R10G10B10_XR_BIAS_A2_UNORM": 32,
    "B8G8R8A8_TYPELESS": 32,
    "B8G8R8A8_UNORM_SRGB": 32,
    "B8G8R8X8_TYPELESS": 32,
    "B8G8R8X8_UNORM_SRGB": 32,
    "AYUV": 32,
    "Y410": 32,
    "YUY2": 32,
    "P010": 24,
    "P016": 24,
    "R8G8_TYPELESS": 16,
    "R8G8_UNORM": 16,
    "R8G8_UINT": 16,
    "R8G8_SNORM": 16,
    "R8G8_SINT": 16,
    "R16_TYPELESS": 16,
    "R16_FLOAT": 16,
    "D16_UNORM": 16,
    "R16_UNORM": 16,
    "R16_UINT": 16,
    "R16_SNORM": 16,
    "R16_SINT": 16,
    "B5G6R5_UNORM": 16,
    "B5G5R5A1_UNORM": 16,
    "A8P8": 16,
    "B4G4R4A4_UNORM": 16,
    "NV12": 12,
    "420_OPAQUE": 12,
    "NV11": 12,
    "BC2_TYPELESS": 8,
    "BC2_UNORM": 8,
    "BC2_UNORM_SRGB": 8,
    "BC3_TYPELESS": 8,
    "BC3_UNORM": 8,
    "BC3_UNORM_SRGB": 8,
    "BC5_TYPELESS": 8,
    "BC5_UNORM": 8,
    "BC5_SNORM": 8,
    "BC6H_TYPELESS": 8,
    "BC6H_UF16": 8,
    "BC6H_SF16": 8,
    "BC7_TYPELESS": 8,
    "BC7_UNORM": 8,
    "BC7_UNORM_SRGB": 8,
    "R8_TYPELESS": 8,
    "R8_UNORM": 8,
    "R8_UINT": 8,
    "R8_SNORM": 8,
    "R8_SINT": 8,
    "A8_UNORM": 8,
    "AI44": 8,
    "IA44": 8,
    "P8": 8,
    "R1_UNORM": 1,
    "BC1_TYPELESS": 4,
    "BC1_UNORM": 4,
    "BC1_UNORM_SRGB": 4,
    "BC4_TYPELESS": 4,
    "BC4_UNORM": 4,
    "BC4_SNORM": 4
}

def sort_human(List):
	convert = lambda text: float(text) if text.isdigit() else text
	return sorted(List, key=lambda mesh: [convert(c) for c in re.split('([-+]?[0-9]*\.?[0-9]*)', mesh.name)])

def meshCheckType(data):
	bs = NoeBitStream(data)
	magic = bs.readUInt()
	
	if magic == 0x4853454D:
		return 1
	else: 
		print("Fatal Error: Unknown file magic: " + str(hex(magic) + " expected 'MESH'!"))
		return 0

def texCheckType(data):
	bs = NoeBitStream(data)
	magic = bs.readUInt()
	if magic == 0x00584554:
		return 1
	else: 
		print("Fatal Error: Unknown file magic: " + str(hex(magic) + " expected TEX!"))
		return 0

def UVSCheckType(data):
	bs = NoeBitStream(data)
	magic = bs.readUInt()
	if magic == 1431720750:
		return 1
	else: 
		print("Fatal Error: Unknown file magic: " + str(hex(magic) + " expected ' SVU'!"))
		return 0
		
def readUIntAt(bs, readAt):
	pos = bs.tell()
	bs.seek(readAt)
	value = bs.readUInt()
	bs.seek(pos)
	return value
	
def readUShortAt(bs, tell):
	pos = bs.tell()
	bs.seek(tell)
	out = bs.readUShort()
	bs.seek(pos)
	return out
	
def readUByteAt(bs, tell):
	pos = bs.tell()
	bs.seek(tell)
	out = bs.readUByte()
	bs.seek(pos)
	return out

def ReadUnicodeString(bs):
	numZeroes = 0
	resultString = ""
	while(numZeroes < 2):
		c = bs.readUByte()
		if c == 0:
			numZeroes+=1
			continue
		else:
			numZeroes = 0
		resultString += chr(c)
	return resultString

def readUnicodeStringAt(bs, tell):
	string = []
	pos = bs.tell()
	bs.seek(tell)
	while(readUShortAt(bs, bs.tell()) != 0):
		string.append(bs.readByte())
		bs.seek(1,1)
	bs.seek(pos)
	buff = struct.pack("<" + 'b'*len(string), *string)
	return str(buff, 'utf-8')
		
def GetRootGameDir(path=""):
	path = rapi.getDirForFilePath(path or rapi.getInputName())
	while len(path) > 3:
		lastFolderName = os.path.basename(os.path.normpath(path)).lower()
		if lastFolderName == "stm" or lastFolderName == "x64":
			break
		else:
			path = os.path.normpath(os.path.join(path, ".."))
	
	return path	+ "\\"
	
def LoadExtractedDir(gameName=None):
	gameName = gameName or sGameName
	nativesPath = ""
	try: 
		with open(noesis.getPluginsPath() + '\python\\' + gameName + 'NativesPath.txt') as fin:
			nativesPath = fin.read()
			fin.close()
	except IOError:
		pass
	if not os.path.isdir(nativesPath):
		return ""
	return nativesPath	
		
def SaveExtractedDir(dirIn, gameName=None):
	gameName = gameName or sGameName
	try: 
		print (noesis.getPluginsPath() + 'python\\' + gameName + 'NativesPath.txt')
		with open(noesis.getPluginsPath() + 'python\\' + gameName + 'NativesPath.txt', 'w') as fout:
			print ("Writing string: " + dirIn + " to " + noesis.getPluginsPath() + 'python\\' + gameName + 'NativesPath.txt')
			fout.flush()
			fout.write(str(dirIn))
			fout.close()
	except IOError:
		print ("Failed to save natives path: IO Error")
		return 0
	return 1
	
def findRootDir(path):
	idx = path.find("\\natives\\")
	if idx != -1:
		return path[:(idx + 9)]
	return path
	
def getGlobalMatrix(noebone, bonesList): #doesnt work 100%?
	mat = noebone.getMatrix()
	parent = bonesList[noebone.parentIndex] if noebone.parentIndex != -1 else None
	if parent:
		mat *= parent.getMatrix().inverse()
	return mat.transpose()
	
def getChildBones(parentBone, boneList, doRecurse=False):
	children = []
	for bone in boneList:
		if bone.parentName == parentBone.name and bone not in children:
			children.append(bone)
			if doRecurse:
				children.extend(getChildBones(bone, boneList, True))
			break
	return children
	
def cleanBoneName(name):
	splitted = name.split(":", 1)
	return splitted[len(splitted)-1]

def generateBoneMap(mdl):
	usedBones = [False for bone in mdl.bones]
	boneNames = [bone.name.lower() for bone in mdl.bones]
	boneMapCount = 0
	for bone in mdl.bones:
		if bone.parentIndex != -1 and bone.parentName:
			bone.parentIndex = boneNames.index(bone.parentName.lower())
	for mesh in mdl.meshes:
		for weightList in mesh.weights:
			for idx in weightList.indices:
				usedBones[idx] = True
	for i, bone in enumerate(mdl.bones):
		bone.name = cleanBoneName(bone.name)
		if usedBones[i]:
			bone.name = "b" + "{:03d}".format(boneMapCount) + ":" + bone.name
			boneMapCount += 1
	for bone in mdl.bones:
		if bone.parentIndex != -1:
			bone.parentName = mdl.bones[bone.parentIndex].name

def collapseBones(mdl, threshold=0.01):
	print("Collapsing skeleton")
	newBones = []
	newBoneMap = []
	newBoneMapNames = []
	allBoneNames = [cleanBoneName(bone.name).lower() for bone in mdl.bones]
	for i, bone in enumerate(mdl.bones):
		boneMapId = i
		name = allBoneNames[i].split(".", 1)[0]
		try:
			sameBoneIdx = allBoneNames.index(name)
		except ValueError:
			print("ERROR:", name, " not in bones list!")
			continue
		#if sameBoneIdx != i and (getGlobalMatrix(mdl.bones[sameBoneIdx], mdl.bones)[3] - getGlobalMatrix(bone, mdl.bones)[3]).length() < threshold * fDefaultMeshScale:
		if sameBoneIdx != i: # and (mdl.bones[sameBoneIdx].getMatrix()[3] - bone.getMatrix()[3]).length() < threshold * fDefaultMeshScale:
			boneMapId = sameBoneIdx
		elif bone.parentIndex != bone.index:
			newBones.append(bone)
		newBoneMapNames.append(mdl.bones[boneMapId].name.lower())
	for i, bone in enumerate(newBones):
		bone.index = i
		if bone.parentIndex != -1:
			mat = bone.getMatrix() * mdl.bones[bone.parentIndex].getMatrix().inverse()
			bone.parentName = newBoneMapNames[bone.parentIndex]
			bone.setMatrix(mat * mdl.bones[allBoneNames.index(bone.parentName)].getMatrix()) #relocate bone
		elif i > 0:
			bone.parentName = newBones[0].name
	newBoneNames = [bone.name.lower() for bone in newBones]
	for i, boneName in enumerate(newBoneMapNames):
		newBoneMap.append(newBoneNames.index(boneName))
	for mesh in mdl.meshes:
		for weightList in mesh.weights:
			weightList.indices = list(weightList.indices)
			for i, idx in enumerate(weightList.indices):
				weightList.indices[i] = newBoneMap[idx]
	'''for anim in mdl.anims:
		anim.bones = newBones
		for kfBone in anim.kfBones:
			kfBone.boneIndex = newBoneMap[kfBone.boneIndex]'''
			
	mdl.setBones(newBones)
	
def recombineNoesisMeshes(mdl):
	
	meshesBySourceName = {}
	for mesh in mdl.meshes:
		meshesBySourceName[mesh.sourceName] = meshesBySourceName.get(mesh.sourceName) or []
		meshesBySourceName[mesh.sourceName].append(mesh)
		
	combinedMeshes = []
	for sourceName, meshList in meshesBySourceName.items():
		newPositions = []
		newUV1 = []
		newUV2 = []
		newUV3 = []
		newTangents = []
		newWeights = []
		newIndices = []
		newColors = []
		for mesh in meshList:
			tempIndices = []
			for index in mesh.indices:
				tempIndices.append(index + len(newPositions))
			newPositions.extend(mesh.positions)
			newUV1.extend(mesh.uvs)
			newUV2.extend(mesh.lmUVs)
			newUV3.extend(mesh.uvxList[0] if len(mesh.uvxList) > 0 else [])
			newColors.extend(mesh.colors)
			newTangents.extend(mesh.tangents)
			newWeights.extend(mesh.weights)
			newIndices.extend(tempIndices)
			
		combinedMesh = NoeMesh(newIndices, newPositions, meshList[0].sourceName, meshList[0].sourceName, mdl.globalVtx, mdl.globalIdx)
		combinedMesh.setTangents(newTangents)
		combinedMesh.setWeights(newWeights)
		combinedMesh.setUVs(newUV1)
		combinedMesh.setUVs(newUV2, 1)
		combinedMesh.setUVs(newUV3, 2)
		combinedMesh.setColors(newColors)
		if len(combinedMesh.positions) > 65535:
			print("Warning: Mesh exceeds the maximum of 65535 vertices (has", str(len(combinedMesh.positions)) + "):\n	", combinedMesh.name)
		else:
			combinedMeshes.append(combinedMesh)
		
	return combinedMeshes

#murmur3 hash algorithm, credit to Darkness for adapting this:
def hash(key, getUnsigned=False):
	
	seed = 0xffffffff
	key = bytearray(key, 'utf8')

	def fmix(h):
		h ^= h >> 16
		h = (h * 0x85ebca6b) & 0xFFFFFFFF
		h ^= h >> 13
		h = (h * 0xc2b2ae35) & 0xFFFFFFFF
		h ^= h >> 16
		return h

	length = len(key)
	nblocks = int(length / 4)

	h1 = seed

	c1 = 0xcc9e2d51
	c2 = 0x1b873593

	for block_start in range(0, nblocks * 4, 4):
		k1 = key[block_start + 3] << 24 | \
			 key[block_start + 2] << 16 | \
			 key[block_start + 1] << 8 | \
			 key[block_start + 0]

		k1 = (c1 * k1) & 0xFFFFFFFF
		k1 = (k1 << 15 | k1 >> 17) & 0xFFFFFFFF
		k1 = (c2 * k1) & 0xFFFFFFFF

		h1 ^= k1
		h1 = (h1 << 13 | h1 >> 19) & 0xFFFFFFFF
		h1 = (h1 * 5 + 0xe6546b64) & 0xFFFFFFFF

	tail_index = nblocks * 4
	k1 = 0
	tail_size = length & 3

	if tail_size >= 3:
		k1 ^= key[tail_index + 2] << 16
	if tail_size >= 2:
		k1 ^= key[tail_index + 1] << 8
	if tail_size >= 1:
		k1 ^= key[tail_index + 0]

	if tail_size > 0:
		k1 = (k1 * c1) & 0xFFFFFFFF
		k1 = (k1 << 15 | k1 >> 17) & 0xFFFFFFFF
		k1 = (k1 * c2) & 0xFFFFFFFF
		h1 ^= k1

	unsigned_val = fmix(h1 ^ length)
	if getUnsigned or unsigned_val & 0x80000000 == 0:
		return unsigned_val
	else:
		return -((unsigned_val ^ 0xFFFFFFFF) + 1)

def hash_wide(key, getUnsigned=False):
    key_temp = ''
    for char in key:
        key_temp += char + '\x00'
    return hash(key_temp, getUnsigned)
	
def forceFindTexture(FileName, startExtension=""):
	global sGameName
	
	for gameName, table in formats.items():
		sGameName = gameName
		ext = table["texExt"]
		texFile = LoadExtractedDir() + FileName + ext
		
		if rapi.checkFileExists(texFile):
			return texFile, ext
			
	return 0, 0

def readTextureData(texData, mipWidth, mipHeight, format):
	
	fmtName = texFormatNames[format] if format in texFormatNames else ""
	
	if format == 71 or format == 72: #ATOS
		texData = rapi.imageDecodeDXT(texData, mipWidth, mipHeight, noesis.FOURCC_DXT1)
	elif format == 77 or format == 78 or fmtName.find("BC3") != -1: #BC3
		texData = rapi.imageDecodeDXT(texData, mipWidth, mipHeight, noesis.FOURCC_BC3)
	elif format == 80 or fmtName.find("BC4") != -1: #BC4 wetmasks
		texData = rapi.imageDecodeDXT(texData, mipWidth, mipHeight, noesis.FOURCC_BC4)
	elif format == 83 or fmtName.find("BC5") != -1: #BC5
		texData = rapi.imageDecodeDXT(texData, mipWidth, mipHeight, noesis.FOURCC_BC5)
		texData = rapi.imageEncodeRaw(texData, mipWidth, mipHeight, "r16g16")
		texData = rapi.imageDecodeRaw(texData, mipWidth, mipHeight, "r16g16")
	elif format == 95 or format == 96 or fmtName.find("BC6") != -1:
		texData = rapi.imageDecodeDXT(texData, mipWidth, mipHeight, noesis.FOURCC_BC6H)
	elif format == 98 or format == 99 or fmtName.find("BC7") != -1:
		texData = rapi.imageDecodeDXT(texData, mipWidth, mipHeight, noesis.FOURCC_BC7)
	elif re.search("[RB]\d\d?", fmtName):
		fmtName = fmtName.split("_")[0].lower()
		texData = rapi.imageDecodeRaw(texData, mipWidth, mipHeight, fmtName)
	else:
		print("Fatal Error: Unsupported texture type: " + str(format))
		return 0
	#print("Detected texture format:", fmtName)
	return texData, fmtName


def isImageBlank(imgData, width=None, height=None, threshold=1):
	first = imgData[0]
	if width and height and width * height > 4096:
		imgData = rapi.imageResample(imgData, width, height, 64, 64)
	for i, b in enumerate(imgData):
		if (i+1) % 4 != 0 and abs(b - first) > threshold: #skip alpha
			return False
	return True
	
def invertRawRGBAChannel(imgData, channelID, bpp=4):
	for i in range(int(len(imgData)/4)):
		imgData[i*4+channelID] = 255 - imgData[i*4+channelID]
	return imgData

def moveChannelsRGBA(sourceBytes, sourceChannel, sourceWidth, sourceHeight, targetBytes, targetChannels, targetWidth, targetHeight):
	outputTargetBytes = copy.copy(targetBytes)
	if sourceBytes == targetBytes and sourceChannel >= 0:
		for ch in targetChannels:
			outputTargetBytes = rapi.imageCopyChannelRGBA32(outputTargetBytes, sourceChannel, ch)
	else:
		resizedSourceBytes = rapi.imageResample(sourceBytes, sourceWidth, sourceHeight, targetWidth, targetHeight)
		nullValue = 1 if sourceChannel == -1 else 255 if sourceChannel == -2 else None
		for i in range(int(len(resizedSourceBytes)/16)):
			for b in range(4):
				for ch in targetChannels:
					outputTargetBytes[i*16 + b*4 + ch] = nullValue or resizedSourceBytes[i*16 + b*4 + sourceChannel]
	return outputTargetBytes

def generateDummyTexture4px(rgbaColor, name="Dummy"):
	imageByteList = []
	for i in range(16):
		imageByteList.extend(rgbaColor)
	imageData = struct.pack("<" + 'B'*len(imageByteList), *imageByteList)
	imageData = rapi.imageDecodeRaw(imageData, 4, 4, "r8g8b8a8")
	return NoeTexture(name, 4, 4, imageData, noesis.NOESISTEX_RGBA32)	

def texLoadDDS(data, texList, texName=""):
	texName = texName or rapi.getInputName()
	bs = NoeBitStream(data)
	magic = bs.readUInt()
	version = bs.readUInt()
	width = bs.readUShort()
	height = bs.readUShort()
	unk00 = bs.readUShort()
	if version == 190820018:
		version = 10
	if version == 143221013:
		version = 36
	
	if version > 27:
		numImages = bs.readUByte()
		oneImgMipHdrSize = bs.readUByte()
		mipCount = int(oneImgMipHdrSize / 16)
	else:
		mipCount = bs.readUByte()
		numImages = bs.readUByte()
	
	format = bs.readUInt()
	unk02 = bs.readUInt()
	unk03 = bs.readUInt()
	unk04 = bs.readUInt()
	
	if version > 27:
		bs.seek(8,1)
	
	mipData = []
	for i in range(numImages):
		mipDataImg = []
		for j in range(mipCount):
			mipDataImg.append([bs.readUInt64(), bs.readUInt(), bs.readUInt()]) #[0]offset, [1]pitch, [2]size
		mipData.append(mipDataImg)
		#bs.seek((mipCount-1)*16, 1) #skip small mipmaps
	
	formatName = texFormatNames[format]
	bpp = fmtNameToBpp[formatName]
	width = int((mipDataImg[0][1] / bpp) * 2)
	print(formatName, bpp)
	
	texFormat = noesis.NOESISTEX_RGBA32
	tex = False
	
	for i in range(numImages):
		mipWidth = width
		mipHeight = height
		
		for j in range(mipCount):
			try:
				bs.seek(mipData[i][j][0])
				texData = bs.readBytes(mipData[i][j][2])
			except:
				if i > 0:
					numImages = i - 1
					print ("Multi-image load stopped early")
					break
				else:
					return 0
			try:
				texData, fmtName = readTextureData(texData, mipWidth, mipHeight, format)
			except:
				print("Failed", mipWidth, mipHeight, format, texData)
				texData, fmtName = readTextureData(texData, mipWidth, mipHeight, format)
			if texData == 0:
				return 0
			
			tex = NoeTexture(texName, int(mipWidth), int(mipHeight), texData, texFormat)
			texList.append(tex)
			
			if not bImportMips:
				break
			if mipWidth > 4: 
				mipWidth = int(mipWidth / 2)
			if mipHeight > 4: 
				mipHeight = int(mipHeight / 2)
				
	return tex
	
def getNoesisDDSType(imgType):
	ddsFmt = noesis.NOE_ENCODEDXT_BC7
	if imgType == 71 or imgType == 72: ddsFmt = noesis.NOE_ENCODEDXT_BC1
	elif imgType == 80: ddsFmt = noesis.NOE_ENCODEDXT_BC4
	elif imgType == 83: ddsFmt = noesis.NOE_ENCODEDXT_BC5
	elif imgType == 95: ddsFmt = noesis.NOE_ENCODEDXT_BC6H
	elif imgType == 98 or imgType == 99: ddsFmt = noesis.NOE_ENCODEDXT_BC7
	elif imgType == 28 or imgType == 29: ddsFmt = "r8g8b8a8"
	elif imgType == 77: ddsFmt = noesis.NOE_ENCODEDXT_BC3;
	elif imgType == 10 or imgType == 95: ddsFmt = "r16g16b16a16"
	elif imgType == 61: ddsFmt = "r8"
	return ddsFmt

def findSourceTexFile(version_no, outputName=None):
	newTexName = outputName or rapi.getOutputName().lower()
	while newTexName.find("out.") != -1: 
		newTexName = newTexName.replace("out.",".")
	newTexName =  newTexName.replace(".dds","").replace(".tex","").replace(".jpg","").replace(".png","").replace(".tga","").replace(".gif","")
	for gameName, tbl in formats.items():
		newTexName = newTexName.replace(tbl["texExt"], "")
	ext = ".tex." + str(version_no)
	if not rapi.checkFileExists(newTexName + ext):
		for other_ext, subDict in extToFormat.items():
			if rapi.checkFileExists(newTexName + ".tex." + other_ext):
				ext = ".tex." + other_ext
	return newTexName + ext, ext
	
def convertTexVersion(version_no): #because RE3R and RE4R randomly decide to use timestamps for version numbers, which doesnt work well with using the others as versions
	if version_no == 143221013:
		return 36
	if version_no == 190820018:
		return 10
	return version_no

def texWriteRGBA2(data, width, height, bs, version_no):
	sourceFile = findSourceTexFile(10)
	#print(str(sourceFile))
	tex = texFile(data, width, height, sourceFile[0], rapi.getOutputName())
	if not hasattr(tex, "error"):
		bs = tex.writeTexHeader(bs)
		tex.writeTexImageData(bs, 1)
		return 1
	else:
		sourceFile = (sourceFile and sourceFile[0]) or rapi.getOutputName()
		print("No format detected for " + sourceFile)
	return 0

def texWriteRGBA(data, width, height, bs):
	
	print ("\n			----RE Engine TEX Export----\n")
	
	version_no = int(os.path.splitext(rapi.getOutputName())[1][1:])
	#if noesis.optWasInvoked("-b"): # and version_no >= 28 and version_no < 1000: #batch / no-prompt
	#	return texWriteRGBA2(data, width, height, bs, version_no)
		
	def getExportName(fileName):		
		if fileName == None:
			newTexName = rapi.getOutputName().lower()
		else: 
			newTexName = fileName
		nonlocal version_no
		guessedName, ext = findSourceTexFile(version_no)
		
		newTexName = noesis.userPrompt(noesis.NOEUSERVAL_FILEPATH, "Inject tex", "Choose a tex file to inject", guessedName, None)
		
		if newTexName == None:
			print("Aborting...!")
			return
		return newTexName
		
	fileName = None
	if noesis.optWasInvoked("-b"):
		newTexName, ext = findSourceTexFile(version_no)
	else:
		newTexName = getExportName(fileName)
		if newTexName == None:
			return 0
		while not (rapi.checkFileExists(newTexName)):
			print ("File not found")
			newTexName = getExportName(fileName)	
			fileName = newTexName
			if newTexName == None:
				return 0
		
	bTexAsSource = False	
	newTEX = rapi.loadIntoByteArray(newTexName)
	oldDDS = rapi.loadIntoByteArray(rapi.getInputName())
	
	f = NoeBitStream(newTEX)
	og = NoeBitStream(oldDDS)
	
	magic = f.readUInt()
	version = f.readUInt()
	fWidth = f.readUShort()
	fHeight = f.readUShort()
	reVerseSize = 0
	
	version = convertTexVersion(version)
	
	f.seek(14)
	if version  > 27:
		reVerseSize = 8
		numImages = f.readUByte()
		oneImgMipHdrSize = f.readUByte()
		maxMips = int(oneImgMipHdrSize / 16)
	else:
		maxMips = f.readUByte()
		numImages = f.readUByte()
	
	ddsMagic = og.readUInt()
	bDoEncode = False
	if magic != 5784916:
		print ("Selected file is not a TEX file!\nAborting...")
		return 0
	
	f.seek(16)
	imgType = f.readUInt()
	print ("TEX type:", imgType)
	
	ddsFmt = 0
	bQuitIfEncode = False
	try:
		if imgType == 71 or imgType == 72: ddsFmt = noesis.NOE_ENCODEDXT_BC1
		elif imgType == 80: ddsFmt = noesis.NOE_ENCODEDXT_BC4
		elif imgType == 83: ddsFmt = noesis.NOE_ENCODEDXT_BC5
		elif imgType == 95: ddsFmt = noesis.NOE_ENCODEDXT_BC6H
		elif imgType == 98 or imgType == 99: ddsFmt = noesis.NOE_ENCODEDXT_BC7
		elif imgType == 28 or imgType == 29: ddsFmt = "r8g8b8a8"
		elif imgType == 77: ddsFmt = noesis.NOE_ENCODEDXT_BC3;
		elif imgType == 10 or imgType == 95: ddsFmt = "r16g16b16a16"
		elif imgType == 61: ddsFmt = "r8"
		else: 
			print ("Unknown TEX type:", imgType)
			if imgType != 10:
				return 0
	except: 
		bQuitIfEncode = True

	print ("Exporting over \"" + rapi.getLocalFileName(newTexName)+ "\"")
	
	texFmt = ddsFmt
	#ogHeaderSize = 0
	if og and ddsMagic == 542327876: #DDS
		ogHeaderSize = og.readUInt() + 4
		og.seek(84)
		if og.readUInt() == 808540228: #DX10
			ogHeaderSize += 20
			if ddsFmt == noesis.NOE_ENCODEDXT_BC1:
				print ("Source DDS encoding (BC7) does not match TEX file (BC1).\nEncoding image...")
				bDoEncode = True
		elif ddsFmt == noesis.NOE_ENCODEDXT_BC7:
			print ("Source DDS encoding (BC1) does not match TEX file (BC7).\nEncoding image...")
			bDoEncode = True
	elif og and ddsMagic == 5784916: #TEX
		bTexAsSource = True
		og.seek(4)
		ogVersion = convertTexVersion(og.readUInt())
		
		if ((ogVersion > 27) and int(os.path.splitext(rapi.getOutputName())[1][1:]) < 27) or ((ogVersion < 27 and int(os.path.splitext(rapi.getOutputName())[1][1:]) > 27)):
			print("\nWARNING: Source tex version does not match your output tex version\n	Selected Output:      tex" + str(os.path.splitext(rapi.getOutputName())[1]), "\n	Source Tex version: tex." + str(ogVersion) + "\n")
		og.seek(8)
		ogWidth = og.readUShort()
		ogHeight = og.readUShort()
		if ogWidth != width or ogHeight != height: 
			print ("Input TEX file uses a different resolution from Source TEX file.\nEncoding image...")
			bDoEncode = True
		og.seek(14)
		
		ogHeaderSize = og.readUByte() * 16 + 32
		if ogVersion  > 27: 
			ogHeaderSize = 40 + og.readUByte()
		og.seek(16)
		srcType = og.readUInt()  
		if srcType == 71 or srcType == 72: texFmt = noesis.NOE_ENCODEDXT_BC1
		elif srcType == 80: texFmt = noesis.NOE_ENCODEDXT_BC4
		elif srcType == 83: texFmt = noesis.NOE_ENCODEDXT_BC5
		elif srcType == 95: texFmt = noesis.NOE_ENCODEDXT_BC6H
		elif srcType == 98 or srcType == 99: texFmt = noesis.NOE_ENCODEDXT_BC7
		elif srcType == 28 or srcType == 29: texFmt = "r8g8b8a8"
		elif srcType == 77: texFmt = noesis.NOE_ENCODEDXT_BC3;
		elif srcType == 10 or srcType == 95: texFmt = "r16g16b16a16"
		elif srcType == 61: texFmt = "r8"
		else: 
			print ("Unknown TEX type:", srcType)
			return 0
		if texFmt != ddsFmt or (os.path.splitext(newTexName)[1] == ".30" and os.path.splitext(rapi.getInputName())[1] != ".30"): 
			print ("Input TEX file uses a different compression or format from Source TEX file.\nEncoding image...")
			bDoEncode = True
	else: 
		print ("Input file is not a DDS or TEX file\nEncoding image...")
		bDoEncode = True
	
	mipSize = width * height
	if texFmt == noesis.NOE_ENCODEDXT_BC1: mipSize = int(mipSize / 2)
	if not bDoEncode and mipSize < int((os.path.getsize(rapi.getInputName())) / 4):
		print ("Unexpected source image size\nEncoding image...")
		bDoEncode = True
		
	if not bDoEncode: 
		print ("Copying image data from \"" + rapi.getLocalFileName(rapi.getInputName()) + "\"")
		
	elif bQuitIfEncode:
		print ("Fatal Error: BC7 Encoding not supported!\nUpdate to Noesis v4434 (Oct 14, 2020) or later to encode BC7 images\nAborting...\n")
		return 0
		
	#copy header
	f.seek(0)
	bs.writeBytes(f.readBytes(32 + reVerseSize))
	
	numMips = 0
	output_mips = 0
	dataSize = 0
	totalData = 0
	sizeArray = []
	fileData = []
	mipWidth = width
	mipHeight = height
	
	exportCycles = 1
	if numImages > 1:
		imgToReplace = 0
		imgToReplace = noesis.userPrompt(noesis.NOEUSERVAL_FILEPATH, "Multi-image texture", "Which image do you want to replace? [0-" + str(numImages-1) + "]", "All", None)
		if imgToReplace == None:
			return 0
		try:
			int(imgToReplace)
		except:
			exportCycles = numImages #if not a number, copy all images
		mipWidth = fWidth
		mipHeight = fHeight
		bs.writeBytes(f.readBytes(os.path.getsize(newTexName) - f.tell())) #copy whole file
	
	print ("Format:", ddsFmt)
	for img in range(exportCycles):
			
		#write mipmap headers & encode image
		if img == 0:
			while mipWidth > 4 or mipHeight > 4:
				if ddsFmt == "r8" and numMips > 1:
					break
				numMips += 1
				output_mips += 1
				if bDoEncode:
					mipData = rapi.imageResample(data, width, height, mipWidth, mipHeight)
					try:
						dxtData = rapi.imageEncodeDXT(mipData, 4, mipWidth, mipHeight, ddsFmt)
					except:
						dxtData = rapi.imageEncodeRaw(mipData, mipWidth, mipHeight, ddsFmt)
					mipSize = len(dxtData)
					fileData.append(dxtData)
					
				else:
					mipSize = mipWidth * mipHeight
					if texFmt == noesis.NOE_ENCODEDXT_BC1:
						mipSize = int(mipSize / 2)
					
				sizeArray.append(dataSize)
				dataSize += mipSize
				
				pitch = mipWidth
				if ddsFmt == noesis.NOE_ENCODEDXT_BC1:
					pitch *= 2
				elif ddsFmt != "r8":
					pitch *= 4
					
				bs.writeUInt64(0)
				bs.writeUInt(pitch)
				bs.writeUInt(mipSize)
				
                
				print ("Mip", numMips, ": ", mipWidth, "x", mipHeight, "\n            ", pitch, "\n            ", mipSize)
				if mipWidth > 4: mipWidth = int(mipWidth / 2)
				if mipHeight > 4: mipHeight = int(mipHeight / 2)
		
		if numImages > 1: #multi image images seek to their image data and encode at the same size 
			if exportCycles > 1:
				mipOffset = readUIntAt(f, (32 + reVerseSize + 16 * (int(img) * maxMips)) ) #copy same image data over the other images
				print ("Img",  img+1, "of", exportCycles)
			else:
				mipOffset = readUIntAt(f, (32 + reVerseSize + 16 * (int(imgToReplace) * maxMips)) )
		
		if bDoEncode: 
			if numImages > 1:
				bs.seek(mipOffset)
			for d in range(len(fileData)): #write image data
				bs.writeBytes(fileData[d])
		elif not numImages > 1:
			og.seek(ogHeaderSize) #copy image data
			bs.writeBytes(og.readBytes(os.path.getsize(rapi.getInputName()) - ogHeaderSize))
		else:
			og.seek(ogHeaderSize)
			bs.seek(mipOffset) #seek to image-to-replace
			bs.writeBytes(og.readBytes(os.path.getsize(rapi.getInputName()) - ogHeaderSize))
		
		#adjust header
		if numImages == 1:
			bs.seek(28)
			if readUByteAt(f, 28) > 127:
				bs.writeUByte(128) #ReVerse streaming
			else: 
				bs.writeUByte(0) #streaming texture
				
			bs.seek(8)
			bs.writeUShort(width)
			bs.writeUShort(height)
			if version  > 27:
				bs.seek(15)
				bs.writeUByte(numMips * 16)
			else:
				bs.seek(14)
				bs.writeUByte(numMips)
			
			bsHeaderSize = output_mips * 16 + 32 + reVerseSize
			bs.seek(32 + reVerseSize)
			
			for mip in range(numMips):
				bs.writeUInt64(sizeArray[mip] + bsHeaderSize)
				bs.seek(8, 1)	
		else:
			if numImages > 1:
				bs.seek(mipOffset)
			for d in range(len(fileData)): #write image data
				bs.writeBytes(fileData[d])

	return 1

class DialogOptions:
	def __init__(self):
		self.doLoadTex = False
		self.doConvertTex = True
		self.doLODs = False
		self.loadAllTextures = False
		self.reparentHelpers = True
		self.doCreateBoneMap = True
		self.doForceCenter = False
		self.doSync = True
		self.doForceMergeAnims = False
		self.doConvertMatsForBlender = doConvertMatsForBlender
		self.width = 600
		self.height = 810 - (380 - iListboxSize)
		self.texDicts = None
		self.gameName = sGameName
		self.currentDir = ""
		self.motDialog = None
		self.dialog = None

dialogOptions = DialogOptions()

DoubleClickTimer = namedtuple("DoubleClickTimer", "name idx timer")

gamesList = [ "RE7", "RE7RT", "RE2", "RERT", "RE3", "RE4", "RE8", "MHRSunbreak", "DMC5", "SF6", "ReVerse", "ExoPrimal", "AJ_AAT", "DD2", "DRDR" ]
fullGameNames = [
	"Resident Evil 7",
	"Resident Evil 7 RT",
	"Resident Evil 2",
	"Resident Evil 2/3 RT",
	"Resident Evil 3",
	"Resident Evil 4",
	"Resident Evil 8",
	"MH Rise Sunbreak",
	"Devil May Cry 5",
	"Street Fighter 6",
	"Resident Evil ReVerse",
	"ExoPrimal",
	"Apollo Justice AAT",
	"Dragon's Dogma 2",
	"Dead Rising DR",
]
		
class openOptionsDialogImportWindow:
	
	def __init__(self, width=dialogOptions.width, height=dialogOptions.height, args={}):
		global dialogOptions
		
		self.width = width
		self.height = height
		self.args = args
		self.pak = args.get("motlist") or args.get("mesh")
		self.isMotlist = args.get("isMotlist")
		self.path = self.pak.path if self.pak else rapi.getInputName()
		self.loadItems = [rapi.getLocalFileName(self.path)] if not self.isMotlist else []
		self.fullLoadItems = [self.path] if not self.isMotlist else []
		if self.isMotlist: 
			if dialogOptions.motDialog and dialogOptions.motDialog.pak:
				self.pak = dialogOptions.motDialog.pak
				self.path = dialogOptions.motDialog.pak.path
				self.loadItems = dialogOptions.motDialog.loadItems
				self.fullLoadItems = dialogOptions.motDialog.fullLoadItems
			if self.pak:
				self.motItems = [mot.name for mot in self.pak.mots]
		self.name = rapi.getLocalFileName(self.path)
		self.localDir = rapi.getDirForFilePath(self.path)
		dialogOptions.currentDir = dialogOptions.currentDir or self.localDir
		self.currentDir = dialogOptions.currentDir
		self.localRoot = findRootDir(self.path)
		self.baseDir = LoadExtractedDir(sGameName)
		self.allFiles = []
		self.pakFiles = []
		self.subDirs = []
		self.motItems = []
		self.loadedMlists = {}
		self.pakIdx = 0
		self.baseIdx = -1
		self.loadIdx = 0
		self.dirIdx = 0
		self.gameIdx = 0
		self.localIdx = 0
		self.clicker = DoubleClickTimer(name="", idx=0, timer=0)
		dialogOptions.dialog = self
		self.isOpen = True
		self.isCancelled = False
		if self.isMotlist and dialogOptions.motDialog:
			self.motItems = dialogOptions.motDialog.motItems
		
	def openMotlistDialogButton(self, noeWnd, controlId, wParam, lParam):
		if not dialogOptions.motDialog or not dialogOptions.motDialog.isOpen:
			dialogOptions.motDialog = openOptionsDialogImportWindow(None, None, {"isMotlist": True})
			self.noeWnd.closeWindow()
			#dialogOptions.motDialog.createMotlistWindow()
	
	def clickLoadButton(self):
		self.isOpen = False
		if self.isMotlist:
			self.loadedMlists = {}
			bones = self.pak.bones
			totalFrames = self.pak.totalFrames
			mdlBoneNames = [bone.name for bone in bones]
			for path in self.fullLoadItems:
				if ".motlist." in path.lower() and path not in self.loadedMlists and rapi.checkFileExists(path):
					self.loadedMlists[path] = motlistFile(rapi.loadIntoByteArray(path), path)
					self.loadedMlists[path].bones = bones
					self.loadedMlists[path].readBoneHeaders(self.loadItems)
			for i, motName in enumerate(self.loadItems):
				if motName.find("[ALL] - ") == 0:
					fullPath = self.fullLoadItems[i]
					for mot in self.loadedMlists[fullPath].mots:
						if mot.name not in self.loadItems:
							self.loadItems.append(mot.name)
							self.fullLoadItems.append(fullPath)
		self.noeWnd.closeWindow()
		
	def openOptionsButtonLoadEntry(self, noeWnd, controlId, wParam, lParam):
		self.clickLoadButton()
			
	def openOptionsButtonCancel(self, noeWnd, controlId, wParam, lParam):
		self.isCancelled = True
		self.isOpen = False
		self.noeWnd.closeWindow()
		
	def openOptionsButtonParentDir(self, noeWnd, controlId, wParam, lParam):
		if self.localIdx == 0: 
			self.localRoot = os.path.dirname(self.localRoot)
		else:
			self.baseDir = os.path.dirname(self.baseDir)
		self.setDirList()
		self.setPakList()
		if self.subDirs:
			self.dirList.selectString(self.subDirs[0])
			
	def pressLoadListUpButton(self, noeWnd, controlId, wParam, lParam):
		selIdx = self.loadList.getSelectionIndex()
		if selIdx > 0:
			self.loadItems[selIdx], self.loadItems[selIdx-1], self.fullLoadItems[selIdx], self.fullLoadItems[selIdx-1] = self.loadItems[selIdx-1], self.loadItems[selIdx], self.fullLoadItems[selIdx-1], self.fullLoadItems[selIdx]
			for item in self.loadItems: self.loadList.removeString(item)
			for item in self.loadItems: self.loadList.addString(item)
			self.loadList.selectString(self.loadItems[selIdx-1])
			
	def pressLoadListDownButton(self, noeWnd, controlId, wParam, lParam):
		selIdx = self.loadList.getSelectionIndex()
		if selIdx != -1 and selIdx < len(self.loadItems)-1:
			self.loadItems[selIdx], self.loadItems[selIdx+1], self.fullLoadItems[selIdx], self.fullLoadItems[selIdx+1] = self.loadItems[selIdx+1], self.loadItems[selIdx], self.fullLoadItems[selIdx+1], self.fullLoadItems[selIdx]
			for item in self.loadItems: self.loadList.removeString(item)
			for item in self.loadItems: self.loadList.addString(item)
			self.loadList.selectString(self.loadItems[selIdx+1])
	
	def selectBaseListItem(self, noeWnd, controlId, wParam, lParam):
		self.baseIdx = self.baseList.getSelectionIndex()
		dialogOptions.baseSkeleton = self.baseList.getStringForIndex(self.baseIdx)
	
	def selectMotlistItem(self, noeWnd, controlId, wParam, lParam):
		self.motIdx = self.motLoadList.getSelectionIndex()
		if self.clicker.name == "motList" and self.motIdx == self.clicker.idx and time.time() - self.clicker.timer < 0.25:
			addedName = self.motLoadList.getStringForIndex(self.motIdx)
			if addedName not in self.loadItems:
				self.loadItems.append(addedName)
				self.fullLoadItems.append(self.pak.path)
				self.loadList.addString(addedName)
		self.clicker = DoubleClickTimer(name="motList", idx=self.motIdx, timer=time.time())
	
	def selectPakListItem(self, noeWnd, controlId, wParam, lParam):
		self.pakIdx = self.pakList.getSelectionIndex()
		if self.clicker.name == "pakList" and self.pakIdx == self.clicker.idx and time.time() - self.clicker.timer < 0.25:
			path = dialogOptions.currentDir + "\\" + self.pakList.getStringForIndex(self.pakIdx)
			if self.pakIdx == 0: #parent directory
				if dialogOptions.currentDir[-1:] == "\\":
					dialogOptions.currentDir = os.path.dirname(dialogOptions.currentDir)
				lastDir = rapi.getLocalFileName(dialogOptions.currentDir)
				dialogOptions.currentDir = os.path.dirname(dialogOptions.currentDir)
				self.setPakList()
				self.pakList.selectString(lastDir)
			elif self.pakIdx <= len(self.subDirs):
				dialogOptions.currentDir += "\\" + self.pakList.getStringForIndex(self.pakIdx)
				self.setPakList()
			elif self.isMotlist:
				self.pak = motlistFile(rapi.loadIntoByteArray(path), path)
				self.setMotLoadList([mot.name for mot in self.pak.mots]) 
			elif self.pakList.getStringForIndex(self.pakIdx) not in self.loadItems:
				self.loadItems.append(self.pakList.getStringForIndex(self.pakIdx))
				self.fullLoadItems.append(path)
				self.loadList.addString(self.pakList.getStringForIndex(self.pakIdx))
				#self.fullLoadItems = [x for _, x in sorted(zip(self.loadItems, self.fullLoadItems))]
				#self.loadItems = sorted(self.loadItems)
		self.clicker = DoubleClickTimer(name="pakList", idx=self.pakIdx, timer=time.time())
		self.currentDir = dialogOptions.currentDir
	
	def selectLoadListItem(self, noeWnd, controlId, wParam, lParam):
		self.loadIdx = self.loadList.getSelectionIndex()
		if self.clicker.name == "loadList" and self.loadIdx == self.clicker.idx and time.time() - self.clicker.timer < 0.25 and (self.isMotlist or self.loadItems[self.loadIdx] != self.name):
			self.loadList.removeString(self.loadItems[self.loadIdx])
			del self.loadItems[self.loadIdx]
			if not self.isMotlist:
				del self.fullLoadItems[self.loadIdx]
			self.loadIdx = self.loadIdx if self.loadIdx < len(self.loadItems) else self.loadIdx - 1
			if abs(self.loadIdx) < len(self.loadItems):
				self.loadList.selectString(self.loadItems[self.loadIdx])
		self.clicker = DoubleClickTimer(name="loadList", idx=self.loadIdx, timer=time.time())
		self.currentDir = dialogOptions.currentDir
		
	def selectGameBoxItem(self, noeWnd, controlId, wParam, lParam):
		global sGameName
		if self.gameIdx != self.gameBox.getSelectionIndex():
			self.gameIdx = self.gameBox.getSelectionIndex()
			restOfPath = dialogOptions.currentDir.replace(self.baseDir, "").replace(formats[sGameName]["nDir"]+"\\", "")
			sGameName = gamesList[self.gameIdx]
			self.baseDir = LoadExtractedDir(sGameName) #BaseDirectories[sGameName]
			if self.localBox.getStringForIndex(self.localIdx) == "Base Directory":
				dialogOptions.currentDir = self.baseDir
				if restOfPath and os.path.isdir(self.baseDir + restOfPath):
					dialogOptions.currentDir = self.baseDir + restOfPath
				self.setPakList()
		self.currentDir = dialogOptions.currentDir
				
	def selectLocalBoxItem(self, noeWnd, controlId, wParam, lParam):
		if self.localIdx != self.localBox.getSelectionIndex():
			self.localIdx = self.localBox.getSelectionIndex()
			restOfPath = dialogOptions.currentDir.replace(self.localRoot, "").replace(self.baseDir, "").replace(formats[sGameName]["nDir"]+"\\", "")
			if self.localBox.getStringForIndex(self.localIdx) == "Base Directory":
				dialogOptions.currentDir = self.baseDir
				if restOfPath and os.path.isdir(self.baseDir + restOfPath):
					dialogOptions.currentDir = self.baseDir + restOfPath
			else:
				dialogOptions.currentDir = os.path.dirname(self.path)
				if restOfPath and os.path.isdir(self.localRoot + restOfPath):
					dialogOptions.currentDir = self.localRoot + restOfPath
			self.setPakList()
		self.currentDir = dialogOptions.currentDir
		
	def setGameBox(self, list_object=None, current_item=None):
		for i, name in enumerate(fullGameNames):
			self.gameBox.addString(name)
		self.gameBox.selectString(fullGameNames[gamesList.index(sGameName)])
		self.gameIdx = self.gameBox.getSelectionIndex()
		
	def setLocalBox(self, list_object=None, current_item=None):
		for name in ["Local Folder", "Base Directory"]:
			self.localBox.addString(name)
		self.localBox.selectString("Local Folder")
		self.localIdx = self.localBox.getSelectionIndex()
	
	def checkLoadTexCheckbox(self, noeWnd, controlId, wParam, lParam):
		dialogOptions.doLoadTex = not dialogOptions.doLoadTex
		self.loadTexCheckbox.setChecked(dialogOptions.doLoadTex)
		
	def checkLODsCheckbox(self, noeWnd, controlId, wParam, lParam):
		dialogOptions.doLODs = not dialogOptions.doLODs
		self.LODsCheckbox.setChecked(dialogOptions.doLODs)
		
	def checkConvTexCheckbox(self, noeWnd, controlId, wParam, lParam):
		dialogOptions.doConvertTex = not dialogOptions.doConvertTex
		self.convTexCheckbox.setChecked(dialogOptions.doConvertTex)
		
	def checkFlipUVsCheckbox(self, noeWnd, controlId, wParam, lParam):
		dialogOptions.doFlipUVs = not dialogOptions.doFlipUVs
		self.flipUVsCheckbox.setChecked(dialogOptions.doFlipUVs)
		
	def checkLoadAllTexCheckbox(self, noeWnd, controlId, wParam, lParam):
		dialogOptions.loadAllTextures = not dialogOptions.loadAllTextures
		self.loadAllTexCheckbox.setChecked(dialogOptions.loadAllTextures)
		
	def checkReparentCheckbox(self, noeWnd, controlId, wParam, lParam):
		dialogOptions.reparentHelpers = not dialogOptions.reparentHelpers
		self.reparentCheckbox.setChecked(dialogOptions.reparentHelpers)
		
	def checkFCenterCheckbox(self, noeWnd, controlId, wParam, lParam):
		dialogOptions.doForceCenter = not dialogOptions.doForceCenter
		self.FCenterCheckbox.setChecked(dialogOptions.doForceCenter)
		
	def checkSyncCheckbox(self, noeWnd, controlId, wParam, lParam):
		dialogOptions.doSync = not dialogOptions.doSync
		self.syncCheckbox.setChecked(dialogOptions.doSync)
		
	def checkForceMergeCheckbox(self, noeWnd, controlId, wParam, lParam):
		dialogOptions.doForceMergeAnims = not dialogOptions.doForceMergeAnims
		self.forceMergeCheckbox.setChecked(dialogOptions.doForceMergeAnims)
		
	def setMotLoadList(self, motItems=[]):
		for name in self.motItems:
			self.motLoadList.removeString(name)
		self.motItems = []
		if motItems:
			motItems.insert(0, "[ALL] - " + self.pak.name)
			for name in motItems:
				self.motLoadList.addString(name)
			self.motItems = motItems
			self.motLoadList.selectString(self.motItems[0])
		
	def setLoadList(self, loadItems=[]):
		for item in self.loadItems:
			self.loadList.removeString(item)
		if loadItems:
			self.loadItems = loadItems
			for item in self.loadItems:
				self.loadList.addString(item)
			self.loadList.selectString(self.loadItems[0])
		else:
			self.loadItems = [self.name] if not self.isMotlist else []
			if self.loadItems:
				self.loadList.addString(self.loadItems[0])
		self.loadList.selectString((self.pak and self.pak.path) or rapi.getInputName())
		
	def setPakList(self):
		for name in self.allFiles:
			self.pakList.removeString(name)
		self.allFiles = [".."]
		self.pakFiles = []
		self.subDirs = []
		fmtKey = "mlistExt" if self.isMotlist else "modelExt"
		exts = [formatTbl[fmtKey] for gameName, formatTbl in formats.items()]
		for item in os.listdir(dialogOptions.currentDir):
			if os.path.isdir(os.path.join(dialogOptions.currentDir, item)):
				self.subDirs.append(item)
			if os.path.isfile(os.path.join(dialogOptions.currentDir, item)) and "." in item and os.path.splitext(item)[1] in exts:
				self.pakFiles.append(item)
		self.subDirs = sorted(self.subDirs)
		self.pakFiles = sorted(self.pakFiles)
		self.allFiles.extend(self.subDirs)
		self.allFiles.extend(self.pakFiles)
		for item in self.allFiles:
			self.pakList.addString(item)
		if self.name in self.allFiles:
			self.pakIdx = self.allFiles.index(self.name)
			self.pakList.selectString(self.name)
		elif self.pak and rapi.getLocalFileName(self.pak.path) in self.allFiles:
			self.pakIdx = self.allFiles.index(rapi.getLocalFileName(self.pak.path))
			self.pakList.selectString(self.pakList.getStringForIndex(self.pakIdx))
		elif self.pakIdx < len(self.allFiles):
			self.pakList.selectString(self.pakList.getStringForIndex(self.pakIdx))
		else:
			self.pakIdx = 0
			self.pakList.selectString(self.pakList.getStringForIndex(0))
		self.currentDirEditBox.setText(dialogOptions.currentDir)
		
	def inputCurrentDirEditBox(self, noeWnd, controlId, wParam, lParam):
		text = self.currentDirEditBox.getText().lower()
		if text != dialogOptions.currentDir.lower() and os.path.exists(text):
			dialogOptions.currentDir = os.path.dirname(text) if os.path.isfile(text) else text
			self.setPakList()
			if os.path.isfile(text):
				lowerAllFiles = [name.lower() for name in self.allFiles]
				if rapi.getLocalFileName(text) in lowerAllFiles:
					self.pakList.selectString(self.pakList.getStringForIndex(lowerAllFiles.index(rapi.getLocalFileName(text))))
				if self.isMotlist and ".motlist" in text:
					self.pak = motlistFile(rapi.loadIntoByteArray(text), text)
					self.setMotLoadList([mot.name for mot in self.pak.mots]) 
					
	def inputGlobalScaleEditBox(self, noeWnd, controlId, wParam, lParam):
		global fDefaultMeshScale
		try:
			if self.globalScaleEditBox.getText():
				newScale = float(self.globalScaleEditBox.getText())
				if newScale:
					fDefaultMeshScale = newScale
		except ValueError:
			print("Non-numeric scale input, resetting to ", fDefaultMeshScale)
			self.globalScaleEditBox.setText(str(fDefaultMeshScale))
			
	def create(self, width=dialogOptions.width, height=dialogOptions.height):
		self.noeWnd = noewin.NoeUserWindow("RE Engine '.mesh' Plugin                                " + rapi.getLocalFileName(self.name), "HTRAWWindowClass", width, height) 
		noeWindowRect = noewin.getNoesisWindowRect()
		if noeWindowRect:
			windowMargin = 100
			self.noeWnd.x = noeWindowRect[0] + windowMargin
			self.noeWnd.y = noeWindowRect[1] + windowMargin  
		return self.noeWnd.createWindow()
		
	def createMotlistWindow(self, width=dialogOptions.width, height=800):
		
		if self.create(width, height):
			self.noeWnd.setFont("Futura", 14)
			
			self.noeWnd.createStatic("Motlist files from:", 5, 5, width-20, 20)
			index = self.noeWnd.createEditBox(5, 25, width-20, 45, dialogOptions.currentDir, self.inputCurrentDirEditBox) #EB
			self.currentDirEditBox = self.noeWnd.getControlByIndex(index)
			
			index = self.noeWnd.createListBox(5, 80, width-20, 160, self.selectPakListItem, noewin.LBS_NOTIFY | noewin.WS_VSCROLL | noewin.WS_BORDER) #LB
			self.pakList = self.noeWnd.getControlByIndex(index)
			
			self.noeWnd.createStatic("Motions:", 5, 240, width-20, 20)
			index = self.noeWnd.createListBox(5, 260, width-20, 200, self.selectMotlistItem, noewin.LBS_NOTIFY | noewin.WS_VSCROLL | noewin.WS_BORDER) #LB
			self.motLoadList = self.noeWnd.getControlByIndex(index)
			
			self.noeWnd.createStatic("Motions to load:", 5, 465, width-20, 20)
			index = self.noeWnd.createListBox(5, 485, width-40, 150, self.selectLoadListItem, noewin.LBS_NOTIFY | noewin.WS_VSCROLL | noewin.WS_BORDER) #LB
			self.loadList = self.noeWnd.getControlByIndex(index)
			
			self.noeWnd.createButton("↑", width-30, 525, 20, 30, self.pressLoadListUpButton)
			self.noeWnd.createButton("↓", width-30, 565, 20, 30, self.pressLoadListDownButton)
			
			if True:
				index = self.noeWnd.createCheckBox("Force Center", 10, 640, 100, 30, self.checkFCenterCheckbox)
				self.FCenterCheckbox = self.noeWnd.getControlByIndex(index)
				self.FCenterCheckbox.setChecked(dialogOptions.doForceCenter)
				
				index = self.noeWnd.createCheckBox("Sync by Frame Count", 10, 670, 160, 30, self.checkSyncCheckbox)
				self.syncCheckbox = self.noeWnd.getControlByIndex(index)
				self.syncCheckbox.setChecked(dialogOptions.doSync)
				
				index = self.noeWnd.createCheckBox("Force Merge All", 10, 700, 160, 30, self.checkForceMergeCheckbox)
				self.forceMergeCheckbox = self.noeWnd.getControlByIndex(index)
				self.forceMergeCheckbox.setChecked(dialogOptions.doForceMergeAnims)

			self.noeWnd.createStatic("Game:", width-218, 645, 60, 20)
			index = self.noeWnd.createComboBox(width-170, 645, 150, 20, self.selectGameBoxItem, noewin.CBS_DROPDOWNLIST) #CB
			self.gameBox = self.noeWnd.getControlByIndex(index)
			
			self.noeWnd.createStatic("View:", width-210, 675, 60, 20)
			index = self.noeWnd.createComboBox(width-170, 675, 150, 20, self.selectLocalBoxItem, noewin.CBS_DROPDOWNLIST) #CB
			self.localBox = self.noeWnd.getControlByIndex(index)
			
			self.noeWnd.createStatic("Scale:", width-215,705, 60, 20)
			index = self.noeWnd.createEditBox(width-170, 705, 80, 20, str(fDefaultMeshScale), self.inputGlobalScaleEditBox, False) #EB
			self.globalScaleEditBox = self.noeWnd.getControlByIndex(index)
			
			self.noeWnd.createButton("Load" if not self.isMotlist or self.args.get("motlist") else "OK", 5, height-70, width-160, 30, self.openOptionsButtonLoadEntry)
			self.noeWnd.createButton("Cancel", width-96, height-70, 80, 30, self.openOptionsButtonCancel)
			
			self.setMotLoadList([mot.name for mot in self.pak.mots] if self.pak else [])
			self.setLoadList(self.loadItems)
			self.setPakList()
			self.setGameBox(self.gameBox)
			self.setLocalBox(self.localBox)
			
			self.noeWnd.doModal()
	
	def createMeshWindow(self, width=dialogOptions.width, height=dialogOptions.height):
		
		if self.create(width, height):
			self.noeWnd.setFont("Futura", 14)
			
			self.noeWnd.createStatic("Mesh files from:", 5, 5, width-20, 20)
			index = self.noeWnd.createEditBox(5, 25, width-20, 45, dialogOptions.currentDir, self.inputCurrentDirEditBox) #EB
			self.currentDirEditBox = self.noeWnd.getControlByIndex(index)
			
			index = self.noeWnd.createListBox(5, 80, width-20, iListboxSize, self.selectPakListItem, noewin.LBS_NOTIFY | noewin.WS_VSCROLL | noewin.WS_BORDER) #LB
			self.pakList = self.noeWnd.getControlByIndex(index)
			
			self.noeWnd.createStatic("Files to load:", 5, iListboxSize+85, width-20, 20)
			index = self.noeWnd.createListBox(5, iListboxSize+105, width-40, 150, self.selectLoadListItem,  noewin.LBS_NOTIFY | noewin.WS_VSCROLL | noewin.WS_BORDER) #LB
			self.loadList = self.noeWnd.getControlByIndex(index)
			
			self.noeWnd.createButton("↑", width-30, iListboxSize+145, 20, 30, self.pressLoadListUpButton)
			self.noeWnd.createButton("↓", width-30, iListboxSize+185, 20, 30, self.pressLoadListDownButton)
			
			if True:
				index = self.noeWnd.createCheckBox("Load Textures", 10, iListboxSize+265, 130, 30, self.checkLoadTexCheckbox)
				self.loadTexCheckbox = self.noeWnd.getControlByIndex(index)
				self.loadTexCheckbox.setChecked(dialogOptions.doLoadTex)
				
				
				index = self.noeWnd.createCheckBox("Load All Textures", 150, iListboxSize+265, 160, 30, self.checkLoadAllTexCheckbox)
				self.loadAllTexCheckbox = self.noeWnd.getControlByIndex(index)
				self.loadAllTexCheckbox.setChecked(dialogOptions.loadAllTextures)
				
				index = self.noeWnd.createCheckBox("Convert Textures", 10, iListboxSize+295, 130, 30, self.checkConvTexCheckbox)
				self.convTexCheckbox = self.noeWnd.getControlByIndex(index)
				self.convTexCheckbox.setChecked(dialogOptions.doConvertTex)
				
				index = self.noeWnd.createCheckBox("Collapse Bones", 150, iListboxSize+295, 120, 30, self.checkReparentCheckbox)
				self.reparentCheckbox = self.noeWnd.getControlByIndex(index)
				self.reparentCheckbox.setChecked(dialogOptions.reparentHelpers)
				
				'''index = self.noeWnd.createCheckBox("Import LODs", 10, iListboxSize+365, 100, 30, self.checkLODsCheckbox) #TODO
				self.LODsCheckbox = self.noeWnd.getControlByIndex(index)
				self.LODsCheckbox.setChecked(dialogOptions.doLODs)'''
				
				self.noeWnd.createButton("Select Animations", 150, iListboxSize+325, 200, 30, self.openMotlistDialogButton)

			self.noeWnd.createStatic("Game:", width-248, iListboxSize+270, 60, 20)
			index = self.noeWnd.createComboBox(width-200, iListboxSize+265, 180, 20, self.selectGameBoxItem, noewin.CBS_DROPDOWNLIST) #CB
			self.gameBox = self.noeWnd.getControlByIndex(index)
			
			self.noeWnd.createStatic("View:", width-240, iListboxSize+300, 60, 20)
			index = self.noeWnd.createComboBox(width-200, iListboxSize+295, 180, 20, self.selectLocalBoxItem, noewin.CBS_DROPDOWNLIST) #CB
			self.localBox = self.noeWnd.getControlByIndex(index)
			
			self.noeWnd.createStatic("Scale:", width-145, iListboxSize+330, 60, 20)
			index = self.noeWnd.createEditBox(width-100, iListboxSize+330, 80, 20, str(fDefaultMeshScale), self.inputGlobalScaleEditBox, False) #EB
			self.globalScaleEditBox = self.noeWnd.getControlByIndex(index)
			
			self.noeWnd.createButton("Load", 5, iListboxSize+360, width-160, 30, self.openOptionsButtonLoadEntry)
			
			self.noeWnd.createButton("Cancel", width-96, iListboxSize+360, 80, 30, self.openOptionsButtonCancel)
			
			self.setLoadList(self.loadItems)
			self.setPakList()
			self.setGameBox(self.gameBox)
			self.setLocalBox(self.localBox)
			
			if noesis.optWasInvoked("-b") or bNoImportMenu:
				self.clickLoadButton()
			
			self.noeWnd.doModal()
	
class openOptionsDialogExportWindow:
	
	def __init__(self, width, height, args):
		self.width = width
		self.height = height
		self.filepath = args.get("filepath") or ""
		self.texformat = args.get("texformat") or 98
		self.exportType = args.get("exportType") or os.path.splitext(rapi.getOutputName())[-1]
		self.sourceList = args.get("sourceList") or getSameExtFilesInDir(self.filepath)
		self.currentIdx = 0
		self.doWriteBones = False
		self.doRewrite = False
		self.doCancel = True
		self.failed = False
		self.doVFX = noesis.optWasInvoked("-vfx")
		self.indices = []
		self.LODDist = 0.02667995
		self.flag = -1
		
	def openOptionsVFXCheckbox(self, noeWnd, controlId, wParam, lParam):
		self.doVFX = not self.doVFX
		self.vfxCheckbox.setChecked(self.doVFX)
		
	def openOptionsButtonRewrite(self, noeWnd, controlId, wParam, lParam):
		self.doCancel = False
		self.doRewrite = True
		self.noeWnd.closeWindow()
	
	def openOptionsButtonExport(self, noeWnd, controlId, wParam, lParam):
		self.doCancel = False
		self.noeWnd.closeWindow()
		
	def openOptionsButtonExportBones(self, noeWnd, controlId, wParam, lParam):
		self.doCancel = False
		self.doWriteBones = True
		self.noeWnd.closeWindow()
		
	def openOptionsButtonCancel(self, noeWnd, controlId, wParam, lParam):
		self.noeWnd.closeWindow()
		
	def openBrowseMenu(self, noeWnd, controlId, wParam, lParam):
		filepath = noesis.userPrompt(noesis.NOEUSERVAL_FILEPATH, "Inject " + self.exportType.upper(), "Choose a " + self.exportType.upper() + " file to inject", self.filepath, None)
		if filepath:
			self.filepath = filepath
			#self.meshFile.setText(self.filepath)
			#if rapi.checkFileExists(filepath):
			self.clearComboBoxList()
			self.sourceList = getSameExtFilesInDir(self.filepath)
			self.setComboBoxList(self.meshFileList, self.filepath)
			
	def openOptionsButtonCancel(self, noeWnd, controlId, wParam, lParam):
		self.noeWnd.closeWindow()
	
	def inputMeshFileEditBox(self, noeWnd, controlId, wParam, lParam):
		self.meshEditText = self.meshFile.getText()
		self.meshFile.setText(self.meshEditText)
		if rapi.checkFileExists(self.meshEditText):
			self.filepath = self.meshEditText
			self.clearComboBoxList()
			self.sourceList = getSameExtFilesInDir(self.filepath)
			self.setComboBoxList(self.meshFileList, self.filepath)
		
	def inputFlagEditBox(self, noeWnd, controlId, wParam, lParam):
		if self.FlagBox.getText() != "":
			self.flag = int(self.FlagBox.getText())
		
	def inputLODDistEditBox(self, noeWnd, controlId, wParam, lParam):
		self.LODDist = float(self.LODEditBox.getText())
	
	def selectTexListItem(self, noeWnd, controlId, wParam, lParam):
		self.currentIdx = self.texType.getSelectionIndex()
		self.texformat = self.indices[self.currentIdx]
		filepath = rapi.getOutputName()
		filename = rapi.getExtensionlessName(filepath)
		self.outputFileName = filepath.replace(filename, filename + "." + str(self.texformat))
		print(self.outputFileName)
		
	def selectSourceListItem(self, noeWnd, controlId, wParam, lParam):
		self.currentIdx = self.meshFileList.getSelectionIndex()
		if self.sourceList and self.currentIdx:
			self.filepath = self.sourceList[self.currentIdx]
	
	def clearComboBoxList(self, list_object=None):
		#list_object = list_object or self.meshFileList
		for item in self.sourceList:
			self.meshFileList.removeString(item)
		#list_object.resetContent()
		
	def setComboBoxList(self, list_object=None, current_item=None):
		for item in self.sourceList:
			self.meshFileList.addString(item)
		self.meshFileList.selectString(current_item)
		self.currentIdx = self.meshFileList.getSelectionIndex()
	
	def create(self, width=None, height=None):
		width = width or self.width
		height = height or self.height
		self.noeWnd = noewin.NoeUserWindow("RE Engine MESH Options", "HTRAWWindowClass", width, height) 
		noeWindowRect = noewin.getNoesisWindowRect()
		if noeWindowRect:
			windowMargin = 100
			self.noeWnd.x = noeWindowRect[0] + windowMargin
			self.noeWnd.y = noeWindowRect[1] + windowMargin  
		return self.noeWnd.createWindow()
		
	def createMeshWindow(self, width=None, height=None):
		width = width or self.width
		height = height or self.height
		if self.create(width, height):
			row1_y = 0
			row2_y = 30
			exportRow_y = 60
			#row4_y = 100
			self.noeWnd.setFont("Futura", 14)
			
			#self.noeWnd.createStatic("Export Over Mesh", 5, row1_y, 140, 20)
			#index = self.noeWnd.createEditBox(5, 25, width-20, 20, self.filepath, self.inputMeshFileEditBox)
			#self.meshFile = self.noeWnd.getControlByIndex(index)
			
			index = self.noeWnd.createCheckBox("VFX Mesh", 5, row1_y, 80, 30, self.openOptionsVFXCheckbox)
			self.vfxCheckbox = self.noeWnd.getControlByIndex(index)
			self.vfxCheckbox.setChecked(self.doVFX)
			
			index = self.noeWnd.createComboBox(5, row2_y, width-20, 20, self.selectSourceListItem, noewin.CBS_DROPDOWNLIST)
			self.meshFileList = self.noeWnd.getControlByIndex(index)
			self.setComboBoxList(self.meshFileList, self.filepath)
			
			self.noeWnd.createButton("Browse", 5, exportRow_y, 80, 30, self.openBrowseMenu)
			if rapi.checkFileExists(self.filepath):
				self.noeWnd.createButton("Export", width-416, exportRow_y, 80, 30, self.openOptionsButtonExport)
				self.noeWnd.createButton("Export New Bones", width-326, exportRow_y, 130, 30, self.openOptionsButtonExportBones)
			self.noeWnd.createButton("Rewrite", width-186, exportRow_y, 80, 30, self.openOptionsButtonRewrite)
			self.noeWnd.createButton("Cancel", width-96, exportRow_y, 80, 30, self.openOptionsButtonCancel)
			
			
			self.noeWnd.createStatic("Rewrite Options:", 450, 100, 140, 20)
			self.noeWnd.createStatic("Flag:", 5, 130, 140, 20)
			index = self.noeWnd.createEditBox(45, 125, 40, 30, "", self.inputFlagEditBox, False)
			self.FlagBox = self.noeWnd.getControlByIndex(index)
			
			self.noeWnd.createStatic("LOD0 Factor:", 775, 130, 140, 20)
			index = self.noeWnd.createEditBox(885, 125, 100, 30, str(self.LODDist), self.inputLODDistEditBox, False)
			self.LODEditBox = self.noeWnd.getControlByIndex(index)
			
			self.noeWnd.doModal()
		else:
			print("Failed to create Noesis Window")
			self.failed = True
			
	def createTexWindow(self, width=None, height=None):
		width = width or self.width
		height = height or self.height
		if self.create(width, height):
			index = self.noeWnd.createComboBox(5, 5, 180, 20, self.selectTexListItem, noewin.CBS_DROPDOWNLIST)
			self.texType = self.noeWnd.getControlByIndex(index)
			for fmt in texFormatNames:
				fmtName = texFormatNames[fmt]
				self.texType.addString(fmtName)
				self.indices.append(fmt)
				if fmt == self.texformat:
					self.texType.selectString(fmtName)
					self.currentIdx = len(self.indices)
			self.noeWnd.createButton("Import", 190, 5, 80, 30, self.openOptionsButtonImport)
			self.noeWnd.createButton("Cancel", 190, 40, 80, 30, self.openOptionsButtonCancel)
			self.noeWnd.doModal()
		else:
			print("Failed to create Noesis Window")
			self.failed = True
			
def UVSLoadModel(data, mdlList):
	global sGameName
	bs = NoeBitStream(data)
	magic = bs.readUInt()
	textureNum = bs.readUInt()
	sequenceNum = bs.readUInt()
	patternNum = bs.readUInt()
	attribute = bs.readUInt()
	reserve = bs.readUInt()
	texturePtr = bs.readUInt64()
	sequencePtr = bs.readUInt64()
	
	patternPtr = bs.readUInt64()
	stringPtr = bs.readUInt64()
		
	uvsTexList = []
	uvsMatList = []
	textures = []
	
	bs.seek(texturePtr)
	texFile = ""
	sGameName = "RE2"
	aspectRatios = []
	for i in range(textureNum):
		aspectRatios.append((1,1))
		mStateHolder = bs.readUInt64()
		mDataPtr = bs.readUInt64()
		mTextureHandleTbl = [bs.readUInt64(), bs.readUInt64(), bs.readUInt64()]
		Name = readUnicodeStringAt(bs, stringPtr + mDataPtr * 2)
		textures.append([mStateHolder, mDataPtr, mTextureHandleTbl, Name])
		
		if texFile != 0:
			texFile, ext = forceFindTexture(Name)
			if texFile != 0:
				textureData = rapi.loadIntoByteArray(texFile)
				matName = rapi.getExtensionlessName(rapi.getExtensionlessName(rapi.getLocalFileName(texFile)))
				noetex = texLoadDDS(textureData, uvsTexList, matName)
				if noetex:
					aspectRatios[len(aspectRatios)-1] = (noetex.width / noetex.height, 1)
					noetex.name = matName
					uvsMatList.append(NoeMaterial(matName, texFile)) 
	
	bs.seek(sequencePtr)
	
	for i in range(sequenceNum):
		ctx = rapi.rpgCreateContext()
		#print ("sequence", bs.tell())
		patternCount = bs.readUInt()
		patternTbl = bs.readUInt()
		pos = bs.tell()
		
		UVs = []
		patterns = []
		bs.seek(patternPtr + patternTbl*32)
		for j in range(patternCount):
			#print("sequence at", bs.tell())
			uvTrans = bs.readUInt64()
			top = bs.readFloat()
			left = bs.readFloat()
			bottom = bs.readFloat()
			right = bs.readFloat()
			textureIndex = bs.readInt()
			cutoutUVCount = bs.readInt()
			patterns.append([uvTrans, left, top, right, bottom, textureIndex, cutoutUVCount])
			topLeft = (top, left, 0)
			bottomLeft = (bottom, left, 0)
			topRight = (top, right, 0)
			bottomRight = (bottom, right, 0)
			UVs = [topLeft, bottomRight, bottomLeft, topRight, bottomRight, topLeft]
			cutOutUVs = []
			for k in range(cutoutUVCount):
				cutoutUV = (bs.readFloat(), bs.readFloat(), 0)
				cutOutUVs.append(cutoutUV)
			cutOutUVsFaces = []
			for k in range(len(cutOutUVs)):
				if k == len(cutOutUVs) - 2:
					cutOutUVsFaces.extend([cutOutUVs[k], cutOutUVs[k+1], cutOutUVs[0]])
				elif k == len(cutOutUVs) - 1:
					cutOutUVsFaces.extend([cutOutUVs[k], cutOutUVs[0], cutOutUVs[1]])
				else:
					cutOutUVsFaces.extend([cutOutUVs[k], cutOutUVs[k+1], cutOutUVs[k+2]])
			UVs.extend(cutOutUVsFaces)
			rapi.rpgSetTransform((NoeVec3((1,0,0)), NoeVec3((0,-1,0)), NoeVec3((0,0,-1)), NoeVec3((0,0,0)))) 
			rapi.rpgSetName("Sequence" + str(i) + "_Pattern_" + str(j))
			if len(uvsMatList) > textureIndex:
				rapi.rpgSetMaterial(uvsMatList[textureIndex].name)
			rapi.immBegin(noesis.RPGEO_TRIANGLE)
			#print ("AR is", aspectRatios[textureIndex][0], aspectRatios[textureIndex][1])
			for k in range(0, len(UVs)):
				stretched = [UVs[k][0] * aspectRatios[textureIndex][0], UVs[k][1] * aspectRatios[textureIndex][1], 0]
				rapi.immUV2(UVs[k])
				rapi.immVertex3(stretched)
			rapi.immEnd()
			
		mdl = rapi.rpgConstructModel()
		if uvsTexList and uvsMatList:
			mdl.setModelMaterials(NoeModelMaterials(uvsTexList, uvsMatList)) 
		mdlList.append(mdl)
		rapi.rpgClearBufferBinds() 
		
		bs.seek(pos)
	return 1

def SCNCheckType(data):
	bs = NoeBitStream(data)
	magic = bs.readUInt()
	if magic == 5129043:
		return 1
	else: 
		print("Fatal Error: Unknown file magic: " + str(hex(magic) + " expected 'SCN '!"))
		return 0

def SCNLoadModel(data, mdlList):
	
	global sGameName
	fName = rapi.getInputName().upper()
	guessedName = "RE8" if "RE8" in fName else "RE7" if "RE7" in fName else "RE2" if "RE2" in fName else "RE3" if "RE3" in fName else "RE7" if "RE7" in fName \
	else "SF6" if "SF6" in fName else "MHRise" if "MHRISE" in fName else "RE4" if "RE4" in fName else "ExoPrimal" if ("EXO" in fName or "EXP" in fName) else "DMC5" if "DMC5" in fName \
	else "DD2" if "DD2" in fName else "DRDR" if "DRDR" in fName else "AJ_AAT"
	guessedName = guessedName + "RT" if (guessedName + "RT") in fName else guessedName
	
	msg = ''.join([name + ", " for name, formatList in formats.items()])
	inputName = noesis.userPrompt(noesis.NOEUSERVAL_FILEPATH, "SCN Import", "Input the game name:  " + msg, guessedName, None)
	if not inputName: 
		return 0
	#inputName = inputName.upper()
	isRTRemake = (inputName != "RE7RT" and "RT" in inputName)
	inputName = inputName.replace("RT", "") if isRTRemake else inputName
	if inputName not in formats:
		print ("Not a valid game!")
		return 0
	sGameName = inputName
	current_pak_location = LoadExtractedDir(sGameName)
	sGameName = "RERT" if isRTRemake else sGameName
	
	def getAlignedOffset(tell, alignment):
		mask = alignment - 1
		return (tell + mask) & ~mask

	def readByteAndReturn(bs):
		out = bs.readByte()
		bs.seek(-1, 1)
		return out

	def detectedFloat(bs):
		if bs.tell() + 4 > bs.getSize():
			return False
		flt = abs(bs.readFloat())
		return flt == 0 or 0.000000001 <= flt <= 100000000.0

	def detectedBools(bs, atAddress):
		returnPos = bs.tell()
		bs.seek(atAddress)
		nonBoolTotal = sum(abs(bs.readByte()) > 1 for i in range(4))
		bs.seek(returnPos)
		#print(returnPos, nonBoolTotal)
		return nonBoolTotal == 0

	def detectedXform(bs):
		if bs.tell() + 32 >= bs.getSize():
			return False
		returnPos = bs.tell()
		bs.seek(getAlignedOffset(returnPos, 16))
		detected = all(detectedFloat(bs) for i in range(12) if i < 3 or i > 7)
		bs.seek(returnPos)
		return detected
	
	def checkByteIsUnicodeAlt(bs):
		altByte = bs.readUByte()
		return altByte == 0 or 30 <= altByte <= 150 #try to detect Japanese
	
	def detectedString(bs, offset):
		returnPos = bs.tell()
		result = False
		bs.seek(offset)
		if bs.readByte() != 0 and checkByteIsUnicodeAlt(bs) and bs.readByte() != 0 and checkByteIsUnicodeAlt(bs) and bs.readByte() != 0 and checkByteIsUnicodeAlt(bs):
			result = True
		bs.seek(returnPos)
		return result

	def redetectStringBehind(bs, is_second_time):
		pos = bs.tell()
		slash_detected = False
		if detectedString(bs, bs.tell()):
			while detectedString(bs, bs.tell()):
				bs.seek(-2, 1)
				slash_detected = slash_detected or ((readByteAndReturn(bs)) == 47)
			bs.seek(-2, 1)
		if not is_second_time and (detectedString(bs, bs.tell())):
			bs.seek(-10, 1)
			redetectStringBehind(bs, True)
			if not detectedString(bs, bs.tell()+4):
				bs.seek(pos)
		if slash_detected:
			bs.seek(pos)
	
	viaGameObject = namedtuple('viaGameObject', ['Name', 'Tag', 'DrawSelf', 'UpdateSelf', 'TimeScale'])
	
	def readViaGameObject(bs, timescale_offset):
		bs.seek(getAlignedOffset(bs.tell(), 4)+4)
		Name = ReadUnicodeString(bs)
		bs.seek(getAlignedOffset(bs.tell(), 4)+4)
		Tag = ReadUnicodeString(bs)
		DrawSelf = bs.readByte()
		UpdateSelf = bs.readByte()
		bs.seek(timescale_offset)
		#bs.seek(getAlignedOffset(bs.tell(), 4))
		TimeScale = bs.readFloat()
		return viaGameObject(Name, Tag, DrawSelf, UpdateSelf, TimeScale)
	
	viaTransform = namedtuple('viaTransform', ['LocalPosition', 'LocalRotation', 'LocalScale', 'ParentBoneSize', 'ParentBone', 'SameJointsConstraints', 'AbsoluteScaling'])

	def readViaTransform(bs):
		bs.seek(getAlignedOffset(bs.tell(), 16))
		LocalPosition = NoeVec3((bs.readFloat(), bs.readFloat(), bs.readFloat()))
		bs.seek(4,1)
		LocalRotation = NoeQuat((bs.readFloat(), bs.readFloat(), bs.readFloat(), bs.readFloat()))
		LocalScale = NoeVec3((bs.readFloat(), bs.readFloat(), bs.readFloat()))
		bs.seek(4,1)
		bs.seek(getAlignedOffset(bs.tell(), 4))
		ParentBoneSize = bs.readInt()
		ParentBone = ReadUnicodeString(bs)
		SameJointsConstraints = bs.readByte()
		AbsoluteScaling = bs.readByte()
		return viaTransform(LocalPosition, LocalRotation, LocalScale, ParentBoneSize, ParentBone, SameJointsConstraints, AbsoluteScaling)
	
	def findMesh(bs, limitPoint):
		pos = bs.tell()
		meshPath = ReadUnicodeString(bs)
		output = [None, None]
		print("Scanning from", pos, "to", limitPoint, "for meshes")
		while meshPath.find(".mesh") == -1:
			if bs.tell() >= limitPoint: break
			while not detectedString(bs, bs.tell()):
				if bs.tell() >= limitPoint: break
				if bs.tell() + 4 > bs.getSize():
					return output
				bs.seek(4,1)
			try:
				bs.seek(getAlignedOffset(bs.tell()-2, 4))
				meshPath = ReadUnicodeString(bs)
				bs.seek(getAlignedOffset(bs.tell()+1, 4))
			except:
				break
		if meshPath.find(".mesh") != -1 and meshPath.lower().find("occ") == -1:
			meshPath = meshPath.replace("/", "\\")
			meshPath = current_pak_location + meshPath + formats[sGameName]["modelExt"]
			print("Found mesh:", meshPath, "\n")
			bs.seek(getAlignedOffset(bs.tell(), 4)+4)
			mdfPath = ReadUnicodeString(bs)
			if mdfPath.find(".mdf2"):
				mdfPath = mdfPath.replace("/", "\\")
				mdfPath = current_pak_location + mdfPath + formats[sGameName]["mdfExt"].replace(".mdf2", "")
			output = [meshPath, mdfPath]
			
		return output

	def findGameObjects(bs):
		GameObjectAddresses = []
		GameObjects = []
		fileSize = bs.getSize()
		pos = 0
		bs.seek(0)
		tester = bs.readUInt()
		while tester != 5919570 and bs.tell() + 4 < fileSize: #find "RSZ" magic
			bs.seek(-3,1)
			tester = bs.readUInt()
		if tester == 5919570:
			bs.seek(getAlignedOffset(bs.tell(), 4))
			while bs.tell() + 4 < fileSize:
				while tester != 3212836864 and bs.tell() + 4 < fileSize: # 00 00 80 BF , timescale -1.0
					tester = bs.readUInt()
				boolSearchPos = bs.tell()-8
				foundBools = detectedBools(bs, bs.tell()-8)
				foundXform = detectedXform(bs)
				if pos < fileSize - 16 and foundBools and foundXform:
					print ("\nFound possible GameObject at ", bs.tell())
					GameObjectAddresses.append(bs.tell())
				else:
					print ("\nFound possible GameObject at ", bs.tell(), "but xform:", foundXform, "and bools:", foundBools, boolSearchPos)
				tester = bs.readUInt()
			
			if len(GameObjectAddresses) > 0:
				GameObjectAddresses.append(fileSize)
				for i in range(len(GameObjectAddresses)-1):
					bs.seek(GameObjectAddresses[i])
					transform = readViaTransform(bs)
					bs.seek(GameObjectAddresses[i]-28)
					pos2 = bs.tell()
					while not detectedString(bs, bs.tell()) and pos2 - bs.tell() < 12:
						bs.seek(-2,1)
					if pos2 - bs.tell() == 12:
						bs.seek(pos2)
					if detectedString(bs, bs.tell()):
						redetectStringBehind(bs, False)
					st = bs.tell()
					gameobject = readViaGameObject(bs, GameObjectAddresses[i]-4)
					if gameobject.Name and abs(gameobject.DrawSelf) <= 1 and abs(gameobject.UpdateSelf) <= 1 and gameobject.TimeScale == -1:
						meshMDF = findMesh(bs, GameObjectAddresses[i+1])
						GameObjects.append([gameobject, transform, meshMDF[0], meshMDF[1]])
					elif not (abs(gameobject.DrawSelf) <= 1 and abs(gameobject.UpdateSelf) <= 1):
						#print("1. Failed to add GameObject at", st, pos2, abs(gameobject.DrawSelf),  abs(gameobject.UpdateSelf), gameobject.TimeScale, gameobject.Name)
						bs.seek(st - 8)
						pos2 = bs.tell()
						while not detectedString(bs, bs.tell()) and pos2 - bs.tell() < 12:
							bs.seek(-2,1)
						if pos2 - bs.tell() == 12:
							bs.seek(pos2)
						if detectedString(bs, bs.tell()):
							redetectStringBehind(bs, False)
						st = bs.tell()
						gameobject = readViaGameObject(bs, GameObjectAddresses[i]-4)
						if gameobject.Name and abs(gameobject.DrawSelf) <= 1 and abs(gameobject.UpdateSelf) <= 1 and gameobject.TimeScale == -1:
							meshMDF = findMesh(bs, GameObjectAddresses[i+1])
							GameObjects.append([gameobject, transform, meshMDF[0], meshMDF[1]])
						else:
							print("2. Failed to add GameObject at", st, pos2, abs(gameobject.DrawSelf),  abs(gameobject.UpdateSelf), gameobject.TimeScale, gameobject.Name)
						
		print("Num detected:", len(GameObjectAddresses), len(GameObjects))
		return GameObjects
	
	ss = NoeBitStream(data)
	gameObjs = findGameObjects(ss)
	ctx = rapi.rpgCreateContext()
	
	totalTexList = []
	totalMatList = []
	totalBoneList = []
	totalRemapTable = []
	ids = []
	parentIds = []
	gameObjsDict = {}
	
	ss.seek(64+16)
	for i in range(readUIntAt(ss, 4)):
		ids.append(ss.readUInt())
		parentIds.append(ss.readUInt())
		ss.seek(24,1)
		
	counter = 0
	usedNames = {}
	print("Expected GameObject count:", len(parentIds), ", Num found GameObjects:", len(gameObjs))
	#for i, gameObj in enumerate(gameObjs):
	#	print(i, gameObj)
	#return 1
	
	for i, tup in enumerate(gameObjs):
		try:
			gameObjsDict[ids[i]] = tup
		except: 
			pass
			
		if tup[2] != None and rapi.checkFileExists(tup[2]) and tup[0].Name.find("AIMap") == -1:
			mesh = meshFile(rapi.loadIntoByteArray(tup[2]), tup[2])
			mesh.meshFile = tup[2]
			mesh.mdfFile = tup[3]
			mesh.pos = tup[1].LocalPosition
			mesh.rot = tup[1].LocalRotation
			mesh.scl = tup[1].LocalScale
			if i < len(parentIds):
				parentPosition = gameObjsDict[parentIds[i]][1].LocalPosition if parentIds[i] in gameObjsDict else NoeVec3((0,0,0))
				parentRotation = gameObjsDict[parentIds[i]][1].LocalRotation if parentIds[i] in gameObjsDict else NoeQuat((0,0,0,1))
				mesh.pos *= parentRotation.transpose()
				mesh.pos += parentPosition
				mesh.rot = parentRotation * mesh.rot
			
			mesh.fullTexList = totalTexList
			mesh.fullMatList = totalMatList
			mesh.fullBoneList = totalBoneList
			mesh.fullRemapTable = totalRemapTable
			mesh.name = tup[0].Name
			nameCtr = 1
			while mesh.name in usedNames:
				mesh.name = tup[0].Name + "#" + str(nameCtr)
				nameCtr += 1
			usedNames[mesh.name] = True
			mesh.loadMeshFile()
			counter += 1
	#return 1
	try:
		mdl = rapi.rpgConstructModelAndSort()
		mdl.setModelMaterials(NoeModelMaterials(totalTexList, totalMatList))
	except:
		mdl = NoeModel()
		
	mdl.setBones(totalBoneList)
	collapseBones(mdl)
		
	mdlList.append(mdl)
	print("\nLoaded", counter, "MESH files comprised of", len(mdl.meshes), "submeshes")
	
	return 1
	
BoneHeader = namedtuple("BoneHeader", "name pos rot index parentIndex hash mat")

BoneClipHeader = namedtuple("BoneClipHeader", "boneIndex trackFlags boneHash trackHeaderOffset")

BoneTrack = namedtuple("BoneTrack", "flags keyCount frameRate maxFrame frameIndOffs frameDataOffs unpackDataOffs")

Unpacks = namedtuple("Unpacks", "max min")

UnpackVec = namedtuple("UnpackVec", "x y z w")

def readPackedBitsVec3(packedInt, numBits):
	limit = 2**numBits-1
	x = ((packedInt >> 0) 		    & limit) / limit
	y = ((packedInt >> (numBits*1)) & limit) / limit
	z = ((packedInt >> (numBits*2)) & limit) / limit
	return NoeVec3((x, y, z))
	
def convertBits(packedInt, numBits):
	return packedInt / (2**numBits-1)	

def skipToNextLine(bs):
	bs.seek(bs.tell() + 16 - (bs.tell() % 16))

def wRot(quat3):
	RotationW = 1.0 - (quat3[0] * quat3[0] + quat3[1] * quat3[1] + quat3[2] * quat3[2]);
	if RotationW > 0:
		return math.sqrt(RotationW)
	return 0

class motFile:
	
	def __init__(self, dataBytesArray, motlist=[], start=0, motionID=""):
		self.bs = NoeBitStream(dataBytesArray)
		bs = self.bs
		self.start = start
		self.anim = None
		self.frameCount = 0
		self.motlist = motlist
		self.bones = []
		self.version = bs.readUInt()
		bs.seek(12)
		self.motSize = bs.readUInt()
		self.offsToBoneHdrOffs = bs.readUInt64()
		self.boneHdrOffset = 0
		self.boneClipHdrOffset = bs.readUInt64()
		bs.seek(8,1)
		if self.version >= 456:
			bs.seek(8,1)
			clipFileOffset = bs.readUInt64()
			jmapOffset = bs.readUInt64()
			exDataOffset  = bs.readUInt64()
			bs.seek(16,1)
		else:
			self.jmapOffset = bs.readUInt64()
			self.clipFileOffset = bs.readUInt64()
			bs.seek(16,1)
			self.exDataOffset = bs.readUInt64()
		nameOffs = bs.readUInt64()
		self.name = readUnicodeStringAt(bs, nameOffs)
		self.frameCount = bs.readFloat()
		self.name += " (" + str(int(self.frameCount)) + " frames)" + motionID
		self.blending = bs.readFloat()
		self.uknFloat0 = bs.readFloat()
		self.uknFloat0 = bs.readFloat()
		self.boneCount = bs.readShort()
		self.boneClipCount = bs.readShort()
		self.clipCount = bs.readByte()
		self.uknCount = bs.readByte()
		self.frameRate = bs.readShort()
		self.uknCount2 = bs.readShort()
		self.ukn3 = bs.readShort()
		self.boneHeaders = []
		self.boneClipHeaders = []
		self.kfBones = []
		self.doSkip = False
		
	def checkIfSyncMot(self, other):
		return (self.frameCount == other.frameCount)
		'''if self.frameCount == other.frameCount:
			boneNames = [self.motlist.bones[kfBone.boneIndex].name.lower() for kfBone in self.kfBones]
			otherBoneNames = [other.motlist.bones[kfBone.boneIndex].name.lower() for kfBone in other.kfBones]
			counter = 0
			for boneName in boneNames:
				if boneName in otherBoneNames:
					counter += 1
			return (counter / len(boneNames) < 0.25)'''
				
	
	def readFrame(self, ftype, flags, unpacks):
		bs = self.bs
		compression = flags & 0xFF000
		if ftype=="pos" or ftype=="scl":
			defScaleVec = NoeVec3((fDefaultMeshScale, fDefaultMeshScale, fDefaultMeshScale))
			if compression == 0x00000:
				output = NoeVec3((bs.readFloat(), bs.readFloat(), bs.readFloat())) * defScaleVec
			elif compression == 0x20000:
				rawVec = readPackedBitsVec3(bs.readUShort(), 5)
				if self.version <= 65:
					output = NoeVec3((unpacks.max.x * rawVec[0] + unpacks.min.x, unpacks.max.y * rawVec[1] + unpacks.min.z, unpacks.max.y * rawVec[2] + unpacks.min.z)) * defScaleVec
				else:
					output = NoeVec3((unpacks.max.x * rawVec[0] + unpacks.max.w, unpacks.max.y * rawVec[1] + unpacks.min.x, unpacks.max.z * rawVec[2] + unpacks.min.y)) * defScaleVec
			elif compression == 0x24000:
				x = y = z = unpacks.max.x * convertBits(bs.readUShort(), 16) + unpacks.min.x
				output = NoeVec3((x, y, z)) * defScaleVec
			elif compression == 0x44000:
				x = y = z = unpacks.max.x * bs.readFloat() + unpacks.min.x
				output = NoeVec3((x, y, z)) * defScaleVec
			elif compression == 0x40000 or (compression == 0x30000 and self.version <= 65):
				rawVec = readPackedBitsVec3(bs.readUInt(), 10)
				if self.version <= 65:
					output = NoeVec3((unpacks.max.x * rawVec[0] + unpacks.min.x, unpacks.max.y * rawVec[1] + unpacks.min.y, unpacks.max.z * rawVec[2] + unpacks.min.z)) * defScaleVec
				else:
					output = NoeVec3((unpacks.max.x * rawVec[0] + unpacks.max.w, unpacks.max.y * rawVec[1] + unpacks.min.x, unpacks.max.z * rawVec[2] + unpacks.min.y)) * defScaleVec
			elif compression == 0x70000:
				rawVec = readPackedBitsVec3(bs.readUInt64(), 21)
				output = NoeVec3((unpacks.max.x * rawVec[0] + unpacks.min.x, unpacks.max.y * rawVec[1] + unpacks.min.y, unpacks.max.z * rawVec[2] + unpacks.min.z)) * defScaleVec
			elif compression == 0x80000:
				rawVec = readPackedBitsVec3(bs.readUInt64(), 21)
				output = NoeVec3((unpacks.max.x * rawVec[0] + unpacks.max.w, unpacks.max.y * rawVec[1] + unpacks.min.x, unpacks.max.z * rawVec[2] + unpacks.min.y)) * defScaleVec
			elif (compression == 0x31000 and self.version <= 65) or (compression == 0x41000 and self.version >= 78): #LoadVector3sXAxis
				output = NoeVec3((bs.readFloat(), unpacks.max.y, unpacks.max.z)) * defScaleVec
			elif (compression == 0x32000 and self.version <= 65) or (compression == 0x42000 and self.version >= 78): #LoadVector3sYAxis
				output = NoeVec3((unpacks.max.x, bs.readFloat(), unpacks.max.z)) * defScaleVec
			elif (compression == 0x33000 and self.version <= 65) or (compression == 0x43000 and self.version >= 78): #LoadVector3sZAxis
				output = NoeVec3((unpacks.max.x, unpacks.max.y, bs.readFloat())) * defScaleVec
			elif compression == 0x21000:
				output = NoeVec3((unpacks.max.x * convertBits(bs.readUShort(), 16) + unpacks.max.y, unpacks.max.z, unpacks.max.w)) * defScaleVec
			elif compression == 0x22000:
				output = NoeVec3((unpacks.max.y, unpacks.max.x * convertBits(bs.readUShort(), 16) + unpacks.max.z, unpacks.max.w)) * defScaleVec
			elif compression == 0x23000:
				output = NoeVec3((unpacks.max.y, unpacks.max.z, unpacks.max.x * convertBits(bs.readUShort(), 16) + unpacks.max.w)) * defScaleVec
			else:
				print("Unknown", "Translation" if ftype=="pos" else "Scale", "type:", "0x"+'{:02X}'.format(compression))
				output = NoeVec3((0,0,0)) if ftype=="pos" else NoeVec3((100,100,100))
		elif ftype=="rot":
			if compression == 0x00000: #LoadQuaternionsFull
				output = NoeQuat((bs.readFloat(), bs.readFloat(), bs.readFloat(), bs.readFloat())).transpose()
			elif compression == 0xB0000 or compression == 0xC0000: #LoadQuaternions3Component
				#rawVec = [bs.readFloat(), bs.readFloat(), bs.readFloat()]
				#output = NoeQuat((rawVec[0], rawVec[1], rawVec[2], wRot(rawVec))).transpose()
				output = NoeQuat3((bs.readFloat(), bs.readFloat(), bs.readFloat())).toQuat().transpose()
			elif compression == 0x20000: #//LoadQuaternions5Bit RE3
				rawVec = readPackedBitsVec3(bs.readUShort(), 5)
				output = NoeQuat3((unpacks.max.x * rawVec[0] + unpacks.min.x, unpacks.max.y * rawVec[1] + unpacks.min.y, unpacks.max.z * rawVec[2] + unpacks.min.z)).toQuat().transpose()
			elif compression == 0x21000:
				output = NoeQuat3((unpacks.max.x * convertBits(bs.readUShort(), 16) + unpacks.max.y, 0, 0)).toQuat().transpose()
			elif compression == 0x22000:
				output = NoeQuat3((0, unpacks.max.x * convertBits(bs.readUShort(), 16) + unpacks.max.y, 0)).toQuat().transpose()
			elif compression == 0x23000:
				output = NoeQuat3((0, 0, unpacks.max.x * convertBits(bs.readUShort(), 16) + unpacks.max.y)).toQuat().transpose()
			elif compression == 0x30000 and self.version >= 78: #LoadQuaternions8Bit RE3
				rawVec = [convertBits(bs.readUByte(), 8), convertBits(bs.readUByte(), 8), convertBits(bs.readUByte(), 8)]
				output = NoeQuat3((unpacks.max.x * rawVec[0] + unpacks.min.x, unpacks.max.y * rawVec[1] + unpacks.min.y, unpacks.max.z * rawVec[2] + unpacks.min.z)).toQuat().transpose()
			elif compression == 0x30000:
				rawVec = readPackedBitsVec3(bs.readUInt(), 10)
				output = NoeQuat3((unpacks.max.x * rawVec[0] + unpacks.min.x, unpacks.max.y * rawVec[1] + unpacks.min.y, unpacks.max.z * rawVec[2] + unpacks.min.z)).toQuat().transpose()
			elif compression == 0x31000 or compression == 0x41000:
				output = NoeQuat3((bs.readFloat(), 0, 0)).toQuat().transpose()
			elif compression == 0x32000 or compression == 0x42000:
				output = NoeQuat3((0, bs.readFloat(), 0)).toQuat().transpose()
			elif compression == 0x33000 or compression == 0x43000:
				output = NoeQuat3((0, 0, bs.readFloat())).toQuat().transpose()
			elif compression == 0x40000: #LoadQuaternions10Bit RE3
				rawVec = readPackedBitsVec3(bs.readUInt(), 10)
				output = NoeQuat3((unpacks.max.x * rawVec[0] + unpacks.min.x, unpacks.max.y * rawVec[1] + unpacks.min.y, unpacks.max.z * rawVec[2] + unpacks.min.z)).toQuat().transpose()
			elif compression == 0x50000 and self.version <= 65: #LoadQuaternions16Bit RE2
				rawVec = [convertBits(bs.readUShort(), 16), convertBits(bs.readUShort(), 16), convertBits(bs.readUShort(), 16)]
				output = NoeQuat3((unpacks.max.x * rawVec[0] + unpacks.min.x, unpacks.max.y * rawVec[1] + unpacks.min.y, unpacks.max.z * rawVec[2] + unpacks.min.z)).toQuat().transpose()
			elif compression == 0x50000: #LoadQuaternions13Bit RE3
				rawBytes = [bs.readUByte(), bs.readUByte(), bs.readUByte(), bs.readUByte(), bs.readUByte()]
				retrieved = (rawBytes[0] << 32) | (rawBytes[1] << 24) | (rawBytes[2] << 16) | (rawBytes[3] << 8) | (rawBytes[4] << 0)
				rawVec = readPackedBitsVec3(retrieved, 13)
				output = NoeQuat3((unpacks.max.x * rawVec[0] + unpacks.min.x, unpacks.max.y * rawVec[1] + unpacks.min.y, unpacks.max.z * rawVec[2] + unpacks.min.z)).toQuat().transpose()
			elif compression == 0x60000: #LoadQuaternions16Bit RE3
				#output = NoeQuat((0,0,0,1))
				rawVec = [convertBits(bs.readUShort(), 16), convertBits(bs.readUShort(), 16), convertBits(bs.readUShort(), 16)]
				output = NoeQuat3((unpacks.max.x * rawVec[0] + unpacks.min.x, unpacks.max.y * rawVec[1] + unpacks.min.y, unpacks.max.z * rawVec[2] + unpacks.min.z)).toQuat().transpose()
			elif (compression == 0x70000 and self.version <= 65) or (compression == 0x80000 and self.version >= 78): #LoadQuaternions21Bit RE2 and LoadQuaternions21Bit RE3
				rawVec = readPackedBitsVec3(bs.readUInt64(), 21)
				output = NoeQuat3((unpacks.max.x * rawVec[0] + unpacks.min.x, unpacks.max.y * rawVec[1] + unpacks.min.y, unpacks.max.z * rawVec[2] + unpacks.min.z)).toQuat().transpose()
			elif compression == 0x70000 and self.version >= 78: #LoadQuaternions18Bit RE3
				rawBytes = [bs.readUByte(), bs.readUByte(), bs.readUByte(), bs.readUByte(), bs.readUByte(), bs.readUByte(), bs.readUByte()]
				retrieved = (rawBytes[0] << 48) | (rawBytes[1] << 40) | (rawBytes[2] << 32) | (rawBytes[3] << 24) | (rawBytes[4] << 16) | (rawBytes[5] << 8) | (rawBytes[6] << 0)
				rawVec = readPackedBitsVec3(retrieved, 18)
				output = NoeQuat3((unpacks.max.x * rawVec[0] + unpacks.min.x, unpacks.max.y * rawVec[1] + unpacks.min.y, unpacks.max.z * rawVec[2] + unpacks.min.z)).toQuat().transpose()
			else:
				print("Unknown Rotation type:", "0x"+'{:02X}'.format(compression))
				output = NoeQuat((0,0,0,1))
		return output
			
	# Used a lot for merging+moving skeletons of animations and meshes together:
	def readBoneHeaders(self):
		bs = self.bs
		boneHdrOffs = 0
		if self.offsToBoneHdrOffs:
			bs.seek(self.offsToBoneHdrOffs)
			self.boneHdrOffset = bs.readUInt64()
			count = bs.readUInt64()
			if self.boneHdrOffset and count == self.boneCount:
				boneHdrOffs = self.boneHdrOffset
		if boneHdrOffs:
			bs.seek(boneHdrOffs)
			for i in range(count):
				bs.seek(self.boneHdrOffset+80*i)
				boneName = readUnicodeStringAt(bs, bs.readUInt64())
				#boneName = self.motlist.meshBones[i].name if i < len(self.motlist.meshBones) else boneName #SF6 facial anims test
				parentOffset = bs.readUInt64()
				parentIndex = int((parentOffset-self.boneHdrOffset)/80) if parentOffset else -1
				bs.seek(16,1)
				translation = NoeVec4((bs.readFloat(), bs.readFloat(), bs.readFloat(), bs.readFloat()))
				quat = NoeQuat((bs.readFloat(), bs.readFloat(), bs.readFloat(), bs.readFloat())).transpose()
				index = bs.readUInt()
				boneHash = bs.readUInt()
				mat = quat.toMat43()
				mat[3] = translation.toVec3() * fDefaultMeshScale
				self.boneHeaders.append(BoneHeader(name=boneName, pos=translation, rot=quat, index=index, parentIndex=parentIndex, hash=boneHash, mat=mat))
			self.motlist.boneHeaders = self.motlist.boneHeaders or self.boneHeaders
		elif self.motlist.boneHeaders:
			self.boneHeaders = self.motlist.boneHeaders
		elif not self.motlist.searchedForBoneHeaders:
			self.motlist.findBoneHeaders()
		else:
			print("Failed to find bone headers:", self.name)
			return 0
			
		self.bones = []
		if not dialogOptions.dialog or not dialogOptions.dialog.args.get("mesh"):
			
			meshBoneNames = [bone.name.lower() for bone in self.motlist.meshBones]
			motlistBoneNames = [bone.name.lower() for bone in self.motlist.bones]
			
			for i, boneHeader in enumerate(self.boneHeaders):
				if not meshBoneNames or boneHeader.name.lower() in meshBoneNames: #always use additive animations when loading onto meshes
					bone = NoeBone(len(self.bones), boneHeader.name, boneHeader.mat, self.boneHeaders[boneHeader.parentIndex].name if boneHeader.parentIndex != -1 else None, boneHeader.parentIndex)
					self.bones.append(bone)
			
			selfBoneNames = [bone.name.lower() for bone in self.bones]
			addedBones = []
			for i, bone in enumerate(self.bones):
				if bone.parentName and bone.parentName.lower() in motlistBoneNames:
					bone.parentIndex = motlistBoneNames.index(bone.parentName.lower())
				if bone.name.lower() not in motlistBoneNames:
					bone.index = len(self.motlist.bones)
					self.motlist.bones.append(bone)
					motlistBoneNames.append(bone.name.lower())
					addedBones.append(bone)
			for b, bone in enumerate(self.bones):
				if bone.parentIndex != -1 and bone.parentName.lower() in motlistBoneNames:
					mat = self.boneHeaders[b].mat
					bone.setMatrix(mat * self.motlist.bones[motlistBoneNames.index(bone.parentName.lower())].getMatrix())
					'''if bone in addedBones:
						mat = NoeMat43() #remove posed rotation from anim skeleton, and relocate bone to merged parent bone
						mat[3] = self.boneHeaders[b].pos.toVec3()'''
					'''if bone in addedBones:
						childBones = getChildBones(bone, self.motlist.bones, True)
						childMats = []
						for childBone in childBones:
							#print("moving child", childBone.name)
							oldIndex = selfBoneNames.index(childBone.name.lower())
							childMats.append(self.boneHeaders[oldIndex].mat * self.bones[selfBoneNames.index(childBone.parentName.lower())].getMatrix())'''

					'''if bone in addedBones:
						for c, childBone in enumerate(childBones):
							#childBone.setMatrix(childMats[c] * self.motlist.bones[motlistBoneNames.index(childBone.parentName.lower())].getMatrix())
							print("moving child", childBone.name)
							childBone.setMatrix(NoeMat43())'''
				
	def read(self):
		bs = self.bs
		
		if not self.boneHeaders:
			self.readBoneHeaders()
		
		bnClipSz = 24 if self.version==65 else 16 if self.version==43 else 12
		for i in range(self.boneClipCount):
			bs.seek(self.boneClipHdrOffset+bnClipSz*i)
			#print(i, "bnCLipHdr at", bs.tell()+self.start)
			if self.version == 65:
				index = bs.readUShort()
				trackFlags = bs.readUShort()
				boneHash = bs.readUInt()
				bs.seek(8,1)
				trackHeaderOffset = bs.readUInt64()
			else:
				index = bs.readUShort()
				trackFlags = bs.readUShort()
				boneHash = bs.readUInt()
				if  self.version == 43:
					trackHeaderOffset = bs.readUInt64()
				else:
					trackHeaderOffset = bs.readUInt()
			self.boneClipHeaders.append(BoneClipHeader(boneIndex=index, trackFlags=trackFlags, boneHash=boneHash, trackHeaderOffset=trackHeaderOffset))
			
		skipToNextLine(bs)
		self.boneClips = []
		for i in range(self.boneClipCount):
			boneClipHdr = self.boneClipHeaders[i]
			#if self.boneHeaders[boneClipHdr.boneIndex].name in 
			#if (i == 0 and self.boneHeaders[boneClipHdr.boneIndex].name != self.motlist.bones[0].name):
			#	print(self.name, "Ignoring all keyframes for ", self.boneHeaders[boneClipHdr.boneIndex].name)
			#	continue
			tracks = {"pos": None, "rot": None, "scl": None }
			bs.seek(boneClipHdr.trackHeaderOffset)
			for t in range(3):
				if boneClipHdr.trackFlags & (1 << t):
					flags = bs.readUInt()
					keyCount = bs.readUInt()
					frameRate = maxFrame = 0
					if self.version >= 78:
						frameIndOffs = bs.readUInt()
						frameDataOffs = bs.readUInt()
						unpackDataOffs = bs.readUInt()
					else:
						frameRate = float(bs.readUInt())
						maxFrame = bs.readFloat()
						frameIndOffs = bs.readUInt64()
						frameDataOffs = bs.readUInt64()
						unpackDataOffs = bs.readUInt64()
					newTrack = BoneTrack(flags=flags, keyCount=keyCount, frameRate=frameRate, maxFrame=maxFrame, frameIndOffs=frameIndOffs, frameDataOffs=frameDataOffs, unpackDataOffs=unpackDataOffs)
					if (boneClipHdr.trackFlags & (1)) and not tracks.get("pos"): 
						tracks["pos"] = newTrack
					elif (boneClipHdr.trackFlags & (1 << 1)) and not tracks.get("rot"): 
						tracks["rot"] = newTrack
					elif (boneClipHdr.trackFlags & (1 << 2)) and not tracks.get("scl"): 
						tracks["scl"] = newTrack
			if i == 0 and dialogOptions.dialog and dialogOptions.dialog.pak and self.boneHeaders[boneClipHdr.boneIndex].name != dialogOptions.dialog.pak.bones[0].name:
			#if i == 0 and self.boneHeaders[boneClipHdr.boneIndex].name != "root":
				print(self.name, ": Ignoring all keyframes for ", self.boneHeaders[boneClipHdr.boneIndex].name)
				tracks["pos"] = tracks["rot"] = tracks["scl"] = None #remove local root bone translations/rotations for mounted animations like facials 
			elif dialogOptions.doForceCenter and (self.boneHeaders[boneClipHdr.boneIndex].parentIndex == 0 or i == 0):
				print(self.name, ": Ignoring position keyframes for ", self.boneHeaders[boneClipHdr.boneIndex].name)
				tracks["pos"] = None
			self.boneClips.append(tracks)
			
		for i, boneClip in enumerate(self.boneClips):
			motlistBoneIndex = self.motlist.boneHashes.get(self.boneClipHeaders[i].boneHash)
			if motlistBoneIndex != None:
				kfBone = NoeKeyFramedBone(motlistBoneIndex)
				for ftype in ["pos", "rot", "scl"]:
					fHeader = boneClip.get(ftype)
					if fHeader:
						keyCompression = fHeader.flags >> 20
						keyReadFunc = bs.readUInt if keyCompression==5 else bs.readUByte if keyCompression==2 else bs.readUShort
						bs.seek(fHeader.frameIndOffs)
						keyTimes = []
						for k in range(fHeader.keyCount):
							keyTimes.append(keyReadFunc() if fHeader.frameIndOffs else 0)
						if fHeader.unpackDataOffs:
							bs.seek(fHeader.unpackDataOffs)
							unpackMax = UnpackVec(x=bs.readFloat(), y=bs.readFloat(), z=bs.readFloat(), w=bs.readFloat())
							unpackMin = UnpackVec(x=bs.readFloat(), y=bs.readFloat(), z=bs.readFloat(), w=bs.readFloat())
						else:
							unpackMax = unpackMin = UnpackVec(x=0, y=0, z=0, w=0)
						unpackValues = Unpacks(max=unpackMax, min=unpackMin)
						frames = []
						bs.seek(fHeader.frameDataOffs)
						for f in range(fHeader.keyCount):
							frame = self.readFrame(ftype, fHeader.flags, unpackValues)
							if ftype == "scl":
								frame /= 100
							kfValue = NoeKeyFramedValue(keyTimes[f], frame)
							frames.append(kfValue)
						if ftype == "pos": # and self.motlist.bones[motlistBoneIndex].parentIndex != 0:#kfBoneNames:
							kfBone.setTranslation(frames, noesis.NOEKF_TRANSLATION_VECTOR_3)
						elif ftype == "rot":
							kfBone.setRotation(frames, noesis.NOEKF_ROTATION_QUATERNION_4)
						elif ftype == "scl":
							kfBone.setScale(frames, noesis.NOEKF_SCALE_VECTOR_3)
				self.kfBones.append(kfBone)
		motEnd = bs.tell()
		
		#bs.seek(0)
		#self.bs = NoeBitStream(bs.readBytes(motEnd))
		#self.anim = NoeKeyFramedAnim(self.name, self.motlist.bones, self.kfBones, 1)
		

class motlistFile:
	
	def __init__(self, data, path=""):
		global sGameName
		self.bs = NoeBitStream(data)
		bs = self.bs
		self.path = path
		self.bones = []
		self.boneHashes = {}
		self.boneHeaders = []
		self.anims = []
		self.mots = []
		self.meshBones = []
		self.searchedForBoneHeaders = False
		self.totalFrames = 0
		self.version = bs.readInt()
		bs.seek(16)
		pointersOffset = bs.readUInt64()
		motionIDsOffset = bs.readUInt64()
		self.name = readUnicodeStringAt(bs, bs.readUInt64())
		bs.seek(8, 1)
		numOffsets = bs.readUInt()
		bs.seek(pointersOffset)
		self.motionIDs = {}
		self.pointers = []
		sGameName = findGameName("."+str(self.version), "mlistExt")
			
		for i in range(numOffsets):
			if "motionIDsData" in formats[sGameName]:
				bs.seek(motionIDsOffset + i*formats[sGameName]["motionIDsData"][0] + formats[sGameName]["motionIDsData"][1])
				self.motionIDs[i] = bs.readUShort()
			
			bs.seek(pointersOffset + i*8)
			motAddress = bs.readUInt64()
			if motAddress and motAddress not in self.pointers and readUIntAt(bs, motAddress+4) == 544501613: # 'mot'
				self.pointers.append(motAddress)
				bs.seek(motAddress)
				mot = motFile(bs.readBytes(bs.getSize()-bs.tell()), self, motAddress, " ID: " + str(self.motionIDs[i] if i in self.motionIDs else ""))
				self.mots.append(mot)
		
	def findBoneHeaders(self):
		self.searchedForBoneHeaders = True
		for mot in self.mots:
			mot.readBoneHeaders()
			if self.boneHeaders:
				print("Using bone headers from", mot.name)
				break
				
	def readBoneHeaders(self, motNamesToLoad=[]):
		self.boneHashes = {}
		for mot in self.mots:
			if not motNamesToLoad or mot.name in motNamesToLoad:
				mot.readBoneHeaders()
		for i, bone in enumerate(self.bones):
			hash = hash_wide(bone.name, True)
			self.boneHashes[hash] = i #bone.index
				
	def read(self, motNamesToLoad=[]):
		bs = self.bs
		self.readBoneHeaders(motNamesToLoad)
		for i, mot in enumerate(self.mots):
			if not motNamesToLoad or mot.name in motNamesToLoad and not mot.doSkip:
				mot.read()
				
	def makeAnims(self, motNamesToLoad=[]):
		bs = self.bs
		motsToLoad = []
		#check for sync mots:
		for i, mot in enumerate(self.mots):
			if not motNamesToLoad or mot.name in motNamesToLoad and not mot.doSkip:
				motsToLoad.append(mot)
		if (dialogOptions.doSync or dialogOptions.doForceMergeAnims) and dialogOptions.motDialog and len(dialogOptions.motDialog.loadItems) > 0:
			allLoadItems = copy.copy(dialogOptions.motDialog.loadItems)
			allLoadPaths = copy.copy(dialogOptions.motDialog.fullLoadItems)
			for j, otherMotName in enumerate(dialogOptions.motDialog.loadItems):
				if "[ALL]" in otherMotName:
					for mot in dialogOptions.motDialog.loadedMlists[dialogOptions.motDialog.fullLoadItems[j]].mots:
						if mot.name not in allLoadItems:
							allLoadItems.append(mot.name)
							allLoadPaths.append(dialogOptions.motDialog.fullLoadItems[j])
			for i, mot in enumerate(motsToLoad):
				mlistBoneNames = [bone.name.lower() for bone in self.bones]
				for otherPath, otherMlist in dialogOptions.motDialog.loadedMlists.items():
					if otherMlist == self:
						continue
					otherMotNames = [otherMot.name for otherMot in otherMlist.mots]
					for j, otherMotName in enumerate(allLoadItems):
						if mot.motlist.path != allLoadPaths[j] and "[ALL]" not in otherMotName and otherMotName in otherMotNames and not otherMlist.mots[otherMotNames.index(otherMotName)].doSkip and (dialogOptions.doForceMergeAnims or mot.checkIfSyncMot(otherMlist.mots[otherMotNames.index(otherMotName)])):
							syncMot = otherMlist.mots[otherMotNames.index(otherMotName)]
							existingKfBoneNames = [self.bones[kfBone.boneIndex].name for kfBone in mot.kfBones]
							for b, kfBone in enumerate(syncMot.kfBones):
								bone = syncMot.motlist.bones[kfBone.boneIndex]
								if bone.name.lower() not in mlistBoneNames:
									bone.index = len(self.bones)
									self.bones.append(bone)
									mlistBoneNames.append(bone.name.lower())
								if (kfBone.hasAnyKeys() and (bone.name not in existingKfBoneNames or not mot.kfBones[existingKfBoneNames.index(bone.name)].hasAnyKeys())):
									kfBone.boneIndex = mlistBoneNames.index(bone.name.lower())
									mot.kfBones.append(kfBone)
							print("Merged animation ", syncMot.name, "into", mot.name)
							syncMot.doSkip = True
		motsByName = []	
		for i, mot in enumerate(motsToLoad):
			if not mot.doSkip:
				mot.anim = NoeKeyFramedAnim(mot.name, self.bones, mot.kfBones, 1)
				self.anims.append(mot.anim)
				motsByName.append(mot.name)
		if len(self.anims) > 0:
			print("\nImported", len(self.anims), "animations from motlist '", self.name, "':")
			for anim in self.anims:
				print(" @ " + str(int(self.totalFrames)), "	'", anim.name, "'")
				self.totalFrames += self.mots[motsByName.index(anim.name)].frameCount

def motlistCheckType(data):
	bs = NoeBitStream(data)
	magic = readUIntAt(bs, 4)
	if magic == 1953721453:
		return 1
	else: 
		print("Fatal Error: Unknown file magic: " + str(hex(magic) + " expected 'mlst'!"))
		return 0

def motlistLoadModel(data, mdlList):
	ctx = rapi.rpgCreateContext()
	
	dialogOptions.motDialog = None
	motlist = motlistFile(data, rapi.getInputName())
	mlDialog = openOptionsDialogImportWindow(None, None, {"motlist":motlist, "isMotlist":True})
	mlDialog.createMotlistWindow()
	
	mdl = NoeModel()
	
	if not mlDialog.isCancelled:
		mdl.setBones(mlDialog.pak.bones)
		collapseBones(mdl, 100)
		bones = list(mdl.bones)
		mdlBoneNames = [bone.name.lower() for bone in bones]
		sortedMlists = []
		for mlist in [mlDialog.loadedMlists[path] for path in mlDialog.fullLoadItems]:
			if mlist not in sortedMlists:
				sortedMlists.append(mlist)
		for mlist in sortedMlists:
			mlist.readBoneHeaders(mlDialog.loadItems)
			for bone in mlist.bones:
				if bone.name.lower() not in mdlBoneNames:
					bone.index = len(bones)
					bones.append(bone)
		anims = []
		for mlist in sortedMlists:
			mlist.bones = bones
			mlist.readBoneHeaders(mlDialog.loadItems)
			mlist.read(mlDialog.loadItems)
		for mlist in sortedMlists:
			mlist.makeAnims(mlDialog.loadItems)
			anims.extend(mlist.anims)
			
		mdl.setBones(bones)
		mdl.setAnims(anims)
		rapi.setPreviewOption("setAnimSpeed", "60.0")
	
	mdlList.append(mdl)
	
	return 1
	

isMeshVer3 = False
BBskipBytes = numNodesLocation = LOD1OffsetLocation = normalsRecalcOffsLocation = bsHdrOffLocation = bsIndicesOffLocation = \
vBuffHdrOffsLocation = bonesOffsLocation = nodesIndicesOffsLocation = namesOffsLocation = floatsHdrOffsLocation = 0

def setOffsets(ver):
	global BBskipBytes, numNodesLocation, LOD1OffsetLocation, normalsRecalcOffsLocation, bsHdrOffLocation, bsIndicesOffLocation, \
	vBuffHdrOffsLocation, bonesOffsLocation, nodesIndicesOffsLocation, namesOffsLocation, floatsHdrOffsLocation
	BBskipBytes = 				8 	if ver == 1 else 0
	numNodesLocation = 			18 	if ver < 3 else 20
	LOD1OffsetLocation = 		24 	if ver < 3 else 32
	bsHdrOffLocation = 			64 	if ver < 3 else 56
	normalsRecalcOffsLocation = 56 	if ver < 3 else 64
	vBuffHdrOffsLocation = 		80 	if ver < 3 else 72
	floatsHdrOffsLocation = 	72 	if ver < 3 else 96
	bonesOffsLocation = 		48 	if ver < 3 else 104
	nodesIndicesOffsLocation = 	96 	if ver < 3 else 112
	bsIndicesOffLocation = 		112 if ver < 3 else 128
	namesOffsLocation = 		120 if ver < 3 else 144
	
	if sGameName == "AJ_AAT" or sGameName == "DD2" or sGameName == "DRDR":
		namesOffsLocation = 136 # on unrigged meshes its still 144
	#if isExoPrimal:
	#	nodesIndicesOffsLocation = 104
	#	namesOffsLocation = 136 
	
def findGameName(number, formatsKey):
	for gameName, dictionary in formats.items():
		if dictionary[formatsKey] == number:
			return gameName
	return ""


class meshFile(object): 

	def __init__(self, data, path=""):
		self.path = path or rapi.getInputName()
		self.inFile = NoeBitStream(data)
		self.boneList = []
		self.matNames = []
		self.groupIDs = []
		self.matHashes = []
		self.matList = []
		self.texList = []
		self.texNames = []
		self.missingTexNames = []
		self.texColors = []
		self.fullBoneList = []
		self.fullTexList = []
		self.fullMatList = []
		self.fullRemapTable = []
		self.setGameName()
		self.gameName = sGameName
		self.ver = formats[sGameName]["meshVersion"]
		self.mdfVer = formats[sGameName]["mdfVersion"]
		self.name = "LOD" if bShorterNames else "LODGroup"
		self.meshFile = None
		self.mdfFile = None
		self.pos = NoeVec3((0,0,0))
		self.rot = NoeQuat((0,0,0,1))
		self.scl = NoeVec3((1,1,1))
		self.uvBias = {}
		setOffsets(self.ver)
		
	def setGameName(self):
		global sGameName, bSkinningEnabled, isMeshVer3
		sGameName = "RE2"
		meshVersion = readUIntAt(self.inFile, 4)
		isMeshVer3 = False
		if meshVersion == 220822879:
			isMeshVer3 = True
			sGameName = "RE4"
		elif (meshVersion == 220705151 and self.path.find(".220907984") != -1):
			isMeshVer3 = True
			sGameName = "ExoPrimal"
		elif ((meshVersion == 230403828 or meshVersion == 220705151) and (self.path.find(".230110883") != -1) or self.path.find(".220721329") != -1):
			isMeshVer3 = True
			sGameName = "SF6"
		elif meshVersion == 21041600: # or self.path.find(".2109108288") != -1: #RE2RT + RE3RT, and RE7RT
			sGameName = "RE7RT" if self.path.find(".220128762") != -1 else "RERT"
		elif self.path.find(".1808282334") != -1:
			sGameName = "DMC5"
		elif self.path.find(".1902042334") != -1:  #386270720
			sGameName = "RE3"
		elif self.path.find(".2102020001") != -1:
			sGameName = "ReVerse"
		elif meshVersion == 2020091500 or self.path.find(".2101050001") != -1:
			sGameName = "RE8"
		elif (meshVersion == 2007158797 or self.path.find(".2008058288") != -1): #Vanilla MHRise
			sGameName = "MHRise"
		elif (meshVersion == 21061800 or self.path.find(".2109148288") != -1):  #MHRise Sunbreak version
			sGameName = "MHRSunbreak"
		elif (meshVersion == 230406984 or self.path.find(".230612127") != -1): #Apollo Justice
			isMeshVer3 = True
			sGameName = "AJ_AAT"
		elif (meshVersion == 230517984 or self.path.find(".231011879") != -1): #DD2
			isMeshVer3 = True
			sGameName = "DD2"
		elif (meshVersion == 240423829 or self.path.find(".240424828") != -1): #DD2
			isMeshVer3 = True
			sGameName = "DRDR"
		
	'''MDF IMPORT ========================================================================================================================================================================'''
	def createMaterials(self, matCount):
		global bColorize, bPrintMDF, sGameName, sExportExtension, extractedNativesPath
		
		doColorize = bColorize
		doPrintMDF = bPrintMDF
		noMDFFound = 0
		skipPrompt = 0
		
		modelExt = formats[sGameName]["modelExt"]
		texExt = formats[sGameName]["texExt"]
		mmtrExt = formats[sGameName]["mmtrExt"]
		nDir = formats[sGameName]["nDir"]
		mdfExt = formats[sGameName]["mdfExt"]
					
		if extractedNativesPath != "":
			print ("Using this extracted natives path:", extractedNativesPath + "\n")
			
		#Try to guess MDF filename
		inputName = self.path.lower() #rapi.getInputName()
		isSCN = (rapi.getInputName().lower().find(".scn") != -1)
		if inputName.find(".noesis") != -1:
			inputName = rapi.getLastCheckedName()
			skipPrompt = 2
			doPrintMDF = 0
		elif dialogOptions.doLoadTex or isSCN: # len(self.fullMatList) > 0 or len(self.fullBoneList) > 0:
			skipPrompt = 2
			doPrintMDF = 0
		
		pathPrefix = inputName
		while pathPrefix.find("out.") != -1: 
			pathPrefix = pathPrefix.replace("out.",".")
		pathPrefix = pathPrefix.replace(".mesh", "").replace(modelExt,"").replace(".NEW", "")
		
		if sGameName == "ReVerse" and os.path.isdir(os.path.dirname(inputName) + "\\Material"):
			pathPrefix = (os.path.dirname(inputName) + "\\Material\\" + rapi.getLocalFileName(inputName).replace("SK_", "M_")).replace(".NEW", "")
			while pathPrefix.find("out.") != -1: 
				pathPrefix = pathPrefix.replace("out.",".")
			pathPrefix = pathPrefix.replace(".mesh" + modelExt,"")
			if not rapi.checkFileExists(pathPrefix + mdfExt):
				pathPrefix = pathPrefix.replace("00_", "")
			if not rapi.checkFileExists(pathPrefix + mdfExt):
				for item in os.listdir(os.path.dirname(pathPrefix + mdfExt)):
					if mdfExt == (".mdf2" + os.path.splitext(os.path.join(os.path.dirname(pathPrefix), item))[1]):
						pathPrefix = os.path.join(os.path.dirname(pathPrefix), item) #.replace(mdfExt, "")
						break
			
		similarityCounter = 0
		ogFileName = rapi.getLocalFileName(inputName)
		if not rapi.checkFileExists(pathPrefix + mdfExt):
			for item in os.listdir(os.path.dirname(pathPrefix + mdfExt)):
				if mdfExt == (".mdf2" + os.path.splitext(item)[1]):
					test = rapi.getLocalFileName(os.path.join(os.path.dirname(pathPrefix), item)).replace(mdfExt, "")
					sameCharCntr = 0
					for c, char in enumerate(test):
						if c < len(ogFileName) and char == ogFileName[c]:
							sameCharCntr += 1
					if sameCharCntr > similarityCounter:
						pathPrefix = os.path.join(os.path.dirname(pathPrefix), item).replace(mdfExt, "")
						similarityCounter = sameCharCntr
		materialFileName = pathPrefix + mdfExt
		
		if not (rapi.checkFileExists(materialFileName)):
			print(materialFileName, "does not exist!") 
			materialFileName = (pathPrefix + "_mat" + mdfExt)
		if not (rapi.checkFileExists(materialFileName)):
			materialFileName = (pathPrefix + "_00" + mdfExt)
		if not (rapi.checkFileExists(materialFileName)):
			if self.mdfVer >= 2: #sGameName == "RERT" or sGameName == "RE3" or sGameName == "ReVerse" or sGameName == "RE8" or sGameName == "MHRise":
				pathPrefix = extractedNativesPath + re.sub(r'.*stm\\', '', inputName)
			else:
				pathPrefix = extractedNativesPath + re.sub(r'.*x64\\', '', inputName) 
			pathPrefix = pathPrefix.replace(modelExt,"").replace(".mesh","")
			materialFileName = (pathPrefix + mdfExt)
			print (materialFileName)
			if not (rapi.checkFileExists(materialFileName)):
				materialFileName = (pathPrefix + "_mat" + mdfExt)
			if not (rapi.checkFileExists(materialFileName)):
				materialFileName = (pathPrefix + "_00" + mdfExt)
				
		if not (rapi.checkFileExists(materialFileName)):
			materialFileName = noesis.userPrompt(noesis.NOEUSERVAL_FILEPATH, "MDF File Not Found", "Manually enter the name of the MDF file or cancel.", os.path.join(os.path.dirname(inputName), rapi.getLocalFileName(materialFileName)) , None)
			if (materialFileName is None):
				print("No material file.")
				return
			elif not (rapi.checkFileExists(materialFileName)):
				noMDFFound = 1
			skipPrompt = 1
			
		msgName = materialFileName
		
		#Prompt for MDF load
		if not skipPrompt or (skipPrompt == 2 and not rapi.checkFileExists(materialFileName)):
			msgName = noesis.userPrompt(noesis.NOEUSERVAL_FILEPATH, "MDF File Detected", "Load materials? This may take some time.", materialFileName, None)
			if msgName is None:
				print("No material file.")
				return False
				
			'''if msgName.endswith(" -c"):
				print (msgName)
				doColorize = 1
				doPrintMDF = 0												
				msgName = msgName.replace(" -c", "")'''											
			
			if ((rapi.checkFileExists(msgName)) and (msgName.endswith(mdfExt))):
				materialFileName = msgName
			else:
				noMDFFound = 1
		
		#if (bPopupDebug == 1):
			#noesis.logPopup()
		
		#Save a manually entered natives directory path name for later
		if (msgName.endswith("\\natives\\" + nDir + "\\")) and (os.path.isdir(msgName)):
			print ("Attempting to write: ")
			if SaveExtractedDir(msgName, sGameName):
				extractedNativesPath = msgName
				
		if (noMDFFound == 1) or not (rapi.checkFileExists(materialFileName)):
			print("Failed to open material file.")
			return False
	
		texBaseColour = []
		texRoughColour = []
		texSpecColour = []
		texAmbiColour = []
		texMetallicColour = []
		texFresnelColour = []
			
		bs = rapi.loadIntoByteArray(materialFileName)
		bs = NoeBitStream(bs)
		#Magic, Unknown, MaterialCount, Unknown, Unknown
		matHeader = [bs.readUInt(), bs.readUShort(), bs.readUShort(), bs.readUInt(), bs.readUInt()]
		matCountMDF = matHeader[2]
		
		if matCountMDF != matCount and len(self.fullMatList) == 0:
			print ("MDF Checkerboard Error: MDF does not have the same material count as the MESH file!\n	MESH materials:", matCount, "\n	MDF Materials:", matCountMDF)
			return 0
		
		usedMats = [mat.name for mat in self.fullMatList]
		usedTexs = [tex.name for tex in self.fullTexList]
		
		#Parse Materials
		for i in range(matCountMDF):
			
			if self.mdfVer > 3:
				bs.seek(0x10 + (i * 100))
			elif self.mdfVer > 2:
				bs.seek(0x10 + (i * 80))
			else:
				bs.seek(0x10 + (i * 64))
			
			materialNamesOffset = bs.readUInt64()
			materialHash = bs.readInt()
			sizeOfFloatStr = bs.readUInt()
			floatCount = bs.readUInt()
			texCount = bs.readUInt()
			
			if self.mdfVer >= 3:
				bs.seek(8,1)
				
			shaderType = bs.readUInt()
			if self.mdfVer >= 4:
				uknSF6int = bs.readUInt()
				
			alphaFlag = bs.readUInt()
			
			if self.mdfVer >= 4:
				uknSF6int2 = bs.readUInt()
				uknSF6int3 = bs.readUInt()
				
			floatHdrOffs = bs.readUInt64()
			texHdrOffs = bs.readUInt64()
			if self.mdfVer >= 3:
				firstMtrlNameOffs = bs.readUInt64()
			floatStartOffs = bs.readUInt64()
			mmtr_PathOffs = bs.readUInt64()
			
			if self.mdfVer >= 4:
				uknSF6offset = bs.readUInt64()
				
			bs.seek(materialNamesOffset)
			materialName = ReadUnicodeString(bs)
			bs.seek(mmtr_PathOffs)
			mmtrName = ReadUnicodeString(bs).lower()
			#hasTransparency = not not (((alphaFlag & ( 1 << 1 )) >> 1) or ((alphaFlag & ( 1 << 4 )) >> 4))
			hasTransparency = "_dirt" in mmtrName or "_decal" in mmtrName or "_hair" in mmtrName 
			
			if bPrintFileList:
				self.texNames.append(("natives/" + nDir + "/" + mmtrName + mmtrExt).lower())
				if not rapi.checkFileExists(extractedNativesPath + (mmtrName + mmtrExt).lower()) and not rapi.checkFileExists(self.rootDir + (mmtrName + mmtrExt).lower()) and rapi.getInputName().find("natives".lower()) != -1:
					self.missingTexNames.append("DOES NOT EXIST " + ("natives/" + nDir + "/" + mmtrName + mmtrExt).lower())
			
			if doPrintMDF:
				print(materialName + "[" + str(i) + "]\n")
			
			self.matNames.append(materialName)
			self.matHashes.append(materialHash)
			materialFlags = 0
			materialFlags2 = 0
			material = NoeMaterial(materialName, "")
			material.setDefaultBlend(0)
			#material.setBlendMode("GL_ONE", "GL_ONE")
		
			#Parse Textures
			textureInfo = []
			paramInfo = []
			
			bFoundBM = False
			bFoundNM = False
			bFoundSSSM = False
				
			bFoundBaseColour = False
			bFoundRoughColour = False
			bFoundSpecColour = False
			bFoundAmbiColour = False
			bFoundMetallicColour = False
			bFoundFresnelColour = False
			
			if doPrintMDF:
				print ("Material Properties:")
				
			for j in range(floatCount): # floats
				bs.seek(floatHdrOffs + (j * 0x18))
				paramInfo.append([bs.readUInt64(), bs.readUInt64(), bs.readUInt(), bs.readUInt()]) #dscrptnOffs[0], type[1], strctOffs[2], numFloats[3] 
				bs.seek(paramInfo[j][0])
				paramType = ReadUnicodeString(bs)
				
				colours = None
				if self.mdfVer >= 2: #sGameName == "RERT" or sGameName == "RE3" or sGameName == "ReVerse" or sGameName == "RE8" or sGameName == "MHRise" or sGameName == "SF6":
					bs.seek(floatStartOffs + paramInfo[j][2])
					if paramInfo[j][3] == 4:
						colours = NoeVec4((bs.readFloat(), bs.readFloat(), bs.readFloat(), bs.readFloat()))
					elif paramInfo[j][3] == 1:
						colours = bs.readFloat()
				else:
					bs.seek(floatStartOffs + paramInfo[j][3])
					if paramInfo[j][2] == 4:
						colours = NoeVec4((bs.readFloat(), bs.readFloat(), bs.readFloat(), bs.readFloat()))
					elif paramInfo[j][2] == 1:
						colours = bs.readFloat()
					
				if doPrintMDF:
					print(paramType + ":", colours)
				
				if paramType == "BaseColor" and not bFoundBaseColour:
					bFoundBaseColour = True
					texBaseColour.append(colours)
				if paramType == "Roughness" and not bFoundRoughColour:
					bFoundRoughColour = True
					texRoughColour.append(colours)
				if paramType == "PrimalySpecularColor" and not bFoundSpecColour:
					bFoundSpecColour = True
					texSpecColour.append(colours)
				if paramType == "AmbientColor" and not bFoundAmbiColour:
					bFoundAmbiColour = True
					texAmbiColour.append(colours)
				if paramType == "Metallic" and not bFoundMetallicColour:
					bFoundMetallicColour = True
					texMetallicColour.append(colours)
				if paramType == "Fresnel_DiffuseIntensity" and not bFoundFresnelColour:
					bFoundFresnelColour = True
					texFresnelColour.append(colours)
				if paramType == "Occlusion_UseSecondaryUV" and colours != 0:
					materialFlags2 |= noesis.NMATFLAG2_OCCL_UV1
			
			#Append defaults
			if not bFoundBaseColour:
				texBaseColour.append(NoeVec4((1.0, 1.0, 1.0, 1.0)))
			if not bFoundRoughColour:
				texRoughColour.append(1.0)
			if not bFoundSpecColour:
				texSpecColour.append(NoeVec4((0.5, 0.5, 0.5, 0.5)))
			if not bFoundAmbiColour:
				texAmbiColour.append(NoeVec4((1.0, 1.0, 1.0, 1.0)))
			if not bFoundMetallicColour:
				texMetallicColour.append(1.0)
			if not bFoundFresnelColour:
				texFresnelColour.append(0.8)
			
			if doPrintMDF:
				print ("\nTextures for " + materialName + "[" + str(i) + "]" + ":")
			
			alreadyLoadedTexs = [tex.name for tex in self.fullTexList]
			alreadyLoadedMats = [mat.name for mat in self.fullMatList]
			secondaryDiffuse = ""
			
			for j in range(texCount): # texture headers
				
				if self.mdfVer >= 2:
					bs.seek(texHdrOffs + (j * 0x20))
					textureInfo.append([bs.readUInt64(), bs.readUInt64(), bs.readUInt64(), bs.readUInt64()]) #TextureTypeOffset[0], uknBytes[1], TexturePathOffset[2], padding[3]
					if self.mdfVer >= 4:
						bs.seek(8,1)
				else:
					bs.seek(texHdrOffs + (j * 0x18))
					textureInfo.append([bs.readUInt64(), bs.readUInt64(), bs.readUInt64()])
				bs.seek(textureInfo[j][0])
				textureType = ReadUnicodeString(bs)
				bs.seek(textureInfo[j][2])
				textureName = ReadUnicodeString(bs).replace("@", "")
				
				textureFilePath = ""
				texName = ""
				isNotMainTexture = False
				opacityName = ""
				extraParam = ""
				
				if bFoundBaseColour:
					material.setDiffuseColor(texBaseColour[i])
				if bFoundSpecColour:
					material.setSpecularColor(texSpecColour[i])
				if bFoundAmbiColour:
					material.setAmbientColor(texAmbiColour[i])
				if bFoundMetallicColour:
					material.setMetal(texMetallicColour[i], 0.25)
				if bFoundRoughColour:
					material.setRoughness(texRoughColour[i], 0.25)
				if bFoundFresnelColour:
					material.setEnvColor(NoeVec4((1.0, 1.0, 1.0, texFresnelColour[i])))
				
				tmpExt = texExt
				for k in range(2):
					if not rapi.checkFileExists(textureFilePath):
						if (rapi.checkFileExists(self.rootDir + "streaming/" + textureName + tmpExt)):
							textureFilePath = self.rootDir + "streaming/" + textureName + tmpExt						
							texName = rapi.getLocalFileName(self.rootDir + "streaming/" + textureName).rsplit('.', 1)[0] + texOutputExt
									
						elif (rapi.checkFileExists(self.rootDir + textureName + tmpExt)):
							textureFilePath = self.rootDir + textureName + tmpExt
							texName = rapi.getLocalFileName(self.rootDir + textureName).rsplit('.', 1)[0] + texOutputExt
							if bPrintFileList and not (rapi.checkFileExists(self.rootDir + textureName + tmpExt)):
								self.missingTexNames.append("DOES NOT EXIST: " + (('natives/' + (re.sub(r'.*natives\\', '', textureFilePath)).lower()).replace("\\","/")).replace(extractedNativesPath,''))
							
						elif (rapi.checkFileExists(extractedNativesPath + "streaming/" + textureName + tmpExt)):
							textureFilePath = extractedNativesPath + "streaming/" + textureName + tmpExt
							texName = rapi.getLocalFileName(extractedNativesPath + "streaming/" + textureName).rsplit('.', 1)[0] + texOutputExt
									
						elif (rapi.checkFileExists(extractedNativesPath + textureName + tmpExt)):
							textureFilePath = extractedNativesPath + textureName + tmpExt
							texName = rapi.getLocalFileName(extractedNativesPath + textureName).rsplit('.', 1)[0] + texOutputExt
							if bPrintFileList and not (rapi.checkFileExists(extractedNativesPath + textureName + tmpExt)):
								self.missingTexNames.append("DOES NOT EXIST: " + ('natives/' + (re.sub(r'.*natives\\', '', textureFilePath)).lower()).replace("\\","/").replace(extractedNativesPath,''))
							
						else:
							textureFilePath = self.rootDir + textureName + tmpExt
							texName = rapi.getLocalFileName(self.rootDir + textureName).rsplit('.', 1)[0] + texOutputExt
							if bPrintFileList and not (textureFilePath.endswith("rtex" + tmpExt)) and (k==1 or sGameName.find("MHR") == -1):
								self.missingTexNames.append("DOES NOT EXIST: " + ('natives/' + (re.sub(r'.*natives\\', '', textureFilePath)).lower()).replace("\\","/").replace("streaming/",""))
						if "MHR" not in sGameName:
							break
						tmpExt += ".stm"
				
				bAlreadyLoadedTexture = (texName in alreadyLoadedTexs)
				bAlreadyLoadedMat = (materialName in alreadyLoadedMats)
						
				if bPrintFileList: #and rapi.getInputName().find("natives".lower()) != -1:
					if not (textureName.endswith("rtex")):
						newTexPath = ((('natives/' + (re.sub(r'.*natives\\', '', textureFilePath))).replace("\\","/")).replace(extractedNativesPath,'')).lower()
						self.texNames.append(newTexPath)
						if newTexPath.find('streaming') != -1:
							testPath = newTexPath.replace('natives/' + nDir + '/streaming/', '')
							if rapi.checkFileExists(self.rootDir + testPath) or rapi.checkFileExists(extractedNativesPath + testPath):
								self.texNames.append(newTexPath.replace('streaming/',''))
				
				lowerTexName = texName.lower()
				#if (("BaseMetal" in textureType or "BaseDielectric" in textureType or "BaseAlpha" in textureType or "BaseShift" in textureType)) and not bFoundBM: #
				if "_alb" in lowerTexName and (not bFoundBM or ("_albd" in lowerTexName)): #goddamn RE8 #"_albm" in lowerTexName or 
					bFoundBM = True
					material.setTexture(texName)
					material.setSpecularColor([.25, .25, .25, 1])
					if "AlphaMap" in textureType:
						extraParam = "isALBA"
					if "Metal" in textureType:
						extraParam = "isALBM"
					if "Dielectric" in textureType:
						extraParam = "isALBD"
					self.uvBias[material.name] = [0.5, 0.5] if sGameName == "RE7RT" and "atlas" in lowerTexName else 1.0
				#elif (("Normal" in textureType or "NR" in textureType) or "_nr" in lowerTexName) and not bFoundNM:
				elif "_nr" in lowerTexName and not bFoundNM:
					bFoundNM = True
					material.setNormalTexture(texName)
					extraParam = "isNRM"
					if textureType == "NormalRoughnessMap":
						materialFlags |= noesis.NMATFLAG_PBR_ROUGHNESS_NRMALPHA
					if "NRR" in textureType or textureType == "NormalRoughnessTranslucentMap" or textureType == "NormalRoughnessCavityMap":
						extraParam = "isNRR"
				elif "AlphaTranslucent" in textureType and not bFoundSSSM:
					bFoundSSSM = True
					material.setOcclTexture(texName.replace(texOutputExt,  "_NoesisAO" + texOutputExt))
					extraParam = "isATOS_Alpha" if hasTransparency else "isATOS"
					material.setOcclTexture(texName.replace(texOutputExt,  "_NoesisAO" + texOutputExt))
				elif textureType == "AlphaMap":
					opacityName = texName
				#elif "_lymo" in lowerTexName:
				#	extraParam = "isLYMO"
				elif re.search("^Base.*Map$", textureType) and not secondaryDiffuse:
				#elif ("_alb" in lowerTexName) and not secondaryDiffuse:
					secondaryDiffuse = texName
				elif not dialogOptions.loadAllTextures:
					isNotMainTexture = True
					
				if not bAlreadyLoadedTexture and not isNotMainTexture:
					if (textureName.endswith("rtex")):
						pass
					elif not (rapi.checkFileExists(textureFilePath)):
						if textureFilePath != "": 
							print("Error: Texture at path: " + str(textureFilePath) + " does not exist!")
					else:
						textureData = rapi.loadIntoByteArray(textureFilePath)
						numTex = len(self.texList)
						noetex = texLoadDDS(textureData, self.texList, texName)
						if noetex:
							if dialogOptions.doConvertTex:
								if "isALBM"  == extraParam or "isALBD" == extraParam:
									if "isALBD" == extraParam:
										noetex.pixelData = invertRawRGBAChannel(noetex.pixelData, 3)
									materialFlags |= noesis.NMATFLAG_PBR_METAL
									if dialogOptions.doConvertMatsForBlender:
										metalTexData = rapi.imageEncodeRaw(noetex.pixelData, noetex.width, noetex.height, "a8a8a8")
										metalTexData = rapi.imageDecodeRaw(metalTexData, noetex.width, noetex.height, "r8g8b8")
										noetex.pixelData = rapi.imageEncodeRaw(noetex.pixelData, noetex.width, noetex.height, "r8g8b8")
										noetex.pixelData = rapi.imageDecodeRaw(noetex.pixelData, noetex.width, noetex.height, "r8g8b8")
										if not isImageBlank(metalTexData, noetex.width, noetex.height, 4):
											metalTexName = texName.replace(texOutputExt,  "_NoesisMET" + texOutputExt)
											material.setSpecularTexture(metalTexName)
											self.texList.append(NoeTexture(metalTexName, noetex.width, noetex.height, metalTexData, noesis.NOESISTEX_RGBA32))
									else:
										material.setSpecularTexture(texName)
										material.setSpecularSwizzle( NoeMat44([[1, 0, 0, 0], [0, 0, 0, 1], [0, 0, 1, 0], [0, 1, 0, 0]])) #move alpha channel to green channel
								if dialogOptions.doConvertMatsForBlender and ("isNRM" == extraParam or "isNRR" in extraParam):
									roughnessTexData = rapi.imageEncodeRaw(noetex.pixelData, noetex.width, noetex.height, "a8a8a8" if "isNRM" == extraParam else "r8r8r8")
									roughnessTexData = rapi.imageDecodeRaw(roughnessTexData, noetex.width, noetex.height, "r8g8b8")
									if not isImageBlank(roughnessTexData, noetex.width, noetex.height, 4):
										roughnessTexName = texName.replace(texOutputExt,  "_NoesisRGH" + texOutputExt)
										self.texList.append(NoeTexture(roughnessTexName, noetex.width, noetex.height, roughnessTexData, noesis.NOESISTEX_RGBA32))
										material.setBumpTexture(roughnessTexName)
								if "isNRR" in extraParam:
									noetex.pixelData = rapi.imageSwapChannelRGBA32(noetex.pixelData, 3, 0)
									#noetex.pixelData = rapi.imageNormalSwizzle(noetex.pixelData, noetex.width, noetex.height, 1, 0, 0)
									#noetex.pixelData = moveChannelsRGBA(noetex.pixelData, 3, noetex.width, noetex.height, noetex.pixelData, [0], noetex.width, noetex.height)
									noetex.pixelData = moveChannelsRGBA(noetex.pixelData, -2, noetex.width, noetex.height, noetex.pixelData, [2,3], noetex.width, noetex.height)
									#noetex.pixelData = invertRawRGBAChannel(noetex.pixelData, 1)
								if "isALBA" == extraParam:
									opacityTexData = rapi.imageEncodeRaw(noetex.pixelData, noetex.width, noetex.height, "a8a8a8")
									opacityTexData = rapi.imageDecodeRaw(opacityTexData, noetex.width, noetex.height, "r8g8b8")
									opacityName = texName.replace(texOutputExt,  "_NoesisAlpha" + texOutputExt)
									self.texList.append(NoeTexture(opacityName, noetex.width, noetex.height, opacityTexData, noesis.NOESISTEX_RGBA32))
								if "isLYMO" == extraParam:
									#r=metal? g= b=roughness? a=ao?
									materialFlags |= noesis.NMATFLAG_PBR_METAL
									metalTexName = texName.replace(texOutputExt,  "_NoesisMET" + texOutputExt)
									metalTexData = rapi.imageEncodeRaw(noetex.pixelData, noetex.width, noetex.height, "r8r8r8")
									metalTexData = rapi.imageDecodeRaw(metalTexData, noetex.width, noetex.height, "r8g8b8")
									material.setSpecularTexture(metalTexName)
									self.texList.append(NoeTexture(metalTexName, noetex.width, noetex.height, metalTexData, noesis.NOESISTEX_RGBA32))
									if dialogOptions.doConvertMatsForBlender:
										roughnessTexName = texName.replace(texOutputExt,  "_NoesisRGH" + texOutputExt)
										roughnessTexData = rapi.imageEncodeRaw(noetex.pixelData, noetex.width, noetex.height, "b8b8b8")
										roughnessTexData = rapi.imageDecodeRaw(roughnessTexData, noetex.width, noetex.height, "r8g8b8")
										self.texList.append(NoeTexture(roughnessTexName, noetex.width, noetex.height, roughnessTexData, noesis.NOESISTEX_RGBA32))
										material.setBumpTexture(roughnessTexName)
									noetex.pixelData = rapi.imageEncodeRaw(noetex.pixelData, noetex.width, noetex.height, "a8a8a8")
									noetex.pixelData = rapi.imageDecodeRaw(noetex.pixelData, noetex.width, noetex.height, "r8g8b8")
									material.setOcclTexture(noetex.name)
									
								if "isATOS" in extraParam:
									imgData = copy.copy(noetex.pixelData)
									aoTexData = rapi.imageEncodeRaw(noetex.pixelData, noetex.width, noetex.height, "b8b8b8")
									aoTexData = rapi.imageDecodeRaw(aoTexData, noetex.width, noetex.height, "r8g8b8")
									if not isImageBlank(aoTexData, noetex.width, noetex.height):
										noetex.name = texName.replace(texOutputExt,  "_NoesisAO" + texOutputExt)
										self.texList[len(self.texList)-1].pixelData = aoTexData
									else:           
										self.texList.remove(noetex)
										material.setOcclTexture("")
									if extraParam == "isATOS_Alpha" and not opacityName:
										opacityTexData = rapi.imageEncodeRaw(imgData, noetex.width, noetex.height, "r8r8r8")
										opacityTexData = rapi.imageDecodeRaw(opacityTexData, noetex.width, noetex.height, "r8g8b8")
										if not isImageBlank(opacityTexData, noetex.width, noetex.height):
											opacityName = texName.replace(texOutputExt,  "_NoesisAlpha" + texOutputExt)
											materialFlags |= noesis.NMATFLAG_TWOSIDED
											self.texList.append(NoeTexture(opacityName, noetex.width, noetex.height, opacityTexData, noesis.NOESISTEX_RGBA32))
							alreadyLoadedTexs.append(texName)
						else:
							print ("Failed to load", texName)
				
				if opacityName and hasTransparency:
					material.setAlphaTest(0.05)
					material.setOpacityTexture(opacityName)
					if dialogOptions.doConvertMatsForBlender:
						material.setEnvTexture(opacityName)
						
				
				if doPrintMDF:
					print(textureType + ":\n    " + textureName)
			
			if secondaryDiffuse and not bFoundBM:
				material.setTexture(secondaryDiffuse)
				material.setSpecularColor([.25, .25, .25, 1])
			
			if texCount:
				if not bFoundBM:
					dummyTexName = textureName.replace(texOutputExt, "_NoesisColor" + texOutputExt)
					material.setTexture(dummyTexName)
					if dummyTexName not in alreadyLoadedTexs:
						try:
							byteColor = [int((1 if color > 1.0 else 0 if color < 0.0 else color) * 255) for color in texBaseColour[i]]
						except:
							byteColor = [127, 127, 127, 255]
						self.texList.append(generateDummyTexture4px(byteColor, dummyTexName))
						alreadyLoadedTexs.append(dummyTexName)
				if not bFoundNM:
					material.setNormalTexture("NoesisNRM" + texOutputExt)
					if "NoesisNRM" + texOutputExt not in alreadyLoadedTexs:
						self.texList.append(generateDummyTexture4px((127, 127, 255, 255), "NoesisNRM" + texOutputExt))
						alreadyLoadedTexs.append("NoesisNRM" + texOutputExt)
					
			material.setFlags(materialFlags)
			material.setFlags2(materialFlags2)
			self.matList.append(material)
			
			lowername = material.name.lower()
			if ("eye" in lowername and not material.texName) or "tearline" in lowername or "lens" in lowername or "destroy" in lowername: #"ao" in lowername or "out" in lowername or 
				material.setSkipRender(True)
			
			if doPrintMDF:
				print("--------------------------------------------------------------------------------------------\n")
					
		if bPrintFileList:
			if len(self.texNames) > 0:
				print ("\nReferenced Files:")
				textureList = sorted(list(set(self.texNames)))
				for x in range (len(textureList)):
					print (textureList[x])
				print ("")
			
			if len(self.missingTexNames) > 0:
				print ("Missing Files:")
				missingTextureList = sorted(list(set(self.missingTexNames)))
				for x in range (len(missingTextureList)):
					print (missingTextureList[x])
				print ("")

		if doColorize:
			colorList = sorted(list(set(self.texColors)))
			print ("Color-coded Materials:")
			for g in range (len(colorList)):
				print (colorList[g])
			print ("")
		
		for mat in self.matList:
			if mat.name not in usedMats:
				usedMats.append(mat.name)
				self.fullMatList.append(mat)
		for tex in self.texList:
			if tex.name not in usedTexs:
				usedTexs.append(tex.name)
				self.fullTexList.append(tex)
		
		return True
		
	'''MESH IMPORT ========================================================================================================================================================================'''
	def loadMeshFile(self): #, mdlList):
		
		global sGameName, bSkinningEnabled, isMeshVer3, namesOffsLocation, extractedNativesPath
		
		self.rootDir = GetRootGameDir(self.path)
		extractedNativesPath = LoadExtractedDir(sGameName)
		
		#Try to find & save extracted game dir for later if extracted game dir is unknown
		if extractedNativesPath == "":
			if (self.rootDir.lower().endswith("chunk_000\\natives\\" + formats[sGameName]["nDir"] + "\\")):
				print ("Saving extracted natives path...")
				if SaveExtractedDir(self.rootDir, sGameName):
					extractedNativesPath = self.rootDir
		
		bs = self.inFile
		magic = bs.readUInt()
		meshVersion = bs.readUInt()
		fileSize = bs.readUInt()
		deferredWarning = ""
		bDoSkin = True
		
		bs.seek(numNodesLocation)
		numNodes = bs.readUInt()
		bs.seek(LOD1OffsetLocation)
		LOD1Offs = bs.readUInt64()
		LOD2Offs = bs.readUInt64()
		occluderMeshOffs = bs.readUInt64()
		bs.seek(vBuffHdrOffsLocation)  
		vBuffHdrOffs = bs.readUInt64() 
		bs.seek(bonesOffsLocation)   
		bonesOffs = bs.readUInt64()  
		bs.seek(nodesIndicesOffsLocation)  
		nodesIndicesOffs = bs.readUInt64()  
		boneIndicesOffs = bs.readUInt64()  
		#if sGameName == "AJ_AAT" or sGameName == "DD2":
		#	namesOffsLocation = 136 if bonesOffs > 0 else 144
		bs.seek(namesOffsLocation)
		namesOffs = bs.readUInt64()
		
		if LOD1Offs:
			bs.seek(LOD1Offs)
			countArray = bs.read("16B") #[0] = LODGroupCount, [1] = MaterialCount, [2] = UVChannelCount
			matCount = countArray[1]
			intFaces = countArray[6]
			bLoadedMats = False
			if not (noesis.optWasInvoked("-noprompt")) and not bRenameMeshesToFilenames and not rapi.noesisIsExporting() and not (dialogOptions.dialog != None and dialogOptions.doLoadTex == False):
				bLoadedMats = self.createMaterials(matCount)
			if bDebugMESH:
				print("Count Array")
				print(countArray)
		
		bs.seek(vBuffHdrOffs)
		vertElemHdrOffs = bs.readUInt64()
		vertBuffOffs = bs.readUInt64()
		
		if self.ver >= 3:
			uknVB = bs.readUInt64()
			vertBuffSize = bs.readUInt()
			face_buffOffsSF6 = bs.readUInt()
			faceBuffOffs = face_buffOffsSF6 + vertBuffOffs;
		else:
			faceBuffOffs = bs.readUInt64()
			if sGameName == "RERT" or sGameName == "RE7RT" or sGameName == "MHRSunbreak":
				uknInt64 = bs.readUInt64()
			vertBuffSize = bs.readUInt()
			faceBuffSize = bs.readUInt()
		
		vertElemCountA = bs.readUShort()
		vertElemCountB = bs.readUShort()
		faceBufferSize2nd = bs.readUInt64()
		blendshapesOffset = bs.readUInt()
		
		bs.seek(vertElemHdrOffs)
		vertElemHeaders = []
		positionIndex = -1
		normalIndex = -1
		colorIndex = -1
		uvIndex = -1
		uv2Index = -1
		weightIndex = -1
		
		for i in range (vertElemCountB):
			vertElemHeaders.append([bs.readUShort(), bs.readUShort(), bs.readUInt()])
			if vertElemHeaders[i][0] == 0 and positionIndex == -1:
				positionIndex = i
			elif vertElemHeaders[i][0] == 1 and normalIndex == -1:
				normalIndex = i
			elif vertElemHeaders[i][0] == 2 and uvIndex == -1:
				uvIndex = i
			elif vertElemHeaders[i][0] == 3 and uv2Index == -1:
				uv2Index = i
			elif vertElemHeaders[i][0] == 4 and weightIndex == -1:
				weightIndex = i
			elif vertElemHeaders[i][0] == 5 and colorIndex == -1:
				colorIndex = i
		bs.seek(vertBuffOffs)
		
		vertexStartIndex = bs.tell()
		#print (vertElemHdrOffs, vertBuffOffs, uknVB, vertBuffSize, faceBuffOffs, vertElemCountA, vertElemCountB)
		vertexBuffer = bs.readBytes(vertBuffSize)
		submeshDataArr = []
		
		if LOD1Offs:	
			
			bs.seek(LOD1Offs + 48 + 16) #unknown floats and bounding box
			
			if self.ver <= 1:  #sGameName != "RERT" and sGameName != "ReVerse" and sGameName != "RE8" and sGameName != "MHRise": 
				bs.seek(bs.readUInt64())
			
			offsetInfo = []
			for i in range(countArray[0]):
				offsetInfo.append(bs.readUInt64())
				
			if bDebugMESH:
				print("Vertex Info Offsets")
				print(offsetInfo)
			
			nameOffsets = []
			names = []
			nameRemapTable = []
			
			bs.seek(nodesIndicesOffs)
			for i in range(numNodes):
				nameRemapTable.append(bs.readUShort())
				
			bs.seek(namesOffs)
			for i in range(numNodes):
				nameOffsets.append(bs.readUInt64())
				
			for i in range(numNodes):
				bs.seek(nameOffsets[i])
				names.append(bs.readString())
				
			if bDebugMESH:
				print("Names:")
				print(names)
				
			bs.seek(nodesIndicesOffs) #material indices
			matIndices =[]
			for i in range(matCount):
				matIndices.append(bs.readUShort())
			
			isSCN = (rapi.getInputName().lower().find(".scn") != -1)
			fullBonesOffs = len(self.fullBoneList)
			fullRemapOffs = len(self.fullRemapTable)
			fullBoneNames = [cleanBoneName(bone.name).lower() for bone in self.fullBoneList]
			boneRemapTable = []
			
			#bSkinningEnabled = bDoSkin = bonesOffs = 0
			
			#Skeleton
			if bonesOffs:
				bs.seek(bonesOffs)
				boneCount = bs.readUInt()
				boneMapCount = bs.readUInt()	
				bAddNumbers = False
				if rapi.getInputName().find(".noesis") == -1 and (not dialogOptions.dialog or len(dialogOptions.dialog.loadItems) == 1) and (not dialogOptions.motDialog or not dialogOptions.motDialog.loadItems) :
					maxBones = 1024 if sGameName == "SF6" else 256
					if bAddBoneNumbers == 1 or noesis.optWasInvoked("-bonenumbers"):
						bAddNumbers = True
					elif bAddBoneNumbers == 2 and boneCount > maxBones and rapi.getInputName().lower().find(".scn") == -1:
						bAddNumbers = True
						print ("Model has more than", maxBones, "bones, auto-enabling bone numbers...")
				
				bs.seek(bonesOffs + 16)
				
				if boneCount:
					hierarchyOffs = bs.readUInt64()
					localOffs = bs.readUInt64()
					globalOffs = bs.readUInt64()
					inverseGlobalOffs = bs.readUInt64()
					
					if boneMapCount:
						for i in range(boneMapCount):
							boneRemapTable.append(bs.readShort() + fullBonesOffs)
					else:
						deferredWarning = "WARNING: Mesh has weights but no bone map"
						print(deferredWarning)
						boneRemapTable.append(0)
						
					if bDebugMESH:
						print("boneRemapTable:", boneRemapTable)

					boneParentInfo = []
					bs.seek(hierarchyOffs)
					for i in range(boneCount):
						boneParentInfo.append([bs.readShort(), bs.readShort(), bs.readShort(), bs.readShort(), bs.readShort(), bs.readShort(), bs.readShort(), bs.readShort()])
					
					bs.seek(localOffs)
					for i in range(boneCount):
						mat = NoeMat44.fromBytes(bs.readBytes(0x40)).toMat43()
						mat[3] *= fDefaultMeshScale
						boneName = names[countArray[1] + i]
						lowerBoneName = boneName.lower()
						#if i==0 and "root" in boneName.lower():
						#	mat[3][1] = 0 #neutralize Y offset for root bone
						
						if bAddNumbers: 
							for j in range(len(boneRemapTable)):
								if boneParentInfo[i][0] == boneRemapTable[j]:
									boneName = "b" + "{:03d}".format(j+1) + ":" + boneName
									break
						parentIdx = boneParentInfo[i][1]
						if not isSCN and lowerBoneName in fullBoneNames:
							if i == 0: #relocate this mesh's root bone onto base skeleton version
							#if lowerBoneName == "cog" or lowerBoneName == "hip" or lowerBoneName == "c_hip":
								print("Relocating bone", boneName)
								newMat = self.fullBoneList[fullBoneNames.index(lowerBoneName)].getMatrix()
								self.pos =  (newMat[3] - mat[3]) / fDefaultMeshScale
								mat = newMat
							ctr = 1
							newBoneName = boneName + "." + rapi.getLocalFileName(self.path).split(".")[0]
							while newBoneName.lower() in fullBoneNames:
								newBoneName = boneName + "." + rapi.getLocalFileName(self.path).split(".")[0] + "-" + str(ctr)
								ctr += 1
							boneName = newBoneName
							
						#if (parentIdx == -1 or parentIdx == i) and (i > 0 or self.fullBoneList):
						#	print("changed parent", boneName)
						#	parentIndex = 0
						
						self.boneList.append(NoeBone(boneParentInfo[i][0], boneName, mat, None, parentIdx))
						
					self.boneList = rapi.multiplyBones(self.boneList)
					
					if bRotateBonesUpright:
						rot_mat = NoeMat43(((1, 0, 0), (0, 0, 1), (0, -1, 0), (0, 0, 0)))
						for bone in self.boneList: 
							bone.setMatrix( (bone.getMatrix().inverse() * rot_mat).inverse()) 	#rotate upright in-place
					for bone in self.boneList: 
						bone.index += fullBonesOffs
						if bone.parentIndex != -1:
							bone.parentIndex += fullBonesOffs
						if bone.parentIndex == -1 and fullBonesOffs > 0:
							bone.parentIndex = 0
				else:
					bDoSkin = False
					
			
			self.fullBoneList.extend(self.boneList)
			self.fullRemapTable.extend(boneRemapTable)
			
			#print(offsetInfo)
			for i in range(countArray[0]): # LODGroups
				
				meshVertexInfo = []
				#ctx = rapi.rpgCreateContext()
				bs.seek(offsetInfo[i])
				numOffsets = bs.readUByte()
				bs.seek(3,1)
				uknFloat = bs.readUInt()
				offsetSubOffsets = bs.readUInt64()
				bs.seek(offsetSubOffsets)
				
				meshOffsetInfo = []
				
				for j in range(numOffsets):
					meshOffsetInfo.append(bs.readUInt64())
				
				numVertsLOD = 0
				
				for j in range(numOffsets): # MainMeshes
					bs.seek(meshOffsetInfo[j])
					meshVertexInfo.append([bs.readUByte(), bs.readUByte(), bs.readUShort(), bs.readUInt(), bs.readUInt(), bs.readUInt()]) #GroupID, NumMesh, unused, unused, numVerts, numFaces
					self.groupIDs.append(meshVertexInfo[len(meshVertexInfo)-1][0])
					submeshData = []
					for k in range(meshVertexInfo[j][1]):
						if sGameName == "DRDR":
							submeshData.append([bs.readUShort(), bs.readUShort(), bs.readUInt(), bs.readUInt(), bs.readUInt(), bs.readUInt(), bs.readUInt64(), self.groupIDs[len(self.groupIDs)-1], bs.readUInt()])
							del submeshData[len(submeshData)-1][2] #this is something new, not sure
						elif self.ver >= 2:
							submeshData.append([bs.readUShort(), bs.readUShort(), bs.readUInt(), bs.readUInt(), bs.readUInt(), bs.readUInt64(), self.groupIDs[len(self.groupIDs)-1]]) 
						else:
							submeshData.append([bs.readUShort(), bs.readUShort(), bs.readUInt(), bs.readUInt(), bs.readUInt(), self.groupIDs[len(self.groupIDs)-1]]) #0 MaterialID, 1 faceCount, 2 indexBufferStartIndex, 3 vertexStartIndex
					
					submeshDataArr.append(submeshData)
					
					for k in range(meshVertexInfo[j][1]): # Submeshes
						
						mainMeshNo = self.groupIDs[len(self.groupIDs)-1] if bReadGroupIds else j+1
						mainMeshStr = "_Group_" if bReadGroupIds else "_MainMesh_" if not bShorterNames else "_Main_"
						
						materialID = submeshData[k][0]
						uknSubmeshID = submeshData[k][1]
						numFaces	 = submeshData[k][2]
						facesBefore  = submeshData[k][3]
						vertsBefore  = submeshData[k][4]
						uknSubmeshInt1 = submeshData[k][5]
						
						numVerts = submeshData[k+1][4] - vertsBefore if k+1 < len(submeshData) else meshVertexInfo[j][4] - (submeshData[k][4] - numVertsLOD)
						
						mainMeshNo = self.groupIDs[len(self.groupIDs)-1] if bReadGroupIds else j+1
						mainMeshStr = "_Group_" if bReadGroupIds else "_MainMesh_" if not bShorterNames else "_Main_"
						
						if bUseOldNamingScheme:
							meshName = "LODGroup_" + str(i+1) + mainMeshStr + str(mainMeshNo) + "_SubMesh_" + str(materialID+1)
						else:
							if bRenameMeshesToFilenames:
								meshName = os.path.splitext(rapi.getLocalFileName(sInputName))[0].replace(".mesh", "") + "_" + str(mainMeshNo) + "_" + str(k+1)
							elif bShorterNames:
								meshName = "LOD_" + str(i+1) + mainMeshStr + str(mainMeshNo) + "_Sub_" + str(k+1)
							else:
								meshName = "LODGroup_" + str(i+1) + mainMeshStr + str(mainMeshNo) + "_SubMesh_" + str(k+1)
						
						if (dialogOptions.dialog and len(dialogOptions.dialog.loadItems) > 1) or isSCN:
							meshName = rapi.getLocalFileName(self.path).split(".")[0].replace("_", "") + "_" + meshName.split("_", 1)[1]
							
						rapi.rpgSetName(meshName)
						if bRenameMeshesToFilenames: 
							rapi.rpgSetMaterial(meshName)
						matName = ""; matHash = 0
						
						#Search for material
						if bLoadedMats:
							matHash = hash_wide(names[matIndices[materialID]])
							if i == 0:
								for m in range(len(self.matHashes)):
									if self.matHashes[m] == matHash:
										if self.matNames[m] != names[nameRemapTable[materialID]]:
											print ("WARNING: " + meshName + "\'s material name \"" + self.matNames[m] + "\" in MDF does not match its material hash! \n	True material name: \"" + names[nameRemapTable[materialID]] + "\"")
										matName = self.matNames[m]
										#rapi.rpgSetLightmap(matArray[k].replace(".dds".lower(), ""))
										break
						if matName == "":
							if matHash == 0: 
								matHash = hash_wide(names[matIndices[materialID]])
							if bLoadedMats:
								print ("WARNING: " + meshName + "\'s material \"" + names[nameRemapTable[materialID]] + "\" hash " + str(matHash) + " not found in MDF!")
							self.matNames.append(names[nameRemapTable[materialID]])
							
							matName = self.matNames[len(self.matNames)-1]
						
						rapi.rpgSetMaterial(matName)
						rapi.rpgSetPosScaleBias((fDefaultMeshScale, fDefaultMeshScale, fDefaultMeshScale), (0, 0, 0))
						if bImportMaterialNames:
							#rapi.rpgSetName(meshName + "__" + matName + "__" + str(submeshData[k][len(submeshData[k])-1]))
							rapi.rpgSetName(meshName + '__' + matName)
						
						if positionIndex != -1:
							if self.pos: #position offset
								posList = []
								for v in range(vertsBefore, vertsBefore+numVerts):
									idx = 12 * v
									transVec = NoeVec3(((struct.unpack_from('f', vertexBuffer, idx))[0], (struct.unpack_from('f', vertexBuffer, idx + 4))[0], (struct.unpack_from('f', vertexBuffer, idx + 8))[0])) * self.rot.transpose()
									posList.append(transVec[0] + self.pos[0]) 
									posList.append(transVec[1] + self.pos[1])
									posList.append(transVec[2] + self.pos[2])
								posBuff = struct.pack("<" + 'f'*len(posList), *posList)
								rapi.rpgBindPositionBufferOfs(posBuff, noesis.RPGEODATA_FLOAT, 12, 0)
							else:
								rapi.rpgBindPositionBufferOfs(vertexBuffer, noesis.RPGEODATA_FLOAT, vertElemHeaders[positionIndex][1], (vertElemHeaders[positionIndex][1] * vertsBefore))
						
						if normalIndex != -1 and bNORMsEnabled:
							if bDebugNormals and not bColorsEnabled:
								rapi.rpgBindColorBufferOfs(vertexBuffer, noesis.RPGEODATA_BYTE, vertElemHeaders[normalIndex][1], vertElemHeaders[normalIndex][2] + (vertElemHeaders[normalIndex][1] * vertsBefore), 4)
							else:
								rapi.rpgBindNormalBufferOfs(vertexBuffer, noesis.RPGEODATA_BYTE, vertElemHeaders[normalIndex][1], vertElemHeaders[normalIndex][2] + (vertElemHeaders[normalIndex][1] * vertsBefore))
								if bTANGsEnabled:
									rapi.rpgBindTangentBufferOfs(vertexBuffer, noesis.RPGEODATA_BYTE, vertElemHeaders[normalIndex][1], 4 + vertElemHeaders[normalIndex][2] + (vertElemHeaders[normalIndex][1] * vertsBefore))
						try:
							rapi.rpgSetUVScaleBias(NoeVec3((self.uvBias[names[nameRemapTable[materialID]]][0], 1, 1)), NoeVec3((self.uvBias[names[nameRemapTable[materialID]]][1], 0, 0)))
						except:
							rapi.rpgSetUVScaleBias(NoeVec3((1,1,1)), NoeVec3((0,0,0)))
						if uvIndex != -1 and bUVsEnabled:
							rapi.rpgBindUV1BufferOfs(vertexBuffer, noesis.RPGEODATA_HALFFLOAT, vertElemHeaders[uvIndex][1], vertElemHeaders[uvIndex][2] + (vertElemHeaders[uvIndex][1] * vertsBefore))
						if uv2Index != -1 and bUVsEnabled:
							rapi.rpgBindUV2BufferOfs(vertexBuffer, noesis.RPGEODATA_HALFFLOAT, vertElemHeaders[uv2Index][1], vertElemHeaders[uv2Index][2] + (vertElemHeaders[uv2Index][1] * vertsBefore))
						
						if weightIndex != -1 and bSkinningEnabled and bDoSkin:
							#rapi.rpgSetBoneMap(boneRemapTable)
							rapi.rpgSetBoneMap(self.fullRemapTable)
							idxList = []
							start = vertexStartIndex + vertElemHeaders[weightIndex][2] + (vertElemHeaders[weightIndex][1] * vertsBefore)
							if sGameName == "SF6":
								for v in range(numVerts):
									bs.seek(start + vertElemHeaders[weightIndex][1] * v)
									for bID in range(3):
										idxList.append(bs.readBits(10)+fullRemapOffs)
									bs.readBits(2)
									for bID in range(3):
										idxList.append(bs.readBits(10)+fullRemapOffs)
									idxList.extend([0,0])
								idxBuff = struct.pack("<" + 'H'*len(idxList), *idxList)
								rapi.rpgBindBoneIndexBufferOfs(idxBuff, noesis.RPGEODATA_USHORT, 16, 0, 8)
							elif fullBonesOffs:
								for v in range(numVerts):
									bs.seek(start + vertElemHeaders[weightIndex][1] * v)
									for w in range(8):
										idxList.append(bs.readUByte()+fullRemapOffs)
								idxBuff = struct.pack("<" + 'H'*len(idxList), *idxList)
								rapi.rpgBindBoneIndexBufferOfs(idxBuff, noesis.RPGEODATA_USHORT, 16, 0, 8)
							else:
								rapi.rpgBindBoneIndexBufferOfs(vertexBuffer, noesis.RPGEODATA_UBYTE, vertElemHeaders[weightIndex][1], vertElemHeaders[weightIndex][2] + (vertElemHeaders[weightIndex][1] * vertsBefore), 8)
							rapi.rpgBindBoneWeightBufferOfs(vertexBuffer, noesis.RPGEODATA_UBYTE, vertElemHeaders[weightIndex][1], vertElemHeaders[weightIndex][2] + (vertElemHeaders[weightIndex][1] * vertsBefore) + 8, 8)
							
						if colorIndex != -1 and bColorsEnabled:
							offs = vertElemHeaders[colorIndex][2] + (vertElemHeaders[colorIndex][1] * vertsBefore)
							if offs + numVerts*4 < len(vertexBuffer):
								rapi.rpgBindColorBufferOfs(vertexBuffer, noesis.RPGEODATA_UBYTE, vertElemHeaders[colorIndex][1], offs, 4)
							else:
								print("WARNING:", meshName, "Color buffer would have been read out of bounds by provided indices", "\n	Buffer Size:", len(vertexBuffer), "\n	Required Size:", offs + numVerts*4)
							
						if numFaces > 0:
							faceSize = 4 if intFaces == 1 else 2
							bs.seek(faceBuffOffs + (facesBefore * faceSize))
							indexBuffer = bs.readBytes(numFaces * faceSize)
							if bRenderAsPoints:
								rapi.rpgCommitTriangles(None, noesis.RPGEODATA_USHORT if faceSize == 2 else noesis.RPGEODATA_UINT, (meshVertexInfo[j][4] - (vertsBefore)), noesis.RPGEO_POINTS, 0x1)
							else:
								#rapi.rpgSetStripEnder(0x10000)
								rapi.rpgCommitTriangles(indexBuffer, noesis.RPGEODATA_USHORT if faceSize == 2 else noesis.RPGEODATA_UINT, numFaces, noesis.RPGEO_TRIANGLE, 0x1)
								rapi.rpgClearBufferBinds()
					
					numVertsLOD += meshVertexInfo[j][4]
					
				'''try:
					mdl = rapi.rpgConstructModelAndSort()
					if mdl.meshes[0].name.find("_") == 4:
						print ("\nWARNING: Noesis split detected!\n   Export this mesh to FBX with the advanced option '-fbxmeshmerge'\n")
						rapi.rpgOptimize()
				except:
					mdl = NoeModel()
				mdl.setBones(self.boneList)
				mdl.setModelMaterials(NoeModelMaterials(self.texList, self.matList))
				mdlList.append(mdl)'''
				
				if not bImportAllLODs:
					break
				
			print ("\nMESH Material Count:", matCount)
			if bLoadedMats:
				print ("MDF Material Count:", len(self.matList))
			
		if occluderMeshOffs:
			#ctx = rapi.rpgCreateContext()
			#rapi.rpgSetOption(noesis.RPGOPT_TRIWINDBACKWARD, 1)
			bs.seek(occluderMeshOffs)
			occluderMeshCount = bs.readUInt()
			uknFloat = bs.readFloat()
			occluderMeshesOffset = bs.readUInt64()
			bs.seek(occluderMeshesOffset)
			occluderMeshes = []
			lastVertPos = vertBuffOffs
			lastFacesPos = faceBuffOffs
			for i in range(occluderMeshCount):
				dataOffset = bs.readUInt64()
				bs.seek(dataOffset)
				uknBytes = [bs.readByte(), bs.readByte(), bs.readByte(), bs.readByte(), bs.readByte(), bs.readByte(), bs.readByte(), bs.readByte()]
				vertexCount = bs.readUInt()
				indexCount = bs.readUInt()
				ukn = bs.readUInt()
				indexCount2 = bs.readUInt()
				occluderMeshes.append([uknBytes, vertexCount, indexCount])
				bs.seek(lastVertPos)
				vertexBuffer = bs.readBytes(12 * vertexCount)
				lastVertPos = bs.tell()
				bs.seek(lastFacesPos)
				indexBuffer = bs.readBytes(indexCount * 2)
				lastFacesPos = bs.tell()
				meshName = "OccluderMesh_" + str(i)
				if (dialogOptions.dialog and len(dialogOptions.dialog.loadItems) > 1) or isSCN:
					meshName = rapi.getLocalFileName(self.path).split(".")[0].replace("_", "") + "_" + meshName
				rapi.rpgSetName(meshName) 
				rapi.rpgBindPositionBuffer(vertexBuffer, noesis.RPGEODATA_FLOAT, 12)
				rapi.rpgSetStripEnder(0x10000)
				try:
					rapi.rpgCommitTriangles(indexBuffer, noesis.RPGEODATA_USHORT, indexCount, noesis.RPGEO_TRIANGLE, 0x1)
					rapi.rpgClearBufferBinds()
					'''try:
						mdl = rapi.rpgConstructModelAndSort()
						if mdl.meshes[0].name.find("_") == 4:
							print ("\nWARNING: Noesis split detected!\n   Export this mesh to FBX with the advanced option '-fbxmeshmerge'\n")
							rapi.rpgOptimize()
					except:
						mdl = NoeModel()
					mdlList.append(mdl)'''
				except:
					print("Failed to read Occluder Mesh")
					
		print (deferredWarning)
		
		return 1 #mdlList


def meshLoadModel(data, mdlList):
	
	#noesis.logPopup()
	print("\n\n	RE Engine MESH model import", Version, "by alphaZomega\n")
	
	ctx = rapi.rpgCreateContext()
	mesh = meshFile(data)
	mesh.setGameName()
	dialogOptions.motDialog = None
	dialogOptions.dialog = None
	dialogOptions.currentDir = ""
	dialog = openOptionsDialogImportWindow(None, None, {"mesh":mesh})
	dialog.path = rapi.getInputName()
	dialog.createMeshWindow()
	
	while dialogOptions.motDialog and dialogOptions.motDialog.isOpen:
		dialogOptions.motDialog.createMotlistWindow()
		dialogOptions.motDialog.isOpen = False
		if dialog.isOpen:
			dialogOptions.currentDir = dialog.currentDir
			dialog.createMeshWindow()
	
	if not dialog.isCancelled:
		for fullMeshPath in dialog.fullLoadItems:
			meshToLoad = meshFile(rapi.loadIntoByteArray(fullMeshPath), fullMeshPath)
			meshToLoad.fullBoneList = dialog.pak.fullBoneList
			meshToLoad.fullRemapTable = dialog.pak.fullRemapTable
			meshToLoad.fullTexList = dialog.pak.fullTexList
			meshToLoad.fullMatList = dialog.pak.fullMatList
			meshToLoad.loadMeshFile()
		try:
			mdl = rapi.rpgConstructModelAndSort()
			if mdl.meshes[0].name.find("_") == 4:
				print ("\nWARNING: Noesis split detected!\n   Export this mesh to FBX with the advanced option '-fbxmeshmerge'\n")
		except:
			print("Failed to construct model from rpgeo context")
			mdl = NoeModel()
	else:
		mdl = NoeModel()
	
	doLoadAnims = (dialogOptions.motDialog and dialogOptions.motDialog.loadItems and not dialogOptions.motDialog.isCancelled)
	if doLoadAnims:
		mlDialog = dialogOptions.motDialog
		sortedMlists = []
		for mlist in [mlDialog.loadedMlists[path] for path in mlDialog.fullLoadItems]:
			if mlist not in sortedMlists:
				sortedMlists.append(mlist)
		motlist = mlDialog.pak
		mdl.setBones(dialog.pak.fullBoneList)
		collapseBones(mdl, 100)
		bones = list(mdl.bones)
		mdlBoneNames = [bone.name.lower() for bone in bones]
		for mlist in sortedMlists:
			mlist.meshBones = bones
			mlist.readBoneHeaders(mlDialog.loadItems)
			for bone in mlist.bones:
				if bone.name.lower() not in mdlBoneNames:
					bone.index = len(bones)
					bones.append(bone)
		anims = []
		startFrame = 0
		for mlist in sortedMlists:
			mlist.bones = bones
			mlist.readBoneHeaders(mlDialog.loadItems)
			mlist.totalFrames = startFrame
			mlist.read(mlDialog.loadItems)
			startFrame = mlist.totalFrames
		for mlist in sortedMlists:
			mlist.makeAnims(mlDialog.loadItems)
			anims.extend(mlist.anims)
		mdl.setBones(bones)
		mdl.setAnims(anims)
		rapi.setPreviewOption("setAnimSpeed", "60.0")
	else:
		mdl.setBones(dialog.pak.fullBoneList)
		
	mdl.setModelMaterials(NoeModelMaterials(dialog.pak.fullTexList, dialog.pak.fullMatList))
	
	if len(dialog.loadItems) > 1:
		if dialogOptions.reparentHelpers and not doLoadAnims:
			collapseBones(mdl, 1)
		if noesis.optWasInvoked("-bonenumbers"):
			generateBoneMap(mdl)
	mdlList.append(mdl)
	
	boneNames = {}
	for i, bone in enumerate(mdl.bones):
		if bone.name.lower() in boneNames:
			print("Duplicate Bone Name:", bone.name)
			collapseBones(mdl, 1)
			break
		boneNames[bone.name.lower()] = True
	
	return 1


def getSameExtFilesInDir(filename=None, ext=None):
	ext = ext or os.path.splitext(rapi.getOutputName())[-1]
	filename = filename or rapi.getOutputName()
	sourceList = []
	for item in os.listdir(os.path.dirname(rapi.getOutputName())):
		if os.path.splitext(item)[1] == ext:
			sourceList.append(os.path.join(os.path.dirname(filename), item))
	return sourceList

def getExportName(fileName, exportType=".mesh"):
	global w1, w2, bWriteBones, bReWrite, bRigToCoreBones, bDoVFX, openOptionsDialog #, doLOD
	bReWrite = False; bWriteBones = False; w1 = 127; w2 = -128
	sourceList = []
	if fileName == None:
		meshExt = os.path.splitext(rapi.getOutputName())[-1]
		newMeshName = rapi.getExtensionlessName(rapi.getOutputName().replace("out.", ".")).replace(".mesh", "").replace(meshExt, "") + ".mesh" + meshExt
		ogFileName = rapi.getLocalFileName(newMeshName)
		similarityCounter = 0
		for item in os.listdir(os.path.dirname(rapi.getOutputName())):
			if os.path.splitext(item)[1] == meshExt:
				sourceList.append(os.path.join(os.path.dirname(newMeshName), item))
				sameCharCntr = 0
				for c, char in enumerate(rapi.getExtensionlessName(item)): 
					if c < len(ogFileName) and char == ogFileName[c]:
						sameCharCntr += 1
				if sameCharCntr > similarityCounter:
					similarityCounter = sameCharCntr
					newMeshName = os.path.join(os.path.dirname(newMeshName), item)
	else:
		newMeshName = fileName
	
	if bNewExportMenu:
		openOptionsDialog = openOptionsDialogExportWindow(1000, 195, {"filepath":newMeshName, "exportType":exportType}) #int(len(newMeshName)*7.5)
		openOptionsDialog.createMeshWindow()
		newMeshName = openOptionsDialog.filepath or newMeshName
		if openOptionsDialog.doCancel:
			newMeshName = None
		else: 
			if openOptionsDialog.doRewrite:
				newMeshName = newMeshName + " -rewrite"
			if openOptionsDialog.doWriteBones:
				newMeshName = newMeshName + " -bones"
			if openOptionsDialog.doVFX:
				newMeshName = newMeshName + " -vfx"
	else:
		newMeshName = noesis.userPrompt(noesis.NOEUSERVAL_FILEPATH, "Inject " + exportType.upper(), "Choose a " + exportType.upper() + " file to inject", newMeshName, None)

	if newMeshName == None:
		print("Aborting...")
		return
		
	if noesis.optWasInvoked("-flip") or newMeshName.find(" -flip") != -1:
		newMeshName = newMeshName.replace(" -flip", "")
		print ("Exporting with OpenGL handedness")
		w1 = -128; w2 = 127
		
	if noesis.optWasInvoked("-vfx") or newMeshName.find(" -vfx") != -1:
		newMeshName = newMeshName.replace(" -vfx", "")
		bDoVFX = True
		print ("Exporting VFX mesh")
	
	if noesis.optWasInvoked("-bones") or newMeshName.find(" -bones") != -1:
		newMeshName = newMeshName.replace(" -bones", "")
		print ("Exporting with new skeleton...")
		bWriteBones = True
		
	if newMeshName.find(" -rewrite") != -1:
		newMeshName = newMeshName.replace(" -rewrite", "")
		print ("Exporting with new skeleton, Group and Submesh order...")
		bReWrite = True
		bWriteBones = True
		
	if newMeshName.find(" -match") != -1:
		newMeshName = newMeshName.replace(" -match", "")
		print ("Unmatched bones will be rigged to the hips and spine")
		bRigToCoreBones = True
		
	return newMeshName
	
#===========================================================================================================================================
#MESH EXPORT
	
def meshWriteModel(mdl, bs):

	global sExportExtension, w1, w2, bWriteBones, bReWrite, bRigToCoreBones, bAddBoneNumbers, sGameName, bNewExportMenu, bDoVFX, isMeshVer3, namesOffsLocation #doLOD
	
	bWriteBones = noesis.optWasInvoked("-bones")
	bReWrite = noesis.optWasInvoked("-rewrite")
	bNewExportMenu = noesis.optWasInvoked("-adv") or bNewExportMenu
	
	w1 = 127; w2 = -128
	if noesis.optWasInvoked("-flip"): 
		w1 = -128; w2 = 127
		
	if bAlwaysRewrite or noesis.optWasInvoked("-b"):
		bReWrite = True
	if bAlwaysWriteBones:
		bWriteBones = True
	
	meshesToExport = mdl.meshes
	bDoUV2 = False
	bDoSkin = False
	bDoColors = False
	bAddNumbers = False
	isDD2Mesh = False
	f = None
	newMeshName = ""
	bDoVFX = noesis.optWasInvoked("-vfx") or (openOptionsDialog and openOptionsDialog.doVFX)
	numLODs = 1
	diff = 0	
	meshVertexInfo = []
	vertElemCountB = 5	
	newScale = (1 / fDefaultMeshScale)
	submeshes = []
	
	
	def padToNextLine(bitstream):
		while bitstream.tell() % 16 != 0:
			bitstream.writeByte(0)
			
	def dot(v1, v2):
		return sum(x*y for x,y in zip(v1,v2))	
			
	def cross(a, b):
		c = [a[1]*b[2] - a[2]*b[1],
			 a[2]*b[0] - a[0]*b[2],
			 a[0]*b[1] - a[1]*b[0]]
		return c
		
	def checkReWriteMeshes():
		global bReWrite
		nonlocal submeshes, meshesToExport, bDoSkin
		for i in objToExport:
			obj = meshesToExport[i]
			sName = obj.name.lower().split('_')
			if len(sName) < 8:
				print("WARNING! Cannot rewrite mesh, an object is missing its material name\nObject Name:", obj.name, "\nMeshes for ReWrite should have 7 underscores in their names.\nExporting with new skeleton...")
				bReWrite = False
				submeshes = []
				break
			else:
				submeshes.append(copy.copy(obj))
		if bReWrite and not bDoSkin: #if still true
			submeshBoneCount = 0
			for bone in mdl.bones:
				for mesh in submeshes:
					if bone.name == mesh.name: #fbx is stupid and adds submeshes as bones to boneless meshes
						submeshBoneCount = submeshBoneCount + 1
						break
			if len(mdl.bones) > 0 and len(mdl.bones) - submeshBoneCount > 0:
				bDoSkin = True
					
	#Prompt for source mesh to export over / export options:
	def showOptionsDialog():
		global bReWrite
		nonlocal bDoSkin, submeshes, f, newMeshName
		fileName = None
		if noesis.optWasInvoked("-meshfile"):
			newMeshName = noesis.optGetArg("-meshfile")
			if noesis.optWasInvoked("-adv"):
				newMeshName = getExportName(newMeshName)
			if newMeshName:
				newMesh = rapi.loadIntoByteArray(newMeshName)
				f = NoeBitStream(newMesh)
				return newMeshName
		else:
			newMeshName = getExportName(fileName)
		if newMeshName == None:
			return 0
		while not bReWrite and not rapi.checkFileExists(newMeshName):
			print ("File not found!")
			newMeshName = getExportName(fileName)	
			fileName = newMeshName
			if newMeshName == None:
				return 0
		if not bReWrite:		
			newMesh = rapi.loadIntoByteArray(newMeshName)
			f = NoeBitStream(newMesh)
		else:
			checkReWriteMeshes()
			if not bReWrite:
				showOptionsDialog()
		
	print ("		----" + Version + " by alphaZomega----\nOpen fmt_RE_MESH.py in your Noesis plugins folder to change global exporter options.\nExport Options:\n Input these options in the `Advanced Options` field to use them, or use in CLI mode\n -flip  =  OpenGL / flipped handedness (fixes seams and inverted lighting on some models)\n -bones = save new skeleton from Noesis to the MESH file\n -bonenumbers = Export with bone numbers, to save a new bone map\n -meshfile [filename]= Input the location of a [filename] to inject that file\n -noprompt = Do not show any prompts\n -rewrite = save new MainMesh and SubMesh order (also saves bones)\n -vfx = Export as a VFX mesh\n -b = Batch conversion mode\n -adv = Show Advanced Options dialog window\n") #\n -lod = export with additional LODGroups") # 
	
	ext = os.path.splitext(rapi.getOutputName())[1]
	RERTBytes = 0
	
	sGameName = "RE2" 
	if ext.find(".1808282334") != -1:
		sGameName = "DMC5"
	elif ext.find(".1902042334") != -1:
		sGameName = "RE3"
	elif ext.find(".2102020001") != -1:
		sGameName = "ReVerse"
	elif ext.find(".2101050001") != -1:
		sGameName = "RE8"
	elif (ext.find(".2109108288") != -1) or (ext.find(".220128762") != -1): #RE2/RE3RT, and RE7RT
		sGameName = "RERT"
		RERTBytes = 8
	elif ext.find(".2109148288") != -1: #MHRise Sunbreak
		realGameName = "MHRise Sunbreak"
		sGameName = "RERT"
		RERTBytes = 8
	elif ext.find(".2008058288") != -1: #Vanilla MHRise
		sGameName = "MHRise"
	elif ext.find(".230110883") != -1 or ext.find(".220721329") != -1: 
		sGameName = "SF6"
		isMeshVer3 = True
	elif ext.find(".220907984") != -1:
		sGameName = "ExoPrimal"
		isMeshVer3 = True
	elif ext.find(".221108797") != -1:
		sGameName = "RE4"
		isMeshVer3 = True
	elif ext.find(".230612127") != -1:
		sGameName = "AJ_AAT"
		isMeshVer3 = True
	elif ext.find(".231011879") != -1:
		sGameName = "DD2"
		isMeshVer3 = True
	elif ext.find(".240424828") != -1:
		sGameName = "DRDR"
		isMeshVer3 = True
		
	setOffsets(formats[sGameName]["meshVersion"])
	
	print ("\n				  ", realGameName if 'realGameName' in locals() else sGameName, "\n")
	
	#merge Noesis-split meshes back together:
	if meshesToExport[0].name.find("_") == 4 and meshesToExport[0].name != meshesToExport[0].sourceName:
		meshesToExport = recombineNoesisMeshes(mdl)
	
	#Remove Blender numbers from all names
	for mesh in mdl.meshes:
		if mesh.name.find('.') != -1:
			print ("Renaming Mesh " + str(mesh.name) + " to " + str(mesh.name.split('.')[0]))
			mesh.name = mesh.name.split('.')[0]
		if len(mesh.lmUVs) == 0: #make sure UV2 exists
			mesh.lmUVs = mesh.uvs
	
	#sort by name (if FBX reorganized):
	meshesToExport = sort_human(meshesToExport) 
	
	#Validate meshes are named correctly
	objToExport = []
	for i, mesh in enumerate(meshesToExport):
		ss = mesh.name.lower().split('_')			
		if len(ss) >= 6:
			if ss[1].isnumeric() and ss[3].isnumeric() and ss[5].isnumeric():
				objToExport.append(i)
				
	if bReWrite:
		if noesis.optWasInvoked("-adv"): # and noesis.optWasInvoked("-noprompt"):
			newMeshName = getExportName(rapi.getOutputName() or None)
		checkReWriteMeshes()
	else:
		showOptionsDialog()

	if f:
		magic = f.readUInt()
		if magic != 1213416781:
			noesis.messagePrompt("Not a MESH file.\nAborting...")
			return 0		
		bonesOffs = readUIntAt(f, bonesOffsLocation)
		if bonesOffs > 0:
			bDoSkin = True
	
	if not bReWrite:
		if newMeshName != None:
			print("Source Mesh:\n", newMeshName)
		else:
			return 0
			
	if bDoSkin:
		print ("  Rigged mesh detected, exporting with skin weights...")
	else:
		print("  No rigging detected")
		
	extension = os.path.splitext(rapi.getInputName())[1]
	vertElemCount = 3 
	
	if sGameName == "AJ_AAT" or sGameName == "DD2" or sGameName == "DRDR":
		#namesOffsLocation = 136 if bDoSkin else 144
		isDD2Mesh = True

	#check if exporting bones and create skin bone map if so:
	if bDoSkin:
		vertElemCount += 1
		bonesList = []
		newSkinBoneMap = []
		maxBones = 1024 if sGameName == "SF6" else 256
		
		if (bReWrite or bWriteBones) and dialogOptions.doCreateBoneMap:
			generateBoneMap(mdl)
		
		if bAddBoneNumbers == 1 or noesis.optWasInvoked("-bonenumbers"):
			bAddNumbers = True
		elif bAddBoneNumbers == 2:
			if len(mdl.bones) > maxBones:
				print ("Model has more than", maxBones, "bones, auto-enabling bone numbers...")
				bAddNumbers = True
			else:
				for bone in mdl.bones:
					if bone.name.find(':') != -1:
						bAddNumbers = True
						print ("  ", bone.name, "has a \':\' (colon) in its name, auto-enabling bone numbers...")
						break
		
		if (bReWrite or bWriteBones) and bForceRootBoneToBone0 and mdl.bones[0] != None and mdl.bones[0].name.lower() != "root" and mdl.bones[len(mdl.bones)-1].name.lower() == "root":
			print("WARNING: root is not bone[0], reorganizing heirarchy...")
			sortedBones = list(mdl.bones)
			rootIdx = len(sortedBones)-1
			sortedBones.remove(sortedBones[rootIdx])
			sortedBones.insert(0, mdl.bones[rootIdx])
			for i, bone in enumerate(sortedBones):
				bone.index = i
				if bone.parentIndex == rootIdx:
					bone.parentIndex = 0
				elif bone.parentIndex != -1:
					bone.parentIndex = bone.parentIndex + 1
			mdl.bones = tuple(sortedBones)
			for mesh in mdl.meshes:
				for weightsList in mesh.weights:
					indicesList = list(weightsList.indices)
					for i, idx in enumerate(indicesList):
						if idx == rootIdx:
							idx = 0
						else:
							indicesList[i] = idx + 1
					weightsList.indices = tuple(indicesList)
		
		for i, bone in enumerate(mdl.bones):
			if bone.name.find('_') == 8 and bone.name.startswith("bone"):
				print ("Renaming Bone " + str(bone.name) + " to " + bone.name[9:len(bone.name)] )
				bone.name = bone.name[9:len(bone.name)] #remove Noesis duplicate numbers
			if bone.name.find('.') != -1:
				print ("Renaming Bone " + str(bone.name) + " to " + str(bone.name.split('.')[0]))
				bone.name = bone.name.split('.')[0] #remove blender numbers
			
			if bone.name.find(':') != -1:
				bonesList.append(bone.name.split(':')[1]) #remove bone numbers
				if len(newSkinBoneMap) < maxBones:
					newSkinBoneMap.append(i)
			else:
				bonesList.append(bone.name)
				if not bAddNumbers and len(newSkinBoneMap) < maxBones:
					newSkinBoneMap.append(i)
					
		if bAddNumbers and len(newSkinBoneMap) == 0: #in case bone numbers is on but the skeleton has no bone numbers:
			print ("WARNING: No bone numbers detected, only the first", maxBones, "bones will be rigged")
			bAddNumbers = False
			for i, bone in enumerate(mdl.bones):
				if len(newSkinBoneMap) < maxBones:
					newSkinBoneMap.append(i)
	
	newBBOffs = 0

	#OLD WAY (reading source file, no rewrite):
	#====================================================================
	if not bReWrite:
		
		#header
		f.seek(numNodesLocation)
		numNodes = f.readUInt()
		f.seek(LOD1OffsetLocation)
		LOD1Offs = f.readUInt64()
		f.seek(vBuffHdrOffsLocation)  
		vBuffHdrOffs = f.readUInt64() 
		f.seek(bonesOffsLocation)   
		bonesOffs = f.readUInt64()  
		f.seek(nodesIndicesOffsLocation)  
		nodesIndicesOffs = f.readUInt64()  
		boneIndicesOffs = f.readUInt64()  
		f.seek(namesOffsLocation)
		namesOffs = f.readUInt64() 
		f.seek(floatsHdrOffsLocation)
		floatsHdrOffs = f.readUInt64() 
		
		if isDD2Mesh:
			f.seek(144)
			DD2HashesOffset = f.readUInt64()
		
		newBBOffs = floatsHdrOffs
		f.seek(LOD1Offs)
		countArray = f.read("16B")
		numMats = countArray[1]
			
		#vertex buffer header
		f.seek(vBuffHdrOffs)
		vertElemHdrOffs = f.readUInt64()
		vertBuffOffs = f.readUInt64()
		
		if isMeshVer3:
			f.seek(8,1)
			vertBuffSize = f.readUInt()
			face_buffOffsSF6 = f.readUInt()
			faceBuffOffs = face_buffOffsSF6 + vertBuffOffs
			vertElemCountA = f.readUShort()
			vertElemCountB = f.readUShort()
		else:
			faceBuffOffs = f.readUInt64()
			if sGameName == "RERT":
				uknIntA = f.readUInt()
				uknIntB = f.readUInt()
			vertBuffSize = f.readUInt()
			faceBuffSize = f.readUInt()
			vertElemCountA = f.readUShort()
			vertElemCountB = f.readUShort()
			faceBufferSize2nd = f.readUInt64()
			blendshapesOffset = f.readUInt()
		
		f.seek(vertElemHdrOffs)
		vertElemHeaders = []
		for i in range(vertElemCountB):
			vertElemHeaders.append([f.readUShort(), f.readUShort(), f.readUInt()])
		
		for i in range(len(vertElemHeaders)):
			if vertElemHeaders[i][0] == 3:
				bDoUV2 = 1
			if vertElemHeaders[i][0] == 4:
				bDoSkin = 1
			if vertElemHeaders[i][0] == 5:
				bDoColors = True
		
		nameOffsets = []	
		f.seek(namesOffs)
		for i in range(numNodes):
			nameOffsets.append(f.readUInt64())
		
		names = []
		for i in range(numNodes):
			f.seek(nameOffsets[i])
			names.append(f.readString())
		
		boneNameAddressList = []
		matNameAddressList = []
		
		if bDoSkin:		
			boneRemapTable = []
			boneInds = []
			
			#Skeleton
			f.seek(bonesOffs+4)
			boneMapCount = f.readUInt()
			
			f.seek(bonesOffs)			
			boneCount = f.readUInt()
			f.seek(12,1)
			hierarchyOffs = f.readUInt64()
			localOffs = f.readUInt64()
			globalOffs = f.readUInt64()
			inverseGlobalOffs = f.readUInt64()
				
			for b in range(boneMapCount):
				boneRemapTable.append(f.readUShort())
			
			f.seek(boneIndicesOffs)
			for i in range(boneCount):
				boneInds.append(f.readUShort())
				boneMapIndex = -1
				for b in range(len(boneRemapTable)):
					if boneRemapTable[b] == i:
						boneMapIndex = b
			
			f.seek(namesOffs)
			for i in range(countArray[1]): 
				matNameAddressList.append(f.readUInt64())
				
			for i in range(boneCount):
				boneNameAddressList.append(f.readUInt64())
		
		if isMeshVer3:
			f.seek(232)
			if sGameName == "AJ_AAT" or sGameName == "DD2" or sGameName == "DRDR":
				f.seek(f.readUInt64())
		elif sGameName == "RERT" or sGameName == "ReVerse" or sGameName == "MHRise" or sGameName == "RE8":
			f.seek(192)
		else:
			f.seek(200)
			f.seek(f.readUInt64())
			
		offsetInfo = []
		for i in range(numLODs): #LODGroup Offsets
			offsetInfo.append(f.readUInt64())
		
		#prepare array of submeshes for export:
		mainmeshCount = 0
		for ldc in range(numLODs): 
			f.seek(offsetInfo[ldc])
			mainmeshCount = f.readUByte()
			f.seek(3,1)
			uknFloat = f.readFloat()
			offsetSubOffsets = f.readUInt64()
			f.seek(offsetSubOffsets)
			meshOffsets = []
			for i in range(mainmeshCount):
				meshOffsets.append(f.readUInt64())
			for mmc in range(mainmeshCount):
				f.seek(meshOffsets[mmc])
				meshVertexInfo.append([f.readUByte(), f.readUByte(), f.readUShort(), f.readUInt(), f.readUInt(), f.readUInt()])
				for smc in range(meshVertexInfo[mmc][1]):
					matID = f.readUInt() + 1
					bFind = 0
					sourceGroupID = meshVertexInfo[len(meshVertexInfo)-1][0] if bReadGroupIds else (mmc+1)
					
					for s in range(len(objToExport)):
						#print (meshesToExport[objToExport[s]].name)
						sName = meshesToExport[objToExport[s]].name.split('_')
						thisGroupID = sName[3]
						if int(sName[1]) == (ldc+1) and int(thisGroupID) == (sourceGroupID) and ((not bUseOldNamingScheme and int(sName[5]) == (smc+1)) or (bUseOldNamingScheme and int(sName[5]) == (matID))):
							submeshes.append(copy.copy(meshesToExport[objToExport[s]]))
							bFind = 1							
							break
					if not bFind:  #create invisible placeholder submesh
						blankMeshName = "LODGroup_" + str(ldc+1) + "_MainMesh_" + str(sourceGroupID) + "_SubMesh_" + str(smc+1)
						blankTangent = NoeMat43((NoeVec3((0,0,0)), NoeVec3((0,0,0)), NoeVec3((0,0,0)), NoeVec3((0,0,0)))) 
						blankWeight = NoeVertWeight([0,0,0,0,0,0,0,0], [1,0,0,0,0,0,0,0])
						blankMesh = NoeMesh([0, 1, 2], [NoeVec3((0.00000000001,0,0)), NoeVec3((0,0.00000000001,0)), NoeVec3((0,0,0.00000000001))], blankMeshName, blankMeshName, -1, -1) #positions and faces
						blankMesh.setUVs([NoeVec3((0,0,0)), NoeVec3((0,0,0)), NoeVec3((0,0,0))]) #UV1
						blankMesh.setUVs([NoeVec3((0,0,0)), NoeVec3((0,0,0)), NoeVec3((0,0,0))], 1) #UV2
						blankMesh.setTangents([blankTangent, blankTangent, blankTangent]) #Normals + Tangents
						if bDoColors:
							blankMesh.setColors((NoeVec4((1,1,1,1)), NoeVec4((1,1,1,1)), NoeVec4((1,1,1,1)))) #vertex colors
						if bonesOffs > 0:
							blankMesh.setWeights([blankWeight,blankWeight,blankWeight]) #Weights + Indices
						submeshes.append(blankMesh)
						print (blankMeshName, "not found in FBX")
					f.seek(12, 1)	
		f.seek(0)
		
	if (len(submeshes) == 0):
		print("No submeshes detected")
		return 0
	
	#will be bounding box:
	min = NoeVec4((10000.0, 10000.0, 10000.0, fDefaultMeshScale))
	max = NoeVec4((-10000.1, -10000.1, -10000.1, fDefaultMeshScale))	
	
	bColorsExist = False
	for mesh in submeshes:
		for col in mesh.colors:
			if not bColorsExist and len(col) > 1:
				bColorsExist = True
				print ("  Vertex colors detected")
				break
		if bReWrite and bColorsExist:
			bDoColors = True
			break
			
	if bRotateBonesUpright:
		rot_mat = NoeMat43(((1, 0, 0), (0, -1, 0), (0, 0, 1), (0, 0, 0)))
		for bone in mdl.bones:
			bone.setMatrix( (bone.getMatrix().inverse() * rot_mat).inverse()) 	#rotate back to normal
			
			
	#NEW WAY (rewrite)
	#====================================================================
	if bReWrite: #save new mesh order	
		bDoUV2 = True
		#prepare new submesh order:
		newMainMeshes = []; newSubMeshes = []; newMaterialNames = []; 
		indicesBefore = 0; vertsBefore = 0; mmIndCount = 0; mmVertCount = 0
		lastMainMesh = submeshes[0].name.split('_')[3]
		meshOffsets= []
		
		for i, mesh in enumerate(submeshes):
			mat = mesh.name.split('__', 1)[1]
			if mat not in newMaterialNames:
				newMaterialNames.append(mat)
				
		numMats = len(newMaterialNames)
		print ("\nMESH Material Count:", numMats)
		
		for i, mesh in enumerate(submeshes):
			splitName = mesh.name.split('_')
			splitMatNames = mesh.name.split('__', 1)
			key = len(newMainMeshes)
			newGroupID = splitName[3]
			#try:
			if len(splitName) <= 6:
				bReWrite = False
				break
			else:
				newMaterialID = newMaterialNames.index(splitMatNames[1])
				if newGroupID != lastMainMesh:
					newMainMesh = (newSubMeshes, mmVertCount, mmIndCount, int(lastMainMesh))
					newMainMeshes.append(newMainMesh)
					newSubMeshes = []; mmIndCount = 0; mmVertCount = 0
					lastMainMesh = newGroupID
					
				newSubMeshes.append((newMaterialID, len(mesh.indices) , vertsBefore, indicesBefore))
				vertsBefore += len(mesh.positions)
				mmVertCount += len(mesh.positions)
				indicesBefore += len(mesh.indices)
				mmIndCount += len(mesh.indices)
				if i == len(submeshes)-1:
					newMainMesh = (newSubMeshes, mmVertCount, mmIndCount,  int(lastMainMesh))
					newMainMeshes.append(newMainMesh)
			#except:
			#	print("Failed to parse mesh name", mesh.name)
		
		#print(newMainMeshes)
		
		LOD1Offs = 176 if (sGameName == "DD2" or sGameName == "AJ_AAT" or sGameName == "DRDR") else 168 if isMeshVer3 else 128 if (sGameName == "RERT" or sGameName == "RE8" or sGameName == "MHRise") else 136
		
		#header:
		bs.writeUInt(1213416781) #MESH
		bs.writeUInt(formats[sGameName]["meshMagic"])
			
		bs.writeUInt(0) #Filesize
		bs.writeUInt(0) #LODGroupHash
		
		if isMeshVer3:
			bs.writeUByte(3) #flag
			if sGameName == "DD2" or sGameName == "AJ_AAT" or sGameName == "DRDR":
				bs.writeUByte(130) #solvedOffset
				bs.writeUShort(84) #uknSF6
			else:
				bs.writeUByte(2) #solvedOffset
				bs.writeUShort(0) #uknSF6
			bs.writeUInt(len(mdl.bones) * bDoSkin + numMats) #Node Count
			bs.writeUInt64(0) #ukn
			bs.writeUInt64(LOD1Offs) #LODOffs
			bs.writeUInt64(0) #ShadowLODOffs
			bs.writeUInt64(0) #OccluderMeshOffs
			bs.writeUInt64(0) #bsHeaderOffs
			bs.writeUInt64(0) #ukn2
			bs.writeUInt64(0) #vert_buffOffs
			bs.writeUInt64(0) #normalsRecalcOffs
			bs.writeUInt64(0) #groupPivotOffs      
			bs.writeUInt64(0) #BBHeaderOffs
			bs.writeUInt64(0) #bonesOffs
			bs.writeUInt64(0) #matIndicesOffs
			bs.writeUInt64(0) #boneIndicesOffs
			bs.writeUInt64(0) #bsIndicesOffs
			bs.writeUInt64(0) #ukn3
			bs.writeUInt64(0) #namesOffs
			bs.writeUInt64(0) #verticesOffset
			bs.writeUInt64(0) #ukn4/padding
			if sGameName == "DD2":
				bs.writeUInt64(0) #ukn5/padding
			
		else:
			bs.writeUShort(3) #flag
			bs.writeUShort(len(mdl.bones) * bDoSkin + numMats) #Node Count
			bs.writeUInt(0) #LODGroupHash
			bs.writeUInt64(LOD1Offs) #LODs address
			bs.writeUInt64(0) #Shadow LODs address
			bs.writeUInt64(0) #occluderMeshOffs
			bs.writeUInt64(0) #Bones Address
			bs.writeUInt64(0) #Normal Recalculation Address
			bs.writeUInt64(0) #Blendshapes Header Address
			bs.writeUInt64(0) #Floats Header Address
			bs.writeUInt64(0) #Vertex Buffer Headers Address
			bs.writeUInt64(0)
			bs.writeUInt64(0) #Material Indices Table Address
			bs.writeUInt64(0) #Bones Indices Table Address
			bs.writeUInt64(0) #Blendshapes Indices Table Address
			bs.writeUInt64(0) #Names Address
			if sGameName == "RE2" or sGameName == "RE3" or sGameName == "DMC5":
				bs.writeUInt64(0)
		
		#LODs:
		bs.writeByte(1) #set to one LODGroup
		bs.writeByte(len(newMaterialNames)) #mat count
		bs.writeByte(2) #set to 2 UV channels
		bs.writeByte(1) #unknown
		bs.writeUInt(len(submeshes)) #total mesh count
		
		if BBskipBytes==8:
			bs.writeUInt64(0)
		
		for i in range(6):
			bs.writeUInt64(0) #main bounding sphere+box placeholder
		
		bs.writeUInt64(bs.tell()+8) #offset to LODOffsets
		
		if (bs.tell()+8) % 16 != 0:
			bs.writeUInt64(bs.tell()+16) #first (and only) LODOffset
		else:
			bs.writeUInt64(bs.tell()+8)
		padToNextLine(bs)
			
		#Write LODGroup (DRDR fucks up here):
		bs.writeUInt(len(newMainMeshes))
		LODDist = openOptionsDialog.LODDist if openOptionsDialog else 0.02667995
		bs.writeFloat(LODDist) #unknown, maybe LOD distance change
		bs.writeUInt64(bs.tell()+8) #Mainmeshes offset
		
		newMainMeshesOffset = bs.tell()
		for i in range(len(newMainMeshes)):
			bs.writeUInt64(0)
		
		while(bs.tell() % 16 != 0):
			bs.writeByte(0)

		#write new MainMeshes:
		for i, mm in enumerate(newMainMeshes):
			
			newMMOffset = bs.tell()
			bs.writeByte(mm[len(mm)-1]) #Group ID
			bs.writeByte(len(mm[0])) #Submesh count
			bs.writeShort(0)
			bs.writeInt(0)
			bs.writeUInt(mm[1]) #MainMesh index count
			bs.writeUInt(mm[2]) #MainMesh vertex count
			meshVertexInfo.append([i, len(mm[0]), 0, 0, mm[1], mm[2]])
			
			for j, submesh in enumerate(mm[0]):
				#print ("New mainmesh GroupID", mm[len(mm)-1], "submesh", j)
				bs.writeUInt(submesh[0])
				if sGameName == "DRDR":
					bs.writeUInt(0)
				bs.writeUInt(submesh[1])
				bs.writeUInt(submesh[2])
				bs.writeUInt(submesh[3])
				if sGameName == "DRDR":
					bs.writeUInt(0)
				if sGameName != "RE7" and sGameName != "RE2" and sGameName != "RE3" and sGameName != "DMC5":
					bs.writeUInt64(0)
			pos = bs.tell()
			bs.seek(newMainMeshesOffset + i * 8)
			bs.writeUInt64(newMMOffset)
			meshOffsets.append(newMMOffset)
			bs.seek(pos)
		
		bonesOffs = bs.tell()
		
		if bDoSkin:
			bs.seek(bonesOffsLocation)
		else:
			bs.seek(nodesIndicesOffsLocation) #to material indices offset instead
			
		bs.writeUInt64(bonesOffs)
		bs.seek(bonesOffs)
		mainmeshCount = len(newMainMeshes)
		
	if bDoUV2:
		vertElemCount += 1
	if bDoColors:
		vertElemCount += 1

	if (bReWrite or bWriteBones): 
		if bDoSkin:
			boneRemapTable = []
			
			maxBoneMapLength = 256 if sGameName != "SF6" else 1024
			
			if bAddNumbers and len(newSkinBoneMap) > 0:
				boneMapLength = len(newSkinBoneMap)
			else:
				boneMapLength = maxBoneMapLength if len(mdl.bones) > maxBoneMapLength else len(mdl.bones)

			if not bReWrite:
				bs.writeBytes(f.readBytes(bonesOffs)) #to bone name start
		
			#write new skeleton header
			bs.writeUInt(len(mdl.bones)) #bone count
			bs.writeUInt(boneMapLength)  #bone map count

			for b in range(5): 
				bs.writeUInt64(0)
			
			#write skin bone map:
			if bAddNumbers and len(newSkinBoneMap) > 0:
				for i in range(len(newSkinBoneMap)):
					bs.writeUShort(newSkinBoneMap[i])
				boneRemapTable = newSkinBoneMap
			else:
				for i in range(boneMapLength): 
					bs.writeUShort(i)
					boneRemapTable.append(i)
			padToNextLine(bs)
			
			if (len(boneRemapTable) > maxBoneMapLength):
				print ("WARNING! Bone map is greater than", maxBoneMapLength, "bones!")
				
			#write hierarchy
			newHierarchyOffs = bs.tell()
			for i, bone in enumerate(mdl.bones):
				bs.writeUShort(i) # bone index
				bs.writeUShort(bone.parentIndex)
				nextSiblingIdx = -1
				for j, bn in enumerate(mdl.bones):
					if i < j and bone != bn and bone.parentIndex == bn.parentIndex:
						nextSiblingIdx = j
						break
				bs.writeUShort(nextSiblingIdx)
				nextChildIdx = -1
				for j, bn in enumerate(mdl.bones):
					if bn.parentIndex == i:
						nextChildIdx = j
						break
				bs.writeUShort(nextChildIdx)
				cousinIdx = -1
				cousinBoneName = ""
				bnName = bonesList[i].lower()
				if bnName.startswith('r_'): 
					cousinBoneName = bnName.replace('r_','l_')
				elif bnName.startswith('l_'):
					cousinBoneName = bnName.replace('l_','r_')
				elif isMeshVer3 or bnName.startswith("root") or bnName.startswith("cog") or bnName.startswith("hip") \
				or bnName.startswith("waist") or bnName.startswith("spine") or bnName.startswith("chest") \
				or bnName.startswith("stomach") or bnName.startswith("neck") or bnName.startswith("head") \
				or bnName.startswith("null_"):
					cousinIdx = i
				if cousinBoneName != "":
					for j in range(len(mdl.bones)):
						if bonesList[j].lower() == cousinBoneName:
							cousinIdx = j
							break
				bs.writeUShort(cousinIdx)
				padToNextLine(bs)
		
			#prepare transform data:
			localTransforms = []
			globalTransforms = []
			for bone in mdl.bones:
				boneGlobalMat = bone.getMatrix().toMat44()
				boneGlobalMat[3] = boneGlobalMat[3] * 0.01
				boneGlobalMat[3][3] = 1.0
				globalTransforms.append(boneGlobalMat)
				if bone.parentIndex != -1:
					pMat = mdl.bones[bone.parentIndex].getMatrix().toMat44()
					boneLocalMat = (bone.getMatrix().toMat44() * pMat.inverse())
					boneLocalMat[3] = boneLocalMat[3] * 0.01
					boneLocalMat[3][3] = 1.0
					localTransforms.append(boneLocalMat)
				else:
					localTransforms.append(boneGlobalMat)
			
			#write local bone transforms:
			newLocalOffs = bs.tell()
			for i in range(len(localTransforms)):
				bs.writeBytes(localTransforms[i].toBytes())
			
			#write global bone transforms:
			newGlobalOffs = bs.tell()
			for i in range(len(globalTransforms)):
				bs.writeBytes(globalTransforms[i].toBytes())
			
			#write inverse global bone transforms:
			newInvGlobOffs = bs.tell()
			for i in range(len(globalTransforms)):
				bs.writeBytes(globalTransforms[i].inverse().toBytes())
		
		#collect material names:
		materialNames = []
		if bReWrite:
			materialNames = newMaterialNames
		else:
			for i in range(numMats): 
				f.seek(matNameAddressList[i])
				materialNames.append(f.readString())
		
		#write material indices:
		newMatIndicesOffs = bs.tell()
		for i in range(numMats): 
			if bReWrite:
				bs.writeUShort(i)
			else:
				f.seek(nodesIndicesOffs + i * 2)
				bs.writeUShort(f.readUShort())
		padToNextLine(bs)
		
		if bDoSkin:
			boneInds = []
			#write bone map indices:
			newBoneMapIndicesOffs = bs.tell()
			for i in range(len(mdl.bones)): 
				bs.writeUShort(numMats + i)
				boneInds.append(numMats + i)
			padToNextLine(bs)
		
		#write names offsets:
		newNamesOffs = bs.tell()
		nameStringsOffs = newNamesOffs + (numMats + len(mdl.bones) * bDoSkin) * 8
		while nameStringsOffs % 16 != 0:
			nameStringsOffs += 1
		
		for i in range(numMats): 
			bs.writeUInt64(nameStringsOffs)
			nameStringsOffs += len(materialNames[i]) + 1
			
		if bDoSkin:
			for i in range(len(mdl.bones)): 
				bs.writeUInt64(nameStringsOffs)
				nameStringsOffs += len(bonesList[i]) + 1
		padToNextLine(bs)
		
		names = []
		#write name strings
		for i in range(len(materialNames)):
			bs.writeString(materialNames[i])
			names.append(materialNames[i])
		if bDoSkin:
			for i in range(len(bonesList)): 
				bs.writeString(bonesList[i])
				names.append(bonesList[i]) 
		padToNextLine(bs)
		
		if bDoSkin:
			#Write unknown DD2 hashes
			if isDD2Mesh:
				DD2HashesOffset = bs.tell()
				bs.writeUInt(2745047434)
				for i in range(mainmeshCount - 1):
					bs.writeUInt(0)
				padToNextLine(bs)
		
			#write bounding boxes
			newBBOffs = bs.tell()
			bs.writeUInt64(len(newSkinBoneMap))
			bs.writeUInt64(bs.tell() + 8)
			for i in range(len(newSkinBoneMap)):
				for j in range(3): bs.writeFloat(-BoundingBoxSize)
				bs.writeFloat(1)
				for j in range(3): bs.writeFloat(BoundingBoxSize)
				bs.writeFloat(1)
			newVertBuffHdrOffs = bs.tell()
			
			#fix bones header
			bs.seek(bonesOffs + 16)
			bs.writeUInt64(newHierarchyOffs)
			bs.writeUInt64(newLocalOffs)
			bs.writeUInt64(newGlobalOffs)
			bs.writeUInt64(newInvGlobOffs)
		else:
			newVertBuffHdrOffs = bs.tell()
		
		#fix main header
		bs.seek(numNodesLocation)
		bs.writeUShort(numMats + len(mdl.bones) * bDoSkin) #numNodes
			
		if bDoSkin:
			bs.seek(floatsHdrOffsLocation)
			bs.writeUInt64(newBBOffs)
			bs.seek(vBuffHdrOffsLocation)
			bs.writeUInt64(newVertBuffHdrOffs)
			bs.seek(nodesIndicesOffsLocation)
			bs.writeUInt64(newMatIndicesOffs)
			bs.writeUInt64(newBoneMapIndicesOffs)
		else:
			bs.seek(vBuffHdrOffsLocation)
			bs.writeUInt64(newVertBuffHdrOffs)
		bs.seek(namesOffsLocation)
		print(newNamesOffs)
		bs.writeUInt64(newNamesOffs)
		
		#fix vertexBufferHeader
		bs.seek(newVertBuffHdrOffs)
		
		SF6SkipBytes = 0 if not isMeshVer3 else 32
		newVertBuffOffs = newVertBuffHdrOffs + 72 + SF6SkipBytes + 8*bDoSkin + 8*bDoUV2 + 8*bDoColors + 2*RERTBytes
		
		bs.writeUInt64(bs.tell() + 48 + SF6SkipBytes + 2*RERTBytes)
		bs.writeUInt64(newVertBuffOffs)
		
		if sGameName == "RERT":
			bs.writeUInt64(0)
		bs.writeUInt64(0)
		bs.writeUInt64(0)
		bs.writeShort(vertElemCount)
		bs.writeShort(vertElemCount)
		bs.writeUInt64(0)
		bs.writeInt(-newVertBuffOffs)
		
		if isMeshVer3:
			for i in range(4):
				bs.writeUInt64(0)
		if sGameName == "RERT": # and (bs.tell() % 8) != 0:
			bs.writeUInt64(0)
		
		bs.writeUInt64(786432) #positions VertElemHeader
		bs.writeUInt64(524289) #normal VertElemHeader
		bs.writeUInt64(262146) #UV0 VertElemHeader
		if bDoUV2:
			bs.writeUInt64(262147) #UV2 VertElemHeader
		if bDoSkin:
			bs.writeUInt64(1048580) #Skin VertElemHeader
		if bDoColors:
			bs.writeUInt64(262149) #Colors VertElemHeader
			
	elif not bReWrite:
		bs.writeBytes(f.readBytes(vertBuffOffs)) #copy to vertex buffer header
		newVertBuffHdrOffs = bs.tell()
	
	if bDoSkin:
		skinBoneMapNames = []
		for b in range(len(boneRemapTable)):
			bnName = names[boneInds[boneRemapTable[b]]]
			skinBoneMapNames.append(bnName)
	
	vertexStrideStart = 0
	submeshVertexCount = []
	submeshVertexStride = []
	submeshFaceCount = []
	submeshFaceStride = []
	submeshFaceSize = []
	boneWeightBBs = {}

	for mesh in submeshes:
		if len(mesh.morphList) > 0:
			mesh.positions = mesh.morphList[0].positions

	#Write vertex data
	vertexPosStart = bs.tell()
	for mesh in submeshes:
		submeshVertexStride.append(vertexStrideStart)
		for vcmp in mesh.positions:
			bs.writeBytes((vcmp * newScale).toBytes())
			if vcmp[0] > max[0]: max[0] = vcmp[0] 	#calculate main bounding box
			if vcmp[0] < min[0]: min[0] = vcmp[0]
			if vcmp[1] > max[1]: max[1] = vcmp[1]
			if vcmp[1] < min[1]: min[1] = vcmp[1]
			if vcmp[2] > max[2]: max[2] = vcmp[2]
			if vcmp[2] < min[2]: min[2] = vcmp[2]
		submeshVertexCount.append(len(mesh.positions))
		vertexStrideStart += len(mesh.positions)
		
	normalTangentStart = bs.tell()	
	for m, mesh in enumerate(submeshes):
		for v, vcmp in enumerate(mesh.tangents):
			bs.writeByte(int(vcmp[0][0] * 127 + 0.5000000001)) #normal
			bs.writeByte(int(vcmp[0][1] * 127 + 0.5000000001))
			bs.writeByte(int(vcmp[0][2] * 127 + 0.5000000001))
			bs.writeByte(0)
			bs.writeByte(int(vcmp[2][0] * 127 + 0.5000000001)) #bitangent
			bs.writeByte(int(vcmp[2][1] * 127 + 0.5000000001))
			bs.writeByte(int(vcmp[2][2] * 127 + 0.5000000001))
			TNW = dot(cross(vcmp[0], vcmp[1]), vcmp[2])
			if (TNW < 0.0): #default way
				bs.writeByte(w1)
			else:
				bs.writeByte(w2)
				
	UV0start = bs.tell()
	for mesh in submeshes:
		for vcmp in mesh.uvs:
			bs.writeHalfFloat(vcmp[0])
			bs.writeHalfFloat(vcmp[1])
				
	UV1start = bs.tell()
	if bDoUV2:
		for mesh in submeshes:
			if len(mesh.lmUVs) != len(mesh.positions):
				mesh.lmUVs = mesh.uvs
			for vcmp in mesh.lmUVs:
				bs.writeHalfFloat(vcmp[0])
				bs.writeHalfFloat(vcmp[1])

	def writeBoneID(bID, i):
		if sGameName == "SF6":
			if i==3:
				bs.writeBits(0, 2)
			bs.writeBits(bID, 10)
		else:
			bs.writeUByte(bID)
	
	boneIdMax = 6 if sGameName == "SF6" else 8
	bnWeightStart = bs.tell()
	
	if bDoSkin:
		doDD2Skin = isDD2Mesh and bDoSkin
		
		for m, mesh in enumerate(submeshes):
			pos = bs.tell()
			for vcmp in mesh.weights: #write 0's
				for i in range(4):
					bs.writeFloat(0)
			bs.seek(pos)
			
			for i, vcmp in enumerate(mesh.weights): #write bone indices & weights over 0's
				total = 0
				tupleList = []
				weightList = []
				vertPos = mesh.positions[i]
				for idx in range(len(vcmp.weights)):
					weightList.append(round(vcmp.weights[idx] * 255.0))
					total += weightList[idx]
				if bNormalizeWeights and total != 255:
					weightList[0] += 255 - total
					print ("Normalizing vertex weight", mesh.name, "vertex", i,",", total)
					
				for idx in range(len(vcmp.weights)):
					if idx > boneIdMax:
						if not isMeshVer3: 
							print ("Warning: ", mesh.name, "vertex", i,"exceeds the vertex weight limit of ", boneIdMax, "!", )
						break
					elif vcmp.weights[idx] != 0:				
						byteWeight = weightList[idx]
						tupleList.append((byteWeight, vcmp.indices[idx]))
					if bCalculateBoundingBoxes:
						thisBoneBB = boneWeightBBs[ vcmp.indices[idx] ] if vcmp.indices[idx] in boneWeightBBs else [999999.0, 999999.0, 999999.0, -999999.0, -999999.0, -999999.0]
						if vertPos[0] < thisBoneBB[0]: thisBoneBB[0] = vertPos[0]
						if vertPos[1] < thisBoneBB[1]: thisBoneBB[1] = vertPos[1]
						if vertPos[2] < thisBoneBB[2]: thisBoneBB[2] = vertPos[2]
						if vertPos[0] > thisBoneBB[3]: thisBoneBB[3] = vertPos[0]
						if vertPos[1] > thisBoneBB[4]: thisBoneBB[4] = vertPos[1]
						if vertPos[2] > thisBoneBB[5]: thisBoneBB[5] = vertPos[2]
						boneWeightBBs[ vcmp.indices[idx] ] = thisBoneBB
						
				tupleList = sorted(tupleList, reverse=True) #sort in ascending order
				pos = bs.tell()
				lastBone = 0
				for idx in range(len(tupleList)):
					bFind = False
					for b in range(len(boneRemapTable)):
						bnName = names[boneInds[boneRemapTable[b]]]
						#if doDD2Skin and "_Edit" in bnName:
						#	bnName = ""
						if bnName == bonesList[tupleList[idx][1]]:
							writeBoneID(b, idx)
							lastBone = b
							bFind = True
							break	
					if bFind == False: #assign unmatched bones
						if not bRigToCoreBones:
							writeBoneID(lastBone, idx)
						else:
							for b in range(lastBone, 0, -1):
								if names[boneInds[boneRemapTable[b]]].find("spine") != -1 or names[boneInds[boneRemapTable[b]]].find("hips") != -1:
									writeBoneID(b, idx)
									break
									
				for x in range(len(tupleList), 8):
					writeBoneID(lastBone, x)
				
				bs.seek(pos+8)
				for wval in range(len(tupleList)):
					bs.writeUByte(tupleList[wval][0])
				bs.seek(pos+16)
							
	colorsStart = bs.tell()
	if bDoColors:
		for m, mesh in enumerate(submeshes):
			if bColorsExist:
				for p, pos in enumerate(mesh.positions):
					RGBA = mesh.colors[p] if p < len(mesh.colors) else NoeVec4((1.0, 1.0, 1.0, 1.0))
					for c in range(4): 
						color = RGBA[c] if c < len(RGBA) else 1.0
						bs.writeUByte(int(color * 255 + 0.5))
			else:
				for p, pos in enumerate(mesh.positions):
					bs.writeInt(-1)
	
	vertexDataEnd = bs.tell()
	
	for mesh in submeshes:
		faceStart = bs.tell()
		submeshFaceStride.append(faceStart - vertexDataEnd)
		submeshFaceCount.append(len(mesh.indices))
		submeshFaceSize.append(len(mesh.indices))
		for idx in mesh.indices:
			bs.writeUShort(idx)
		if ((bs.tell() - faceStart) / 6) % 2 != 0: #padding
			bs.writeUShort(0)
	faceDataEnd = bs.tell()
	
	#update mainmesh and submesh headers
	loopSubmeshCount = 0
	for ldc in range(numLODs): 
		for mmc in range(mainmeshCount):
			mainmeshVertexCount = 0
			mainmeshFaceCount = 0
			bs.seek(meshOffsets[mmc] + 16)
			
			for smc in range(meshVertexInfo[mmc][1]):
				bs.seek(4, 1)
				bs.writeUInt(submeshFaceCount[loopSubmeshCount])
				bs.writeUInt(int(submeshFaceStride[loopSubmeshCount] / 2))
				bs.writeUInt(submeshVertexStride[loopSubmeshCount])
				if formats[sGameName]["meshVersion"] >= 3 or sGameName == "RERT" or sGameName == "ReVerse" or sGameName == "MHRise" or sGameName == "RE8" or sGameName == "SF6" or sGameName == "RE4":
					bs.seek(8, 1)
				mainmeshVertexCount += submeshVertexCount[loopSubmeshCount]
				mainmeshFaceCount += submeshFaceSize[loopSubmeshCount]
				loopSubmeshCount += 1
			bs.seek(meshOffsets[mmc]+8)
			bs.writeUInt(mainmeshVertexCount)
			bs.writeUInt(mainmeshFaceCount)
		
	#Fix vertex buffer header:
	skipAmt = 16 if not isMeshVer3 else 24
	fcBuffSize = faceDataEnd - vertexDataEnd
	if bReWrite or bWriteBones:
		bs.seek(newVertBuffHdrOffs+skipAmt) 
	else: 
		bs.seek(vBuffHdrOffs+skipAmt)
	
	if isMeshVer3:
		facesDiff = (80 + 8*vertElemCountB if bWriteBones else 0) if not bReWrite else (80 + 8*vertElemCount)
		bs.writeUInt(faceDataEnd - vertexPosStart) #total buffer size
		#print("faces offset", bs.tell(), vertexDataEnd, newVertBuffHdrOffs, facesDiff, vertexDataEnd - newVertBuffHdrOffs - facesDiff)
		bs.writeUInt(vertexDataEnd - newVertBuffHdrOffs - facesDiff) #face buffer offset
		bs.seek(4,1) #element counts
		bs.writeUInt(faceDataEnd - vertexPosStart) #total buffer size2
		bs.writeUInt(faceDataEnd - vertexPosStart) #total buffer size3
		bs.writeInt(-(vertexPosStart))
		bs.seek(32, 1)
	else:
		bs.writeUInt64(vertexDataEnd) #face buffer offset
		bs.seek(RERTBytes, 1)
		bs.writeUInt(vertexDataEnd - vertexPosStart) #vertex buffer size
		bs.writeUInt(fcBuffSize) #face buffer size
		bs.seek(4,1) #element counts
		bs.writeUInt64(fcBuffSize)
		bs.writeInt(-(vertexPosStart))
	
	if bReWrite:
		bs.seek(newVertBuffHdrOffs + 48 + SF6SkipBytes + (RERTBytes * 2))
	else:
		bs.seek(RERTBytes, 1)
	
	vertElemHdrStart = bs.tell()
	
	for i in range (vertElemCount):
		elementType = bs.readUShort()
		elementSize = bs.readUShort()
		if elementType == 0:
			bs.writeUInt(vertexPosStart - vertexPosStart)
		elif elementType == 1:
			bs.writeUInt(normalTangentStart - vertexPosStart)
		elif elementType == 2:
			bs.writeUInt(UV0start - vertexPosStart)
		elif elementType == 3:
			bs.writeUInt(UV1start - vertexPosStart)
		elif elementType == 4:
			bs.writeUInt(bnWeightStart - vertexPosStart)
		elif elementType == 5:
			bs.writeUInt(colorsStart - vertexPosStart) 
	
	if isMeshVer3: 
		DD2amt = 8 if isDD2Mesh else 0
		bs.seek(136 + int(DD2amt * 2))
		bs.writeUInt64(vertElemHdrStart-16) #fix ukn3
		bs.seek(152 + DD2amt)
		bs.writeUInt64(vertexPosStart) #fix Vertices offset
		if isDD2Mesh:
			bs.seek(144)
			bs.writeUInt64(DD2HashesOffset)
	
	#fix main bounding box:
	bs.seek(LOD1Offs+8+BBskipBytes)
	
	#Calculate Bounding Sphere:
	BBcenter = NoeVec3((min[0]+(max[0]-min[0])/2, min[1]+(max[1]-min[1])/2, min[2]+(max[2]-min[2])/2))
	sphereRadius = 0
	for mesh in mdl.meshes:
		for position in mesh.positions:
			distToCenter = (position - BBcenter).length()
			if distToCenter > sphereRadius: 
				sphereRadius = distToCenter
				
	bs.writeBytes((BBcenter * newScale).toBytes()) #Bounding Sphere
	bs.writeFloat(sphereRadius * newScale) #Bounding Sphere radius
	bs.writeBytes((min * newScale).toBytes()) #BBox min
	bs.writeBytes((max * newScale).toBytes()) #BBox max
	if isMeshVer3:
		bs.seek(-20,1); bs.writeUInt(1)
		bs.seek(12,1); bs.writeUInt(1)
	
	#fix skeleton bounding boxes:
	if bDoSkin and bCalculateBoundingBoxes:
		for idx, box in boneWeightBBs.items():
			try:
				if bReWrite or bWriteBones:
					remappedBoneIdx = newSkinBoneMap.index(idx)
				else:
					remappedBoneIdx = boneRemapTable.index(idx)
				pos = mdl.bones[newSkinBoneMap[remappedBoneIdx]].getMatrix()[3]
			except:
				continue
			bs.seek(newBBOffs+16+remappedBoneIdx*32) 
			boneWeightBBs[idx] = [(box[0]-pos[0])*newScale, (box[1]-pos[1])*newScale, (box[2]-pos[2])*newScale, 1.0, (box[3]-pos[0])*newScale, (box[4]-pos[1])*newScale, (box[5]-pos[2])*newScale, 1.0]
			box = boneWeightBBs[idx]
			for coord in box:
				bs.writeFloat(coord)
	
	#set to only one LODGroup
	bs.seek(LOD1Offs)
	bs.writeByte(1)
	
	#disable shadow LODs
	bs.seek(LOD1OffsetLocation+8)
	bs.writeUInt(0)
	
	#disable normals recalculation data
	bs.seek(normalsRecalcOffsLocation)
	bs.writeUInt(0)
	
	#disable group pivots data
	bs.seek(88)
	bs.writeUInt(0)
	
	#set numModels flag
	doSetFlag = bSetNumModels or bDoVFX or (openOptionsDialog and openOptionsDialog.flag != -1)
	if doSetFlag or bReWrite:
		bs.seek(16)
		if openOptionsDialog and openOptionsDialog.flag != -1:
			bitFlag = openOptionsDialog.flag
		else:
			bitFlag = 0x00
			if bDoVFX or rapi.getOutputName().find("2109148288") != -1 or isMeshVer3: 
				bitFlag = bitFlag + 0x80
			if bDoSkin: 
				bitFlag = bitFlag + 0x03
		print("Flag: ", bitFlag)
		bs.writeUByte(bitFlag)
	
	#remove blendshapes offsets
	bs.seek(bsHdrOffLocation)
	bs.writeUInt(0)
	bs.seek(bsIndicesOffLocation)
	bs.writeUInt(0)
	
	fileEnd = faceDataEnd
	
	if sGameName == "DD2" and bDoSkin:
		bs.seek(12)
		bs.writeUInt(2303293740) #Unknown
		
		#Extra weights buffer:
		bs.seek(bnWeightStart)
		bnWtBuffSz = colorsStart - bnWeightStart
		weightBytes = bs.readBytes(bnWtBuffSz)
		bs.seek(bnWeightStart)
		dd2BoneIdx = skinBoneMapNames.index("Spine_1") if "Spine_1" in skinBoneMapNames else 0
		
		for i in range(int(bnWtBuffSz / 16)):
			bID = bs.readUByte()
			bs.seek(-1, 1)
			bs.writeUInt64(0)
			bs.writeUInt64(255)
		bs.seek(fileEnd)
		padToNextLine(bs)
		exWeightsBuffStart = bs.tell()
		bs.writeBytes(weightBytes)
		fileEnd = bs.tell()
		bs.seek(newVertBuffHdrOffs+16) 
		bs.writeUInt64(exWeightsBuffStart)
		bs.seek(newVertBuffHdrOffs+48) 
		bs.writeUInt64(bnWtBuffSz)
	
	#fileSize
	bs.seek(8)
	bs.writeUInt(fileEnd) 
	
	return 1