From 2ac43ba7df3b2c3dc40c6b87c2bc57c4b02ffa42 Mon Sep 17 00:00:00 2001 From: hop311 Date: Sun, 5 May 2024 18:36:21 +0100 Subject: Add GDScript XAC and XSM loaders --- .../openvic-extension/singletons/GameSingleton.cpp | 5 + .../openvic-extension/singletons/GameSingleton.hpp | 1 + game/src/Game/Model/FileAccessUtils.gd | 88 ++ game/src/Game/Model/UnitModel.gd | 160 +++ game/src/Game/Model/XACLoader.gd | 1127 ++++++++++++++++++++ game/src/Game/Model/XSMLoader.gd | 363 +++++++ game/src/Game/Model/flag.gdshader | 26 + game/src/Game/Model/flag_mat.tres | 7 + game/src/Game/Model/unit_colours.gdshader | 31 + game/src/Game/Model/unit_colours_mat.tres | 9 + 10 files changed, 1817 insertions(+) create mode 100644 game/src/Game/Model/FileAccessUtils.gd create mode 100644 game/src/Game/Model/UnitModel.gd create mode 100644 game/src/Game/Model/XACLoader.gd create mode 100644 game/src/Game/Model/XSMLoader.gd create mode 100644 game/src/Game/Model/flag.gdshader create mode 100644 game/src/Game/Model/flag_mat.tres create mode 100644 game/src/Game/Model/unit_colours.gdshader create mode 100644 game/src/Game/Model/unit_colours_mat.tres diff --git a/extension/src/openvic-extension/singletons/GameSingleton.cpp b/extension/src/openvic-extension/singletons/GameSingleton.cpp index 838542d..12e2df1 100644 --- a/extension/src/openvic-extension/singletons/GameSingleton.cpp +++ b/extension/src/openvic-extension/singletons/GameSingleton.cpp @@ -44,6 +44,7 @@ void GameSingleton::_bind_methods() { OV_BIND_METHOD(GameSingleton::load_defines_compatibility_mode, { "file_paths" }); OV_BIND_SMETHOD(search_for_game_path, { "hint_path" }, DEFVAL(String {})); + OV_BIND_METHOD(GameSingleton::lookup_file_path, { "path" }); OV_BIND_METHOD(GameSingleton::setup_game, { "bookmark_index" }); @@ -593,3 +594,7 @@ Error GameSingleton::load_defines_compatibility_mode(PackedStringArray const& fi String GameSingleton::search_for_game_path(String const& hint_path) { return std_to_godot_string(Dataloader::search_for_game_path(godot_to_std_string(hint_path)).string()); } + +String GameSingleton::lookup_file_path(String const& path) const { + return std_to_godot_string(dataloader.lookup_file(godot_to_std_string(path)).string()); +} diff --git a/extension/src/openvic-extension/singletons/GameSingleton.hpp b/extension/src/openvic-extension/singletons/GameSingleton.hpp index f2b88ac..f793fae 100644 --- a/extension/src/openvic-extension/singletons/GameSingleton.hpp +++ b/extension/src/openvic-extension/singletons/GameSingleton.hpp @@ -59,6 +59,7 @@ namespace OpenVic { godot::Error load_defines_compatibility_mode(godot::PackedStringArray const& file_paths); static godot::String search_for_game_path(godot::String const& hint_path = {}); + godot::String lookup_file_path(godot::String const& path) const; /* Post-load/restart game setup - reset the game to post-load state and load the specified bookmark. */ godot::Error setup_game(int32_t bookmark_index); diff --git a/game/src/Game/Model/FileAccessUtils.gd b/game/src/Game/Model/FileAccessUtils.gd new file mode 100644 index 0000000..f5e02e1 --- /dev/null +++ b/game/src/Game/Model/FileAccessUtils.gd @@ -0,0 +1,88 @@ +class_name FileAccessUtils + +static func read_vec2(file : FileAccess) -> Vector2: + return Vector2(file.get_float(), file.get_float()) + +static func read_vec3(file : FileAccess) -> Vector3: + return Vector3(file.get_float(), file.get_float(), file.get_float()) + +static func read_pos(file : FileAccess) -> Vector3: + var pos : Vector3 = read_vec3(file) + pos.x = -pos.x + return pos + +static func read_vec4(file : FileAccess) -> Vector4: + return Vector4(file.get_float(), file.get_float(), file.get_float(), file.get_float()) + +# Because paradox may or may not be consistent with the xsm spec depending on if its Tuesday or not +static func read_quat(file : FileAccess, int16 : bool = false) -> Quaternion: + if int16: + return Quaternion(read_f16(file), -read_f16(file), -read_f16(file), read_f16(file)) + else: + return Quaternion(file.get_float(), -file.get_float(), -file.get_float(), file.get_float()) + +static func read_f16(file : FileAccess) -> float: + # 32767 or 0x7FFF is the max magnitude of a signed int16 + return float(read_int16(file)) / 32767.0 + +static func replace_chars(string : String) -> String: + return string.replace(":", "_").replace("\\", "_").replace("/", "_") + +static func read_xac_str(file : FileAccess) -> String: + var length : int = file.get_32() + var buffer : PackedByteArray = file.get_buffer(length) + return buffer.get_string_from_ascii() + +static func read_int32(file : FileAccess) -> int: + var bytes : int = file.get_32() + var negative : bool = bytes >> 31 + var val : int = bytes & 0x7FFFFFFF + if negative: + val = -((val ^ 0x7FFFFFFF) + 1) + return val + +static func read_int16(file : FileAccess) -> int: + var bytes : int = file.get_16() + var negative : bool = bytes >> 15 + var val : int = bytes & 0x7FFF + if negative: + val = -((val ^ 0x7FFF) + 1) + return val + +static func read_Color32(file : FileAccess) -> Color: + return Color8(file.get_8(), file.get_8(), file.get_8(), file.get_8()) + +static func read_Color128(file : FileAccess) -> Color: + return Color( + file.get_32() / 0xFFFFFFFF, + file.get_32() / 0xFFFFFFFF, + file.get_32() / 0xFFFFFFFF, + file.get_32() / 0xFFFFFFFF + ) + +static func read_mat4x4(file : FileAccess) -> xac_mat4x4: + return xac_mat4x4.new(read_vec4(file), read_vec4(file), read_vec4(file), read_vec4(file)) + +# This datatype is only ever used to hold a transform for nodes (bones) +class xac_mat4x4: + var col1 : Vector4 + var col2 : Vector4 + var col3 : Vector4 + var col4 : Vector4 + + func _init(col1 : Vector4, col2 : Vector4, col3 : Vector4, col4 : Vector4) -> void: + self.col1 = col1 + self.col2 = col2 + self.col3 = col3 + self.col4 = col4 + + func debugPrint() -> void: + print("\t\tMat4x4 col1:", col1, " col2:", col2, " col3:", col3, " col4:", col4) + + func getAsTransform() -> Transform3D: # godot wants 3x4 matrix + return Transform3D( + Vector3(col1.x, col1.y, col1.z), + Vector3(col2.x, col2.y, col2.z), + Vector3(col3.x, col3.y, col3.z), + Vector3(col4.x, col4.y, col4.z) + ) diff --git a/game/src/Game/Model/UnitModel.gd b/game/src/Game/Model/UnitModel.gd new file mode 100644 index 0000000..0a4fe2f --- /dev/null +++ b/game/src/Game/Model/UnitModel.gd @@ -0,0 +1,160 @@ +class_name UnitModel +extends Node3D + +var skeleton : Skeleton3D = null +var anim_player : AnimationPlayer = null +var anim_lib : AnimationLibrary = null +var sub_units : Array[UnitModel] +var meshes : Array[MeshInstance3D] + +# COLOUR VARIABLES +@export_group("Colors") +@export var primary_colour : Color: + set(col_in): + primary_colour = col_in + change_colour_prop(&"colour_primary", primary_colour) + for unit : UnitModel in sub_units: + unit.primary_colour = col_in + +@export var secondary_colour: Color: + set(col_in): + secondary_colour = col_in + change_colour_prop(&"colour_secondary", secondary_colour) + for unit : UnitModel in sub_units: + unit.secondary_colour = col_in + +@export var tertiary_colour : Color: + set(col_in): + tertiary_colour = col_in + change_colour_prop(&"colour_tertiary", tertiary_colour) + for unit : UnitModel in sub_units: + unit.tertiary_colour = col_in + +# ANIMATION VARIABLES +@export_group("Animation") +@export var idle_anim : Animation: + set(anim_in): + load_animation("idle", anim_in) + idle_anim = anim_in + +@export var move_anim : Animation: + set(anim_in): + load_animation("move", anim_in) + move_anim = anim_in + +@export var attack_anim : Animation: + set(anim_in): + load_animation("attack", anim_in) + attack_anim = anim_in + +enum Anim { NONE, IDLE, MOVE, ATTACK } + +const ANIMATION_LIBRARY : StringName = &"default_lib" +const ANIMATION_IDLE : String = ANIMATION_LIBRARY + "/idle" +const ANIMATION_MOVE : String = ANIMATION_LIBRARY + "/move" +const ANIMATION_ATTACK : String = ANIMATION_LIBRARY + "/attack" + +@export var current_anim : Anim: + set(anim_in): + for unit : UnitModel in sub_units: + unit.current_anim = anim_in + + if anim_player: + match anim_in: + Anim.IDLE: + if idle_anim: + anim_player.set_current_animation(ANIMATION_IDLE) + current_anim = Anim.IDLE + return + Anim.MOVE: + if move_anim: + anim_player.set_current_animation(ANIMATION_MOVE) + current_anim = Anim.MOVE + return + Anim.ATTACK: + if attack_anim: + anim_player.set_current_animation(ANIMATION_ATTACK) + current_anim = Anim.ATTACK + return + _: #None + pass + + anim_player.stop() + + current_anim = Anim.NONE + +# TEXTURE SCROLL SPEEDS (TANKS TRACKS AND SMOKE) +@export_subgroup("Texture_Scroll") +@export var scroll_speed_idle : float = 0.0 +@export var scroll_speed_move : float = 0.0 +@export var scroll_speed_attack : float = 0.0 + +func unit_init() -> void: + for child : Node in get_children(): + if child is MeshInstance3D: + meshes.append(child) + elif child is Skeleton3D: + skeleton = child + +func add_anim_player() -> void: + anim_player = AnimationPlayer.new() + anim_player.name = "anim_player" + + anim_lib = AnimationLibrary.new() + anim_lib.resource_name = ANIMATION_LIBRARY + anim_player.add_animation_library(ANIMATION_LIBRARY, anim_lib) + + add_child(anim_player) + +func has_bone(bone_name : String) -> bool: + return skeleton and skeleton.find_bone(bone_name) > -1 + +func attach_model(bone_name : String, model : Node3D) -> Error: + if not model: + push_error("Cannot attach null model to bone \"", bone_name, "\" of UnitModel ", get_name()) + return FAILED + + if not skeleton: + push_error("Cannot attach model \"", model.get_name(), "\" to bone \"", bone_name, "\" of UnitModel ", get_name(), " - has no skeleton!") + return FAILED + + var bone_idx : int = skeleton.find_bone(bone_name) + if bone_idx < 0 or bone_idx >= skeleton.get_bone_count(): + push_error("Invalid bone \"", bone_name, "\" (index ", bone_idx, ") for attachment \"", model.get_name(), "\" to UnitModel \"", get_name(), "\"") + return FAILED + + var bone_attachment := BoneAttachment3D.new() + bone_attachment.name = bone_name + bone_attachment.bone_idx = bone_idx + bone_attachment.add_child(model) + skeleton.add_child(bone_attachment) + + if model is UnitModel: + sub_units.push_back(model) + model.current_anim = current_anim + model.primary_colour = primary_colour + model.secondary_colour = secondary_colour + model.tertiary_colour = tertiary_colour + + return OK + +func _set_tex_scroll(speed : float) -> void: + for mesh : MeshInstance3D in meshes: + if mesh.get_active_material(0) is ShaderMaterial: + mesh.set_instance_shader_parameter(&"scroll", Vector2(0, speed)) + +func set_flag_index(index : int) -> void: + for mesh : MeshInstance3D in meshes: + mesh.set_instance_shader_parameter(&"flag_index", index) + +func change_colour_prop(prop_name : StringName, prop_val : Color) -> void: + for mesh : MeshInstance3D in meshes: + if mesh.get_active_material(0) is ShaderMaterial: + mesh.set_instance_shader_parameter(prop_name, prop_val) + +func load_animation(prop_name : String, animIn : Animation) -> void: + if not animIn: + return + if not anim_player: + add_anim_player() + anim_lib.add_animation(prop_name,animIn) 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 diff --git a/game/src/Game/Model/XSMLoader.gd b/game/src/Game/Model/XSMLoader.gd new file mode 100644 index 0000000..332bbec --- /dev/null +++ b/game/src/Game/Model/XSMLoader.gd @@ -0,0 +1,363 @@ +class_name XSMLoader + +# Keys: source_file (String) +# Values: loaded animation (Animation) or LOAD_FAILED_MARKER (StringName) +static var xsm_cache : Dictionary + +const LOAD_FAILED_MARKER : StringName = &"XSM LOAD FAILED" + +static func get_xsm_animation(source_file : String) -> Animation: + var cached : Variant = xsm_cache.get(source_file) + if not cached: + cached = _load_xsm_animation(source_file) + if cached: + xsm_cache[source_file] = cached + else: + xsm_cache[source_file] = LOAD_FAILED_MARKER + push_error("Failed to get XSM model \"", source_file, "\" (current load failed)") + return null + + if not cached is Animation: + push_error("Failed to get XSM model \"", source_file, "\" (previous load failed)") + return null + + return cached + +const SKELETON_PATH : String = "./skeleton:%s" + +static func _load_xsm_animation(source_file : String) -> Animation: + 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 XSM ", source_file, " from looked up path ", source_path) + return null + + readHeader(file) + + var metadataChunk : MetadataChunk = null + var boneAnimationChunks : Array[BoneAnimationChunk] = [] + + 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: + 0xC9: #Metadata + metadataChunk = readMetadataChunk(file) + 0xCA: #Bone Animation + boneAnimationChunks.push_back(readBoneAnimationChunk(file, metadataChunk.use_quat_16())) + _: + push_error(">> INVALID XSM CHUNK TYPE %X" % type) + break + + var animLength : float = 0.0 + var anim : Animation = Animation.new() + for anim_Chunk : BoneAnimationChunk in boneAnimationChunks: + for submotion : SkeletalSubMotion in anim_Chunk.SkeletalSubMotions: + # NOTE: godot uses ':' to specify properties, so we replace such characters with '_' + var skeleton_path : String = SKELETON_PATH % FileAccessUtils.replace_chars(submotion.nodeName) + + if submotion.numPosKeys > 0: + var id : int = anim.add_track(Animation.TYPE_POSITION_3D) + anim.track_set_path(id, skeleton_path) + for key : PosKey in submotion.PosKeys: + anim.position_track_insert_key(id, key.fTime, key.pos) + if key.fTime > animLength: + animLength = key.fTime + else: # EXPERIMENTAL: see if setting posePos fixes idle3 + var id : int = anim.add_track(Animation.TYPE_POSITION_3D) + anim.track_set_path(id, skeleton_path) + anim.position_track_insert_key(id, 0, submotion.posePos) + + if submotion.numRotKeys > 0: + var id : int = anim.add_track(Animation.TYPE_ROTATION_3D) + anim.track_set_path(id, skeleton_path) + for key : RotKey in submotion.RotKeys: + anim.rotation_track_insert_key(id, key.fTime, key.rot) + if key.fTime > animLength: + animLength = key.fTime + else: # EXPERIMENTAL: see if setting posePos fixes idle3 + var id : int = anim.add_track(Animation.TYPE_ROTATION_3D) + anim.track_set_path(id, skeleton_path) + anim.rotation_track_insert_key(id, 0, submotion.poseRot) + + if submotion.numScaleKeys > 0: + var id : int = anim.add_track(Animation.TYPE_SCALE_3D) + anim.track_set_path(id, skeleton_path) + for key : ScaleKey in submotion.ScaleKeys: + anim.scale_track_insert_key(id, key.fTime, key.scale) + if key.fTime > animLength: + animLength = key.fTime + + # TODO: submotion.numScaleRotKeys + + anim.length = animLength + anim.loop_mode = Animation.LOOP_LINEAR + + xsm_cache[source_file] = anim + return anim + +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 pad : int = file.get_8() + #print(magic, ", version: ", version, ", bigEndian: ", bBigEndian, " pad: ", pad) + +# NOTE: the "pad" variable is actually very important! +# It seems to have something to do with whether paradox uses int16 or int32 +# for quaternions (it's "pad" or version number, can't tell) + +static func readMetadataChunk(file : FileAccess) -> MetadataChunk: + return MetadataChunk.new( + file.get_float(), file.get_float(), FileAccessUtils.read_int32(file), + file.get_8(), file.get_8(), file.get_16(), + FileAccessUtils.read_xac_str(file), FileAccessUtils.read_xac_str(file), FileAccessUtils.read_xac_str(file), FileAccessUtils.read_xac_str(file) + ) + +class MetadataChunk: + var unused : float + var fMaxAcceptableError : float + var fps : int # int32 + var exporterMajorVersion : int # byte + var exporterMinorVersion : int # byte + var pad : int # 2x byte + var sourceApp : String + var origFileName : String + var exportDate : String + var motionName : String + + func _init( + unused : float, + fMaxAcceptableError : float, + fps : int, + exporterMajorVersion : int, + exporterMinorVersion : int, + pad : int, + sourceApp : String, + origFileName : String, + exportDate : String, + motionName : String + ) -> void: + self.unused = unused + self.fMaxAcceptableError = fMaxAcceptableError + self.fps = fps + self.exporterMajorVersion = exporterMajorVersion + self.exporterMinorVersion = exporterMinorVersion + self.pad = pad + self.sourceApp = sourceApp + self.origFileName = origFileName + self.exportDate = exportDate + self.motionName = motionName + + func debugPrint() -> void: + print("FileName: %s, sourceApp: %s, exportDate: %s, ExporterV:%d.%d" % + [origFileName, sourceApp, exportDate, exporterMajorVersion, exporterMinorVersion]) + print("MotionName: %s, fps: %d, MaxError: %s, Use 16-bit int Quaternions?:%s" % + [motionName, fps, fMaxAcceptableError, use_quat_16()]) + + func use_quat_16() -> bool: + return pad == 0x0 + +static func readPosKey(file : FileAccess) -> PosKey: + return PosKey.new(FileAccessUtils.read_pos(file), file.get_float()) + +class PosKey: + var pos : Vector3 + var fTime : float + + func _init(pos : Vector3, fTime : float) -> void: + self.pos = pos + self.fTime = fTime + + func debugPrint() -> void: + print("\t\tPos:%s, time:%s" % [pos, fTime]) + +static func readRotKey(file : FileAccess, use_quat16 : bool) -> RotKey: + return RotKey.new(FileAccessUtils.read_quat(file, use_quat16), file.get_float()) + +class RotKey: + var rot : Quaternion + var fTime : float + + func _init(rot : Quaternion, fTime : float) -> void: + self.rot = rot + self.fTime = fTime + + func debugPrint() -> void: + print("\t\tRot:%s, time:%s" % [rot, fTime]) + +static func readScaleKey(file : FileAccess) -> ScaleKey: + return ScaleKey.new(FileAccessUtils.read_vec3(file), file.get_float()) + +class ScaleKey: + var scale : Vector3 + var fTime : float + + func _init(scale : Vector3, fTime : float) -> void: + self.scale = scale + self.fTime = fTime + + func debugPrint() -> void: + print("\t\tScale:%s, time:%s" % [scale, fTime]) + +static func readScaleRotKey(file : FileAccess, use_quat16 : bool) -> ScaleRotKey: + return ScaleRotKey.new(FileAccessUtils.read_quat(file, use_quat16), file.get_float()) + +class ScaleRotKey: + var rot : Quaternion + var fTime : float + + func _init(rot : Quaternion, fTime : float) -> void: + self.rot = rot + self.fTime = fTime + + func debugPrint() -> void: + print("\t\tScaleRot:%s, time:%s" % [rot, fTime]) + +static func readSkeletalSubMotion(file : FileAccess, use_quat16 : bool) -> SkeletalSubMotion: + var a : Quaternion = FileAccessUtils.read_quat(file, use_quat16) + var b : Quaternion = FileAccessUtils.read_quat(file, use_quat16) + var c : Quaternion = FileAccessUtils.read_quat(file, use_quat16) + var d : Quaternion = FileAccessUtils.read_quat(file, use_quat16) + + var e : Vector3 = FileAccessUtils.read_pos(file) + var f : Vector3 = FileAccessUtils.read_vec3(file) + var g : Vector3 = FileAccessUtils.read_pos(file) + var h : Vector3 = FileAccessUtils.read_vec3(file) + + var p : int = FileAccessUtils.read_int32(file) + var j : int = FileAccessUtils.read_int32(file) + var k : int = FileAccessUtils.read_int32(file) + var l : int = FileAccessUtils.read_int32(file) + + var m : float = file.get_float() + var n : String = FileAccessUtils.read_xac_str(file) + + var submotion : SkeletalSubMotion = SkeletalSubMotion.new( + a, b, c, d, # quats + e, f, g, h, # vec3 + p, j, k, l, # ints + m, n + ) + var poskeys : Array[PosKey] = [] + var rotkeys : Array[RotKey] = [] + var scalekeys : Array[ScaleKey] = [] + var scalerotkeys : Array[ScaleRotKey] = [] + #FIXME: Did paradox store the number of pos keys as a float instead of int? + + for i : int in submotion.numPosKeys: + poskeys.push_back(readPosKey(file)) + for i : int in submotion.numRotKeys: + rotkeys.push_back(readRotKey(file, use_quat16)) + for i : int in submotion.numScaleKeys: + scalekeys.push_back(readScaleKey(file)) + for i : int in submotion.numScaleRotKeys: + scalerotkeys.push_back(readScaleRotKey(file, use_quat16)) + submotion.setPosKeys(poskeys) + submotion.setRotKeys(rotkeys) + submotion.setScaleKeys(scalekeys) + submotion.setScaleRotKeys(scalerotkeys) + return submotion + +class SkeletalSubMotion: + var poseRot : Quaternion + var bindPoseRot : Quaternion + var poseScaleRot : Quaternion + var bindPoseScaleRot : Quaternion + var posePos : Vector3 + var poseScale : Vector3 + var bindPosePos : Vector3 + var bindPoseScale : Vector3 + var numPosKeys : int # int32 + var numRotKeys : int # int32 + var numScaleKeys : int # int32 + var numScaleRotKeys : int # int32 + var fMaxError : float + var nodeName : String + + var PosKeys : Array[PosKey] + var RotKeys : Array[RotKey] + var ScaleKeys : Array[ScaleKey] + var ScaleRotKeys : Array[ScaleRotKey] + + func _init( + poseRot : Quaternion, + bindPoseRot : Quaternion, + poseScaleRot : Quaternion, + bindPoseScaleRot : Quaternion, + posePos : Vector3, + poseScale : Vector3, + bindPosePos : Vector3, + bindPoseScale : Vector3, + numPosKeys : int, + numRotKeys : int, + numScaleKeys : int, + numScaleRotKeys : int, + fMaxError : float, + nodeName : String + ) -> void: + self.poseRot = poseRot + self.bindPoseRot = bindPoseRot + self.poseScaleRot = poseScaleRot + self.bindPoseScaleRot = bindPoseScaleRot + self.posePos = posePos + self.poseScale = poseScale + self.bindPosePos = bindPosePos + self.bindPoseScale = bindPoseScale + self.numPosKeys = numPosKeys + self.numRotKeys = numRotKeys + self.numScaleKeys = numScaleKeys + self.numScaleRotKeys = numScaleRotKeys + self.fMaxError = fMaxError + self.nodeName = nodeName + + func setPosKeys(PosKeys : Array[PosKey]) -> void: + self.PosKeys = PosKeys + + func setRotKeys(RotKeys : Array[RotKey]) -> void: + self.RotKeys = RotKeys + + func setScaleKeys(ScaleKeys : Array[ScaleKey]) -> void: + self.ScaleKeys = ScaleKeys + + func setScaleRotKeys(ScaleRotKeys : Array[ScaleRotKey]) -> void: + self.ScaleRotKeys = ScaleRotKeys + + func debugPrint() -> void: + print("Node: %s, #PosKeys %d, #RotKeys %d, #ScaleKeys %d, #ScaleRotKeys %d" % [nodeName, numPosKeys, numRotKeys, numScaleKeys, numScaleRotKeys]) + print("\tposeScaleRot %s,\tbindPoseScaleRot %s,\tposeScale %s,\tbindPoseScale %s" % [poseScaleRot, bindPoseScaleRot, poseScale, bindPoseScale]) + for key : PosKey in PosKeys: + key.debugPrint() + for key : RotKey in RotKeys: + key.debugPrint() + for key : ScaleKey in ScaleKeys: + key.debugPrint() + for key : ScaleRotKey in ScaleRotKeys: + key.debugPrint() + +static func readBoneAnimationChunk(file : FileAccess, use_quat16 : bool) -> BoneAnimationChunk: + var numSubMotions : int = FileAccessUtils.read_int32(file) + var animChunk : BoneAnimationChunk = BoneAnimationChunk.new(numSubMotions) + var submotions : Array[SkeletalSubMotion] = [] + for i : int in animChunk.numSubMotions: + submotions.push_back(readSkeletalSubMotion(file, use_quat16)) + animChunk.setSkeletalSubMotions(submotions) + return animChunk + +class BoneAnimationChunk: + var numSubMotions : int # int32 + var SkeletalSubMotions : Array[SkeletalSubMotion] + + func _init(numSubMotions : int) -> void: + self.numSubMotions = numSubMotions + + func setSkeletalSubMotions(SkeletalSubMotions : Array[SkeletalSubMotion]) -> void: + self.SkeletalSubMotions = SkeletalSubMotions + + func debugPrint() -> void: + print("Number of Submotions: %d" % numSubMotions) + for submotion : SkeletalSubMotion in SkeletalSubMotions: + submotion.debugPrint() diff --git a/game/src/Game/Model/flag.gdshader b/game/src/Game/Model/flag.gdshader new file mode 100644 index 0000000..d338e36 --- /dev/null +++ b/game/src/Game/Model/flag.gdshader @@ -0,0 +1,26 @@ +shader_type spatial; + +render_mode cull_disabled; + +// Both vanilla flags use the same normal texture +uniform uvec2 flag_dims; +uniform sampler2D texture_flag_sheet_diffuse : source_color; +uniform sampler2D texture_normal : hint_normal; + +instance uniform uint flag_index; + +uniform vec2 scroll_speed = vec2(-0.25,0); + +// Scroll the Normal map, but leave the albedo alone +void fragment() { + uvec2 flag_sheet_dims = uvec2(textureSize(texture_flag_sheet_diffuse, 0)); + uint scaled_index = flag_index * flag_dims.x; + + uvec2 flag_pos = uvec2(scaled_index % flag_sheet_dims.x, scaled_index / flag_sheet_dims.x * flag_dims.y); + + vec2 flag_uv = (vec2(flag_pos) + UV * vec2(flag_dims)) / vec2(flag_sheet_dims); + + ALBEDO = texture(texture_flag_sheet_diffuse, flag_uv).rgb; + //ALBEDO = vec3(1, 0, 0); + NORMAL_MAP = texture(texture_normal, UV + TIME*scroll_speed).rgb; +} diff --git a/game/src/Game/Model/flag_mat.tres b/game/src/Game/Model/flag_mat.tres new file mode 100644 index 0000000..beefd15 --- /dev/null +++ b/game/src/Game/Model/flag_mat.tres @@ -0,0 +1,7 @@ +[gd_resource type="ShaderMaterial" load_steps=2 format=3 uid="uid://5utra6tpdqag"] + +[ext_resource type="Shader" path="res://src/Game/Model/flag.gdshader" id="1_oqkkj"] + +[resource] +render_priority = 0 +shader = ExtResource("1_oqkkj") diff --git a/game/src/Game/Model/unit_colours.gdshader b/game/src/Game/Model/unit_colours.gdshader new file mode 100644 index 0000000..dd0f5e2 --- /dev/null +++ b/game/src/Game/Model/unit_colours.gdshader @@ -0,0 +1,31 @@ + +shader_type spatial; + +render_mode blend_mix, depth_draw_opaque, cull_disabled, diffuse_burley, specular_schlick_ggx; + +//hold all the textures for the units that need this shader to mix in their +//nation colours (mostly generic infantry units) +uniform sampler2D texture_albedo[32] : source_color, filter_linear_mipmap, repeat_enable; +uniform sampler2D texture_nation_colors_mask[32] : source_color, filter_linear_mipmap, repeat_enable; + +instance uniform vec3 colour_primary : source_color; +instance uniform vec3 colour_secondary : source_color; +instance uniform vec3 colour_tertiary : source_color; + +//used to access the right textures since different units (with different textures) +//will use this same shader +instance uniform uint tex_index_specular; +instance uniform uint tex_index_diffuse; + +void fragment() { + vec2 base_uv = UV; + vec4 albedo_tex = texture(texture_albedo[tex_index_diffuse], base_uv); + vec4 nation_colours_tex = texture(texture_nation_colors_mask[tex_index_specular], base_uv); + + //set colours to either be white (1,1,1) or the nation colour based on the mask + vec3 primary_col = mix(vec3(1.0, 1.0, 1.0), colour_primary, nation_colours_tex.g); + vec3 secondary_col = mix(vec3(1.0, 1.0, 1.0), colour_secondary, nation_colours_tex.b); + vec3 tertiary_col = mix(vec3(1.0, 1.0, 1.0), colour_tertiary, nation_colours_tex.r); + + ALBEDO = albedo_tex.rgb * primary_col * secondary_col * tertiary_col; +} diff --git a/game/src/Game/Model/unit_colours_mat.tres b/game/src/Game/Model/unit_colours_mat.tres new file mode 100644 index 0000000..43ca523 --- /dev/null +++ b/game/src/Game/Model/unit_colours_mat.tres @@ -0,0 +1,9 @@ +[gd_resource type="ShaderMaterial" load_steps=2 format=3 uid="uid://bkham060cwpr3"] + +[ext_resource type="Shader" path="res://src/Game/Model/unit_colours.gdshader" id="1_axmiw"] + +[resource] +render_priority = 0 +shader = ExtResource("1_axmiw") +shader_parameter/texture_albedo = [] +shader_parameter/texture_nation_colors_mask = [] -- cgit v1.2.3-56-ga3b1