global debug = true -- set as true for verbose output to Listener

-------------------------- Data structure definitions

Struct VSkeleton	-- the top level structure. It contains a list of Bones (see below), a list
					-- of textures indaexed to the Object's MaterialID's,
					-- and a list of Faces (including geometry and texture coords, and normals)
					-- it also contains a creation function (Vskeleton.Build)
					-- and functions which gather a texturemap list (Vskeleton.GetTextureList)
					-- and an index list of vertex to bone attachments
					-- finally includes a "printout" function
	(
	Bones,\ -- array of Vbones
	Faces,\-- array of faces
	MeshObject,\ -- the Editable Mesh node that's being exported
	VertexAttachmentList,\ -- Index of mesh vertex > bone attachments
	TextureList, -- indexed to Max material ID's
	
	fn Build RootNode = -- collects the skeleton bone array given root node
		(
		NewSkeleton = Vskeleton ()
		MeshList = for items in Rootnode where classof items == Editable_mesh\ -- collect a list of meshes
			and (GetUserProp items "NoExport" == false or \
				GetUserProp items "NoExport" == undefined)\
				 collect items
		case of -- is there a mesh object to export?
			(
			(Meshlist.count == 0):(return "Error: This hierarchy contains no Editable Mesh objects. It cannot be exported.")
			(Meshlist.count > 1):(return "Error: This hierarchy contains multiple Editable Mesh objects. It cannot be exported.")
			default:(NewSkeleton = Vskeleton \ -- create a new skeleton record
				MeshObject:Meshlist[1] \ -- this is it's meshobject
				TextureList:(Vskeleton.GetTextureList Meshlist[1]) \ --call GetTextureList on the mesh to generate the texturelist 
				)
			)
		NewSkeleton.bones = #() -- initialize the bone record array
		global VS_InputID = -1 -- set the bone counter to 0
		progressStart "Analyzing skeleton"
		progressUpdate (1)
		fn Traverse InputNode = -- defined as a function so we can call this recursively
			(
			VS_InputID += 1
			progressupdate (VS_InputID as float / (objects.count) * 100)
			InputParent = case of
				(
				(NewSkeleton.bones.count == 0): #(-1)
				default:for item in NewSkeleton.bones \
							where item.name == Inputnode.parent.name collect item.ID
				)
			InputParent = InputParent[1]
			if debug do format "% % %\n" VS_InputID InputNode InputParent 
			NewBone = (Vbone.Build (copy VS_InputID) InputNode InputParent)
			append NewSkeleton.Bones Newbone -- make a bone record
			if InputNode.children.count > 0 then 
				(
				for EachChild in InputNode.children do traverse EachChild -- call function on all the children
				)
			return "OK"	
			)
		if debug do format "Collecting node Data\n"	
		traverse Rootnode  -- call the traverse function defined above, which will recurse down the whole skeleton
		ProgressEnd ()
		-- call the GetVAlist function to decide what kind of vertex attachment there is and generate a list
		NewSkeleton.VertexAttachmentList =(GetVAlist NewSkeleton)
		If NewSkeleton.VertexAttachmentList.count == 1 and findstring (NewSkeleton.VertexAttachmentList[1]) "Error:" > 0 do return NewSkeleton.VertexAttachmentList[1] -- pass it back if there was an error	
		if debug do format "completed Vertex Attachment List\n"
		-- now that we know what verts belong to what faces, build the Faces array
		NewSkeleton.Faces = #()
		if debug do format "Collecting face data"
		progressStart "Collecting Face Data"
		for MakeFace = 1 to (NewSkeleton.MeshObject.NumFaces) do -- call the Vface build method for each face
			(NewFace = (Vface.build NewSkeleton MakeFace)
			if classof NewFace != Vface do (Return NewFace)
			ProgressUpdate (MakeFace as float / NewSkeleton.Meshobject.Numfaces * 100)
			append NewSkeleton.Faces NewFace
			)
		ProgressEnd ()
		return NewSkeleton	
		),
		
	Fn GetTextureList MeshNode =
		(if debug do format "Collecting texture list for object %\n" Meshnode
		case  Classof MeshNode.Material of --- What kind of material is it?
			(
			Default: -- no material or something exotic
				(
				Messagebox "Warning: This object has no material. It will be exported using 'null.bmp' as a placeholder"
				NewTextureList = #("Null.bmp")
				)
			Standardmaterial: -- no multimaterial. THIS WILL NOT BE COMPATIBLE WITH SMDLEXP.DLO
				(
				Messagebox ("Warning: Material on " + (MeshObject.name) + " is not a Multi-SubObject Material")
				OnlyTexture = If Classof MeshNode.material.diffusemap != Bitmaptexture \
						 then "Null.bmp" \ 
						 else FileNameFromPath MeshObject.Material.diffusemap.bitmap.filename
				NewTextureList = #(OnlyTexture) -- in this case the list has only one entry
				)
			MultiMaterial: -- the preferred case
				(NewTextureList = #()
				for SubMats in MeshNode.Material.Materiallist do -- collects a list of textures (or dummies) indexed by materialID
					Append NewTextureList (if classof submats.diffusemap != Bitmaptexture then \
						"null.bmp" else FileNameFromPath submats.diffusemap.bitmap.filename)
				)	
			)
		if debug then format "%\n" NewTextureList
		return NewTextureList
		),
		
	Fn PrintOut Skeleton Output:#Window =
		( 	if Output == #Window then
				(Output = newscript())
			else
				(
				testFileName = MaxfilePath + Maxfilename
				FilePath = GetSaveFileName types:"*.smd|*.smd|" filename:TestfileName
				if filepath == undefined do (return "no file destination supplied; export aborted")
				Output = createfile FilePath
				
				)
		
		format "printing smd file to: %\n" output
		format "version 1\n" to:Output -- start with header info
		format "nodes\n" to:Output
		 -- then format out header section, prettied up for readability
		for item in Skeleton.bones do 
			(padding = case of 
				(
				(item.ID > 99):("")
				(item.ID > 9):(" ")
					default:("  ")
				)
			padding2 = " "
			if item.name.count < 20 then " " for i = 1 to (20 - item.name.count) do (padding2 = padding2 + " ")
			
			format "%% \"%\"%%\n" padding item.ID Item.name padding2 Item.parent to:Output
			)
		format "end\n" to:Output
		-- now format out the bone data by repeatedly calling the Vbone printout function
		format "skeleton\ntime 0\n" to:output
		for item in Skeleton.bones do
			(
			ThisBoneRec = Vbone.printout item -- a little fancy footwork for stream redirection.
			Seek ThisBoneRec 0
			PrintBone = Readline ThisBoneRec
			Format "%\n" Printbone to:Output
			)
		format "end\n" to:Output
		-- now the triangles, using the VFace printout method
		format "triangles\n" to:Output
			for item in Skeleton.Faces do
			(
			ThisFaceRec = VFace.printout item -- same trick as above; all face records must have 4 lines
			Seek ThisFaceRec 0
			for I = 1 to 4 do 
				(
		 	PrintFaceRec = Readline ThisFaceRec
			format "%\n" PrintFaceRec to:Output
				)
			)
		format "end\n\n" to:Output
		try (close output)
		catch (print "Done")
		if classof output == filestream and (Querybox "View output .SMD?" == true) do (edit filepath)
		return "OK" -- and we're all done
		)
	)


