aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
author Hop311 <Hop3114@gmail.com>2024-05-12 17:17:32 +0200
committer GitHub <noreply@github.com>2024-05-12 17:17:32 +0200
commitbfccdb87d66304604ad018037db1581746646bfa (patch)
treeac4394e8ceaca22fa0deaeebd8cf5eafedecaa3e
parentb0a533f945bbc6201fd7df4bc60746cb98efaba4 (diff)
parentac29e4040fc20c50c8f0eb64b1194f6398165eb0 (diff)
Merge pull request #227 from OpenVicProject/models
Models
-rw-r--r--extension/src/openvic-extension/register_types.cpp9
-rw-r--r--extension/src/openvic-extension/singletons/GameSingleton.cpp9
-rw-r--r--extension/src/openvic-extension/singletons/GameSingleton.hpp2
-rw-r--r--extension/src/openvic-extension/singletons/ModelSingleton.cpp458
-rw-r--r--extension/src/openvic-extension/singletons/ModelSingleton.hpp48
-rw-r--r--game/src/Game/GameSession/GameSession.gd4
-rw-r--r--game/src/Game/GameSession/GameSession.tscn11
-rw-r--r--game/src/Game/GameSession/MapView.tscn6
-rw-r--r--game/src/Game/GameSession/ModelManager.gd150
-rw-r--r--game/src/Game/Model/FileAccessUtils.gd88
-rw-r--r--game/src/Game/Model/UnitModel.gd160
-rw-r--r--game/src/Game/Model/XACLoader.gd1127
-rw-r--r--game/src/Game/Model/XSMLoader.gd363
-rw-r--r--game/src/Game/Model/flag.gdshader26
-rw-r--r--game/src/Game/Model/flag_mat.tres7
-rw-r--r--game/src/Game/Model/unit_colours.gdshader31
-rw-r--r--game/src/Game/Model/unit_colours_mat.tres9
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 = []