class_name X # this is awful. i hate microsoft so much. # https://learn.microsoft.com/en-us/windows/win32/direct3d9/dx9-graphics-reference-x-file-interfaces # NOTE: adaptation of https://github.com/oguna/Blender-XFileImporter/blob/master/xfile_parser.py static var loadCache: Dictionary var currentFilePath = "" var majorVersion = 0 var minorVersion = 0 var isBinaryFormat = false var lineNumber = 0 var binaryFloatSize = 0 var binaryNumCount = 0 var p = -1 var end = -1 var compressed = false var file: PackedByteArray var scene = Node3D.new() var meshes: Array var materials: Array var finalMeshes: Array const MAX_TEX_COORDS = 2 static func _CalcMinMaxPos(verts: PackedVector3Array) -> Vector3: var minX: float = 0 var maxX: float = 0 var minY: float = 0 var maxY: float = 0 var minZ: float = 0 var maxZ: float = 0 for vert in verts: if vert.x < minX: minX = vert.x if vert.x > maxX: maxX = vert.x if vert.y < minY: minY = vert.y if vert.y > maxY: maxY = vert.y if vert.z < minZ: minZ = vert.z if vert.z > maxZ: maxZ = vert.z return Vector3(abs(minX - maxX), abs(minY - maxY), abs(minZ - maxZ)) static func _CalcMinMaxPosTotal(finalMeshes: Array) -> Vector3: var minX: float = 0 var maxX: float = 0 var minY: float = 0 var maxY: float = 0 var minZ: float = 0 var maxZ: float = 0 for mesh in finalMeshes: var meshX = mesh.get_meta("meshWidth") var meshY = mesh.get_meta("meshHeight") var meshZ = mesh.get_meta("meshDepth") if meshX < minX: minX = meshX if meshX > maxX: maxX = meshX if meshY < minY: minY = meshY if meshY > maxY: maxY = meshY if meshZ < minZ: minZ = meshZ if meshZ > maxZ: maxZ = meshZ return Vector3(abs(minX - maxX), abs(minY - maxY), abs(minZ - maxZ)) static func MeshWidth(mesh: Node) -> float: return mesh.get_meta("meshWidth") static func MeshHeight(mesh: Node) -> float: return mesh.get_meta("meshHeight") static func MeshDepth(mesh: Node) -> float: return mesh.get_meta("meshDepth") func GetStringBytes(start: int, end: int): return file.slice(start, end).get_string_from_ascii() func ReadUntilEndOfLine(): if isBinaryFormat: return while p < end: var tmp = GetStringBytes(p, p + 1) if tmp == "\n": p += 1 lineNumber += 1 return p += 1 func FindNextNonWhiteSpace(): if isBinaryFormat: return while true: while p < end and (GetStringBytes(p, p + 1) == " " or GetStringBytes(p, p + 1) == "\n"): if GetStringBytes(p, p + 1) == "\n": lineNumber += 1 p += 1 if p >= end: return # is comment? if GetStringBytes(p, p + 2) == "//" or GetStringBytes(p, p + 1) == "#": ReadUntilEndOfLine() pass else: break static func LoadModel(filePath: String): var sillyPath = str("" if filePath.contains("res://") else "res://", filePath.replace("/Map/", "/map/")) var file = FileAccess.open(sillyPath, FileAccess.READ) # Do a case insensitive lookup only if we have to, it's more expensive. if file == null: sillyPath = Utils.GetCaseiFileName(sillyPath) file = FileAccess.open(sillyPath, FileAccess.READ) if loadCache.has(sillyPath): file.close() return loadCache.get(sillyPath).duplicate() var x = X.new() x.currentFilePath = Utils.StripFilename(sillyPath) x.majorVersion = 0 x.minorVersion = 0 x.isBinaryFormat = false x.binaryFloatSize = 0 x.binaryNumCount = 0 x.p = 0 x.end = file.get_length() x.file = file.get_buffer(x.end) file.close() if x.GetStringBytes(x.p, x.p + 4) != "xof ": assert(false, "This is not a DirectX file!") return x.majorVersion = int(x.GetStringBytes(4, 6)) x.minorVersion = int(x.GetStringBytes(6, 8)) x.compressed = false var fileTypeRaw = x.GetStringBytes(8, 12) if fileTypeRaw == "txt ": x.isBinaryFormat = false elif fileTypeRaw == "bin ": x.isBinaryFormat = true elif fileTypeRaw == "tzip": x.isBinaryFormat = false x.compressed = true elif fileTypeRaw == "bzip": x.isBinaryFormat = true x.compressed = true else: assert(false, str("Unsupported DirectX file format: ", fileTypeRaw)) if x.isBinaryFormat or x.compressed: assert(false, "Binary and Compressed files are currently unsupported!") return x.binaryFloatSize = int(x.GetStringBytes(12, 16)) if x.binaryFloatSize != 32 and x.binaryFloatSize != 64: assert(false, str("Unknown float size ", x.binaryFloatSize, " specified in DirectX file header.")) x.p += 16 if x.compressed: # hard pass pass else: x.ReadUntilEndOfLine() x.ParseFile() for mesh: XMesh in x.meshes: var arr_mesh = ArrayMesh.new() var arr = [] arr.resize(Mesh.ARRAY_MAX) arr[Mesh.ARRAY_VERTEX]=mesh.verts arr[Mesh.ARRAY_TEX_UV]=mesh.uvs arr[Mesh.ARRAY_INDEX]=mesh.indices var meshInstance = MeshInstance3D.new() arr_mesh.add_surface_from_arrays(Mesh.PRIMITIVE_TRIANGLES, arr) meshInstance.mesh = arr_mesh var mat = StandardMaterial3D.new() mat.albedo_color = Color(randi() % 255 / 255.0, randi() % 255 / 255.0, randi() % 255 / 255.0) #mat.albedo_texture = texture #mat.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA_DEPTH_PRE_PASS if activeAlbedoHasAlpha else BaseMaterial3D.TRANSPARENCY_DISABLED meshInstance.set_surface_override_material(0, mat) #meshInstance.create_trimesh_collision() meshInstance.name = str(filePath, "_", Time.get_unix_time_from_system()) var meshSize = _CalcMinMaxPos(mesh.verts) meshInstance.set_meta("meshWidth", meshSize.x) meshInstance.set_meta("meshHeight", meshSize.y) meshInstance.set_meta("meshDepth", meshSize.z) x.finalMeshes.push_back(meshInstance) mesh.parent.add_child(meshInstance) var meshSize = _CalcMinMaxPosTotal(x.finalMeshes) x.scene.set_meta("meshWidth", meshSize.x) x.scene.set_meta("meshHeight", meshSize.y) x.scene.set_meta("meshDepth", meshSize.z) loadCache[sillyPath] = x.scene.duplicate() return x.scene func ReadBinWord() -> int: assert(end - p >= 2) var tmp = file.decode_u16(p) p += 2 return tmp func GetNextToken(): var s: String = "" if isBinaryFormat: # I am NOT doing this right now pass else: FindNextNonWhiteSpace() if p >= end: return s while p < end and GetStringBytes(p, p + 1) != " ": var tmp = GetStringBytes(p, p + 1) if tmp == ";" or tmp == "}" or tmp == "{" or tmp == ",": if not s: s += tmp p += 1 break s += GetStringBytes(p, p + 1) p += 1 return s func ReadHead(): var nameOrBrace = GetNextToken() if nameOrBrace != "{": if GetNextToken() != "{": assert(false, "Opening brace expected.") return return nameOrBrace return "" # We don't really care about the templates, just skip through them. func ParseTemplate(): var name = ReadHead() var guid = GetNextToken() #print(name) #print(guid) while true: var s = GetNextToken() if s == "}": break if s == null: assert(false, "Unexpected end of file reached while parsing template definition") var notSplitChar = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '+', '.', '-', 'e', 'E'] func CheckForSeparator(): if self.isBinaryFormat: return var token = GetNextToken() assert(token == "," or token == ";", "Separator character (';' or ',') expected.") func CheckForClosingBrace(): if isBinaryFormat: return var token = GetNextToken() assert(token == "}", "Closing brace expected.") func CheckForSemicolon(): if isBinaryFormat: return var token = GetNextToken() assert(token == ";", "Semicolon expected.") func ReadFloat(): if isBinaryFormat: assert(false, "Binary models are currently unsupported.") else: FindNextNonWhiteSpace() if GetStringBytes(p, p + 9) == "-1.#IND00" or GetStringBytes(p, p + 8) == "1.#IND00": p += 9 CheckForSeparator() return 0.0 elif GetStringBytes(p, p + 8) == "1.#QNAN0": p += 8 CheckForSeparator() return 0.0 var result_ = 0.0 var digitStart = p var digitEnd = p while p < end: var c = GetStringBytes(p, p + 1) if c in notSplitChar: digitEnd = p p += 1 else: break var tmp = GetStringBytes(digitStart, digitEnd) result_ = float(tmp) CheckForSeparator() return result_ var digitTable = ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9"] func ReadInt(): if isBinaryFormat: assert(false, "Binary models are currently unsupported.") else: FindNextNonWhiteSpace() var isNegative = false if GetStringBytes(p, p + 1) == "-": isNegative = true p += 1 var number = 0 while p < end: if not (GetStringBytes(p, p + 1) in digitTable): break number = number * 10 + int(GetStringBytes(p, p + 1)) p += 1 CheckForSeparator() if isNegative: return -number else: return number func TestForSeparator(): if isBinaryFormat: return FindNextNonWhiteSpace() if p >= end: return if GetStringBytes(p, p + 1) == ";" or GetStringBytes(p, p + 1) == ",": p += 1 func ReadVector2() -> Vector2: var x = ReadFloat() var y = ReadFloat() TestForSeparator() return Vector2(x, y) func ReadVector3(invertX:bool = false) -> Vector3: var x = -ReadFloat() if invertX else ReadFloat() var y = ReadFloat() var z = ReadFloat() TestForSeparator() return Vector3(x, y, z) func ParseTransformationMatrix(): ReadHead() var M11 = ReadFloat() var M21 = ReadFloat() var M31 = ReadFloat() var M41 = ReadFloat() var M12 = ReadFloat() var M22 = ReadFloat() var M32 = ReadFloat() var M42 = ReadFloat() var M13 = ReadFloat() var M23 = ReadFloat() var M33 = ReadFloat() var M43 = ReadFloat() var M14 = ReadFloat() var M24 = ReadFloat() var M34 = ReadFloat() var M44 = ReadFloat() CheckForSemicolon() CheckForClosingBrace() return [M11, M21, M31, M41, M12, M22, M32, M42, M13, M23, M33, M43, M14, M24, M34, M44] func ParseMesh(parent: Node): var mesh = XMesh.new() mesh.parent = parent meshes.push_back(mesh) var name = ReadHead() mesh.name = name var vertCount = ReadInt() for i in range(vertCount): mesh.verts.push_back(ReadVector3(true)) var faceCount = ReadInt() mesh.indexCount = faceCount for i in range(faceCount): var numIndices = ReadInt() if numIndices < 3: assert(false, str("Invalid index count ", numIndices, " for face ", i)) for i1 in range(numIndices): mesh.indices.push_back(ReadInt()) TestForSeparator() while true: var objectName = GetNextToken().replace("\r", "").replace("\n", "") #print(str("\"",objectName,"\"")) if not objectName: assert(false, "Unexpected end of file while parsing mesh structure") elif objectName == "}": break elif objectName == "MeshNormals": ParseMeshNormals(mesh) elif objectName == "MeshTextureCoords": ParseMeshTextureCoords(mesh) elif objectName == "MeshVertexColors": #ParseMeshVertexColors(mesh) ParseUnknownDataObject() elif objectName == "MeshMaterialList": ParseUnknownDataObject() elif objectName == "VertexDuplicationIndices": ParseUnknownDataObject() elif objectName == "XSkinMeshHeader": ParseUnknownDataObject() elif objectName == "SkinWeights": ParseUnknownDataObject() else: print("Unknown data object in mesh in x file") ParseUnknownDataObject() return mesh func ParseUnknownDataObject(): while true: var t = GetNextToken() if len(t) == 0: assert(false, "Unexpected end of file while parsing unknown segment.") if t == "{": break var counter = 1 while counter > 0: var t = GetNextToken() if len(t) == 0: assert(false, "Unexpected end of file while parsing unknown segment.") if t == "{": counter += 1 elif t == "}": counter -= 1 return func ParseMeshNormals(mesh: XMesh): ReadHead() var normalCount = ReadInt() for i in range(normalCount): mesh.normals.push_back(ReadVector3()) var faceCount = ReadInt() if faceCount != mesh.indexCount: assert(false, "Normal face count does not match vertex face count.") for i in range(faceCount): var numIndices = ReadInt() for i1 in range(numIndices): mesh.normalIndices.push_back(ReadInt()) TestForSeparator() CheckForClosingBrace() func ParseMeshTextureCoords(mesh: XMesh): ReadHead() if mesh.textureCount + 1 > MAX_TEX_COORDS: assert(false, "Too many sets of texture coordinates") var numCoords = ReadInt() if numCoords != mesh.verts.size(): assert(false, "Texture coord count does not match vertex count") for i in range(numCoords): mesh.uvs.push_back(ReadVector2()) mesh.textureCount += 1 CheckForClosingBrace() func ParseMeshVertexColors(mesh: XMesh): pass func ParseFrame(parent: Node = null): var name = ReadHead() var node = Node3D.new() node.name = name if parent: parent.add_child(node) else: scene.add_child(node) while true: var objectName = GetNextToken().replace("\r", "").replace("\n", "") if not objectName: assert(false, "Unexpected end of file reached while parsing frame") return if objectName == "}": break elif objectName == "Frame": ParseFrame(node) elif objectName == "FrameTransformMatrix": # TODO: Do something with this? ParseTransformationMatrix() #print(ParseTransformationMatrix()) elif objectName == "Mesh": ParseMesh(node) else: break func ReadRGB() -> Color: var r = ReadFloat() var g = ReadFloat() var b = ReadFloat() TestForSeparator() return Color(r, g, b, 1) func ReadRGBA() -> Color: var r = ReadFloat() var g = ReadFloat() var b = ReadFloat() var a = ReadFloat() TestForSeparator() return Color(r, g, b, a) func ParseTextureFilename(): ReadHead() var name = GetNextToken().replace("\"", "") CheckForSemicolon() CheckForClosingBrace() if not name: push_warning("Unexpected end of file while parsing unknown segment.") # some exporers make weird paths, fix em. while name.contains("\\\\"): name = name.replace("\\\\", "\\") return name func ParseMaterial(): var material = StandardMaterial3D.new() var name = ReadHead() if not name: name = str("material", lineNumber) var diffuse = ReadRGBA() var specularExponent = ReadFloat() var specular = ReadRGB() var emissive = ReadRGB() while true: var objectName = GetNextToken().replace("\r", "").replace("\n", "") if not objectName: assert(false, "Unexpected end of file while parsing mesh material") elif objectName == "}": break elif objectName == "TextureFilename" or objectName == "TextureFileName": var fileName = ParseTextureFilename() material.albedo_texture = Global.LoadTexture(str(currentFilePath.replace("\\", "/"), fileName)) elif objectName == "NormalmapFilename" or objectName == "NormalmapFileName": var fileName = ParseTextureFilename() material.normal_texture = Global.LoadTexture(str(currentFilePath.replace("\\", "/"), fileName)) else: push_warning("Unknown data in material in DirectX file.") ParseUnknownDataObject() return material func ParseFile(): while true: var objectName = GetNextToken().replace("\r", "").replace("\n", "") #print(str("\"",objectName,"\"")) if not objectName: break if objectName == "template": ParseTemplate() elif objectName == "Frame": ParseFrame() elif objectName == "Mesh": ParseMesh(scene) elif objectName == "AnimTicksPerSecond": ParseUnknownDataObject() elif objectName == "AnimationSet": ParseUnknownDataObject() elif objectName == "Header": # 3D World Studio weird header ParseUnknownDataObject() elif objectName == "Material": var material = ParseMaterial() materials.push_back(material) else: break #static func LoadModel(filePath: String): #var sillyPath = str("res://", filePath.replace("/Map/", "/map/")) #var file = FileAccess.open(sillyPath, FileAccess.READ) # ## Do a case insensitive lookup only if we have to, it's more expensive. #if file == null: #file = FileAccess.open(Utils.GetCaseiFileName(sillyPath), FileAccess.READ) # #var fileLines = file.get_as_text(true).split("\n") # #var meshDataStart = false #var hitMeshOnce = false #var indexDataStart = false #var meshTextureCoordsStart = false #var hitTexCoordsOnce = false #var textureNameStart = false #var hitTextureOnce = false #var textureName = "" #var texture: Texture2D = null #var meshDataLength = -1 #var uvsDataLength = -1 #var indexDataLength = -1 #var verts = PackedVector3Array() #var uvs = PackedVector2Array() #var indexes = PackedInt32Array() ## oh god oh fuck i hope not #var is3dWorldStudioFile = false #for line in fileLines: #if line.contains("3D World Studio"): #is3dWorldStudioFile = true # #var stripLine = line.strip_edges().strip_escapes().replace(" ", "") #if stripLine == "{" or stripLine == "}": #continue # #if meshDataStart or indexDataStart: #if meshDataStart and meshDataLength == -1: #meshDataLength = int(line.strip_edges().split(";")[0]) #elif indexDataStart and indexDataLength == -1: #indexDataLength = int(line.strip_edges().split(";")[0]) #elif meshDataStart: #var pointData = line.strip_edges().split(";") #if pointData.size() == 5: #meshDataStart = false #indexDataStart = true #verts.push_back(Vector3(-float(pointData[0]), float(pointData[1]), float(pointData[2]))) #elif indexDataStart: #var indexParts = line.strip_edges().split(";") #if indexParts.size() == 4: #indexDataStart = false #var indexData = indexParts[1].split(",") #var amount = int(indexParts[0]) #for i in range(amount): #indexes.push_back(int(indexData[i])) #elif textureNameStart: #textureName = line.strip_edges().split(";")[0].replace("\"", "") #var texturePath = str("GFX/map/Props/", textureName) #texture = Global.GetTextureFromCache(texturePath) #if not texture: #texture = Global.LoadTexture(texturePath) #textureNameStart = false #elif meshTextureCoordsStart: #if uvsDataLength == -1: #uvsDataLength = int(line.strip_edges().split(";")[0]) #else: ## NOTE: THE FORMAT IS DIFFERENT ON FILES MADE IN ## 3D WORLD STUDIO FOR SOME REASON?! #if is3dWorldStudioFile: #var uvParts = line.strip_edges().strip_escapes().split(";") #if uvParts.size() == 3: #meshTextureCoordsStart = false #else: #var uvShit = uvParts[0].split(",") #uvs.push_back(Vector2(float(uvShit[0]), float(uvShit[1]))) #else: #var uvParts = line.strip_edges().strip_escapes().split(";") #if uvParts.size() == 4: #meshTextureCoordsStart = false #else: #uvs.push_back(Vector2(float(uvParts[0]), float(uvParts[1]))) # #if line.strip_edges().contains("Mesh ") and not hitMeshOnce: #meshDataStart = true #hitMeshOnce = true #elif line.strip_edges().contains("TextureFilename") and not hitTextureOnce: #textureNameStart = true #hitTextureOnce = true #elif line.strip_edges().contains("MeshTextureCoords") and not hitTexCoordsOnce: #meshTextureCoordsStart = true #hitTexCoordsOnce = true # #var mesh = MeshInstance3D.new() #var arr_mesh = ArrayMesh.new() #var arr = [] #arr.resize(Mesh.ARRAY_MAX) # #arr[Mesh.ARRAY_VERTEX]=verts #arr[Mesh.ARRAY_TEX_UV]=uvs #arr[Mesh.ARRAY_INDEX]=indexes # #var meshInstance = MeshInstance3D.new() #arr_mesh.add_surface_from_arrays(Mesh.PRIMITIVE_TRIANGLES, arr) #meshInstance.mesh = arr_mesh #var mat = StandardMaterial3D.new() ##mat.albedo_color = Color(randi() % 255 / 255.0, randi() % 255 / 255.0, randi() % 255 / 255.0) #mat.albedo_texture = texture ##mat.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA_DEPTH_PRE_PASS if activeAlbedoHasAlpha else BaseMaterial3D.TRANSPARENCY_DISABLED #meshInstance.set_surface_override_material(0, mat) #meshInstance.create_trimesh_collision() #meshInstance.name = str(filePath, "_", Time.get_unix_time_from_system()) #var meshSize = _CalcMinMaxPos(verts) #meshInstance.set_meta("meshWidth", meshSize.x) #meshInstance.set_meta("meshHeight", meshSize.y) #meshInstance.set_meta("meshDepth", meshSize.z) #return meshInstance