Struct VBone -- data structure for bones. Includes a create function and a printout function
	(
	ID,\ -- the identifying number of the bone 
	name,\ -- the name of the Max file bone
	parent, \-- the ID number (= index position -1) of the bone's parent bone
	TrnX, TrnY, TrnZ,\ -- worldspace X Y and Z coords of the bone
	RotX, RotY, RotZ, \-- Euler X Y and Z rotations of bone in it's parent's space

	fn Build InputID InputNode InputParent = -- returns a new bone from input data
			(
			NewBone = Vbone ID:inputID name:InputNode.name Parent:InputParent
			MyXform = copy InputNode.transform -- the input's transform
			if InputParent > -1 then -- transform into parent's space
				(
				ParentTM = InputNode.parent.objecttransform
				MyXform = MyXform *(inverse(ParentTM))
				ScaleComp = (copy ParentTM).scalepart
	  		  	Myxform = scale MyXform ScaleComp true -- corrects for scaled bones making their units equal to world units
				)
			--- set the new bone's rotation and translation from components	
			NewBone.TrnX = MyXform.translationpart.X
			NewBone.TrnY = MyXform.translationpart.Y
			NewBone.TrnZ = MyXform.translationpart.Z
			NewBone.RotX = DegtoRad (MyXform.rotationpart as eulerangles).X  -- eulerized as radians
			NewBone.RotY = DegtoRad (MyXform.rotationpart as eulerangles).Y	 --  		" "
			NewBone.RotZ = DegtoRad (MyXform.rotationpart as eulerangles).Z	 -- 		" "
			If debug do format "%\n" NewBone
			return NewBone
			),

	fn Printout InputBone = -- printout function for bones
		(BoneStream = "" as stringstream 
		padding = case of 
			(
			(InputBone.ID > 99):("")
			(InputBone.ID > 9):(" ")
			default:("  ")
			)
		format "%%   " padding InputBone.ID to:BoneStream
		format "%   %   %       " (NF InputBone.TrnX) (NF InputBone.TrnY) (NF InputBone.TrnZ) to:BoneStream
		format "%   %   %\n" (NF InputBone.RotX) (NF InputBone.RotY) (NF InputBone.RotZ) to:BoneStream
		if debug do print Bonestream
		return BoneStream
		)
	)	
	
