diff options
author | hop311 <hop3114@gmail.com> | 2024-05-05 19:36:21 +0200 |
---|---|---|
committer | hop311 <hop3114@gmail.com> | 2024-05-07 23:06:38 +0200 |
commit | 2ac43ba7df3b2c3dc40c6b87c2bc57c4b02ffa42 (patch) | |
tree | f251adec091f704d889360a0eb32bd4ad018192d /game/src/Game/Model/XACLoader.gd | |
parent | c29cc0dabe3e3c7d03280e74d2d10fc3cc479c7f (diff) |
Add GDScript XAC and XSM loaders
Diffstat (limited to 'game/src/Game/Model/XACLoader.gd')
-rw-r--r-- | game/src/Game/Model/XACLoader.gd | 1127 |
1 files changed, 1127 insertions, 0 deletions
diff --git a/game/src/Game/Model/XACLoader.gd b/game/src/Game/Model/XACLoader.gd new file mode 100644 index 0000000..330384b --- /dev/null +++ b/game/src/Game/Model/XACLoader.gd @@ -0,0 +1,1127 @@ +class_name XACLoader + +static var shader : ShaderMaterial = preload("res://src/Game/Model/unit_colours_mat.tres") +const MAX_UNIT_TEXTURES : int = 32 # max number of textures supported by the shader +static var added_textures_spec : PackedStringArray +static var added_textures_diffuse : PackedStringArray + +static var flag_shader : ShaderMaterial = preload("res://src/Game/Model/flag_mat.tres") + +static func setup_flag_shader() -> void: + flag_shader.set_shader_parameter(&"flag_dims", GameSingleton.get_flag_dims()) + flag_shader.set_shader_parameter(&"texture_flag_sheet_diffuse", GameSingleton.get_flag_sheet_texture()) + +# Keys: source_file (String) +# Values: loaded model (UnitModel or Node3D) or LOAD_FAILED_MARKER (StringName) +static var xac_cache : Dictionary + +const LOAD_FAILED_MARKER : StringName = &"XAC LOAD FAILED" + +static func get_xac_model(source_file : String, is_unit : bool) -> Node3D: + var cached : Variant = xac_cache.get(source_file) + if not cached: + cached = _load_xac_model(source_file, is_unit) + if cached: + xac_cache[source_file] = cached + else: + xac_cache[source_file] = LOAD_FAILED_MARKER + push_error("Failed to get XAC model \"", source_file, "\" (current load failed)") + return null + + if not cached is Node3D: + push_error("Failed to get XAC model \"", source_file, "\" (previous load failed)") + return null + + var node : Node3D = cached.duplicate() + if node is UnitModel: + node.unit_init() + return node + +static func _load_xac_model(source_file : String, is_unit : bool) -> Node3D: + var source_path : String = GameSingleton.lookup_file_path(source_file) + var file : FileAccess = FileAccess.open(source_path, FileAccess.READ) + if file == null: + push_error("Failed to load XAC ", source_file, " from looked up path ", source_path) + return null + + var metaDataChunk : MetadataChunk + var nodeHierarchyChunk : NodeHierarchyChunk + var materialTotalsChunk : MaterialTotalsChunk + var materialDefinitionChunks : Array[MaterialDefinitionChunk] = [] + var mesh_chunks : Array[MeshChunk] = [] + var skinningChunks : Array[SkinningChunk] = [] + var chunkType6s : Array[ChunkType6] = [] + var nodeChunks : Array[NodeChunk] = [] + var chunkType4s : Array[ChunkTypeUnknown] = [] + var chunkUnknowns : Array[ChunkTypeUnknown] = [] + + readHeader(file) + + while file.get_position() < file.get_length(): + var type : int = FileAccessUtils.read_int32(file) + var length : int = FileAccessUtils.read_int32(file) + var version : int = FileAccessUtils.read_int32(file) + match type: + 0x7: + metaDataChunk = readMetaDataChunk(file) + 0xB: + nodeHierarchyChunk = readNodeHierarchyChunk(file) + 0xD: + materialTotalsChunk = readMaterialTotalsChunk(file) + 0x3: + # Ver=1 Appears on old version of format + var chunk : MaterialDefinitionChunk = readMaterialDefinitionChunk(file, version==1) + if chunk.has_specular(): + is_unit = true + materialDefinitionChunks.push_back(chunk) + 0x1: + mesh_chunks.push_back(readMeshChunk(file)) + 0x2: + skinningChunks.push_back(readSkinningChunk(file,mesh_chunks, version==2)) + 0x6: + chunkType6s.push_back(readChunkType6(file)) + 0x0: # Appears on old version of format + nodeChunks.push_back(readNodeChunk(file)) + 0xA: # Appears on old version of format + chunkUnknowns.push_back(readChunkTypeUnknown(file, length)) + 0x4: # Appears on old version of format + chunkType4s.push_back(readChunkTypeUnknown(file, length)) + 0x8: + push_warning("XAC model ", source_file, " contains junk data chunk 0x8 (skipping)") + break + _: + push_error(">> INVALID XAC CHUNK TYPE %s in model %s" % [type, source_file]) + break + + #BUILD THE GODOT MATERIALS + var materials : Array[MaterialDefinition] = make_materials(materialDefinitionChunks) + + #BUILD THE MESH + var node : Node3D = null + + if is_unit: + node = UnitModel.new() + else: + node = Node3D.new() + + node.name = metaDataChunk.origFileName.replace("\\", "/").split("/", false)[-1].get_slice(".", 0) + + var skeleton : Skeleton3D = null + + # build the skeleton hierarchy + if nodeHierarchyChunk: + skeleton = build_armature(nodeHierarchyChunk) + elif not nodeChunks.is_empty(): + skeleton = build_armature_chunk0(nodeChunks) + + if skeleton: + node.add_child(skeleton) + else: + push_warning("MODEL HAS NO SKELETON: ", source_file) + + var st : SurfaceTool = SurfaceTool.new() + + for chunk : MeshChunk in mesh_chunks: + var mesh_chunk_name : String + if nodeHierarchyChunk: + mesh_chunk_name = nodeHierarchyChunk.nodes[chunk.nodeId].name + elif not nodeChunks.is_empty(): + mesh_chunk_name = nodeChunks[chunk.nodeId].name + + const INVALID_MESHES : PackedStringArray = ["polySurface95"] + if mesh_chunk_name in INVALID_MESHES: + push_warning("Skipping unused mesh \"", mesh_chunk_name, "\" in model \"", node.name, "\"") + continue + + var mesh : ArrayMesh = null + var verts : PackedVector3Array + var normals : PackedVector3Array + var tangents : Array[Vector4] + var uvs : Array[PackedVector2Array] = [] + var influenceRangeInd : PackedInt64Array + + # vert attributes could be in any order, so search for them + for vertAttrib : VerticesAttribute in chunk.VerticesAttributes: + match vertAttrib.type: + 0: # position + verts = vertAttrib.data + 1: # normals vec3 + normals = vertAttrib.data + 2: # tangents vec4 + tangents = vertAttrib.data + 3: # uv coords vec2 + uvs.push_back(vertAttrib.data) #can have multiple sets of uv data + 5: # influence range, uint32 + influenceRangeInd = vertAttrib.data + _: # type 4 32bit colours and type 6 128bit colours aren't used + pass + + if chunk.bIsCollisionMesh or mesh_chunk_name == "pCube1": + var ar3d : Area3D = Area3D.new() + node.add_child(ar3d) + ar3d.owner = node + for submesh : SubMesh in chunk.SubMeshes: + var shape : ConvexPolygonShape3D = ConvexPolygonShape3D.new() + shape.points = verts + + var col : CollisionShape3D = CollisionShape3D.new() + col.shape = shape + + ar3d.add_child(col) + col.owner = node + continue + + #TODO will this produce correct results? + var applyVertexWeights : bool = true + var skinning_chunk_ind : int = 0 + for skin : SkinningChunk in skinningChunks: + if skin.nodeId == chunk.nodeId: + break + skinning_chunk_ind += 1 + if skinning_chunk_ind >= len(skinningChunks): + skinning_chunk_ind = 1 + applyVertexWeights = false + + var meshInstance : MeshInstance3D = MeshInstance3D.new() + node.add_child(meshInstance) + meshInstance.owner = node + + if mesh_chunk_name: + meshInstance.name = mesh_chunk_name + + if skeleton: + meshInstance.skeleton = meshInstance.get_path_to(skeleton) + + if not verts.is_empty(): + var vert_total : int = 0 + var surfaceIndex : int = 0 + + for submesh : SubMesh in chunk.SubMeshes: + st.begin(Mesh.PRIMITIVE_TRIANGLES) + + for i : int in submesh.relativeIndices.size(): + var rel_index : int = vert_total + submesh.relativeIndices[i] + + if not normals.is_empty(): + st.set_normal(normals[rel_index]) + + if not tangents.is_empty(): + st.set_tangent(Plane( + -tangents[rel_index].x, + tangents[rel_index].y, + tangents[rel_index].z, + tangents[rel_index].w + )) + + if not uvs.is_empty(): + st.set_uv(uvs[0][rel_index]) + + if not influenceRangeInd.is_empty() and not skinningChunks.is_empty() and applyVertexWeights: + #TODO: Which skinning Chunk? + # likely look at the skinning chunk's nodeId, see if it matches our mesh's id + var vert_inf_range_ind : int = influenceRangeInd[rel_index] + var skin_chunk : SkinningChunk = skinningChunks[skinning_chunk_ind] + var influenceRange : InfluenceRange = skin_chunk.influenceRange[vert_inf_range_ind] + var boneWeights : Array[InfluenceData] = skinningChunks[skinning_chunk_ind].influenceData.slice( + influenceRange.firstInfluenceIndex, + influenceRange.firstInfluenceIndex + influenceRange.numInfluences + ) + if len(boneWeights) > 4: + push_error("num BONE WEIGHTS WAS > 4, GODOT DOESNT LIKE THIS") + # TODO: Less hacky fix? + boneWeights = boneWeights.slice(0,4) + + var godotBoneIds : PackedInt32Array = PackedInt32Array() + var godotBoneWeights : PackedFloat32Array = PackedFloat32Array() + godotBoneIds.resize(4) + godotBoneIds.fill(0) + godotBoneWeights.resize(4) + godotBoneWeights.fill(0) + + var index : int = 0 + for bone : InfluenceData in boneWeights: + godotBoneIds.set(index, bone.boneId) + godotBoneWeights.set(index, bone.fWeight) + index += 1 + + if skeleton: + st.set_bones(godotBoneIds) + st.set_weights(godotBoneWeights) + + st.add_vertex(verts[rel_index]) + + vert_total += submesh.numVertices + + mesh = st.commit(mesh) # add a new surface to the mesh + meshInstance.mesh = mesh + + st.clear() + + mesh.surface_set_material(surfaceIndex, materials[submesh.materialId].mat) + surfaceIndex += 1 + + if materials[submesh.materialId].spec_index != -1: + meshInstance.set_instance_shader_parameter(&"tex_index_specular", materials[submesh.materialId].spec_index) + + if materials[submesh.materialId].diffuse_index != -1: + meshInstance.set_instance_shader_parameter(&"tex_index_diffuse", materials[submesh.materialId].diffuse_index) + + return node + +# Information needed to set up a material +# Leave the indices -1 if not using the unit shader +class MaterialDefinition: + var spec_index : int = -1 + var diffuse_index : int = -1 + var normal_index : int = -1 + var mat : Material + + func _init(mat : Material, diffuse_ind : int = -1, spec_ind : int = -1, normal_ind : int = -1) -> void: + self.mat = mat + self.diffuse_index = diffuse_ind + self.spec_index = spec_ind + self.normal_index = normal_ind + +static func make_materials(materialDefinitionChunks : Array[MaterialDefinitionChunk]) -> Array[MaterialDefinition]: + const TEXTURES_PATH : String = "gfx/anims/%s.dds" + + var materials : Array[MaterialDefinition] = [] + + for matdef : MaterialDefinitionChunk in materialDefinitionChunks: + var diffuse_name : String + var specular_name : String + var normal_name : String + + # Find important textures + for layer : Layer in matdef.Layers: + if layer.texture in ["nospec", "unionjacksquare", "test256texture"]: + continue + + match layer.mapType: + 2: # diffuse + if not diffuse_name: + diffuse_name = layer.texture + else: + push_error("Multiple diffuse layers in material: ", diffuse_name, " and ", layer.texture) + + 3: # specular + if not specular_name: + specular_name = layer.texture + else: + push_error("Multiple specular layers in material: ", specular_name, " and ", layer.texture) + + 4: # currently unused + pass + + 5: # normal + if not normal_name: + normal_name = layer.texture + else: + push_error("Multiple normal layers in material: ", normal_name, " and ", layer.texture) + + _: + push_error("Unknown layer type: ", layer.mapType) + pass + + # Unit colour mask + if diffuse_name and specular_name: + if normal_name: + push_error("Normal texture present in unit colours material: ", normal_name) + + var textures_index_spec : int = added_textures_spec.find(specular_name) + if textures_index_spec < 0: + var unit_colours_mask_texture : ImageTexture = AssetManager.get_texture(TEXTURES_PATH % specular_name) + if unit_colours_mask_texture: + added_textures_spec.push_back(specular_name) + + # Should we still attempt to add the texture to the shader? + if len(added_textures_spec) >= MAX_UNIT_TEXTURES: + push_error("Colour masks have exceeded max number of textures supported by unit shader!") + + var colour_masks : Array = shader.get_shader_parameter(&"texture_nation_colors_mask") + colour_masks.push_back(unit_colours_mask_texture) + textures_index_spec = len(colour_masks) - 1 + shader.set_shader_parameter(&"texture_nation_colors_mask", colour_masks) + else: + push_error("Failed to load specular texture: ", specular_name) + + var textures_index_diffuse : int = added_textures_diffuse.find(diffuse_name) + if textures_index_diffuse < 0: + var diffuse_texture : ImageTexture = AssetManager.get_texture(TEXTURES_PATH % diffuse_name) + if diffuse_texture: + added_textures_diffuse.push_back(diffuse_name) + + # Should we still attempt to add the texture to the shader? + if len(added_textures_diffuse) >= MAX_UNIT_TEXTURES: + push_error("Albedos have exceeded max number of textures supported by unit shader!") + + var albedoes : Array = shader.get_shader_parameter(&"texture_albedo") + albedoes.push_back(diffuse_texture) + textures_index_diffuse = len(albedoes) - 1 + shader.set_shader_parameter(&"texture_albedo", albedoes) + else: + push_error("Failed to load diffuse texture: ", diffuse_name) + + materials.push_back(MaterialDefinition.new(shader, textures_index_diffuse, textures_index_spec)) + + # Flag (diffuse is unionjacksquare which is ignored) + elif normal_name and not diffuse_name: + if specular_name: + push_error("Specular texture present in flag material: ", specular_name) + + var flag_normal_texture : ImageTexture = AssetManager.get_texture(TEXTURES_PATH % normal_name) + if flag_normal_texture: + flag_shader.set_shader_parameter(&"texture_normal", flag_normal_texture) + else: + push_error("Failed to load normal texture: ", normal_name) + + materials.push_back(MaterialDefinition.new(flag_shader)) + + # Standard material + else: + if specular_name: + push_error("Specular texture present in standard material: ", specular_name) + + var mat : StandardMaterial3D = StandardMaterial3D.new() + mat.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA_DEPTH_PRE_PASS + + if diffuse_name: + var diffuse_texture : ImageTexture = AssetManager.get_texture(TEXTURES_PATH % diffuse_name) + if diffuse_texture: + mat.set_texture(BaseMaterial3D.TEXTURE_ALBEDO, diffuse_texture) + else: + push_error("Failed to load diffuse texture: ", diffuse_name) + + if normal_name: + var normal_texture : ImageTexture = AssetManager.get_texture(TEXTURES_PATH % normal_name) + if normal_texture: + mat.normal_enabled = true + mat.set_texture(BaseMaterial3D.TEXTURE_NORMAL, normal_texture) + else: + push_error("Failed to load normal texture: ", normal_name) + + #TODO: Verify that this is correct thing to do to make sure + #that places where models are double sided are correct + mat.cull_mode = BaseMaterial3D.CULL_DISABLED + + materials.push_back(MaterialDefinition.new(mat)) + + return materials + +static func build_armature(hierarchy_chunk : NodeHierarchyChunk) -> Skeleton3D: + var skeleton : Skeleton3D = Skeleton3D.new() + skeleton.name = "skeleton" + var cur_id : int = 0 + for node : NodeData in hierarchy_chunk.nodes: + # godot doesn't like the ':' in bone names unlike paradox + skeleton.add_bone(FileAccessUtils.replace_chars(node.name)) + skeleton.set_bone_parent(cur_id, node.parentNodeId) + + #For now assume rest and current position are the same + skeleton.set_bone_rest(cur_id, Transform3D(Basis(node.rotation).scaled(node.scale), node.position)) + + skeleton.set_bone_pose_position(cur_id, node.position) + skeleton.set_bone_pose_rotation(cur_id, node.rotation) + skeleton.set_bone_pose_scale(cur_id, node.scale) + + cur_id += 1 + # conveniently both godot and xac use a parent node id of -1 to represent no parent + # TODO: What is the point of xac having both a transform and separate vec3s for rotation, scale, pos, etc.? + # for now, will assume the separate components are the truth + # it might be that one is a current position and the other is a rest position? + return skeleton + +static func build_armature_chunk0(nodeChunks : Array[NodeChunk]) -> Skeleton3D: + var skeleton : Skeleton3D = Skeleton3D.new() + skeleton.name = "skeleton" + var cur_id : int = 0 + for node : NodeChunk in nodeChunks: + # godot doesn't like the ':' in bone names unlike paradox + skeleton.add_bone(FileAccessUtils.replace_chars(node.name)) + skeleton.set_bone_parent(cur_id, node.parentBone) + + # For now assume rest and current position are the same + skeleton.set_bone_rest(cur_id, Transform3D(Basis(node.rotation).scaled(node.scale), node.position)) + + skeleton.set_bone_pose_position(cur_id, node.position) + skeleton.set_bone_pose_rotation(cur_id, node.rotation) + skeleton.set_bone_pose_scale(cur_id, node.scale) + + cur_id += 1 + return skeleton + +static func readHeader(file : FileAccess) -> void: + var magic_bytes : PackedByteArray = [file.get_8(), file.get_8(), file.get_8(), file.get_8()] + var magic : String = magic_bytes.get_string_from_ascii() + var version : String = "%d.%d" % [file.get_8(), file.get_8()] + var bBigEndian : bool = file.get_8() + var multiplyOrder : int = file.get_8() + #print(magic, ", version: ", version, ", bigEndian: ", bBigEndian, " multiplyOrder: ", multiplyOrder) + +static func readMetaDataChunk(file : FileAccess) -> MetadataChunk: + return MetadataChunk.new( + file.get_32(), FileAccessUtils.read_int32(file), file.get_8(), file.get_8(), + Vector2i(file.get_8(), file.get_8()), file.get_float(), + FileAccessUtils.read_xac_str(file), FileAccessUtils.read_xac_str(file), + FileAccessUtils.read_xac_str(file), FileAccessUtils.read_xac_str(file) + ) + +class MetadataChunk: + var repositionMask : int # uint32 + var repositioningNode : int # int32 + var exporterMajorVersion : int # byte + var exporterMinorVersion : int # byte + var unused : Vector2i # 2x byte + var retargetRootOffset : float + var sourceApp : String + var origFileName : String + var exportDate : String + var actorName : String + + func _init( + repMask : int, + repNode : int, + exMajV : int, + exMinV : int, + un : Vector2i, + retRootOff : float, + sourceApp : String, + origFile : String, + date : String, + actorName : String + ) -> void: + self.repositionMask = repMask + self.repositioningNode = repNode + self.exporterMajorVersion = exMajV + self.exporterMinorVersion = exMinV + self.unused = un + self.retargetRootOffset = retRootOff + self.sourceApp = sourceApp + self.origFileName = origFile + self.exportDate = date + self.actorName = actorName + + func debugPrint() -> void: + print("Ver: %d.%d, exported: %s, fileName: %s, sourceApp: %s" % + [exporterMajorVersion, exporterMinorVersion, exportDate, origFileName, sourceApp]) + print("Actor: %s, retargetRootOffset: %d, repositionMask: %d, repositionNode: %d" % + [actorName, retargetRootOffset, repositionMask, repositioningNode]) + +# HIERARCHY + +static func readNodeData(file : FileAccess) -> NodeData: + return NodeData.new( + FileAccessUtils.read_quat(file), FileAccessUtils.read_quat(file), + FileAccessUtils.read_pos(file), FileAccessUtils.read_vec3(file), + FileAccessUtils.read_vec3(file), # 3x unused floats + FileAccessUtils.read_int32(file), FileAccessUtils.read_int32(file), FileAccessUtils.read_int32(file), + FileAccessUtils.read_int32(file), FileAccessUtils.read_int32(file), + FileAccessUtils.read_mat4x4(file), file.get_float(), FileAccessUtils.read_xac_str(file) + ) + +class NodeData: + var rotation : Quaternion + var scaleRotation : Quaternion + var position : Vector3 + var scale : Vector3 + var unused : Vector3 # 3x unused floats + var unknown : int # int32 + var unknown2 : int # int32 + var parentNodeId : int # int32 + var numChildNodes : int # int32 + var bIncludeInBoundsCalc : bool # int32 + var transform : FileAccessUtils.xac_mat4x4 + var fImportanceFactor : float + var name : String + + func _init( + rot : Quaternion, + scaleRot : Quaternion, + pos : Vector3, + scale : Vector3, + unused : Vector3, + unknown : int, + unknown2 : int, + parentNodeId : int, + numChildNodes : int, + incInBoundsCalc : bool, + transform : FileAccessUtils.xac_mat4x4, + fImportanceFactor : float, + name : String + ) -> void: + self.rotation = rot + self.scaleRotation = scaleRot + self.position = pos + self.scale = scale + self.unused = unused + self.unknown = unknown2 + self.unknown2 = unknown2 + self.parentNodeId = parentNodeId + self.numChildNodes = numChildNodes + self.bIncludeInBoundsCalc = incInBoundsCalc + self.transform = transform + self.fImportanceFactor = fImportanceFactor + self.name = name + + func debugPrint() -> void: + print("\tparentNodeId: %d,\t numChildNodes: %d,\t Node Name: %s" % [parentNodeId, numChildNodes, name]) + print("\tunused %s,%s,%s, -1: %s, -1: %s" % [unused[0], unused[1], unused[2], unknown, unknown2]) + +static func readNodeHierarchyChunk(file : FileAccess) -> NodeHierarchyChunk: + var numNodes : int = FileAccessUtils.read_int32(file) + var numRootNodes : int = FileAccessUtils.read_int32(file) + var nodes : Array[NodeData] = [] + for i : int in numNodes: + nodes.push_back(readNodeData(file)) + return NodeHierarchyChunk.new(numNodes, numRootNodes, nodes) + +class NodeHierarchyChunk: + var numNodes : int # int32 + var numRootNodes : int # int32 + var nodes : Array[NodeData] + + func _init(numNodes : int, numRootNodes : int, nodes : Array[NodeData]) -> void: + self.numNodes = numNodes + self.numRootNodes = numRootNodes + self.nodes = nodes + + func debugPrint() -> void: + print("numNodes: %d, numRootNodes: %d" % [numNodes, numRootNodes]) + for node : NodeData in nodes: + node.debugPrint() + +# MATERIAL TOTALS + +static func readMaterialTotalsChunk(file : FileAccess) -> MaterialTotalsChunk: + return MaterialTotalsChunk.new(FileAccessUtils.read_int32(file), FileAccessUtils.read_int32(file), FileAccessUtils.read_int32(file)) + +class MaterialTotalsChunk: + var numTotalMaterials : int # int32 + var numStandMaterials : int # int32 + var numFxMaterials : int # int32 + + func _init(numTotalMaterials : int, numStandMaterials : int, numFxMaterials : int) -> void: + self.numTotalMaterials = numTotalMaterials + self.numStandMaterials = numStandMaterials + self.numFxMaterials = numFxMaterials + + func debugPrint() -> void: + print("totalMaterials: %d, standardMaterials: %d, fxMaterials: %d" % + [numTotalMaterials, numStandMaterials, numFxMaterials]) + +# MATERIAL DEFINITION + +static func readLayer(file : FileAccess, isV1 : bool) -> Layer: + var unknown : Vector3i + if isV1: + unknown = Vector3i( + FileAccessUtils.read_int32(file), + FileAccessUtils.read_int32(file), + FileAccessUtils.read_int32(file) + ) + var layer : Layer = Layer.new( + file.get_float(), file.get_float(), file.get_float(), file.get_float(), + file.get_float(), file.get_float(), FileAccessUtils.read_int16(file), + file.get_8(), file.get_8(), FileAccessUtils.read_xac_str(file), unknown + ) + return layer + +class Layer: + var amount : float + var uOffset : float + var vOffset : float + var uTiling : float + var vTiling : float + var rotInRad : float + var matId : int # int16 + var mapType : int # byte + var unused : int # byte + var texture : String + var unknown : Vector3i # Unknown 3 integers present in v1 of the chunk + + func _init( + amount : float, + uOffset : float, + vOffset : float, + uTiling : float, + vTiling : float, + rotInRad : float, + matId : int, + mapType : int, + unused : int, + texture : String, + unknown : Vector3i + ) -> void: + self.amount = amount + self.uOffset = uOffset + self.vOffset = vOffset + self.uTiling = uTiling + self.vTiling = vTiling + self.rotInRad = rotInRad + self.matId = matId + self.mapType = mapType + self.unused = unused + self.texture = texture + self.unknown = unknown + + func debugPrint() -> void: + print("\tLayer MatId:%d,\t UVOffset:%d,%d,\t UVTiling %d,%d mapType: %d,\t Texture Name: %s" % + [matId, uOffset, vOffset, uTiling, vTiling, mapType, texture]) + print("\t amount:%s,\t rot:%s,\t unused:%d" % [amount, rotInRad, unused]) + + func is_specular() -> bool: + return mapType == 3 + +# TODO: Might want to change this from vec4d to colours where appropriate +static func readMaterialDefinitionChunk(file : FileAccess, isV1 : bool) -> MaterialDefinitionChunk: + var chunk : MaterialDefinitionChunk = MaterialDefinitionChunk.new( + FileAccessUtils.read_vec4(file), FileAccessUtils.read_vec4(file), + FileAccessUtils.read_vec4(file), FileAccessUtils.read_vec4(file), + file.get_float(), file.get_float(), file.get_float(), file.get_float(), + file.get_8(), file.get_8(), file.get_8(), file.get_8(), + FileAccessUtils.read_xac_str(file) + ) + var layers : Array[Layer] = [] + for i : int in chunk.numLayers: + layers.push_back(readLayer(file, isV1)) + chunk.setLayers(layers) + return chunk + +class MaterialDefinitionChunk: + var ambientColor : Vector4 + var diffuseColor : Vector4 + var specularColor : Vector4 + var emissiveColor : Vector4 + var shine : float + var shineStrength : float + var opacity : float + var ior : float + var bDoubleSided : bool # byte + var bWireframe : bool # byte + var unused : int # 1 byte + var numLayers : int # byte + var name : String + var Layers : Array[Layer] + + func _init( + ambientColor : Vector4, + diffuseColor : Vector4, + specularColor : Vector4, + emissiveColor : Vector4, + shine : float, + shineStrength : float, + opacity : float, + ior : float, + bDoubleSided : bool, + bWireframe : bool, + unused : int, + numLayers : int, + name : String + ) -> void: + self.ambientColor = ambientColor + self.diffuseColor = diffuseColor + self.specularColor = specularColor + self.emissiveColor = emissiveColor + self.shine = shine + self.shineStrength = shineStrength + self.opacity = opacity + self.ior = ior + self.bDoubleSided = bDoubleSided + self.bWireframe = bWireframe + self.unused = unused + self.numLayers = numLayers + self.name = name + + func setLayers(layers : Array[Layer]) -> void: + self.Layers = layers + + # Specular textures are used for country-specific unit colours, + # which need a UnitModel to be set through + func has_specular() -> bool: + for layer : Layer in Layers: + if layer.is_specular(): + return true + return false + + func debugPrint() -> void: + print("Material Name: %s, num layers: %d, doubleSided %s" % [name, numLayers, bDoubleSided]) + print("\tshine:%s\tshineStrength:%s\t,opacity:%s,\tior:%s,\tunused:%s" % [shine, shineStrength, opacity, ior, unused]) + for layer : Layer in Layers: + layer.debugPrint() + +# MESH + +static func readVerticesAttribute(file : FileAccess, numVerts : int) -> VerticesAttribute: + var vertAttrib : VerticesAttribute = VerticesAttribute.new( + FileAccessUtils.read_int32(file), FileAccessUtils.read_int32(file), + file.get_8(), file.get_8(), Vector2i(file.get_8(), file.get_8())) + var data : Variant + match vertAttrib.type: + 0: # position + data = PackedVector3Array() + for i : int in numVerts: + data.push_back(FileAccessUtils.read_pos(file)) + 1: # normals + data = PackedVector3Array() + for i : int in numVerts: + data.push_back(FileAccessUtils.read_pos(file)) + 2: # tangents + data = [] as Array[Vector4] + for i : int in numVerts: + var tangent : Vector4 = FileAccessUtils.read_vec4(file) + #tangent.w *= -1 + data.push_back(tangent) + 3: # uvs + data = PackedVector2Array() + for i : int in numVerts: + data.push_back(FileAccessUtils.read_vec2(file)) + 4: # 32bit colors + data = PackedColorArray() + for i : int in numVerts: + data.push_back(FileAccessUtils.read_Color32(file)) + 5: # influence range indices + data = PackedInt64Array() + for i : int in numVerts: + data.push_back(file.get_32()) + 6: # 128bit colors + data = PackedColorArray() + for i : int in numVerts: + data.push_back(FileAccessUtils.read_Color128(file)) + _: + push_error("INVALID XAC VERTATTRIBUTE TYPE %d" % vertAttrib.type) + vertAttrib.setData(data) + return vertAttrib + +# mesh has one of these for each vertex property (position, normals, etc) +class VerticesAttribute: + var type : int # int32 + var attribSize : int # int32 + var bKeepOriginals : bool # byte + var bIsScaleFactor : bool # byte + var pad : Vector2i # 2x byte + var data : Variant # numVerts * attribSize + + func _init( + type : int, + attribSize : int, + bKeepOriginals : bool, + bIsScaleFactor : bool, + pad : Vector2i + ) -> void: + self.type = type + self.attribSize = attribSize + self.bKeepOriginals = bKeepOriginals + self.bIsScaleFactor = bIsScaleFactor + self.pad = pad + + func setData(data : Variant) -> void: + self.data = data + + func debugPrint() -> void: + var typeStr : String + match type: + 0: + typeStr = "Positions" + 1: + typeStr = "Normals" + 2: + typeStr = "Tangents" + 3: + typeStr = "UV Coords" + 4: + typeStr = "32bit Colors" + 5: + typeStr = "Influence Range Indices (u32)" + 6: + typeStr = "128bit Colors" + _: + typeStr = "invalid type %d" % type + print("\tattribSize:%d (bytes),\t keepOriginals:%s,\t isScaleFactor:%s,\t VertAttrib: type:%s" % + [attribSize, bKeepOriginals, bIsScaleFactor, typeStr]) + print("\tpad: %s" % pad) + +static func readSubMesh(file : FileAccess) -> SubMesh: + var subMesh : SubMesh = SubMesh.new( + FileAccessUtils.read_int32(file), FileAccessUtils.read_int32(file), + FileAccessUtils.read_int32(file), FileAccessUtils.read_int32(file) + ) + var relIndices : PackedInt32Array = PackedInt32Array() + var boneIds : PackedInt32Array = PackedInt32Array() + for i : int in subMesh.numIndices: + relIndices.push_back(FileAccessUtils.read_int32(file)) + for i : int in subMesh.numBones: + boneIds.push_back(FileAccessUtils.read_int32(file)) + subMesh.setBoneIds(boneIds) + subMesh.setRelIndices(relIndices) + return subMesh + +class SubMesh: + var numIndices : int # int32 + var numVertices : int # int32 + var materialId : int # int32 + var numBones : int # int32 + var relativeIndices : PackedInt32Array # int32 [numIndices] + var boneIds : PackedInt32Array # int32 [numBones], unused + + func _init(numIndices : int, numVertices : int, materialId : int, numBones : int) -> void: + self.numIndices = numIndices + self.numVertices = numVertices + self.materialId = materialId + self.numBones = numBones + + func setRelIndices(relativeIndices : PackedInt32Array) -> void: + self.relativeIndices = relativeIndices + + func setBoneIds(boneIds : PackedInt32Array) -> void: + self.boneIds = boneIds + + func debugPrint() -> void: + print("\tSubMesh:\t numIndices:%d,\t numVerts:%d,\t matId:%d,\t numBones:%d" % + [numIndices, numVertices, materialId, numBones]) + +static func readMeshChunk(file : FileAccess) -> MeshChunk: + var mesh : MeshChunk = MeshChunk.new( + FileAccessUtils.read_int32(file), FileAccessUtils.read_int32(file), + FileAccessUtils.read_int32(file), FileAccessUtils.read_int32(file), + FileAccessUtils.read_int32(file), FileAccessUtils.read_int32(file), file.get_8(), + Vector3i(file.get_8(), file.get_8(), file.get_8()) + ) + var vertAttribs : Array[VerticesAttribute] = [] + var submeshes : Array[SubMesh] = [] + for i : int in mesh.numAttribLayers: + vertAttribs.push_back(readVerticesAttribute(file,mesh.numVertices)) + for i : int in mesh.numSubMeshes: + submeshes.push_back(readSubMesh(file)) + mesh.setVerticesAttributes(vertAttribs) + mesh.setSubMeshes(submeshes) + return mesh + +class MeshChunk: + var nodeId : int # int32 + var numInfluenceRanges : int # int32 + var numVertices : int # int32 + var numIndices : int # int32 + var numSubMeshes : int # int32 + var numAttribLayers : int # int32 + var bIsCollisionMesh : bool # byte + var pad : Vector3i # 3x byte + var VerticesAttributes : Array[VerticesAttribute] + var SubMeshes : Array[SubMesh] + + func _init( + nodeId : int, + numInfluenceRanges : int, + numVertices : int, + numIndices : int, + numSubMeshes : int, + numAttribLayers : int, + bIsCollisionMesh : bool, + pad : Vector3i + ) -> void: + self.nodeId = nodeId + self.numInfluenceRanges = numInfluenceRanges + self.numVertices = numVertices + self.numIndices = numIndices + self.numSubMeshes = numSubMeshes + self.numAttribLayers = numAttribLayers + self.bIsCollisionMesh = bIsCollisionMesh + self.pad = pad + + func setVerticesAttributes(VerticesAttributes : Array[VerticesAttribute]) -> void: + self.VerticesAttributes = VerticesAttributes + + func setSubMeshes(SubMeshes : Array[SubMesh]) -> void: + self.SubMeshes = SubMeshes + + func debugPrint() -> void: + print("Mesh: nodeId:%d, numVerts:%d, numSubMeshes:%d, numVertAttribs:%d, isCollisionMesh:%s" % + [nodeId, numVertices, numSubMeshes, numAttribLayers, bIsCollisionMesh]) + for vertAttrib in VerticesAttributes: + vertAttrib.debugPrint() + for subMesh : SubMesh in SubMeshes: + subMesh.debugPrint() + +# SKINNING +static func readInfluenceData(file : FileAccess) -> InfluenceData: + return InfluenceData.new(file.get_float(), FileAccessUtils.read_int16(file), Vector2i(file.get_8(), file.get_8())) + +class InfluenceData: + var fWeight : float # (0..1) + var boneId : int # int16 + var pad : Vector2i # 2x byte + + func _init(fWeight : float, boneId : int, pad : Vector2i) -> void: + self.fWeight = fWeight + self.boneId = boneId + self.pad = pad + + func debugPrint() -> void: + print("\tInfluenceData:\t boneId: %d,\t Weight: %s" % [boneId, fWeight]) + +# For some reason influenceRange isn't being loaded, needs investigation +# Weird data on the flag + +static func readInfluenceRange(file : FileAccess) -> InfluenceRange: + return InfluenceRange.new(FileAccessUtils.read_int32(file), FileAccessUtils.read_int32(file)) + +class InfluenceRange: + var firstInfluenceIndex : int # int32 + var numInfluences : int # int32 + + func _init(firstInfluenceIndex : int, numInfluences : int) -> void: + self.firstInfluenceIndex = firstInfluenceIndex + self.numInfluences = numInfluences + + func debugPrint() -> void: + print("\tInfluenceRange:\t firstIndex: %d,\t numInfluences: %d" % [firstInfluenceIndex, numInfluences]) + +static func readSkinningChunk(file : FileAccess, meshChunks : Array[MeshChunk], isV2 : bool) -> SkinningChunk: + var skinning : SkinningChunk = null + skinning = SkinningChunk.new( + FileAccessUtils.read_int32(file), -1 if isV2 else FileAccessUtils.read_int32(file), + FileAccessUtils.read_int32(file), file.get_8(), Vector3i(file.get_8(), file.get_8(), file.get_8()) + ) + var influenceData : Array[InfluenceData] = [] + var influenceRange : Array[InfluenceRange] = [] + for i : int in skinning.numInfluences: + influenceData.push_back(readInfluenceData(file)) + # search the list of mesh chunks for the one which matches IsCollisionMesh and NodeId? + # documentation is a little unclear about this (lists the mesh accessing by node, which isn't possible) + for chunk : MeshChunk in meshChunks: + if chunk.nodeId == skinning.nodeId and chunk.bIsCollisionMesh == skinning.bIsForCollisionMesh: + for i : int in chunk.numInfluenceRanges: + influenceRange.push_back(readInfluenceRange(file)) + break + skinning.setInfluenceData(influenceData) + skinning.setInfluenceRange(influenceRange) + return skinning + +class SkinningChunk: + var nodeId : int # int32 + var numLocalBones : int # int32, of bones in the influence data + var numInfluences : int # int32 + var bIsForCollisionMesh : bool # byte boolean + var pad : Vector3i # 3x pad + var influenceData : Array[InfluenceData] + var influenceRange : Array[InfluenceRange] + + func _init( + nodeId : int, + numLocalBones : int, + numInfluences : int, + bIsForCollisionMesh : bool, + pad : Vector3i + ) -> void: + self.nodeId = nodeId + self.numLocalBones = numLocalBones + self.numInfluences = numInfluences + self.bIsForCollisionMesh = bIsForCollisionMesh + self.pad = pad + + func setInfluenceData(influenceData : Array[InfluenceData]) -> void: + self.influenceData = influenceData + + func setInfluenceRange(influenceRange : Array[InfluenceRange]) -> void: + self.influenceRange = influenceRange + + func debugPrint() -> void: + print("Skinning: nodeId:%d, numInfluencedBones: %d, numInfluences: %d, CollisionMesh?: %d" % + [nodeId, numLocalBones, numInfluences, bIsForCollisionMesh]) + for infDat : InfluenceData in influenceData: + infDat.debugPrint() + for infRange : InfluenceRange in influenceRange: + infRange.debugPrint() + +# TODO: What is chunk type6, figure out what the plausible datatypes are +# Currently, since XACs tend to use int32s and float (32bit) + strings, and no strings +# are evident in chunk type 6, to load these as arrays of int32 + +static func readChunkType6(file : FileAccess) -> ChunkType6: + var intArr : PackedInt32Array = [] + var floatArr : PackedFloat32Array = [] + for i : int in 12: + intArr.push_back(FileAccessUtils.read_int32(file)) + for i : int in 9: + floatArr.push_back(file.get_float()) + return ChunkType6.new(intArr, floatArr, FileAccessUtils.read_int32(file)) + +class ChunkType6: + var unknown : PackedInt32Array + var unknown_floats : PackedFloat32Array + var maybe_node_id : int + + func _init(unknown : PackedInt32Array, unknown_floats : PackedFloat32Array, maybe_node_id : int) -> void: + self.unknown = unknown + self.unknown_floats = unknown_floats + self.maybe_node_id = maybe_node_id + + func debugPrint() -> void: + print("\tUnknown: %s\n\tFloats: %s\n\tPerhaps NodeId? %s" % [self.unknown, self.unknown_floats, self.maybe_node_id]) + +# Chunk type 0x0 +static func readNodeChunk(file : FileAccess) -> NodeChunk: + return NodeChunk.new( + FileAccessUtils.read_quat(file), FileAccessUtils.read_quat(file), + FileAccessUtils.read_pos(file), FileAccessUtils.read_vec3(file), + FileAccessUtils.read_vec3(file), # unused 3 floats + FileAccessUtils.read_int32(file), FileAccessUtils.read_int32(file), #-1-1 + # 17 x 32bit unknowns, likely matrix and some other info + # last 32bit unknown is likely fImportanceFactor, (float 1f). Rest is likely matrix + (func() -> PackedInt32Array: + var array : PackedInt32Array = PackedInt32Array() + for i : int in 17: + array.push_back(FileAccessUtils.read_int32(file)) + return array).call(), + FileAccessUtils.read_xac_str(file) + ) + +class NodeChunk: + var rotation : Quaternion + var scaleRotation : Quaternion + var position : Vector3 + var scale : Vector3 + var unused : Vector3 # 3x unused floats + var unknown : int # int32 + var parentBone : int # int32 + var unknown2 : PackedInt32Array # 17x int32 sized numbers + var name : String + + func _init( + rot : Quaternion, + scaleRot : Quaternion, + pos : Vector3, + scale : Vector3, + unused : Vector3, + unknown : int, + parentBone : int, + unknown2 : PackedInt32Array, + name : String + ) -> void: + self.rotation = rot + self.scaleRotation = scaleRot + self.position = pos + self.scale = scale + self.unused = unused + self.unknown = unknown + self.parentBone = parentBone + self.unknown2 = unknown2 + self.name = name + + func debugPrint() -> void: + print("\tNode Name: %s, parentBone %s" % [name, parentBone]) + print("\tunused (%s) unknown: %s" % [unused, unknown]) + print("\tunknown after -1-1: %s" % unknown2) + +static func readChunkTypeUnknown(file : FileAccess, length : int) -> ChunkTypeUnknown: + return ChunkTypeUnknown.new(file.get_buffer(length)) + +class ChunkTypeUnknown: + var unknown : PackedByteArray + + func _init(unknown : PackedByteArray) -> void: + self.unknown = unknown + + func debugPrint() -> void: + print("\tUnknown: %s" % self.unknown) + +# TODO: MorphTarget, Deformation, Transformation, MorphTargetsChunk |