diff options
-rw-r--r-- | extension/src/openvic-extension/register_types.cpp | 9 | ||||
-rw-r--r-- | extension/src/openvic-extension/singletons/GameSingleton.cpp | 9 | ||||
-rw-r--r-- | extension/src/openvic-extension/singletons/GameSingleton.hpp | 2 | ||||
-rw-r--r-- | extension/src/openvic-extension/singletons/ModelSingleton.cpp | 458 | ||||
-rw-r--r-- | extension/src/openvic-extension/singletons/ModelSingleton.hpp | 48 | ||||
-rw-r--r-- | game/src/Game/GameSession/GameSession.gd | 4 | ||||
-rw-r--r-- | game/src/Game/GameSession/GameSession.tscn | 11 | ||||
-rw-r--r-- | game/src/Game/GameSession/MapView.tscn | 6 | ||||
-rw-r--r-- | game/src/Game/GameSession/ModelManager.gd | 150 | ||||
-rw-r--r-- | game/src/Game/Model/FileAccessUtils.gd | 88 | ||||
-rw-r--r-- | game/src/Game/Model/UnitModel.gd | 160 | ||||
-rw-r--r-- | game/src/Game/Model/XACLoader.gd | 1127 | ||||
-rw-r--r-- | game/src/Game/Model/XSMLoader.gd | 363 | ||||
-rw-r--r-- | game/src/Game/Model/flag.gdshader | 26 | ||||
-rw-r--r-- | game/src/Game/Model/flag_mat.tres | 7 | ||||
-rw-r--r-- | game/src/Game/Model/unit_colours.gdshader | 31 | ||||
-rw-r--r-- | game/src/Game/Model/unit_colours_mat.tres | 9 |
17 files changed, 2506 insertions, 2 deletions
diff --git a/extension/src/openvic-extension/register_types.cpp b/extension/src/openvic-extension/register_types.cpp index fffb370..b2b6731 100644 --- a/extension/src/openvic-extension/register_types.cpp +++ b/extension/src/openvic-extension/register_types.cpp @@ -16,6 +16,7 @@ #include "openvic-extension/singletons/GameSingleton.hpp" #include "openvic-extension/singletons/LoadLocalisation.hpp" #include "openvic-extension/singletons/MenuSingleton.hpp" +#include "openvic-extension/singletons/ModelSingleton.hpp" using namespace godot; using namespace OpenVic; @@ -24,6 +25,7 @@ static Checksum* _checksum_singleton = nullptr; static LoadLocalisation* _load_localisation = nullptr; static GameSingleton* _game_singleton = nullptr; static MenuSingleton* _menu_singleton = nullptr; +static ModelSingleton* _model_singleton = nullptr; static AssetManager* _asset_manager_singleton = nullptr; void initialize_openvic_types(ModuleInitializationLevel p_level) { @@ -47,6 +49,10 @@ void initialize_openvic_types(ModuleInitializationLevel p_level) { _menu_singleton = memnew(MenuSingleton); Engine::get_singleton()->register_singleton("MenuSingleton", MenuSingleton::get_singleton()); + ClassDB::register_class<ModelSingleton>(); + _model_singleton = memnew(ModelSingleton); + Engine::get_singleton()->register_singleton("ModelSingleton", ModelSingleton::get_singleton()); + ClassDB::register_class<AssetManager>(); _asset_manager_singleton = memnew(AssetManager); Engine::get_singleton()->register_singleton("AssetManager", AssetManager::get_singleton()); @@ -86,6 +92,9 @@ void uninitialize_openvic_types(ModuleInitializationLevel p_level) { Engine::get_singleton()->unregister_singleton("MenuSingleton"); memdelete(_menu_singleton); + Engine::get_singleton()->unregister_singleton("ModelSingleton"); + memdelete(_model_singleton); + Engine::get_singleton()->unregister_singleton("AssetManager"); memdelete(_asset_manager_singleton); } diff --git a/extension/src/openvic-extension/singletons/GameSingleton.cpp b/extension/src/openvic-extension/singletons/GameSingleton.cpp index 838542d..ef19a6c 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" }); @@ -158,6 +159,10 @@ float GameSingleton::get_map_aspect_ratio() const { return static_cast<float>(get_map_width()) / static_cast<float>(get_map_height()); } +Vector2 GameSingleton::map_position_to_world_coords(fvec2_t const& position) const { + return Utilities::to_godot_fvec2(position) / get_map_dims(); +} + Ref<Texture2DArray> GameSingleton::get_terrain_texture() const { return terrain_texture; } @@ -593,3 +598,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..7f86eb2 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); @@ -69,6 +70,7 @@ namespace OpenVic { int32_t get_map_height() const; godot::Vector2i get_map_dims() const; float get_map_aspect_ratio() const; + godot::Vector2 map_position_to_world_coords(fvec2_t const& position) const; /* The cosmetic terrain textures stored in a Texture2DArray. */ godot::Ref<godot::Texture2DArray> get_terrain_texture() const; diff --git a/extension/src/openvic-extension/singletons/ModelSingleton.cpp b/extension/src/openvic-extension/singletons/ModelSingleton.cpp new file mode 100644 index 0000000..88a69c9 --- /dev/null +++ b/extension/src/openvic-extension/singletons/ModelSingleton.cpp @@ -0,0 +1,458 @@ +#include "ModelSingleton.hpp" + +#include <numbers> + +#include <godot_cpp/variant/utility_functions.hpp> + +#include "openvic-extension/singletons/GameSingleton.hpp" +#include "openvic-extension/utility/ClassBindings.hpp" +#include "openvic-extension/utility/Utilities.hpp" + +using namespace godot; +using namespace OpenVic; + +using OpenVic::Utilities::godot_to_std_string; +using OpenVic::Utilities::std_to_godot_string; +using OpenVic::Utilities::std_view_to_godot_string; + +void ModelSingleton::_bind_methods() { + OV_BIND_METHOD(ModelSingleton::get_units); + OV_BIND_METHOD(ModelSingleton::get_cultural_gun_model, { "culture" }); + OV_BIND_METHOD(ModelSingleton::get_cultural_helmet_model, { "culture" }); + OV_BIND_METHOD(ModelSingleton::get_flag_model, { "floating" }); + OV_BIND_METHOD(ModelSingleton::get_buildings); +} + +ModelSingleton* ModelSingleton::get_singleton() { + return singleton; +} + +ModelSingleton::ModelSingleton() { + ERR_FAIL_COND(singleton != nullptr); + singleton = this; +} + +ModelSingleton::~ModelSingleton() { + ERR_FAIL_COND(singleton != this); + singleton = nullptr; +} + +GFX::Actor const* ModelSingleton::get_actor(std::string_view name, bool error_on_fail) const { + GameSingleton const* game_singleton = GameSingleton::get_singleton(); + ERR_FAIL_NULL_V(game_singleton, nullptr); + + GFX::Actor const* actor = + game_singleton->get_game_manager().get_ui_manager().get_cast_object_by_identifier<GFX::Actor>(name); + + if (error_on_fail) { + ERR_FAIL_NULL_V_MSG(actor, nullptr, vformat("Failed to find actor \"%s\"", std_view_to_godot_string(name))); + } + + return actor; +} + +GFX::Actor const* ModelSingleton::get_cultural_actor( + std::string_view culture, std::string_view name, std::string_view fallback_name +) const { + GameSingleton const* game_singleton = GameSingleton::get_singleton(); + ERR_FAIL_NULL_V(game_singleton, nullptr); + + ERR_FAIL_COND_V_MSG( + culture.empty() || name.empty(), nullptr, vformat( + "Failed to find actor \"%s\" for culture \"%s\" - neither can be empty", + std_view_to_godot_string(name), std_view_to_godot_string(culture) + ) + ); + + std::string actor_name = StringUtils::append_string_views(culture, name); + + GFX::Actor const* actor = get_actor(actor_name, false); + + // Which should be tried first: "Generic***" or "***Infantry"? + + if (actor == nullptr) { + /* If no Actor exists for the specified GraphicalCultureType then try the default instead. */ + GraphicalCultureType const* default_graphical_culture_type = + game_singleton->get_game_manager().get_pop_manager().get_culture_manager().get_default_graphical_culture_type(); + + if (default_graphical_culture_type != nullptr && default_graphical_culture_type->get_identifier() != culture) { + actor_name = StringUtils::append_string_views(default_graphical_culture_type->get_identifier(), name); + + actor = get_actor(actor_name, false); + } + + if (actor == nullptr && !fallback_name.empty() && fallback_name != name) { + return get_cultural_actor(culture, fallback_name, {}); + } + } + + ERR_FAIL_NULL_V_MSG( + actor, nullptr, vformat( + "Failed to find actor \"%s\" for culture \"%s\"", std_view_to_godot_string(name), + std_view_to_godot_string(culture) + ) + ); + + return actor; +} + +Dictionary ModelSingleton::make_animation_dict(GFX::Actor::Animation const& animation) const { + static const StringName file_key = "file"; + static const StringName time_key = "time"; + + Dictionary dict; + + dict[file_key] = std_view_to_godot_string(animation.get_file()); + dict[time_key] = animation.get_scroll_time().to_float(); + + return dict; +} + +Dictionary ModelSingleton::make_model_dict(GFX::Actor const& actor) const { + static const StringName file_key = "file"; + static const StringName scale_key = "scale"; + static const StringName idle_key = "idle"; + static const StringName move_key = "move"; + static const StringName attack_key = "attack"; + static const StringName attachments_key = "attachments"; + + Dictionary dict; + + dict[file_key] = std_view_to_godot_string(actor.get_model_file()); + dict[scale_key] = actor.get_scale().to_float(); + + const auto set_animation = [this, &dict](StringName const& key, std::optional<GFX::Actor::Animation> const& animation) { + if (animation.has_value()) { + dict[key] = make_animation_dict(*animation); + } + }; + + set_animation(idle_key, actor.get_idle_animation()); + set_animation(move_key, actor.get_move_animation()); + set_animation(attack_key, actor.get_attack_animation()); + + std::vector<GFX::Actor::Attachment> const& attachments = actor.get_attachments(); + + if (!attachments.empty()) { + static const StringName attachment_node_key = "node"; + static const StringName attachment_model_key = "model"; + + TypedArray<Dictionary> attachments_array; + + if (attachments_array.resize(attachments.size()) == OK) { + + for (size_t idx = 0; idx < attachments_array.size(); ++idx) { + + GFX::Actor::Attachment const& attachment = attachments[idx]; + + GFX::Actor const* attachment_actor = get_actor(attachment.get_actor_name()); + + ERR_CONTINUE_MSG( + attachment_actor == nullptr, vformat( + "Failed to find \"%s\" attachment actor for actor \"%s\"", + std_view_to_godot_string(attachment.get_actor_name()), std_view_to_godot_string(actor.get_name()) + ) + ); + + Dictionary attachment_dict; + + attachment_dict[attachment_node_key] = std_view_to_godot_string(attachment.get_attach_node()); + attachment_dict[attachment_model_key] = make_model_dict(*attachment_actor); + + attachments_array[idx] = std::move(attachment_dict); + + } + + if (!attachments_array.is_empty()) { + dict[attachments_key] = std::move(attachments_array); + } + + } else { + UtilityFunctions::push_error( + "Failed to resize attachments array to the correct size (", static_cast<int64_t>(attachments.size()), + ") for model for actor \"", std_view_to_godot_string(actor.get_name()), "\"" + ); + } + } + + return dict; +} + +/* Returns false if an error occurs while trying to add a unit model for the province, true otherwise. + * Returning true doesn't necessarily mean a unit was added, e.g. when units is empty. */ +template<utility::is_derived_from_specialization_of<UnitInstanceGroup> T> +bool ModelSingleton::add_unit_dict(ordered_set<T*> const& units, TypedArray<Dictionary>& unit_array) const { + GameSingleton const* game_singleton = GameSingleton::get_singleton(); + ERR_FAIL_NULL_V(game_singleton, false); + + static const StringName culture_key = "culture"; + static const StringName model_key = "model"; + static const StringName mount_model_key = "mount_model"; + static const StringName mount_attach_node_key = "mount_attach_node"; + static const StringName flag_index_key = "flag_index"; + static const StringName flag_floating_key = "flag_floating"; + static const StringName position_key = "position"; + static const StringName rotation_key = "rotation"; + static const StringName primary_colour_key = "primary_colour"; + static const StringName secondary_colour_key = "secondary_colour"; + static const StringName tertiary_colour_key = "tertiary_colour"; + + if (units.empty()) { + return true; + } + + bool ret = true; + + /* Last unit to enter the province is shown on top. */ + T const& unit = *units.back(); + ERR_FAIL_COND_V_MSG(unit.empty(), false, vformat("Empty unit \"%s\"", std_view_to_godot_string(unit.get_name()))); + + Country const* country = unit.get_country()->get_base_country(); + + GraphicalCultureType const& graphical_culture_type = country->get_graphical_culture(); + UnitType const* display_unit_type = unit.get_display_unit_type(); + ERR_FAIL_NULL_V_MSG( + display_unit_type, false, vformat( + "Failed to get display unit type for unit \"%s\"", std_view_to_godot_string(unit.get_name()) + ) + ); + + std::string_view actor_name = display_unit_type->get_sprite(); + std::string_view mount_actor_name, mount_attach_node_name; + + if constexpr (std::same_as<T, ArmyInstance>) { + RegimentType const* regiment_type = reinterpret_cast<RegimentType const*>(display_unit_type); + + if (!regiment_type->get_sprite_override().empty()) { + actor_name = regiment_type->get_sprite_override(); + } + + if (regiment_type->get_sprite_mount().empty() == regiment_type->get_sprite_mount_attach_node().empty()) { + if (!regiment_type->get_sprite_mount().empty()) { + mount_actor_name = regiment_type->get_sprite_mount(); + mount_attach_node_name = regiment_type->get_sprite_mount_attach_node(); + } + } else { + UtilityFunctions::push_error( + "Mount sprite and attach node must both be set or both be empty - regiment type \"", + std_view_to_godot_string(regiment_type->get_identifier()), "\" has mount \"", + std_view_to_godot_string(regiment_type->get_sprite_mount()), "\" and attach node \"", + std_view_to_godot_string(regiment_type->get_sprite_mount_attach_node()), "\"" + ); + ret = false; + } + } + + // TODO - default without requiring hardcoded name + static constexpr std::string_view default_fallback_actor_name = "Infantry"; + GFX::Actor const* actor = get_cultural_actor( + graphical_culture_type.get_identifier(), actor_name, default_fallback_actor_name + ); + + ERR_FAIL_NULL_V_MSG( + actor, false, vformat( + "Failed to find \"%s\" actor of graphical culture type \"%s\" for unit \"%s\"", + std_view_to_godot_string(display_unit_type->get_sprite()), + std_view_to_godot_string(graphical_culture_type.get_identifier()), + std_view_to_godot_string(unit.get_name()) + ) + ); + + Dictionary dict; + + dict[culture_key] = std_view_to_godot_string(graphical_culture_type.get_identifier()); + + dict[model_key] = make_model_dict(*actor); + + if (!mount_actor_name.empty() && !mount_attach_node_name.empty()) { + GFX::Actor const* mount_actor = get_actor(mount_actor_name); + + if (mount_actor != nullptr) { + dict[mount_model_key] = make_model_dict(*mount_actor); + dict[mount_attach_node_key] = std_view_to_godot_string(mount_attach_node_name); + } else { + UtilityFunctions::push_error(vformat( + "Failed to find \"%s\" mount actor of graphical culture type \"%s\" for unit \"%s\"", + std_view_to_godot_string(mount_actor_name), + std_view_to_godot_string(graphical_culture_type.get_identifier()), + std_view_to_godot_string(unit.get_name()) + )); + ret = false; + } + } + + // TODO - government type based flag type + dict[flag_index_key] = game_singleton->get_flag_sheet_index(country->get_index(), {}); + + if (display_unit_type->has_floating_flag()) { + dict[flag_floating_key] = true; + } + + dict[position_key] = game_singleton->map_position_to_world_coords(unit.get_position()->get_unit_position()); + + if (display_unit_type->get_unit_category() != UnitType::unit_category_t::INFANTRY) { + dict[rotation_key] = -0.25f * std::numbers::pi_v<float>; + } + + dict[primary_colour_key] = Utilities::to_godot_color(country->get_primary_unit_colour()); + dict[secondary_colour_key] = Utilities::to_godot_color(country->get_secondary_unit_colour()); + dict[tertiary_colour_key] = Utilities::to_godot_color(country->get_tertiary_unit_colour()); + + // TODO - move dict into unit_array ? + unit_array.push_back(dict); + + return ret; +} + +TypedArray<Dictionary> ModelSingleton::get_units() const { + GameSingleton const* game_singleton = GameSingleton::get_singleton(); + ERR_FAIL_NULL_V(game_singleton, {}); + + TypedArray<Dictionary> ret; + + for (Province const& province : game_singleton->get_game_manager().get_map().get_provinces()) { + if (province.is_water()) { + if (!add_unit_dict(province.get_navies(), ret)) { + UtilityFunctions::push_error( + "Error adding navy to province \"", std_view_to_godot_string(province.get_identifier()), "\"" + ); + } + } else { + if (!add_unit_dict(province.get_armies(), ret)) { + UtilityFunctions::push_error( + "Error adding army to province \"", std_view_to_godot_string(province.get_identifier()), "\"" + ); + } + } + + // TODO - land units in ships + } + + return ret; +} + +Dictionary ModelSingleton::get_cultural_gun_model(String const& culture) const { + static constexpr std::string_view gun_actor_name = "Gun1"; + + GFX::Actor const* actor = get_cultural_actor(godot_to_std_string(culture), gun_actor_name, {}); + + ERR_FAIL_NULL_V(actor, {}); + + return make_model_dict(*actor); +} + +Dictionary ModelSingleton::get_cultural_helmet_model(String const& culture) const { + static constexpr std::string_view helmet_actor_name = "Helmet1"; + + GFX::Actor const* actor = get_cultural_actor(godot_to_std_string(culture), helmet_actor_name, {}); + + ERR_FAIL_NULL_V(actor, {}); + + return make_model_dict(*actor); +} + +Dictionary ModelSingleton::get_flag_model(bool floating) const { + static constexpr std::string_view flag_name = "Flag"; + static constexpr std::string_view flag_floating_name = "FlagFloating"; + + GFX::Actor const* actor = get_actor(floating ? flag_floating_name : flag_name); + + ERR_FAIL_NULL_V(actor, {}); + + return make_model_dict(*actor); +} + +bool ModelSingleton::add_building_dict( + BuildingInstance const& building, Province const& province, TypedArray<Dictionary>& building_array +) const { + GameSingleton const* game_singleton = GameSingleton::get_singleton(); + ERR_FAIL_NULL_V(game_singleton, false); + + static const StringName model_key = "model"; + static const StringName position_key = "position"; + static const StringName rotation_key = "rotation"; + + std::string suffix; + + if ( + &building.get_building_type() == + game_singleton->get_game_manager().get_economy_manager().get_building_type_manager().get_port_building_type() + ) { + /* Port */ + if (!province.has_port()) { + return true; + } + + if (building.get_level() > 0) { + suffix = std::to_string(building.get_level()); + } + + if (!province.get_navies().empty()) { + suffix += "_ships"; + } + } else if (building.get_identifier() == "fort") { + /* Fort */ + if (building.get_level() < 1) { + return true; + } + + if (building.get_level() > 1) { + suffix = std::to_string(building.get_level()); + } + } else { + // TODO - railroad (trainstations) + return true; + } + + fvec2_t const* position_ptr = province.get_building_position(&building.get_building_type()); + const float rotation = province.get_building_rotation(&building.get_building_type()); + + const std::string actor_name = StringUtils::append_string_views("building_", building.get_identifier(), suffix); + + GFX::Actor const* actor = get_actor(actor_name); + ERR_FAIL_NULL_V_MSG( + actor, false, vformat( + "Failed to find \"%s\" actor for building \"%s\" in province \"%s\"", + std_to_godot_string(actor_name), std_view_to_godot_string(building.get_identifier()), + std_view_to_godot_string(province.get_identifier()) + ) + ); + + Dictionary dict; + + dict[model_key] = make_model_dict(*actor); + + dict[position_key] = + game_singleton->map_position_to_world_coords(position_ptr != nullptr ? *position_ptr : province.get_centre()); + + if (rotation != 0.0f) { + dict[rotation_key] = rotation; + } + + // TODO - move dict into unit_array ? + building_array.push_back(dict); + + return true; +} + +TypedArray<Dictionary> ModelSingleton::get_buildings() const { + GameSingleton const* game_singleton = GameSingleton::get_singleton(); + ERR_FAIL_NULL_V(game_singleton, {}); + + TypedArray<Dictionary> ret; + + for (Province const& province : game_singleton->get_game_manager().get_map().get_provinces()) { + if (!province.is_water()) { + for (BuildingInstance const& building : province.get_buildings()) { + if (!add_building_dict(building, province, ret)) { + UtilityFunctions::push_error( + "Error adding building \"", std_view_to_godot_string(building.get_identifier()), "\" to province \"", + std_view_to_godot_string(province.get_identifier()), "\"" + ); + } + } + } + } + + return ret; +} diff --git a/extension/src/openvic-extension/singletons/ModelSingleton.hpp b/extension/src/openvic-extension/singletons/ModelSingleton.hpp new file mode 100644 index 0000000..17c2dd0 --- /dev/null +++ b/extension/src/openvic-extension/singletons/ModelSingleton.hpp @@ -0,0 +1,48 @@ +#pragma once + +#include <godot_cpp/classes/object.hpp> + +#include <openvic-simulation/interface/GFXObject.hpp> +#include <openvic-simulation/military/UnitInstance.hpp> + +namespace OpenVic { + class ModelSingleton : public godot::Object { + GDCLASS(ModelSingleton, godot::Object) + + static inline ModelSingleton* singleton = nullptr; + + protected: + static void _bind_methods(); + + public: + static ModelSingleton* get_singleton(); + + ModelSingleton(); + ~ModelSingleton(); + + private: + GFX::Actor const* get_actor(std::string_view name, bool error_on_fail = true) const; + GFX::Actor const* get_cultural_actor( + std::string_view culture, std::string_view name, std::string_view fallback_name + ) const; + + godot::Dictionary make_animation_dict(GFX::Actor::Animation const& animation) const; + godot::Dictionary make_model_dict(GFX::Actor const& actor) const; + + template<utility::is_derived_from_specialization_of<UnitInstanceGroup> T> + bool add_unit_dict(ordered_set<T*> const& units, godot::TypedArray<godot::Dictionary>& unit_array) const; + + bool add_building_dict( + BuildingInstance const& building, Province const& province, godot::TypedArray<godot::Dictionary>& building_array + ) const; + + public: + godot::TypedArray<godot::Dictionary> get_units() const; + godot::Dictionary get_cultural_gun_model(godot::String const& culture) const; + godot::Dictionary get_cultural_helmet_model(godot::String const& culture) const; + + godot::Dictionary get_flag_model(bool floating) const; + + godot::TypedArray<godot::Dictionary> get_buildings() const; + }; +} diff --git a/game/src/Game/GameSession/GameSession.gd b/game/src/Game/GameSession/GameSession.gd index f085073..843ecd8 100644 --- a/game/src/Game/GameSession/GameSession.gd +++ b/game/src/Game/GameSession/GameSession.gd @@ -1,5 +1,6 @@ extends Control +@export var _model_manager : ModelManager @export var _game_session_menu : Control func _ready() -> void: @@ -7,6 +8,9 @@ func _ready() -> void: if GameSingleton.setup_game(0) != OK: push_error("Failed to setup game") + _model_manager.generate_units() + _model_manager.generate_buildings() + func _process(_delta : float) -> void: GameSingleton.try_tick() diff --git a/game/src/Game/GameSession/GameSession.tscn b/game/src/Game/GameSession/GameSession.tscn index 343ddfe..d54970f 100644 --- a/game/src/Game/GameSession/GameSession.tscn +++ b/game/src/Game/GameSession/GameSession.tscn @@ -1,9 +1,10 @@ -[gd_scene load_steps=18 format=3 uid="uid://bgnupcshe1m7r"] +[gd_scene load_steps=19 format=3 uid="uid://bgnupcshe1m7r"] [ext_resource type="Script" path="res://src/Game/GameSession/GameSession.gd" id="1_eklvp"] [ext_resource type="PackedScene" uid="uid://cvl76duuym1wq" path="res://src/Game/MusicConductor/MusicPlayer.tscn" id="2_kt6aa"] [ext_resource type="PackedScene" uid="uid://g524p8lr574w" path="res://src/Game/GameSession/MapControlPanel/MapControlPanel.tscn" id="3_afh6d"] [ext_resource type="PackedScene" uid="uid://dvdynl6eir40o" path="res://src/Game/GameSession/GameSessionMenu.tscn" id="3_bvmqh"] +[ext_resource type="Script" path="res://src/Game/GameSession/ModelManager.gd" id="3_qwk4j"] [ext_resource type="Script" path="res://src/Game/GameSession/Topbar.gd" id="4_2kbih"] [ext_resource type="PackedScene" uid="uid://dkehmdnuxih2r" path="res://src/Game/GameSession/MapView.tscn" id="4_xkg5j"] [ext_resource type="Script" path="res://src/Game/GameSession/NationManagementScreen/ProductionMenu.gd" id="5_16755"] @@ -18,7 +19,7 @@ [ext_resource type="Script" path="res://src/Game/GameSession/NationManagementScreen/DiplomacyMenu.gd" id="11_fu7ys"] [ext_resource type="Script" path="res://src/Game/GameSession/NationManagementScreen/MilitaryMenu.gd" id="12_6h6nc"] -[node name="GameSession" type="Control" node_paths=PackedStringArray("_game_session_menu")] +[node name="GameSession" type="Control" node_paths=PackedStringArray("_model_manager", "_game_session_menu")] editor_description = "SS-102, UI-546" layout_mode = 3 anchors_preset = 15 @@ -28,10 +29,15 @@ grow_horizontal = 2 grow_vertical = 2 mouse_filter = 2 script = ExtResource("1_eklvp") +_model_manager = NodePath("ModelManager") _game_session_menu = NodePath("UICanvasLayer/UI/GameSessionMenu") [node name="MapView" parent="." instance=ExtResource("4_xkg5j")] +[node name="ModelManager" type="Node3D" parent="." node_paths=PackedStringArray("_map_view")] +script = ExtResource("3_qwk4j") +_map_view = NodePath("../MapView") + [node name="UICanvasLayer" type="CanvasLayer" parent="."] [node name="UI" type="Control" parent="UICanvasLayer"] @@ -143,6 +149,7 @@ offset_left = -150.0 offset_right = 0.0 grow_horizontal = 0 +[connection signal="detailed_view_changed" from="MapView" to="ModelManager" method="set_visible"] [connection signal="map_view_camera_changed" from="MapView" to="UICanvasLayer/UI/MapControlPanel" method="_on_map_view_camera_changed"] [connection signal="game_session_menu_button_pressed" from="UICanvasLayer/UI/MapControlPanel" to="." method="_on_game_session_menu_button_pressed"] [connection signal="minimap_clicked" from="UICanvasLayer/UI/MapControlPanel" to="MapView" method="_on_minimap_clicked"] diff --git a/game/src/Game/GameSession/MapView.tscn b/game/src/Game/GameSession/MapView.tscn index dff02a6..385a24d 100644 --- a/game/src/Game/GameSession/MapView.tscn +++ b/game/src/Game/GameSession/MapView.tscn @@ -50,4 +50,10 @@ mesh = SubResource("MapMesh_3gtsd") transform = Transform3D(10, 0, 0, 0, 10, 0, 0, 0, 10, 0, -1, 0) mesh = SubResource("PlaneMesh_fnhgl") +[node name="DirectionalLight3D" type="DirectionalLight3D" parent="."] +transform = Transform3D(1, 0, 0, 0, 0.707107, 0.707107, 0, -0.707107, 0.707107, 0, 10, 10) +light_energy = 1.5 +light_bake_mode = 0 +sky_mode = 1 + [connection signal="detailed_view_changed" from="." to="MapText" method="set_visible"] diff --git a/game/src/Game/GameSession/ModelManager.gd b/game/src/Game/GameSession/ModelManager.gd new file mode 100644 index 0000000..8cec49d --- /dev/null +++ b/game/src/Game/GameSession/ModelManager.gd @@ -0,0 +1,150 @@ +class_name ModelManager +extends Node3D + +@export var _map_view : MapView + +const MODEL_SCALE : float = 1.0 / 256.0 + +func generate_units() -> void: + XACLoader.setup_flag_shader() + + for unit : Dictionary in ModelSingleton.get_units(): + _generate_unit(unit) + +func _generate_unit(unit_dict : Dictionary) -> void: + const culture_key : StringName = &"culture" + const model_key : StringName = &"model" + const mount_model_key : StringName = &"mount_model" + const mount_attach_node_key : StringName = &"mount_attach_node" + const flag_index_key : StringName = &"flag_index" + const flag_floating_key : StringName = &"flag_floating" + const position_key : StringName = &"position" + const rotation_key : StringName = &"rotation" + const primary_colour_key : StringName = &"primary_colour" + const secondary_colour_key : StringName = &"secondary_colour" + const tertiary_colour_key : StringName = &"tertiary_colour" + + var model : Node3D = _generate_model(unit_dict[model_key], unit_dict[culture_key]) + if not model: + return + + if mount_model_key in unit_dict and mount_attach_node_key in unit_dict: + # This must be a UnitModel so we can attach the rider to it + var mount_model : Node3D = _generate_model(unit_dict[mount_model_key], unit_dict[culture_key], true) + if mount_model: + mount_model.attach_model(unit_dict[mount_attach_node_key], model) + model = mount_model + + var rotation : float = unit_dict.get(rotation_key, 0.0) + + var flag_dict : Dictionary = ModelSingleton.get_flag_model(unit_dict.get(flag_floating_key, false)) + if flag_dict: + var flag_model : UnitModel = _generate_model(flag_dict, "", true) + if flag_model: + flag_model.set_flag_index(unit_dict[flag_index_key]) + flag_model.current_anim = UnitModel.Anim.IDLE + flag_model.scale /= model.scale + flag_model.rotate_y(-rotation) + + model.add_child(flag_model) + + model.scale *= MODEL_SCALE + model.rotate_y(PI + rotation) + model.set_position(_map_view._map_to_world_coords(unit_dict[position_key]) + Vector3(0, 0.1 * MODEL_SCALE, 0)) + + if model is UnitModel: + model.current_anim = UnitModel.Anim.IDLE + + model.primary_colour = unit_dict[primary_colour_key] + model.secondary_colour = unit_dict[secondary_colour_key] + model.tertiary_colour = unit_dict[tertiary_colour_key] + + add_child(model) + +func generate_buildings() -> void: + for building : Dictionary in ModelSingleton.get_buildings(): + _generate_building(building) + +func _generate_building(building_dict : Dictionary) -> void: + const model_key : StringName = &"model" + const position_key : StringName = &"position" + const rotation_key : StringName = &"rotation" + + var model : Node3D = _generate_model(building_dict[model_key]) + if not model: + return + + model.scale *= MODEL_SCALE + model.rotate_y(PI + building_dict.get(rotation_key, 0.0)) + model.set_position(_map_view._map_to_world_coords(building_dict[position_key]) + Vector3(0, 0.1 * MODEL_SCALE, 0)) + + add_child(model) + +func _generate_model(model_dict : Dictionary, culture : String = "", is_unit : bool = false) -> Node3D: + const file_key : StringName = &"file" + const scale_key : StringName = &"scale" + const idle_key : StringName = &"idle" + const move_key : StringName = &"move" + const attack_key : StringName = &"attack" + const attachments_key : StringName = &"attachments" + + const animation_file_key : StringName = &"file" + const animation_time_key : StringName = &"time" + + const attachment_node_key : StringName = &"node" + const attachment_model_key : StringName = &"model" + + # Model + is_unit = is_unit or ( + # Needed for animations + idle_key in model_dict or move_key in model_dict or attack_key in model_dict + # Currently needs UnitModel's attach_model helper function + or attachments_key in model_dict + ) + + var model : Node3D = XACLoader.get_xac_model(model_dict[file_key], is_unit) + if not model: + return null + model.scale *= model_dict[scale_key] + + if model is UnitModel: + # Animations + var idle_dict : Dictionary = model_dict.get(idle_key, {}) + if idle_dict: + model.idle_anim = XSMLoader.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.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.scroll_speed_attack = attack_dict[animation_time_key] + + # Attachments + for attachment_dict : Dictionary in model_dict.get(attachments_key, []): + var attachment_model : Node3D = _generate_model(attachment_dict[attachment_model_key], culture) + if attachment_model: + model.attach_model(attachment_dict[attachment_node_key], attachment_model) + + if culture: + const gun_bone_name : String = "GunNode" + if model.has_bone(gun_bone_name): + var gun_dict : Dictionary = ModelSingleton.get_cultural_gun_model(culture) + if gun_dict: + var gun_model : Node3D = _generate_model(gun_dict, culture) + if gun_model: + model.attach_model(gun_bone_name, gun_model) + + const helmet_bone_name : String = "HelmetNode" + if model.has_bone(helmet_bone_name): + var helmet_dict : Dictionary = ModelSingleton.get_cultural_helmet_model(culture) + if helmet_dict: + var helmet_model : Node3D = _generate_model(helmet_dict, culture) + if helmet_model: + model.attach_model(helmet_bone_name, helmet_model) + + return model 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 = [] |