Struct VFace -- a single triange. includes a texturemap and 3 VVerts
	(Texture, -- the TextureMap of this face 
	VertexArray, -- array of 3 VVerts
	fn Build Skeleton FaceIndex = -- returns a face record
		(
		NewFace = Vface ()-- create a new face record
		FaceMatID = GetFaceMatID Skeleton.MeshObject FaceIndex
		NewFace.texture = (if Skeleton.TextureList[FaceMatID] == undefined then Skeleton.TextureList[1] else Skeleton.TextureList[FaceMatID])-- attach appropriate texture
		if debug do format "Face texture index = % (%)\n" FaceMatID Skeleton.TextureList[FaceMatID]
		NewFace.VertexArray = #()
		For CornerIndex = 1 to 3 do -- iterates once for each corner of the triangle
			(
			NewVert=  (VVert.build Skeleton FaceIndex CornerIndex)
			if NewVert == "Error:Could Not find TexCoords on mesh" do return NewVert
			append NewFace.VertexArray NewVert
			)
		if Debug do format "%\n" NewFace 
		return NewFace
		),
	fn Printout InputFace = ( -- printout function for 
		FaceStream = "" as stringstream
		format "%\n" InputFace.Texture to:FaceStream
		for I = 1 to 3 do 
			(
			ThisVert = VVert.printout InputFace.VertexArray[I]
			seek ThisVert 0
			format "%\n" (readline ThisVert) to:FaceStream
			)
		if debug do print FaceStream
		return FaceStream
		)
	)

