From 6016b8f55b10db8b498a0e18496e0dca06baeb10 Mon Sep 17 00:00:00 2001
From: Nemrav <>
Date: Sun, 19 Jan 2025 09:42:52 -0400
Subject: [PATCH] Load xac and xsm with cpp
---
extension/doc_classes/ModelSingleton.xml | 14 +
.../singletons/ModelSingleton.cpp | 40 +
.../singletons/ModelSingleton.hpp | 12 +
.../openvic-extension/utility/XACLoader.cpp | 0
.../openvic-extension/utility/XACLoader.hpp | 1026 +++++++++++++++++
.../utility/XACUtilities.hpp | 221 ++++
.../openvic-extension/utility/XSMLoader.cpp | 0
.../openvic-extension/utility/XSMLoader.hpp | 428 +++++++
game/src/Game/GameSession/ModelManager.gd | 29 +-
game/src/Game/Model/unit_colours.gdshader | 3 +-
10 files changed, 1762 insertions(+), 11 deletions(-)
create mode 100644 extension/src/openvic-extension/utility/XACLoader.cpp
create mode 100644 extension/src/openvic-extension/utility/XACLoader.hpp
create mode 100644 extension/src/openvic-extension/utility/XACUtilities.hpp
create mode 100644 extension/src/openvic-extension/utility/XSMLoader.cpp
create mode 100644 extension/src/openvic-extension/utility/XSMLoader.hpp
diff --git a/extension/doc_classes/ModelSingleton.xml b/extension/doc_classes/ModelSingleton.xml
index 74c669af..a6889019 100644
--- a/extension/doc_classes/ModelSingleton.xml
+++ b/extension/doc_classes/ModelSingleton.xml
@@ -35,5 +35,19 @@
+
+
+
+
+ Gets the animation from an internal cache or file system using the provided file path.
+
+
+
+
+
+
+ Gets the animation from an internal cache or file system using the provided file path.
+
+
diff --git a/extension/src/openvic-extension/singletons/ModelSingleton.cpp b/extension/src/openvic-extension/singletons/ModelSingleton.cpp
index 5b2fa8b8..2cae1804 100644
--- a/extension/src/openvic-extension/singletons/ModelSingleton.cpp
+++ b/extension/src/openvic-extension/singletons/ModelSingleton.cpp
@@ -1,5 +1,6 @@
#include "ModelSingleton.hpp"
+#include
#include
#include
@@ -9,6 +10,7 @@
#include "openvic-extension/singletons/GameSingleton.hpp"
#include "openvic-extension/utility/ClassBindings.hpp"
#include "openvic-extension/utility/Utilities.hpp"
+#include "godot_cpp/classes/node3d.hpp"
using namespace godot;
using namespace OpenVic;
@@ -19,6 +21,8 @@ void ModelSingleton::_bind_methods() {
OV_BIND_METHOD(ModelSingleton::get_cultural_helmet_model, { "culture" });
OV_BIND_METHOD(ModelSingleton::get_flag_model, { "floating" });
OV_BIND_METHOD(ModelSingleton::get_buildings);
+ OV_BIND_METHOD(ModelSingleton::get_xsm_animation,{ "animation_name" });
+ OV_BIND_METHOD(ModelSingleton::get_xac_model,{ "model_name" });
}
ModelSingleton* ModelSingleton::get_singleton() {
@@ -481,3 +485,39 @@ TypedArray ModelSingleton::get_buildings() {
return ret;
}
+
+Ref ModelSingleton::get_xsm_animation(String source_file) {
+ const xsm_map_t::const_iterator it = xsm_cache.find(source_file);
+ if(it != xsm_cache.end()) {
+ return it->second;
+ }
+
+ GameSingleton const* game_singleton = GameSingleton::get_singleton();
+ ERR_FAIL_NULL_V(game_singleton, {});
+
+ String path = game_singleton->lookup_file_path(source_file);
+
+ //Logger::info("Load XSM Animation from file: ",Utilities::godot_to_std_string(source_file));
+
+ Ref anim = _load_xsm_animation(FileAccess::open(path, FileAccess::READ));
+ xsm_cache.emplace(source_file,anim);
+ return anim;
+}
+
+Node3D* ModelSingleton::get_xac_model(String source_file) {
+ const xac_map_t::const_iterator it = xac_cache.find(source_file);
+ if(it != xac_cache.end()) {
+ return (Node3D*)it->second->duplicate();
+ }
+
+ GameSingleton const* game_singleton = GameSingleton::get_singleton();
+ ERR_FAIL_NULL_V(game_singleton, {});
+
+ String path = game_singleton->lookup_file_path(source_file);
+
+ Logger::info("Load XAC Model from file: ",Utilities::godot_to_std_string(source_file));
+
+ Node3D* node = _load_xac_model(FileAccess::open(path, FileAccess::READ));
+ xac_cache.emplace(source_file,node);
+ return node;
+}
\ No newline at end of file
diff --git a/extension/src/openvic-extension/singletons/ModelSingleton.hpp b/extension/src/openvic-extension/singletons/ModelSingleton.hpp
index ebec7504..f0598e10 100644
--- a/extension/src/openvic-extension/singletons/ModelSingleton.hpp
+++ b/extension/src/openvic-extension/singletons/ModelSingleton.hpp
@@ -1,10 +1,15 @@
#pragma once
+#include
+#include
#include
#include
#include
#include
+#include "../utility/XSMLoader.hpp"
+#include "../utility/XACLoader.hpp"
+#include "godot_cpp/classes/node3d.hpp"
namespace OpenVic {
struct BuildingInstance;
@@ -31,9 +36,13 @@ namespace OpenVic {
using animation_map_t = deque_ordered_map;
using model_map_t = deque_ordered_map;
+ using xsm_map_t = deque_ordered_map>;
+ using xac_map_t = deque_ordered_map;
animation_map_t animation_cache;
model_map_t model_cache;
+ xsm_map_t xsm_cache;
+ xac_map_t xac_cache;
godot::Dictionary get_animation_dict(GFX::Actor::Animation const& animation);
godot::Dictionary get_model_dict(GFX::Actor const& actor);
@@ -56,5 +65,8 @@ namespace OpenVic {
godot::Dictionary get_flag_model(bool floating);
godot::TypedArray get_buildings();
+
+ godot::Ref get_xsm_animation(godot::String source_file);
+ godot::Node3D* get_xac_model(godot::String source_file);
};
}
diff --git a/extension/src/openvic-extension/utility/XACLoader.cpp b/extension/src/openvic-extension/utility/XACLoader.cpp
new file mode 100644
index 00000000..e69de29b
diff --git a/extension/src/openvic-extension/utility/XACLoader.hpp b/extension/src/openvic-extension/utility/XACLoader.hpp
new file mode 100644
index 00000000..6dc824a8
--- /dev/null
+++ b/extension/src/openvic-extension/utility/XACLoader.hpp
@@ -0,0 +1,1026 @@
+#pragma once
+#include
+//#include
+#include
+#include
+//#include
+#include
+#include "Utilities.hpp"
+#include "XACUtilities.hpp"
+//#include "godot_cpp/classes/animation.hpp"
+#include "godot_cpp/classes/array_mesh.hpp"
+#include "godot_cpp/classes/image.hpp"
+#include "godot_cpp/classes/image_texture.hpp"
+#include "godot_cpp/classes/material.hpp"
+//#include "godot_cpp/classes/node.hpp"
+#include
+#include "godot_cpp/classes/mesh_instance3d.hpp"
+#include "godot_cpp/classes/resource_loader.hpp"
+#include "godot_cpp/classes/resource_preloader.hpp"
+#include "godot_cpp/classes/shader_material.hpp"
+#include "godot_cpp/classes/skeleton3d.hpp"
+//#include "godot_cpp/variant/basis.hpp"
+//#include "godot_cpp/variant/dictionary.hpp"
+#include "godot_cpp/variant/dictionary.hpp"
+#include "godot_cpp/variant/packed_byte_array.hpp"
+#include "godot_cpp/variant/packed_float32_array.hpp"
+#include "godot_cpp/variant/packed_int32_array.hpp"
+#include "godot_cpp/variant/packed_int64_array.hpp"
+#include "godot_cpp/variant/packed_string_array.hpp"
+#include "godot_cpp/variant/packed_vector2_array.hpp"
+#include "godot_cpp/variant/packed_vector3_array.hpp"
+#include "godot_cpp/variant/packed_vector4_array.hpp"
+#include "godot_cpp/variant/transform3d.hpp"
+#include "godot_cpp/variant/typed_array.hpp"
+#include "godot_cpp/variant/variant.hpp"
+#include "godot_cpp/variant/vector3.hpp"
+#include "openvic-simulation/utility/Logger.hpp"
+
+#include
+#include
+#include
+#include
+
+//#include "openvic-extension/utility/Utilities.hpp"
+
+namespace OpenVic {
+
+ //using OpenVic::Utilities::std_view_to_godot_string;
+
+ static constexpr uint32_t XAC_FORMAT_SPECIFIER = ' CAX'; // Order reversed due to little endian
+ static constexpr uint8_t XAC_VERSION_MAJOR = 1, XAC_VERSION_MINOR = 0;
+
+ #pragma pack(push)
+ #pragma pack(1)
+
+ struct xac_header_t {
+ uint32_t format_identifier;
+ uint8_t version_major;
+ uint8_t version_minor;
+ uint8_t big_endian;
+ uint8_t multiply_order;
+ };
+
+ struct xac_metadata_v2_pack_t {
+ uint32_t reposition_mask; //1=position, 2=rotation, 4=scale
+ int32_t repositioning_node;
+ uint8_t exporter_major_version;
+ uint8_t exporter_minor_version;
+ uint16_t pad;
+ float retarget_root_offset;
+ };
+
+ struct node_hierarchy_pack_t { //v1
+ int32_t node_count;
+ int32_t root_node_count; //nodes with parent_id == -1
+ };
+
+ struct node_data_pack_t { //v1
+ quat_v1_t rotation;
+ quat_v1_t scale_rotation;
+ vec3d_t position;
+ vec3d_t scale;
+ float unused[3];
+ int32_t unknown[2];
+ int32_t parent_node_id;
+ int32_t child_nodes_count;
+ int32_t include_in_bounds_calculation; //bool
+ matrix44_t transform;
+ float importance_factor;
+ };
+
+ struct material_totals_t { //v1
+ int32_t total_materials_count;
+ int32_t standard_materials_count;
+ int32_t fx_materials_count;
+ };
+
+ struct material_definition_pack_t {
+ vec4d_t ambient_color;
+ vec4d_t diffuse_color;
+ vec4d_t specular_color;
+ vec4d_t emissive_color;
+ float shine;
+ float shine_strength;
+ float opacity;
+ float ior; //index of refraction
+ uint8_t double_sided; //bool
+ uint8_t wireframe; //bool
+ uint8_t unused; //in v1, unknown, but used
+ uint8_t layers_count; //in v1, unknown, but used
+ };
+
+ struct material_layer_pack_t { //also chunk 0x4, v2
+ float amount;
+ vec2d_t uv_offset;
+ vec2d_t uv_tiling;
+ float rotation_in_radians;
+ int16_t material_id;
+ uint8_t map_type; // (1-5) enum MAP_TYPE
+ uint8_t unused;
+ };
+
+ struct mesh_pack_t {
+ int32_t node_id;
+ int32_t influence_ranges_count;
+ int32_t vertices_count;
+ int32_t indices_count;
+ int32_t submeshes_count;
+ int32_t attribute_layers_count;
+ uint8_t is_collision_mesh; //bool
+ uint8_t pad[3];
+ };
+
+ struct vertices_attribute_pack_t {
+ int32_t type; //0-6 (enum ATTRIBUTE)
+ int32_t attribute_size;
+ uint8_t keep_originals; //bool
+ uint8_t is_scale_factor; //bool
+ uint16_t pad;
+ };
+
+ struct submesh_pack_t {
+ int32_t indices_count;
+ int32_t vertices_count;
+ int32_t material_id;
+ int32_t bones_count;
+ };
+
+ struct skinning_pack_t {
+ int32_t influences_count;
+ uint8_t is_for_collision; //bool
+ uint8_t pad[3];
+ };
+
+ struct influence_data_t {
+ float weight;
+ int16_t bone_id;
+ int16_t pad;
+ };
+
+ struct influence_range_t {
+ int32_t first_influence_index;
+ int32_t influences_count;
+ };
+
+ // 0x4, 0x6, 0xA chunk types appear in vic2, but what they do
+ // is unknown
+ // 0x8 is junk data
+ // 0x0 is the older node/bone chunk
+
+ //0x0, v3
+ struct node_chunk_pack_t {
+ quat_v1_t rotation;
+ quat_v1_t scale_rotation;
+ vec3d_t position;
+ vec3d_t scale;
+ vec3d_t unused; //-1, -1, -1
+ int32_t unknown; //-1 (0xFFFF)
+ int32_t parent_node_id;
+ float uncertain[17]; //likely a matrix44 + fimportancefactor
+ };
+
+ /*
+ 0x6, v? unknown
+ 12x int32?
+ 09x float?
+ */
+
+ #pragma pack(pop)
+
+ struct MAP_TYPE {
+ enum Values : int32_t {
+ DIFFUSE = 2,
+ SPECULAR,
+ SHADOW,
+ NORMAL
+ };
+ };
+
+ struct ATTRIBUTE {
+ enum Values : int32_t {
+ POSITION,
+ NORMAL,
+ TANGENT,
+ UV,
+ COL_32,
+ INFLUENCE_RANGE,
+ COL_128
+ };
+ };
+
+ struct xac_metadata_v2_t {
+ xac_metadata_v2_pack_t packed = {};
+ String source_app;
+ String original_file_name;
+ String export_date;
+ String actor_name;
+ };
+
+ struct node_data_t { //v1
+ node_data_pack_t packed = {};
+ String name;
+ };
+
+ struct node_hierarchy_t { //v1
+ node_hierarchy_pack_t packed = {};
+ std::vector node_data;
+ };
+
+ struct material_layer_t {
+ material_layer_pack_t packed = {};
+ String texture;
+ };
+
+ struct material_definition_t {
+ material_definition_pack_t packed = {};
+ String name;
+ std::vector layers;
+ };
+
+ struct vertices_attribute_t {
+ vertices_attribute_pack_t packed = {};
+ std::vector data;
+ };
+
+ struct submesh_t {
+ submesh_pack_t packed = {};
+ std::vector relative_indices;
+ std::vector bone_ids;
+ };
+
+ struct mesh_t {
+ mesh_pack_t packed = {};
+ std::vector vertices_attributes;
+ std::vector submeshes;
+ };
+
+ struct skinning_t {
+ int32_t node_id = 0;
+ int32_t local_bones_count = -1; //v3 only
+ skinning_pack_t packed = {};
+ std::vector influence_data;
+ std::vector influence_ranges;
+ };
+
+ struct node_chunk_t {
+ node_chunk_pack_t packed = {};
+ String name;
+ };
+
+ static bool read_xac_header(Ref const& file) {
+ xac_header_t header = {};
+ ERR_FAIL_COND_V(!read_struct(file, header), false);
+
+ ERR_FAIL_COND_V_MSG(
+ header.format_identifier != XAC_FORMAT_SPECIFIER, false, vformat(
+ "Invalid XAC format identifier: %x (should be %x)", header.format_identifier, XAC_FORMAT_SPECIFIER
+ )
+ );
+
+ ERR_FAIL_COND_V_MSG(
+ header.version_major != XAC_VERSION_MAJOR || header.version_minor != XAC_VERSION_MINOR, false, vformat(
+ "Invalid XAC version: %d.%d (should be %d.%d)",
+ header.version_major, header.version_minor, XAC_VERSION_MAJOR, XAC_VERSION_MINOR
+ )
+ );
+
+ ERR_FAIL_COND_V_MSG(
+ header.big_endian != 0, false, "Invalid XAC endianness: big endian (only little endian is supported)"
+ );
+
+ /*ERR_FAIL_COND_V_MSG(
+ header.multiply_order != 0, false, "Invalid XAC multiply order: ???"
+ );*/
+
+ return true;
+ }
+
+ static bool read_xac_metadata(Ref const& file, xac_metadata_v2_t& metadata) {
+ bool ret = read_struct(file, metadata.packed);
+ ret &= read_string(file, metadata.source_app, false);
+ ret &= read_string(file, metadata.original_file_name, false);
+ ret &= read_string(file, metadata.export_date, false);
+ ret &= read_string(file, metadata.actor_name, false);
+ return ret;
+ }
+
+ static bool read_node_data(Ref const& file, node_data_t& node_data) {
+ bool ret = read_struct(file, node_data.packed);
+ ret &= read_string(file, node_data.name);
+ return ret;
+ }
+
+ static bool read_node_hierarchy(Ref const& file, node_hierarchy_t& hierarchy) {
+ bool ret = read_struct(file, hierarchy.packed);
+ for(int32_t i=0; i const& file, material_totals_t& totals) {
+ return read_struct(file, totals);
+ }
+
+ static bool read_layer(Ref const& file, material_layer_t& layer) {
+ bool ret = true;
+ ret &= read_struct(file, layer.packed);
+ ret &= read_string(file, layer.texture, false);
+ //Logger::info(Utilities::godot_to_std_string(layer.texture));
+ return ret;
+ }
+
+ static bool read_material_definition(Ref const& file, material_definition_t& def, int32_t version) {
+ bool ret = read_struct(file, def.packed);
+ ret &= read_string(file, def.name, false);
+ if (version > 1) { //in version 1, the layers are defined in separate chunks of type 0x4
+ for(int i=0; i const& file, vertices_attribute_t& attribute, int32_t vertices_count) {
+ bool ret = read_struct(file, attribute.packed);
+ ret &= read_struct_array(file, attribute.data, vertices_count*attribute.packed.attribute_size);
+ return ret;
+ }
+
+ static bool read_submesh(Ref const& file, submesh_t& submesh) {
+ bool ret = read_struct(file, submesh.packed);
+ ret &= read_struct_array(file, submesh.relative_indices, submesh.packed.indices_count);
+ ret &= read_struct_array(file, submesh.bone_ids, submesh.packed.bones_count);
+ return ret;
+ }
+
+ static bool read_mesh(Ref const& file, mesh_t& mesh) {
+ bool ret = read_struct(file, mesh.packed);
+ for(int i=0; i const& file, skinning_t& skin, std::vector const& meshes, int32_t version) {
+ bool ret = read_struct(file, skin.node_id);
+ if (version == 3) {
+ ret &= read_struct(file, skin.local_bones_count);
+ }
+ ret &= read_struct(file, skin.packed);
+ ret &= read_struct_array(file, skin.influence_data, skin.packed.influences_count);
+ bool found = false;
+ for(mesh_t const& mesh : meshes) {
+ if (mesh.packed.is_collision_mesh == skin.packed.is_for_collision && mesh.packed.node_id == skin.node_id) {
+ ret &= read_struct_array(file, skin.influence_ranges, mesh.packed.influence_ranges_count);
+ found = true;
+ break;
+ }
+ }
+ ret &= found;
+ return ret;
+ }
+
+ static bool read_node_chunk(Ref const& file, node_chunk_t& node) {
+ bool ret = read_struct(file, node.packed);
+ ret &= read_string(file, node.name);
+ //Logger::info(Utilities::godot_to_std_string(node.name));
+ return ret;
+ }
+
+ /*
+ ====================================
+ Skeleton helper functions
+
+ ====================================
+ */
+
+ //TODO: Verify
+ static Transform3D make_transform(vec3d_t const& position, quat_v1_t const& quaternion, vec3d_t const& scale) {
+ Transform3D transform = Transform3D();
+
+ Basis basis = Basis();
+ basis.set_quaternion(quat_v1_to_godot(quaternion));
+ basis.scale(vec3d_to_godot(scale));
+
+ transform.set_basis(basis);
+ transform.translate_local(vec3d_to_godot(position, true));
+
+ return transform;
+ }
+
+ //TODO: do we want to use node's bone id instead of current_id?
+ static Skeleton3D* build_armature_hierarchy(node_hierarchy_t const& hierarchy) {
+ static const StringName skeleton_name = "skeleton";
+ Skeleton3D* skeleton = memnew(Skeleton3D);
+ skeleton->set_name(skeleton_name);
+
+ uint32_t current_id = 0;
+
+ for(node_data_t const node : hierarchy.node_data) {
+ skeleton->add_bone(node.name);
+ skeleton->set_bone_parent(current_id, node.packed.parent_node_id);
+
+ Transform3D transform = make_transform(node.packed.position, node.packed.rotation, node.packed.scale);
+ skeleton->set_bone_rest(current_id, transform);
+ skeleton->set_bone_pose(current_id, transform);
+
+ current_id += 1;
+ }
+
+ return skeleton;
+ }
+
+ static Skeleton3D* build_armature_nodes(std::vector const& nodes) {
+ static const StringName skeleton_name = "skeleton";
+ Skeleton3D* skeleton = memnew(Skeleton3D);
+ skeleton->set_name(skeleton_name);
+
+ uint32_t current_id = 0;
+
+ for(node_chunk_t const node : nodes) {
+ skeleton->add_bone(node.name);
+ skeleton->set_bone_parent(current_id, node.packed.parent_node_id);
+
+ Transform3D transform = make_transform(node.packed.position, node.packed.rotation, node.packed.scale);
+ skeleton->set_bone_rest(current_id, transform);
+ skeleton->set_bone_pose(current_id, transform);
+
+ current_id += 1;
+ }
+
+ return skeleton;
+ }
+
+ /*
+ ====================================
+ Material helper functions
+
+ TODO: wasn't there an alpha mode (alpha clip, ...) needed
+ to fix the textures
+
+ ====================================
+ */
+
+ struct material_mapping {
+ //-1 means unused
+ Ref godot_material;
+ int32_t diffuse_texture_index = -1;
+ int32_t specular_texture_index = -1;
+ int32_t scroll_index = -1;
+ };
+
+ static Error setup_flag_shader() {
+ Error result = OK;
+ GameSingleton const* game_singleton = GameSingleton::get_singleton();
+ ERR_FAIL_NULL_V(game_singleton, {});
+
+ static const StringName Param_flag_dimensions = "flag_dims";
+ static const StringName Param_flag_texture_sheet = "texture_flag_sheet_diffuse";
+ static const Ref flag_shader = ResourcePreloader().get_resource("res://src/Game/Model/flag_mat.tres");
+
+ flag_shader->set_shader_parameter(Param_flag_dimensions, game_singleton->get_flag_dims());
+ flag_shader->set_shader_parameter(Param_flag_texture_sheet, game_singleton->get_flag_sheet_texture());
+ return result;
+ }
+
+ static std::vector build_materials(std::vector const& materials) {
+
+ static const StringName Textures_path = "gfx/anims/%s.dds";
+
+ // Parameters for the default model shader
+ static const StringName Param_texture_diffuse = "texture_diffuse";
+ //red channel is specular, green and blue are nation colours
+ static const StringName Param_texture_nation_colors_mask = "texture_nation_colors_mask";
+
+ // Scrolling textures (smoke, tank tracks)
+ static const StringName Param_Scroll_texture_diffuse = "scroll_texture_diffuse";
+ static const StringName Param_Scroll_factor = "scroll_factor";
+ static Dictionary SCROLLING_MATERIAL_FACTORS;
+ SCROLLING_MATERIAL_FACTORS["TexAnim"] = 2.5;
+ SCROLLING_MATERIAL_FACTORS["Smoke"] = 0.3;
+
+ static PackedStringArray Scrolling_textures_diffuse;
+ static PackedStringArray unit_textures_diffuse;
+ static PackedStringArray unit_textures_specular;
+
+ // Flag textures
+
+ static const StringName Param_texture_normal = "texture_normal";
+
+ //General
+ static const uint32_t MAX_UNIT_TEXTURES = 32;
+
+ static const StringName Texture_skip_nospec = "nospec";
+ static const StringName Texture_skip_flag = "unionjacksquare";
+ static const StringName Texture_skip_diff = "test256texture";
+
+ ResourceLoader* loader = ResourceLoader::get_singleton();
+
+ static const Ref unit_shader = loader->load("res://src/Game/Model/unit_colours_mat.tres");
+ static const Ref scrolling_shader = loader->load("res://src/Game/Model/scrolling_mat.tres");
+ static const Ref flag_shader = loader->load("res://src/Game/Model/flag_mat.tres");
+
+
+ std::vector mappings;
+
+ AssetManager* asset_manager = AssetManager::get_singleton();
+ ERR_FAIL_NULL_V(asset_manager, {});
+
+ // *** Collect the textures ***
+
+ for(material_definition_t const& mat : materials) {
+ String diffuse_name;
+ String specular_name;
+ String normal_name;
+
+ for(material_layer_t const& layer : mat.layers) {
+ if (layer.texture == Texture_skip_diff || layer.texture == Texture_skip_flag || layer.texture == Texture_skip_nospec) {
+ continue;
+ }
+ //Get the texture names
+ switch(layer.packed.map_type) {
+ case MAP_TYPE::DIFFUSE:
+ if (diffuse_name.is_empty()) {
+ diffuse_name = layer.texture;
+ }
+ else {
+ Logger::error("Multiple diffuse layers in material: ", Utilities::godot_to_std_string(diffuse_name), " and ", Utilities::godot_to_std_string(layer.texture));
+ }
+ break;
+ case MAP_TYPE::SPECULAR:
+ if (specular_name.is_empty()) {
+ specular_name = layer.texture;
+ }
+ else {
+ Logger::error("Multiple specular layers in material: ", Utilities::godot_to_std_string(specular_name), " and ", Utilities::godot_to_std_string(layer.texture));
+ }
+ break;
+ case MAP_TYPE::NORMAL:
+ if (normal_name.is_empty()) {
+ normal_name = layer.texture;
+ }
+ else {
+ Logger::error("Multiple normal layers in material: ", Utilities::godot_to_std_string(normal_name), " and ", Utilities::godot_to_std_string(layer.texture));
+ }
+ break;
+ case MAP_TYPE::SHADOW:
+ //Layer type 4 (Fort_Early_Shadow.dds) is used by Fort_Early.xac
+ // it corresponds to the blob shadow?
+ // for now, skip
+ break;
+ default:
+ //Logger::warning("Unknown layer type: ", layer.packed.map_type);
+ UtilityFunctions::print(
+ vformat("Unknown layer type: %x", layer.packed.map_type)
+ );
+ break;
+ }
+ }
+
+ Ref diffuse_texture;
+ Ref specular_texture;
+ Ref normal_texture;
+ material_mapping mapping;
+
+ if (!diffuse_name.is_empty()) {
+ diffuse_texture = asset_manager->get_texture(vformat(Textures_path, diffuse_name));
+ }
+ if (!specular_name.is_empty()) {
+ specular_texture = asset_manager->get_texture(vformat(Textures_path, specular_name));
+ }
+ if (!normal_name.is_empty()) {
+ normal_texture = asset_manager->get_texture(vformat(Textures_path, normal_name));
+ }
+
+ // *** Determine the correct material to use, and set it up ***
+
+ //flag TODO: perhaps flag should be determined by hard-coding so that other models can have a normal texture?
+ if (!normal_texture.is_null() && diffuse_texture.is_null()) {
+ //There shouldn't be a specular texture
+ flag_shader->set_shader_parameter(Param_texture_normal, normal_texture);
+ mapping.godot_material = flag_shader;
+ }
+ //Scrolling texture
+ else if (!diffuse_texture.is_null() && SCROLLING_MATERIAL_FACTORS.has(mat.name)) {
+ int32_t scroll_textures_index_diffuse = Scrolling_textures_diffuse.find(diffuse_name);
+ if (scroll_textures_index_diffuse < 0) {
+ Scrolling_textures_diffuse.push_back(diffuse_name);
+ //err check
+ TypedArray scroll_diffuse_textures = scrolling_shader->get_shader_parameter(Param_Scroll_texture_diffuse);
+ scroll_diffuse_textures.push_back(diffuse_texture);
+ scrolling_shader->set_shader_parameter(Param_Scroll_texture_diffuse, scroll_diffuse_textures);
+
+ PackedFloat32Array scroll_factors = scrolling_shader->get_shader_parameter(Param_Scroll_factor);
+
+ scroll_factors.push_back(SCROLLING_MATERIAL_FACTORS[mat.name]);
+ scrolling_shader->set_shader_parameter(Param_Scroll_factor, scroll_factors);
+ }
+ mapping.godot_material = scrolling_shader;
+ mapping.scroll_index = scroll_textures_index_diffuse;
+
+ }
+ //standard material (diffuse optionally with a specular/unit colours)
+ else {
+ int32_t textures_index_diffuse = unit_textures_diffuse.find(diffuse_name);
+ int32_t textures_index_specular = unit_textures_specular.find(specular_name);
+
+ if (textures_index_diffuse < 0 && !diffuse_name.is_empty()) {
+ unit_textures_diffuse.push_back(diffuse_name);
+ if (unit_textures_diffuse.size() >= MAX_UNIT_TEXTURES) {
+ Logger::error("Number of diffuse textures exceeded max supported by a shader!");
+ }
+ TypedArray diffuse_textures = unit_shader->get_shader_parameter(Param_texture_diffuse);
+ diffuse_textures.push_back(diffuse_texture);
+ textures_index_diffuse = diffuse_textures.size() - 1;
+ unit_shader->set_shader_parameter(Param_texture_diffuse, diffuse_textures);
+ }
+ //in a vic2 "specular" texture -> r=specular, g=secondary unit color, b=tertiary unit color
+ if (textures_index_specular < 0 && !specular_name.is_empty()) {
+ unit_textures_specular.push_back(specular_name);
+ if (unit_textures_specular.size() >= MAX_UNIT_TEXTURES) {
+ Logger::error("Number of specular textures exceeded max supported by a shader!");
+ }
+ TypedArray specular_textures = unit_shader->get_shader_parameter(Param_texture_nation_colors_mask);
+ //Logger::info(specular_textures[0].)
+ specular_textures.push_back(specular_texture);
+ textures_index_specular = specular_textures.size() - 1;
+ unit_shader->set_shader_parameter(Param_texture_nation_colors_mask, specular_textures);
+ }
+
+ mapping.godot_material = unit_shader;
+ mapping.diffuse_texture_index = textures_index_diffuse;
+ mapping.specular_texture_index = textures_index_specular;
+ }
+
+ mappings.push_back(mapping);
+ }
+
+
+ return mappings;
+ }
+
+ /*
+ ====================================
+ Mesh helper functions
+
+ ====================================
+ */
+ //
+
+ //TODO: before, we used surface tool to generate the mesh
+ // this time around, it might be faster/easier to use the array tools
+
+ //TODO: last time we loaded collision mesh chunks, since we can likely get
+ // away with a radius around a unit's position, why not skip them
+ //TODO: last time, we hardcoded lists of other chunk names not to load
+ // why not try this:
+
+ // if skeleton but mesh chunk has no corresponding skinning chunk:
+ // skip
+ // if collision mesh chunk
+ // skip
+
+ // alternatively, see if a mesh name corresponds to a name in the hierarchy
+ // if yes, keep the mesh
+
+ //this should get rid of mesh chunks that aren't attached to anything
+ // Is this actually a case for reinterpret_cast?
+ template
+ static void convert_data(std::vector& target, std::vector const& source) {
+ target.resize(source.size()/sizeof(T));
+ std::memcpy(target.data(),source.data(),source.size());
+ }
+
+ static MeshInstance3D* build_mesh(mesh_t const& mesh_chunk, skinning_t* skin, std::vector const& materials) {
+ static const uint32_t EXTRA_CULL_MARGIN = 2;
+
+ MeshInstance3D* mesh_inst = memnew(MeshInstance3D);
+ mesh_inst->set_extra_cull_margin(EXTRA_CULL_MARGIN);
+
+ Ref mesh = Ref();
+ mesh.instantiate();
+
+ std::vector verts = {};
+ std::vector normals = {};
+ std::vector tangents = {};
+ int uvs_read = 0;
+ std::vector uv1 = {};
+ std::vector uv2 = {};
+ std::vector influence_range_indices = {};
+
+ for(vertices_attribute_t const& attribute : mesh_chunk.vertices_attributes) {
+ switch(attribute.packed.type) {
+ case ATTRIBUTE::POSITION:
+ convert_data(verts, attribute.data);
+ break;
+ case ATTRIBUTE::NORMAL:
+ convert_data(normals, attribute.data);
+ break;
+ case ATTRIBUTE::TANGENT:
+ convert_data(tangents, attribute.data);
+ break;
+ case ATTRIBUTE::UV:
+ if (uvs_read == 0) {
+ convert_data(uv1, attribute.data);
+ }
+ else if (uvs_read == 1) {
+ convert_data(uv2, attribute.data);
+ }
+ uvs_read += 1;
+ break;
+ case ATTRIBUTE::INFLUENCE_RANGE:
+ if (skin == nullptr) {
+ Logger::warning("Mesh chunk has influence attribute but no corresponding skinning chunk");
+ break;
+ }
+ convert_data(influence_range_indices, attribute.data);
+ break;
+ //for now, ignore color data
+ case ATTRIBUTE::COL_32:
+ case ATTRIBUTE::COL_128:
+ default:
+ break;
+ }
+ }
+
+ //if (skin != nullptr) Logger::info("skin infdat size: ", skin->influence_data.size(), " ", skin->packed.influences_count);
+ //if (skin != nullptr) Logger::info("skin infran size: ", skin->influence_ranges.size(), " ", mesh_chunk.packed.influence_ranges_count);
+ //else Logger::info("no skinning chunk");
+
+
+ uint32_t vert_total = 0;
+ static const StringName key_diffuse = "tex_index_diffuse";
+ static const StringName key_specular = "tex_index_specular";
+ static const StringName key_scroll = "scroll_tex_index_diffuse";
+
+ //for now we treat a submesh as a godot mesh surface
+ for(submesh_t submesh : mesh_chunk.submeshes) {
+ Array arrs; //surface vertex data arrays
+ ERR_FAIL_COND_V(arrs.resize(Mesh::ARRAY_MAX) != OK, {});
+
+ PackedVector3Array verts_submesh = {};
+ PackedVector3Array normals_submesh = {};
+ PackedFloat32Array tangents_submesh = {};
+ PackedVector2Array uv1_submesh = {};
+ PackedVector2Array uv2_submesh = {};
+ PackedInt32Array bone_ids = {};
+ PackedFloat32Array weights = {};
+ //godot uses a fixed 4 bones influencing a vertex, so size the array accordingly
+ bone_ids.resize(submesh.relative_indices.size()*4);
+ bone_ids.fill(0);
+
+ weights.resize(submesh.relative_indices.size()*4);
+ weights.fill(0);
+
+ //1 vertex is in the surface per relative index
+ for(uint32_t rel_ind : submesh.relative_indices) {
+ uint32_t index = rel_ind + vert_total;
+
+ verts_submesh.push_back(vec3d_to_godot(verts[index], true));
+ if (normals.size() > index) {
+ normals_submesh.push_back( vec3d_to_godot(normals[index],true));
+ }
+
+ //TODO: verify if we need to invert the x for each tangent
+ if (tangents.size() > index) {
+ vec4d_t const& tan = tangents[index];
+ tangents_submesh.push_back(-tan.x);
+ tangents_submesh.push_back(tan.y);
+ tangents_submesh.push_back(tan.z);
+ tangents_submesh.push_back(tan.w);
+ }
+
+ if (uv1.size() > index) {
+ uv1_submesh.push_back(vec2d_to_godot(uv1[index]));
+ }
+ if (uv2.size() > index) {
+ uv2_submesh.push_back(vec2d_to_godot(uv2[index]));
+ }
+
+ if (skin != nullptr && influence_range_indices.size() > index) {
+ uint32_t const& influence_range_ind = influence_range_indices[index];
+ uint32_t const& influences_ind = skin->influence_ranges[influence_range_ind].first_influence_index;
+ uint32_t influences_count = Math::min(skin->influence_ranges[influence_range_ind].influences_count, 4);
+
+ for(int i = 0; i < influences_count; i++) {
+ bone_ids[rel_ind + i] = skin->influence_data[influences_ind + i].bone_id;
+ weights[rel_ind + i] = skin->influence_data[influences_ind + i].weight;
+ }
+ }
+ }
+
+ if (!verts_submesh.is_empty()) {
+ arrs[Mesh::ARRAY_VERTEX] = std::move(verts_submesh);
+ }
+ if (!normals_submesh.is_empty()) {
+ arrs[Mesh::ARRAY_NORMAL] = std::move(normals_submesh);
+ }
+ if (!tangents_submesh.is_empty()) {
+ arrs[Mesh::ARRAY_TANGENT] = std::move(tangents_submesh);
+ }
+ if (!uv1_submesh.is_empty()) {
+ arrs[Mesh::ARRAY_TEX_UV] = std::move(uv1_submesh);
+ }
+ if (!uv2_submesh.is_empty()) {
+ arrs[Mesh::ARRAY_TEX_UV2] = std::move(uv2_submesh);
+ }
+ if (skin != nullptr && influence_range_indices.size() > 0) {
+ //arrs[Mesh::ARRAY_BONES] = std::move(bone_ids);
+ //arrs[Mesh::ARRAY_WEIGHTS] = std::move(weights);
+ }
+ mesh->add_surface_from_arrays(Mesh::PRIMITIVE_TRIANGLES, arrs);
+
+ //setup materials for the surface
+ material_mapping const& material_mapping = materials[submesh.packed.material_id];
+ mesh->surface_set_material(mesh->get_surface_count()-1, material_mapping.godot_material);
+ if (material_mapping.diffuse_texture_index != -1) {
+ mesh_inst->set_instance_shader_parameter(key_diffuse, material_mapping.diffuse_texture_index);
+ }
+ if (material_mapping.specular_texture_index != -1) {
+ mesh_inst->set_instance_shader_parameter(key_specular, material_mapping.diffuse_texture_index);
+ }
+ if (material_mapping.scroll_index != -1) {
+ mesh_inst->set_instance_shader_parameter(key_scroll, material_mapping.diffuse_texture_index);
+ }
+
+ vert_total += submesh.relative_indices.size();
+ }
+
+ mesh_inst->set_mesh(mesh);
+
+ return mesh_inst;
+ }
+
+ /*
+ ====================================
+ Main loading function
+
+ ====================================
+ */
+ static Node3D* _load_xac_model(Ref const& file) {
+ bool res = read_xac_header(file);
+
+ Node3D* base = memnew(Node3D);
+
+ if (!res) {
+ return base;
+ }
+
+ xac_metadata_v2_t metadata;
+
+ bool hierarchy_read = false;
+ node_hierarchy_t hierarchy;
+ std::vector nodes; //other way of making a bone hierarchy
+
+ material_totals_t material_totals = {};
+ std::vector materials;
+
+ std::vector meshes;
+ std::vector skinnings;
+
+ bool log_all = false;
+
+ //0xA unknown (ex. Spanish_Helmet1.xac), only 2 bytes long...
+ //0x6 unknown (ex. ...)
+ //0x8 junk data (ie. old versions of the file duplicated in append mode)
+ // (ex. Port_Empty.xac)
+ //0xC morphtargets (not used in vic2)
+
+ while (!file->eof_reached()) {
+ chunk_header_t header = {};
+ if (!read_chunk_header(file, header)) {
+ break;
+ }
+
+ if (log_all) {
+ UtilityFunctions::print(
+ vformat("XAC chunk: type = %x, length = %x, version = %d at %x", header.type, header.length, header.version, file->get_position())
+ );
+ }
+
+ if (header.type == 0x7 && header.version == 2) {
+ read_xac_metadata(file, metadata);
+ }
+ else if (header.type == 0xB && header.version == 1) {
+ hierarchy_read = read_node_hierarchy(file, hierarchy);
+ }
+ else if (header.type == 0x0 && header.version == 3) {
+ node_chunk_t node;
+ read_node_chunk(file, node);
+ nodes.push_back(node);
+ }
+ else if (header.type == 0x1 && header.version == 1) {
+ mesh_t mesh;
+ read_mesh(file, mesh);
+ meshes.push_back(mesh);
+ }
+ else if (header.type == 0x2) {
+ skinning_t skin;
+ read_skinning(file, skin, meshes, header.version);
+ skinnings.push_back(skin);
+ }
+ else if (header.type == 0x3) {
+ material_definition_t mat;
+ read_material_definition(file, mat, header.version);
+ materials.push_back(mat);
+ }
+ else if (header.type == 0x4 && header.version == 2) {
+ material_layer_t layer;
+ read_layer(file, layer);
+ if (layer.packed.material_id < materials.size()) {
+ materials[layer.packed.material_id].layers.push_back(layer);
+ }
+ else{
+ Logger::error("No material of id ", layer.packed.material_id, " to attach layer to");
+ }
+ }
+ else if (header.type == 0xD && header.version == 1) {
+ read_material_totals(file, material_totals);
+ }
+ else if (header.type == 0x8 || header.type == 0xA) { //skip junk data and whatever 0xA is...
+ if (header.length + file->get_position() < file->get_length()) {
+ file->get_buffer(header.length);
+ }
+ else {
+ res = false;
+ break;
+ }
+ }
+ else {
+ UtilityFunctions::print(
+ vformat("Unsupported XAC chunk: type = %x, length = %x, version = %d at %x", header.type, header.length, header.version, file->get_position())
+ );
+ log_all = true;
+ // Skip unsupported chunks by using get_buffer, make sure this doesn't break anything, since chunk length can be wrong
+ if (header.length + file->get_position() < file->get_length()) {
+ file->get_buffer(header.length);
+ }
+ else {
+ res = false;
+ break;
+ }
+ }
+ }
+
+ //Setup skeleton
+ Skeleton3D* skeleton = nullptr;
+ if (hierarchy_read) {
+ skeleton = build_armature_hierarchy(hierarchy);
+ base->add_child(skeleton);
+ }
+ else if (!nodes.empty()) {
+ skeleton = build_armature_nodes(nodes);
+ base->add_child(skeleton);
+ }
+
+ //Setup materials
+ std::vector const& material_mappings = build_materials(materials);
+
+ //setup mesh
+ for(mesh_t const& mesh : meshes) {
+ if (mesh.packed.is_collision_mesh) { continue; } //we'll use our own collision meshes where needed
+
+ skinning_t* mesh_skin = nullptr;
+ //for(skinning skin : skinnings) {
+ for(int i=0; i < skinnings.size(); i++) {
+ skinning_t& skin = skinnings[i];
+ if (skin.node_id == mesh.packed.node_id && skin.packed.is_for_collision == mesh.packed.is_collision_mesh) {
+ mesh_skin = &skin;
+ break;
+ }
+ }
+
+ if (skeleton != nullptr && mesh_skin == nullptr) { continue; }
+
+ MeshInstance3D* mesh_inst = build_mesh(mesh, mesh_skin, material_mappings);
+ base->add_child(mesh_inst);
+ mesh_inst->set_owner(base);
+
+ if (skeleton != nullptr) {
+ mesh_inst->set_skeleton_path(mesh_inst->get_path_to(skeleton));
+ int32_t node_id = mesh.packed.node_id;
+ if (hierarchy_read && node_id < hierarchy.node_data.size()) {
+ mesh_inst->set_name(hierarchy.node_data[node_id].name);
+ }
+ else if (!nodes.empty() && node_id < nodes.size()) {
+ mesh_inst->set_name(nodes[node_id].name);
+ }
+ }
+ }
+
+ return base;
+ }
+}
\ No newline at end of file
diff --git a/extension/src/openvic-extension/utility/XACUtilities.hpp b/extension/src/openvic-extension/utility/XACUtilities.hpp
new file mode 100644
index 00000000..e9d3b787
--- /dev/null
+++ b/extension/src/openvic-extension/utility/XACUtilities.hpp
@@ -0,0 +1,221 @@
+#pragma once
+#include
+#include
+#include
+//#include "openvic-simulation/utility/Logger.hpp"
+//#include "openvic-extension/utility/Utilities.hpp"
+#include
+#include
+
+namespace OpenVic {
+ #pragma pack(push)
+ #pragma pack(1)
+
+ struct chunk_header_t {
+ int32_t type;
+ int32_t length;
+ int32_t version;
+ };
+
+ struct vec2d_t { //not a real datatype in the files. Just using it for convenience
+ float x;
+ float y;
+ };
+
+ struct vec3d_t {
+ float x;
+ float y;
+ float z;
+ };
+
+ struct vec4d_t {
+ float x;
+ float y;
+ float z;
+ float w;
+ };
+
+ struct quat_v1_t {
+ float x;
+ float y;
+ float z;
+ float w;
+ };
+
+ struct quat_v2_t { // divide by 32767 to get proper quat
+ int16_t x;
+ int16_t y;
+ int16_t z;
+ int16_t w;
+ };
+
+ struct matrix44_t {
+ vec4d_t col1;
+ vec4d_t col2;
+ vec4d_t col3;
+ vec4d_t col4;
+ };
+
+ struct color_32_t {
+ int8_t r;
+ int8_t g;
+ int8_t b;
+ int8_t a;
+ };
+
+ struct color_128_t {
+ int32_t r;
+ int32_t g;
+ int32_t b;
+ int32_t a;
+ };
+
+ #pragma pack(pop)
+
+ using namespace godot;
+ using namespace OpenVic;
+
+ static bool read_string(Ref const& file, String& str, bool replace_chars = true, bool log = false) {
+ //string = uint32 len, char[len]
+ uint32_t length = file->get_32();
+ if (file->get_length() - file->get_position() < length) {
+ return false;
+ }
+
+ str = file->get_buffer(length).get_string_from_ascii();
+ if (log) {
+ UtilityFunctions::print(vformat("before %s", str));
+ }
+ if (replace_chars) {
+ str = str.replace(":", "_");
+ str = str.replace("\\", "_");
+ str = str.replace("/", "_");
+ }
+ if (log) {
+ UtilityFunctions::print(vformat("after %s", str));
+ }
+ return true;
+ }
+
+ template
+ static bool read_struct(Ref const& file, T& t) {
+ if (file->get_length() - file->get_position() < sizeof(T)) {
+ return false;
+ }
+ bool res = file->get_buffer(reinterpret_cast(&t), sizeof(t)) == sizeof(t);
+ return res;
+ }
+
+ //Warning: works on the assumption of it being a packed struct being loaded into the array
+ template
+ static bool read_struct_array(Ref const& file, std::vector& t, uint32_t size) {
+ if (file->get_length() - file->get_position() < size*sizeof(T)) {
+ return false;
+ }
+ t.resize(size*sizeof(T));
+ bool res = file->get_buffer(reinterpret_cast(t.data()), sizeof(T)*size) == sizeof(t);
+ return res;
+ }
+
+ static bool read_chunk_header(Ref const& file, chunk_header_t& header) {
+ //ERR_FAIL_COND_V(!read_struct(file, header), false);
+ bool res = read_struct(file, header);
+
+ //UtilityFunctions::print(
+ // vformat("XAC/XSM chunk: type = %x, length = %x, version = %d, successful? %s", header.type, header.length, header.version, res)
+ //);
+
+ return res;
+ }
+
+ static Vector2 vec2d_to_godot(vec2d_t const& vec2_in) {
+ Vector2 vec2_out = {
+ vec2_in.x,
+ vec2_in.y
+ };
+ return vec2_out;
+ }
+
+ static Vector3 vec3d_to_godot(vec3d_t const& vec3_in, bool is_position = false) {
+ Vector3 vec3_out = {
+ vec3_in.x,
+ vec3_in.y,
+ vec3_in.z
+ };
+ if (is_position) {
+ vec3_out.x *= -1;
+ }
+ return vec3_out;
+ }
+
+ static Vector4 vec4d_to_godot(vec4d_t const& vec4_in) {
+ Vector4 vec4_out = {
+ vec4_in.x,
+ vec4_in.y,
+ vec4_in.z,
+ vec4_in.w
+ };
+ return vec4_out;
+ }
+
+ static Quaternion quat_v1_to_godot(quat_v1_t const& quat_in) {
+ Quaternion quat_out = Quaternion(
+ quat_in.x,
+ -quat_in.y,
+ -quat_in.z,
+ quat_in.w
+ );
+ if (!quat_out.is_normalized()) {
+ quat_out = Quaternion();
+ }
+ return quat_out;
+ }
+
+ static Quaternion quat_v2_to_godot(quat_v2_t const& quat_in) {
+ static const float scale = 32767;
+ Quaternion quat_out = Quaternion(
+ static_cast(quat_in.x) / scale,
+ static_cast(-quat_in.y) / scale,
+ static_cast(-quat_in.z) / scale,
+ static_cast(quat_in.w) / scale
+ );
+ if (!quat_out.is_normalized()) {
+ quat_out = Quaternion();
+ }
+
+ return quat_out;
+ }
+
+ static Color color_32_to_godot(color_32_t const& color_in) {
+ static const float scale = 256;
+ Color color_out = {
+ (float)color_in.r / scale,
+ (float)color_in.g / scale,
+ (float)color_in.b / scale,
+ (float)color_in.a / scale
+ };
+ return color_out;
+ }
+
+ //TODO: verify this conversion is correct >> don't think it is
+ static Color color_128_to_godot(color_128_t const& color_in) {
+ static const double scale = 2147483647;
+ Color color_out = {
+ static_cast(color_in.r / scale),
+ static_cast(color_in.g / scale),
+ static_cast(color_in.b / scale),
+ static_cast(color_in.a / scale)
+ };
+ return color_out;
+ }
+
+ static Color vec4d_to_godot_color(vec4d_t const& color_in) {
+ Color color_out = {
+ color_in.x,
+ color_in.y,
+ color_in.z,
+ color_in.w
+ };
+ return color_out;
+ }
+}
\ No newline at end of file
diff --git a/extension/src/openvic-extension/utility/XSMLoader.cpp b/extension/src/openvic-extension/utility/XSMLoader.cpp
new file mode 100644
index 00000000..e69de29b
diff --git a/extension/src/openvic-extension/utility/XSMLoader.hpp b/extension/src/openvic-extension/utility/XSMLoader.hpp
new file mode 100644
index 00000000..8a719e23
--- /dev/null
+++ b/extension/src/openvic-extension/utility/XSMLoader.hpp
@@ -0,0 +1,428 @@
+#pragma once
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include "XACUtilities.hpp"
+
+using namespace godot;
+using namespace OpenVic;
+
+static constexpr uint32_t XSM_FORMAT_SPECIFIER = ' MSX'; /* Order reversed due to little endian */
+static constexpr uint8_t XSM_VERSION_MAJOR = 1, XSM_VERSION_MINOR = 0;
+
+// Pack structs of data that can be read directly
+
+#pragma pack(push)
+#pragma pack(1)
+
+struct xsm_header_t {
+ uint32_t format_identifier;
+ uint8_t version_major;
+ uint8_t version_minor;
+ uint8_t big_endian;
+ uint8_t pad;
+};
+
+//v2 of the header adds these 2 properties at the start
+struct xsm_metadata_v2_pack_extra_t {
+ float unused;
+ float max_acceptable_error;
+};
+
+struct xsm_metadata_pack_t {
+ int32_t fps;
+ uint8_t exporter_version_major; //these 3 properties are uncertain
+ uint8_t exporter_version_minor;
+ uint16_t pad;
+};
+
+struct position_key_t {
+ vec3d_t position;
+ float time;
+};
+
+struct rotation_key_v2_t {
+ quat_v2_t rotation;
+ float time;
+};
+
+struct rotation_key_v1_t {
+ quat_v1_t rotation;
+ float time;
+};
+
+struct scale_key_t {
+ vec3d_t scale;
+ float time;
+};
+
+//the quaternions come at the start of the skeletal submotion
+struct submotion_rot_v2_pack_t {
+ quat_v2_t pose_rotation;
+ quat_v2_t bind_pose_rotation;
+ quat_v2_t pose_scale_rotation;
+ quat_v2_t bind_pose_scale_rotation;
+};
+
+struct submotion_rot_v1_pack_t {
+ quat_v1_t pose_rotation;
+ quat_v1_t bind_pose_rotation;
+ quat_v1_t pose_scale_rotation;
+ quat_v1_t bind_pose_scale_rotation;
+};
+
+struct skeletal_submotion_pack_t {
+ vec3d_t pose_position;
+ vec3d_t pose_scale;
+ vec3d_t bind_pose_position;
+ vec3d_t bind_pose_scale;
+ int32_t position_key_count;
+ int32_t rotation_key_count;
+ int32_t scale_key_count;
+ int32_t scale_rotation_key_count;
+ float max_error;
+};
+
+struct node_submotion_pack_t {
+ vec3d_t pose_position;
+ vec3d_t pose_scale;
+ vec3d_t bind_pose_position;
+ vec3d_t bind_pose_scale;
+ int32_t position_key_count;
+ int32_t rotation_key_count;
+ int32_t scale_key_count;
+ int32_t scale_rotation_key_count;
+};
+
+#pragma pack(pop)
+
+struct xsm_metadata_t {
+ bool has_v2_data = false;
+ xsm_metadata_v2_pack_extra_t v2_data = {};
+ xsm_metadata_pack_t packed = {};
+ String source_app;
+ String original_file_name;
+ String export_date;
+ String motion_name;
+};
+
+struct rotation_key_t {
+ Quaternion rotation;
+ float time;
+};
+
+struct skeletal_submotion_t {
+ Quaternion pose_rotation;
+ Quaternion bind_pose_rotation;
+ Quaternion pose_scale_rotation;
+ Quaternion bind_pose_scale_rotation;
+ skeletal_submotion_pack_t packed = {};
+ String node_name;
+ std::vector position_keys;
+ std::vector rotation_keys;
+ std::vector scale_keys;
+ std::vector scale_rotation_keys;
+};
+
+struct bone_animation_t {
+ int32_t submotion_count = 0;
+ std::vector submotions;
+};
+
+/*
+0xC8, v1 chunk documentation: (Let's call it node_submotion)
+ 4*8x 32 bit sections = 32
+ 4x quat32 = 16
+ 4x vec3 = 12
+ 4x keycount 04
+ 32
+ string node_name
+ data...
+
+ so this chunk is the same as a skeletal submotion v1
+ but without the fMaxError
+*/
+
+struct node_submotion_t {
+ Quaternion pose_rotation;
+ Quaternion bind_pose_rotation;
+ Quaternion pose_scale_rotation;
+ Quaternion bind_pose_scale_rotation;
+ node_submotion_pack_t packed = {};
+ String node_name;
+ std::vector position_keys;
+ std::vector rotation_keys;
+ std::vector scale_keys;
+ std::vector scale_rotation_keys;
+};
+
+static bool read_xsm_header(Ref const& file) {
+ xsm_header_t header = {};
+ ERR_FAIL_COND_V(!read_struct(file, header), false);
+
+ //Logger::info("XSM file version: ", header.version_major, ".", header.version_minor," big endian?: ",header.big_endian);
+
+ ERR_FAIL_COND_V_MSG(
+ header.format_identifier != XSM_FORMAT_SPECIFIER, false, vformat(
+ "Invalid XSM format identifier: %x (should be %x)", header.format_identifier, XSM_FORMAT_SPECIFIER
+ )
+ );
+
+ ERR_FAIL_COND_V_MSG(
+ header.version_major != XSM_VERSION_MAJOR || header.version_minor != XSM_VERSION_MINOR, false, vformat(
+ "Invalid XSM version: %d.%d (should be %d.%d)",
+ header.version_major, header.version_minor, XSM_VERSION_MAJOR, XSM_VERSION_MINOR
+ )
+ );
+
+ ERR_FAIL_COND_V_MSG(
+ header.big_endian != 0, false, "Invalid XSM endianness: big endian (only little endian is supported)"
+ );
+
+ return true;
+}
+
+static bool read_xsm_metadata(Ref const& file, xsm_metadata_t& metadata, int32_t version) {
+ bool res = true;
+ if (version != 1) {
+ res &= read_struct(file, metadata.v2_data);
+ metadata.has_v2_data = true;
+ }
+ res &= read_struct(file, metadata.packed);
+ res &= read_string(file, metadata.source_app);
+ res &= read_string(file, metadata.original_file_name);
+ res &= read_string(file, metadata.export_date);
+ res &= read_string(file, metadata.motion_name);
+
+ return res;
+}
+
+static bool read_rot_keys(Ref const& file, std::vector* keys_out, int32_t count, int32_t version) {
+ bool res = true;
+
+ if (version == 1) {
+ std::vector rot_keys_v1;
+ res &= read_struct_array(file,rot_keys_v1,count);
+ for(rotation_key_v1_t key : rot_keys_v1) {
+ keys_out->push_back({quat_v1_to_godot(key.rotation), key.time});
+ }
+ }
+ else {
+ std::vector rot_keys_v2;
+ res &= read_struct_array(file,rot_keys_v2,count);
+ for(rotation_key_v2_t key : rot_keys_v2) {
+ keys_out->push_back({quat_v2_to_godot(key.rotation), key.time});
+ }
+ }
+
+ return res;
+}
+
+static bool read_skeletal_submotion(Ref const& file, skeletal_submotion_t& submotion, int32_t version) {
+ bool res = true;
+ if (version == 1) { //float component quats (v1)
+ submotion_rot_v1_pack_t rot_comps = {};
+ res &= read_struct(file,rot_comps);
+ submotion.pose_rotation = quat_v1_to_godot(rot_comps.pose_rotation);
+ submotion.bind_pose_rotation = quat_v1_to_godot(rot_comps.bind_pose_rotation);
+ submotion.pose_scale_rotation = quat_v1_to_godot(rot_comps.pose_scale_rotation);
+ submotion.bind_pose_scale_rotation = quat_v1_to_godot(rot_comps.bind_pose_scale_rotation);
+ }
+ else { //int16 component quats (v2)
+ submotion_rot_v2_pack_t rot_comps = {};
+ res &= read_struct(file,rot_comps);
+ submotion.pose_rotation = quat_v2_to_godot(rot_comps.pose_rotation);
+ submotion.bind_pose_rotation = quat_v2_to_godot(rot_comps.bind_pose_rotation);
+ submotion.pose_scale_rotation = quat_v2_to_godot(rot_comps.pose_scale_rotation);
+ submotion.bind_pose_scale_rotation = quat_v2_to_godot(rot_comps.bind_pose_scale_rotation);
+ }
+
+ ERR_FAIL_COND_V(!read_struct(file, submotion.packed), false);
+ res &= read_string(file, submotion.node_name,true);
+ res &= read_struct_array(file,submotion.position_keys,submotion.packed.position_key_count);
+ res &= read_rot_keys(file,&submotion.rotation_keys,submotion.packed.rotation_key_count, version);
+ res &= read_struct_array(file,submotion.scale_keys,submotion.packed.scale_key_count);
+ res &= read_rot_keys(file,&submotion.scale_rotation_keys,submotion.packed.scale_rotation_key_count, version);
+
+ return res;
+}
+
+static bool read_xsm_bone_animation(Ref const& file, bone_animation_t& bone_animation, int32_t version) {
+ bone_animation.submotion_count = file->get_32();
+ bool ret = true;
+
+ for(int i=0; i const& file, std::vector* submotions) {
+ bool res = true;
+ node_submotion_t submotion;
+
+ submotion_rot_v1_pack_t rot_comps = {};
+ res &= read_struct(file,rot_comps);
+ submotion.pose_rotation = quat_v1_to_godot(rot_comps.pose_rotation);
+ submotion.bind_pose_rotation = quat_v1_to_godot(rot_comps.bind_pose_rotation);
+ submotion.pose_scale_rotation = quat_v1_to_godot(rot_comps.pose_scale_rotation);
+ submotion.bind_pose_scale_rotation = quat_v1_to_godot(rot_comps.bind_pose_scale_rotation);
+
+ res &= read_struct(file, submotion.packed);
+ res &= read_string(file, submotion.node_name);
+ res &= read_struct_array(file,submotion.position_keys,submotion.packed.position_key_count);
+ res &= read_rot_keys(file,&submotion.rotation_keys,submotion.packed.rotation_key_count, 1);
+ res &= read_struct_array(file,submotion.scale_keys,submotion.packed.scale_key_count);
+ res &= read_rot_keys(file,&submotion.scale_rotation_keys,submotion.packed.scale_rotation_key_count, 1);
+ submotions->push_back(submotion);
+
+ return res;
+}
+
+//returns highest time in the submotion
+template
+float add_submotion(Ref anim, T submotion) {
+ static const StringName SKELETON_NODE_PATH = "./skeleton:%s";
+ float max_time = 0;
+
+ //NOTE: godot uses ':' to specify properties, so we replaced such characters with '_' when we read them in
+ String skeleton_path = vformat(SKELETON_NODE_PATH, submotion.node_name);
+
+ int pos_id = anim->add_track(Animation::TYPE_POSITION_3D);
+ int rot_id = anim->add_track(Animation::TYPE_ROTATION_3D);
+ int scale_id = anim->add_track(Animation::TYPE_SCALE_3D);
+
+ anim->track_set_path(pos_id, skeleton_path);
+ anim->track_set_path(rot_id, skeleton_path);
+ anim->track_set_path(scale_id, skeleton_path);
+
+ for(position_key_t key : submotion.position_keys) {
+ anim->position_track_insert_key(pos_id, key.time, vec3d_to_godot(key.position, true));
+ if (key.time > max_time) {
+ max_time = key.time;
+ }
+ }
+
+ for(rotation_key_t key : submotion.rotation_keys) {
+ anim->rotation_track_insert_key(rot_id, key.time, key.rotation);
+ if (key.time > max_time) {
+ max_time = key.time;
+ }
+ }
+
+ //not needed for vic2 animations, but we can still support it
+ for(scale_key_t key : submotion.scale_keys) {
+ anim->scale_track_insert_key(scale_id, key.time, vec3d_to_godot(key.scale));
+ if (key.time > max_time) {
+ max_time = key.time;
+ }
+ }
+
+ // TODO: SCALEROTATION
+ for(rotation_key_t key : submotion.scale_rotation_keys) {
+ if (key.time > max_time) {
+ max_time = key.time;
+ }
+ }
+
+ //TODO: why is this hack needed to make the animations work?
+ if (anim->track_find_key(pos_id, 0) != -1) {
+ anim->track_remove_key_at_time(pos_id, 0);
+ }
+ if (anim->track_find_key(rot_id, 0) != -1) {
+ anim->track_remove_key_at_time(rot_id, 0);
+ }
+ if (anim->track_find_key(scale_id, 0) != -1) {
+ anim->track_remove_key_at_time(scale_id, 0);
+ }
+ //TODO: if you pay close attention, there's still a small jump along the loop point
+ // though this is visible in vic2 aswell
+
+ //add the "default" positions/rotation/... as keys
+ anim->position_track_insert_key(pos_id, 0, vec3d_to_godot(submotion.packed.pose_position, true));
+ anim->rotation_track_insert_key(rot_id, 0, submotion.pose_rotation);
+ anim->scale_track_insert_key(scale_id, 0, vec3d_to_godot(submotion.packed.pose_scale));
+
+
+ return max_time;
+}
+
+//a variable string or stringname with %s makes godot fail silently if placed outside a function
+
+static Ref _load_xsm_animation(Ref const& file) {
+
+ read_xsm_header(file);
+
+ xsm_metadata_t metadata;
+ bone_animation_t bone_anim;
+ std::vector node_submotions;
+
+ bool res = true;
+
+ while(!file->eof_reached()) {
+ chunk_header_t header = {};
+ if (!read_chunk_header(file, header)) { break; }
+ if (header.type == 0xC9) {
+ read_xsm_metadata(file, metadata, header.version); //in zulu_moving.xsm, this is v1
+ }
+ else if (header.type == 0xC8) {
+ read_node_submotion(file,&node_submotions);
+ }
+ else if (header.type == 0xCA) {
+ read_xsm_bone_animation(file, bone_anim, header.version);
+ }
+ else {
+ UtilityFunctions::print(
+ vformat("Unsupported XSM chunk: file: %s, type = %x, length = %x, version = %d at %x", file->get_path(), header.type, header.length, header.version, file->get_position())
+ );
+
+ // Skip unsupported chunks
+ if (header.length + file->get_position() < file->get_length()) {
+ PackedByteArray buf = file->get_buffer(header.length);
+ UtilityFunctions::print(buf);
+ }
+ else {
+ res = false;
+ break;
+ }
+ }
+ }
+
+ float animation_length = 0.0;
+ Ref anim = Ref();
+ anim.instantiate();
+
+ anim->set_step(1.0/metadata.packed.fps);
+ anim->set_loop_mode(Animation::LOOP_LINEAR);
+
+ if (res == false) {
+ return anim; //exit early if reading the chunks in failed
+ }
+
+ for(skeletal_submotion_t submotion : bone_anim.submotions) {
+ float submotion_len = add_submotion(anim, submotion);
+ if (submotion_len > animation_length) {
+ animation_length = submotion_len;
+ }
+ }
+
+ for(node_submotion_t submotion : node_submotions) {
+ float submotion_len = add_submotion(anim, submotion);
+ if (submotion_len > animation_length) {
+ animation_length = submotion_len;
+ }
+ }
+
+ anim->set_length(animation_length);
+
+ return anim;
+}
\ No newline at end of file
diff --git a/game/src/Game/GameSession/ModelManager.gd b/game/src/Game/GameSession/ModelManager.gd
index de9de547..1c841d04 100644
--- a/game/src/Game/GameSession/ModelManager.gd
+++ b/game/src/Game/GameSession/ModelManager.gd
@@ -6,7 +6,7 @@ extends Node3D
const MODEL_SCALE : float = 1.0 / 256.0
func generate_units() -> void:
- XACLoader.setup_flag_shader()
+ XACLoader.setup_flag_shader() #TODO: replace with C++ call
for unit : Dictionary in ModelSingleton.get_units():
_generate_unit(unit)
@@ -104,26 +104,35 @@ func _generate_model(model_dict : Dictionary, culture : String = "", is_unit : b
or attachments_key in model_dict
)
- var model : Node3D = XACLoader.get_xac_model(model_dict[file_key], is_unit)
+ var model : Node3D
+ if !is_unit:
+ model = ModelSingleton.get_xac_model(model_dict[file_key])
+ #model = XACLoader.get_xac_model(model_dict[file_key], is_unit)
+ # print(file_key)
+ #ModelSingleton.get_xac_model(model_dict[file_key])
+ else:
+ model = XACLoader.get_xac_model(model_dict[file_key], is_unit)
+ #var model : Node3D = XACLoader.get_xac_model(model_dict[file_key], is_unit)
if not model:
return null
model.scale *= model_dict[scale_key]
-
+ #print("GENERATE MODEL")
if model is UnitModel:
# Animations
var idle_dict : Dictionary = model_dict.get(idle_key, {})
+ var move_dict : Dictionary = model_dict.get(move_key, {})
+ var attack_dict : Dictionary = model_dict.get(attack_key, {})
+
if idle_dict:
- model.idle_anim = XSMLoader.get_xsm_animation(idle_dict[animation_file_key])
+ model.idle_anim = ModelSingleton.get_xsm_animation(idle_dict[animation_file_key])
model.scroll_speed_idle = idle_dict[animation_time_key]
- var move_dict : Dictionary = model_dict.get(move_key, {})
if move_dict:
- model.move_anim = XSMLoader.get_xsm_animation(move_dict[animation_file_key])
+ model.move_anim = ModelSingleton.get_xsm_animation(move_dict[animation_file_key])
model.scroll_speed_move = move_dict[animation_time_key]
- var attack_dict : Dictionary = model_dict.get(attack_key, {})
if attack_dict:
- model.attack_anim = XSMLoader.get_xsm_animation(attack_dict[animation_file_key])
+ model.attack_anim = ModelSingleton.get_xsm_animation(attack_dict[animation_file_key])
model.scroll_speed_attack = attack_dict[animation_time_key]
# Attachments
@@ -133,7 +142,7 @@ func _generate_model(model_dict : Dictionary, culture : String = "", is_unit : b
attachment_model.free()
if culture:
- const gun_bone_name : String = "GunNode"
+ const gun_bone_name : StringName = &"GunNode"
if model.has_bone(gun_bone_name):
var gun_dict : Dictionary = ModelSingleton.get_cultural_gun_model(culture)
if gun_dict:
@@ -141,7 +150,7 @@ func _generate_model(model_dict : Dictionary, culture : String = "", is_unit : b
if gun_model and model.attach_model(gun_bone_name, gun_model) != OK:
gun_model.free()
- const helmet_bone_name : String = "HelmetNode"
+ const helmet_bone_name : StringName = &"HelmetNode"
if model.has_bone(helmet_bone_name):
var helmet_dict : Dictionary = ModelSingleton.get_cultural_helmet_model(culture)
if helmet_dict:
diff --git a/game/src/Game/Model/unit_colours.gdshader b/game/src/Game/Model/unit_colours.gdshader
index 2ffc7710..f291c544 100644
--- a/game/src/Game/Model/unit_colours.gdshader
+++ b/game/src/Game/Model/unit_colours.gdshader
@@ -1,7 +1,7 @@
shader_type spatial;
-render_mode cull_disabled;
+render_mode cull_disabled, depth_prepass_alpha;
//hold all the textures for the units that need this shader to mix in their
//nation colours (mostly generic infantry units)
@@ -28,4 +28,5 @@ void fragment() {
vec3 tertiary_col = mix(vec3(1.0, 1.0, 1.0), colour_tertiary, nation_colours_tex.r);
ALBEDO = diffuse_tex.rgb * primary_col * secondary_col * tertiary_col;
+ ALPHA = diffuse_tex.a;
}