scpcb-godot/src/file_parsers/X.gd
2025-02-23 23:00:07 +00:00

722 lines
19 KiB
GDScript

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