Struct VVert -- record for a single vert in the mesh
	(Parent, -- associated bone name
	Position, -- point3 value for the vertex world space position
	Normal, -- point3 of vertex normal
	U,V,-- Texture Coordinates
	
	fn Build Skeleton FaceIndex CornerIndex = -- returns vertex record
		(ThisVertexIndex = (GetFace Skeleton.MeshObject FaceIndex)
		if debug do format "Triangle: %\n" ThisVertexIndex
		case CornerIndex of -- this is just getting around the fact that you can't access parts of a [] number by index (!)
			(
				1:	ThisVertexIndex = (ThisVertexIndex.x as integer)
				2:	ThisVertexIndex = (ThisVertexIndex.y as integer)
				3:	ThisVertexIndex = (ThisVertexIndex.z as integer)
			)
		if debug do format "corner % = vertex %\n" CornerIndex ThisVertexIndex
		NewVert = VVert Position:((GetVert Skeleton.MeshObject ThisVertexIndex) as point3) \
						Normal:((GetNormal Skeleton.MeshObject ThisVertexIndex)as point3) \
						Parent:(Skeleton.VertexAttachmentList[ThisVertexIndex])
		try (ThisTFace = GetTVface Skeleton.MeshObject FaceIndex)
		catch (return "Error:Could Not find TexCoords on mesh")
		case CornerIndex of -- this is just getting around the fact that you can't access parts of a [] number by index (!)
			(
				1:	ThisTvert = GetTVert Skeleton.MeshObject (ThisTface.x as integer)
				2:	ThisTvert = GetTVert Skeleton.MeshObject (ThisTface.y as integer)
				3:	ThisTvert = GetTVert Skeleton.MeshObject (ThisTface.z as integer)
			)

		NewVert.U = ThisTvert.x
		NewVert.V = ThisTvert.y
		if debug do format "%\n" NewVert
		return NewVert
		),
	fn Printout InputVert =
		(VertStream = "" as stringstream
		format "%   " InputVert.Parent to:VertStream
		IVP = (InputVert.position) as point3
		for VertValue in #(IVP.x, IVP.Y, IVP.Z) do (format "%   " (NF VertValue) to:VertStream)
		format "       " to:VertStream
		IVN = (InputVert.Normal) as point3
		for VertValue in #(IVN.x, IVN.Y, IVN.Z) do (format "%   " (NF VertValue) to:VertStream)
		format "       %   %\n" (NF InputVert.U) (NF InputVert.V) to:VertStream   
		return VertStream
		)
	 ) 

fn NF InputNumber = -- formatting function to convert Maxscript floats for export as non-scientific-notation numbers. 
				    -- NB due to max's way of storing floating point #'s there are some errors below 10^-4
	( InputNumber = InputNumber as float
		case of
		(
			(InputNumber < 0):(sign = -1)
			(InputNumber == 0):(return " 0.00000")
			default:(sign = 1)
		)
		InputNumber = abs InputNumber
		power = (log10 InputNumber)
		case of
			(
			(power < -4): return " 0.00000"
			(power < 1 ): InputNumber = (((InputNumber * 10^6) as integer)as float)/10^6
			)
		InputNumber = InputNumber * sign
		pad = if Inputnumber < 0 then "" else " "
		InputNumber = pad + (InputNumber as string)
		if InputNumber.count < 8 do
			(for i = 1 to (8 - InputNumber.count) do (InputNumber = InputNumber + "0"))
		if inputNumber.count > 8 do inputnumber = (substring Inputnumber 1 8)
		return InputNumber
	)


---------------------------- Vertex List Generation Functions
fn GetSkinModVerts mesh = -- generates vertex attachment list (of bone names)
						  -- NB results com from TOP skin mod applied to mesh
	(if Mesh.modifiers.count == 0 do return "Error: No Modifiers Present"
	Skinmod = for item in mesh.modifiers where classof item == Skin collect item
	Skinmod = Skinmod[1]
	--- open the command panel
	Max Modify mode
	modpanel.Setcurrentobject SkinMod
	
	VertexAttachmentList = #()
	VertCount = Skinops.GetNumberVertices Skinmod
	Reassign = true
	Reweight = "Yes"
	for VertIndex = 1 to VertCount do
		(LocalBones = Skinops.GetVertexWeightCount Skinmod VertIndex
		
		case of
			(
				(LocalBones == 0):	(if Reassign == false do \
										Reassign = Querybox "This Mesh contains vertices which are not attached to any bone.\nOK to assign to root node?\n Not reassigning will abort export."
									if Reassign == no do (return "Error: Unparented Vertices")
									Skinops.ReplaceVertexWeights SkinMod VertIndex 1 1.0
									LocalBoneName = skinOps.GetBoneName SkinMod 1 0 
									if debug do format "Reassigned unparented vertex % to bone %\n" VertIndex LocalBoneName
									)
				(Localbones > 1):	(if Reweight == empty do\
										Reweight = Querybox "This Mesh contains vertices assigned to multiple bones.\nOK to modify mesh for better preview?"
									
									TestWeight = Skinops.GetVertexWeight Skinmod VertIndex 1
									TestBone = 1
									format "Multiple Weights for vertex %\n" vertindex
									for EachBone = 1 to Localbones do
										(
										ComparisonWeight = Skinops.GetVertexWeight Skinmod VertIndex  EachBone
										format "% " ComparisonWeight
										format "% \n" (Skinops.GetVertexWeightBoneID Skinmod Vertindex EachBone) 
										if ComparisonWeight > TestWeight do
											(
											TestWeight = ComparisonWeight
											TestBone = Skinops.GetVertexWeightBoneID Skinmod Vertindex EachBone 
											)
										)
									if Reweight == "Yes" do Skinops.ReplaceVertexWeights SkinMod VertIndex TestBone 1.0
									LocalBoneName = Skinops.GetBoneName skinmod testbone 0
									format "\n Assigned vertex % to bone % with new weight of 1\n" vertindex LocalBoneName
									
									)
				default: 	(
							LocalBoneID = Skinops.GetVertexWeightBoneID SkinMod VertIndex 1 
							LocalBoneName = skinOps.GetBoneName SkinMod LocalBoneID 0  
							format "Vertex % belongs only to bone %\n" vertindex Localbonename
						  	)
			)
		
		Append VertexAttachmentList LocalBoneName		
		)
	if debug do format "returning named attachment list: %\n" VertexAttachmentList
	return VertexAttachmentList
	)

fn GetLinkedXformVerts mesh = -- returns vertex attachment list for a Linked-xform mesh
	(
	if Mesh.modifiers.count == 0 do return "Error: No Modifiers Present"
	messagebox "Attempting to export a model with Linked_Xform vertex attachments\n. Please click 'Yes' in response to following Max dialogs"
	Modlist = mesh.modifiers
	ModList = for I = 1 to (Mesh.Modifiers.count) where classof Mesh.modifiers[I] == Linked_xform collect I -- an index of bone attachments
	NameList = for I = 1 to (Mesh.Modifiers.count) where classof Mesh.modifiers[I] == Linked_xform \
		collect Mesh.modifiers[I].control.name
	Outputlist = #()

	-- traverse up the modifiers list to get the vert selections leading into the mods
	for index in Modlist do 
		(
		if debug do format "found Link_Xform modifier at index [%] \n" index
		CurrentIndex = #()
		GetSel = index
		--- walk up the list of modifiers, looking for one with a selection bitarray
		while Getsel < (Mesh.modifiers.count + 1) != UndefinedClass do
			(
			GetSel +=1
			ThisMod = classof Mesh.modifiers[GetSel]
			if ThisMod == Mesh_Select \
			 	or ThisMod == Editable_Mesh do\
					(
					Modpanel.SetcurrentObject Mesh.Modifiers[GetSel]
					bob = GetVertSelection Mesh
					append CurrentIndex bob
					exit
					)
			if GetSel > Mesh.Modifiers.count do
					(ModPanel.SetCurrentObject Mesh.baseobject
					append CurrentIndex (GetVertSelection Mesh)
					exit
					)
			if debug do format "No vertex selection found at modifier %\n" Getsel
			)
			Append Outputlist CurrentIndex		
		)
	
	-- we now have an array of bitarrays matching the name arrays above
	VertexAttachmentList = #()
	for ThisVert = 1 to (Mesh.numverts) do
		(format "looking for vert %...\n" ThisVert
		for K = 1 to (NameList.count) do
			(
			if debug do format "Selbits for bone % = %\n" k outputlist[k][1]
			if OutPutList[k][1][ThisVert] == true do append VertexBoneList Namelist[K]
			)
		)
	if debug do format "returning named attachment list: %\n" VertexAttachmentList
	return VertexAttachmentList 	
	)
	

fn GetVAlist Skeleton = -- determines the presence of Skin or Linked_Xform mods, calls either GetSkinVerts or 
						-- GetLinkedXformVerts as appropriate, and converts the list from names to indices
	(SkinMods = for item in Skeleton.MeshObject.modifiers	where classof item == Skin collect item
	LinkXforms = for item in Skeleton.MeshObject.modifiers where classof item == Linked_Xform collect item
	case of 
		(
			((Skinmods.count) > 0 and (LinkXforms.count) > 0):
				(
				Messagebox "Models can only be exported with either Skin modifiers OR Linked_Xform modifiers,\nnot both. This model cannot be exported."
				return "Error:Multiple skin attachment methods found."
				)
			(Skinmods.count > 0):RawAttachList = GetSkinModVerts Skeleton.MeshObject -- call GetSkinModVerts and get an indexed list of bone attachment names
			(LinkXforms.count > 0):RawAttachList = GetLinkedXformVerts Skeleton.MeshObject -- do the same for LinkedXforms
			default:return "Error:No skin attachment method found"
		)
	if RawAttachlist.count == 1 and Findstring RawAttachList[1] "Error" > 0 do (format "%\n" Rawattachlist ;  return "Could Not Generate Raw Attachment List") -- if its an error, pass it back upstream
	IndexedAttachList = #()
	for item in RawAttachList do
		(
		for BoneName in Skeleton.Bones do
			(
			if BoneName.name == item do	(
											if debug do format "Matched Bone % to ID % \n" item BoneName.ID
											append IndexedAttachList BoneName.ID
											exit
											)
			if debug do format  "seeking %\n..." item
			)
		)
		
	if debug do format "returned Vertex list: %\n" IndexedAttachList
	return IndexedAttachList
	)

------------------------------ alias functions for use from the listener

fn GetSkelFrom rootbone debugkey:false =
	(
	global debug = debugkey
	return Vskeleton.build Rootbone
	)

	
fn getRoot = 
	(
	case of
		( 
		(($/*).count == 0):(Messagebox "There is nothing to Export"; return undefined) -- no object in scene!
		(($/*).count > 1):(Messagebox "There are too many things to export.\n Exportable scenes must contain only one object\nwhich has no parent."; return undefined) -- multiple objects at world level
		default:return $/*[1]
		)
	)


fn SMD switch = 
	(
	case switch of
		(
		#window:
				(
					WorkRoot = getroot()
					if WorkRoot == undefined do return "Could not export because hierarchy is unsuitable."
					SetWaitCursor()
					OpenSkel = GetSkelFrom WorkRoot debugkey:true
					SetWaitCursor()
					Vskeleton.printout OpenSKel output:#window
					SetArrowCursor()
				)
		#help: edit  (scriptspath + "SMDExportHelp.txt")
		#debug: 
				(
					WorkRoot = getroot()
					if WorkRoot == undefined do return "Could not export because hierarchy is unsuitable."
					SetWaitCursor()
					OpenSkel = GetSkelFrom $bone01 debugkey:true
					SetWaitCursor()
					Vskeleton.printout OpenSkel output:#file
					SetArrowCursor()
				)
		default:
				(
					WorkRoot = getroot()
					if WorkRoot == undefined do return "Could not export because hierarchy is unsuitable."
					SetWaitCursor()
					OpenSkel = GetSkelFrom WorkRoot debugkey:false
					SetWaitCursor()
					Vskeleton.printout OpenSKel output:#file
					SetArrowCursor()
				)
	
		)
		
	)
