From fd375bdb35d8a7b2ac9cf3dd02cdb0f197451a0b Mon Sep 17 00:00:00 2001 From: hop311 Date: Thu, 7 Dec 2023 22:45:19 +0000 Subject: Big UI commit - GUINode, MaskedFlag, PieChart, etc --- .../classes/GFXMaskedFlagTexture.cpp | 183 +++++++++++++++++++++ .../classes/GFXMaskedFlagTexture.hpp | 55 +++++++ .../classes/GFXPieChartTexture.cpp | 167 +++++++++++++++++++ .../classes/GFXPieChartTexture.hpp | 49 ++++++ .../src/openvic-extension/classes/GUINode.cpp | 161 ++++++++++++++++++ .../src/openvic-extension/classes/GUINode.hpp | 53 ++++++ 6 files changed, 668 insertions(+) create mode 100644 extension/src/openvic-extension/classes/GFXMaskedFlagTexture.cpp create mode 100644 extension/src/openvic-extension/classes/GFXMaskedFlagTexture.hpp create mode 100644 extension/src/openvic-extension/classes/GFXPieChartTexture.cpp create mode 100644 extension/src/openvic-extension/classes/GFXPieChartTexture.hpp create mode 100644 extension/src/openvic-extension/classes/GUINode.cpp create mode 100644 extension/src/openvic-extension/classes/GUINode.hpp (limited to 'extension/src/openvic-extension/classes') diff --git a/extension/src/openvic-extension/classes/GFXMaskedFlagTexture.cpp b/extension/src/openvic-extension/classes/GFXMaskedFlagTexture.cpp new file mode 100644 index 0000000..3636855 --- /dev/null +++ b/extension/src/openvic-extension/classes/GFXMaskedFlagTexture.cpp @@ -0,0 +1,183 @@ +#include "GFXMaskedFlagTexture.hpp" + +#include + +#include "openvic-extension/singletons/AssetManager.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_view_to_godot_string; +using OpenVic::Utilities::std_view_to_godot_string_name; + +Error GFXMaskedFlagTexture::_generate_combined_image() { + ERR_FAIL_NULL_V(overlay_image, FAILED); + bool can_update = true; + if (combined_image.is_null() || combined_image->get_size() != overlay_image->get_size()) { + combined_image = Image::create( + overlay_image->get_width(), overlay_image->get_height(), false, overlay_image->get_format() + ); + ERR_FAIL_NULL_V(combined_image, FAILED); + can_update = false; + } + + if (mask_image.is_valid() && flag_image.is_valid()) { + const Vector2i centre_translation = (mask_image->get_size() - combined_image->get_size()) / 2; + for (Vector2i combined_image_point { 0, 0 }; combined_image_point.y < combined_image->get_height(); ++combined_image_point.y) { + for (combined_image_point.x = 0; combined_image_point.x < combined_image->get_width(); ++combined_image_point.x) { + const Color overlay_image_colour = overlay_image->get_pixelv(combined_image_point); + // Translate to mask_image coordinates, keeping the centres of each image aligned. + const Vector2i mask_image_point = combined_image_point + centre_translation; + if ( + 0 <= mask_image_point.x && mask_image_point.x < mask_image->get_width() && + 0 <= mask_image_point.y && mask_image_point.y < mask_image->get_height() + ) { + const Color mask_image_colour = mask_image->get_pixelv(mask_image_point); + // Rescale from mask_image to flag_image coordinates. + const Vector2i flag_image_point = mask_image_point * flag_image->get_size() / mask_image->get_size(); + Color flag_image_colour = flag_image->get_pixelv(flag_image_point); + flag_image_colour.a = mask_image_colour.a; + combined_image->set_pixelv(combined_image_point, flag_image_colour.blend(overlay_image_colour)); + } else { + combined_image->set_pixelv(combined_image_point, overlay_image_colour); + } + } + } + } else { + combined_image->blit_rect(overlay_image, overlay_image->get_used_rect(), {}); + } + + if (can_update) { + update(combined_image); + } else { + set_image(combined_image); + } + return OK; +} + +void GFXMaskedFlagTexture::_bind_methods() { + OV_BIND_METHOD(GFXMaskedFlagTexture::clear); + + OV_BIND_METHOD(GFXMaskedFlagTexture::set_gfx_masked_flag_name, { "gfx_masked_flag_name" }); + OV_BIND_METHOD(GFXMaskedFlagTexture::get_gfx_masked_flag_name); + + OV_BIND_METHOD(GFXMaskedFlagTexture::set_flag_country_name_and_type, { "new_flag_country_name", "new_flag_type" }); + OV_BIND_METHOD(GFXMaskedFlagTexture::get_flag_country_name); + OV_BIND_METHOD(GFXMaskedFlagTexture::get_flag_type); +} + +GFXMaskedFlagTexture::GFXMaskedFlagTexture() : gfx_masked_flag { nullptr }, flag_country { nullptr } {} + +Ref GFXMaskedFlagTexture::make_gfx_masked_flag_texture(GFX::MaskedFlag const* gfx_masked_flag) { + Ref masked_flag_texture; + masked_flag_texture.instantiate(); + ERR_FAIL_NULL_V(masked_flag_texture, nullptr); + if (masked_flag_texture->set_gfx_masked_flag(gfx_masked_flag) == OK) { + return masked_flag_texture; + } else { + return nullptr; + } +} + +void GFXMaskedFlagTexture::clear() { + gfx_masked_flag = nullptr; + flag_country = nullptr; + flag_type = String {}; + + overlay_image.unref(); + mask_image.unref(); + flag_image.unref(); +} + +Error GFXMaskedFlagTexture::set_gfx_masked_flag(GFX::MaskedFlag const* new_gfx_masked_flag) { + if (gfx_masked_flag == new_gfx_masked_flag) { + return OK; + } + if (new_gfx_masked_flag == nullptr) { + clear(); + return OK; + } + AssetManager* asset_manager = AssetManager::get_singleton(); + ERR_FAIL_NULL_V(asset_manager, FAILED); + + const StringName overlay_file = std_view_to_godot_string_name(new_gfx_masked_flag->get_overlay_file()); + const Ref new_overlay_image = asset_manager->get_image(overlay_file); + ERR_FAIL_NULL_V_MSG(new_overlay_image, FAILED, vformat("Failed to load flag overlay image: %s", overlay_file)); + + const StringName mask_file = std_view_to_godot_string_name(new_gfx_masked_flag->get_mask_file()); + const Ref new_mask_image = asset_manager->get_image(mask_file); + ERR_FAIL_NULL_V_MSG(new_mask_image, FAILED, vformat("Failed to load flag mask image: %s", mask_file)); + + gfx_masked_flag = new_gfx_masked_flag; + overlay_image = new_overlay_image; + mask_image = new_mask_image; + + return _generate_combined_image(); +} + +Error GFXMaskedFlagTexture::set_gfx_masked_flag_name(String const& gfx_masked_flag_name) { + if (gfx_masked_flag_name.is_empty()) { + return set_gfx_masked_flag(nullptr); + } + GameSingleton* game_singleton = GameSingleton::get_singleton(); + ERR_FAIL_NULL_V(game_singleton, FAILED); + GFX::Sprite const* sprite = game_singleton->get_game_manager().get_ui_manager().get_sprite_by_identifier( + godot_to_std_string(gfx_masked_flag_name) + ); + ERR_FAIL_NULL_V_MSG(sprite, FAILED, vformat("GFX sprite not found: %s", gfx_masked_flag_name)); + GFX::MaskedFlag const* new_masked_flag = sprite->cast_to(); + ERR_FAIL_NULL_V_MSG( + new_masked_flag, FAILED, vformat( + "Invalid type for GFX sprite %s: %s (expected %s)", gfx_masked_flag_name, + std_view_to_godot_string(sprite->get_type()), std_view_to_godot_string(GFX::MaskedFlag::get_type_static()) + ) + ); + return set_gfx_masked_flag(new_masked_flag); +} + +String GFXMaskedFlagTexture::get_gfx_masked_flag_name() const { + return gfx_masked_flag != nullptr ? std_view_to_godot_string(gfx_masked_flag->get_name()) : String {}; +} + +Error GFXMaskedFlagTexture::set_flag_country_and_type(Country const* new_flag_country, StringName const& new_flag_type) { + if (flag_country == new_flag_country && flag_type == new_flag_type) { + return OK; + } + if (new_flag_country != nullptr) { + GameSingleton* game_singleton = GameSingleton::get_singleton(); + ERR_FAIL_NULL_V(game_singleton, FAILED); + + const Ref new_flag_image = game_singleton->get_flag_image(new_flag_country, new_flag_type); + ERR_FAIL_NULL_V(new_flag_image, FAILED); + + flag_country = new_flag_country; + flag_type = new_flag_type; + flag_image = new_flag_image; + } else { + flag_country = nullptr; + flag_type = String {}; + flag_image.unref(); + } + return _generate_combined_image(); +} + +Error GFXMaskedFlagTexture::set_flag_country_name_and_type(String const& new_flag_country_name, StringName const& new_flag_type) { + if (new_flag_country_name.is_empty()) { + return set_flag_country_and_type(nullptr, {}); + } + GameSingleton* game_singleton = GameSingleton::get_singleton(); + ERR_FAIL_NULL_V(game_singleton, FAILED); + Country const* new_flag_country = game_singleton->get_game_manager().get_country_manager().get_country_by_identifier( + godot_to_std_string(new_flag_country_name) + ); + ERR_FAIL_NULL_V_MSG(new_flag_country, FAILED, vformat("Country not found: %s", new_flag_country_name)); + return set_flag_country_and_type(new_flag_country, new_flag_type); +} + +String GFXMaskedFlagTexture::get_flag_country_name() const { + return flag_country != nullptr ? std_view_to_godot_string(flag_country->get_identifier()) : String {}; +} diff --git a/extension/src/openvic-extension/classes/GFXMaskedFlagTexture.hpp b/extension/src/openvic-extension/classes/GFXMaskedFlagTexture.hpp new file mode 100644 index 0000000..be3b15a --- /dev/null +++ b/extension/src/openvic-extension/classes/GFXMaskedFlagTexture.hpp @@ -0,0 +1,55 @@ +#pragma once + +#include + +#include +#include + +namespace OpenVic { + class GFXMaskedFlagTexture : public godot::ImageTexture { + GDCLASS(GFXMaskedFlagTexture, godot::ImageTexture) + + GFX::MaskedFlag const* PROPERTY(gfx_masked_flag); + Country const* PROPERTY(flag_country); + godot::StringName PROPERTY(flag_type); + + godot::Ref overlay_image, mask_image, flag_image, combined_image; + + godot::Error _generate_combined_image(); + + protected: + static void _bind_methods(); + + public: + GFXMaskedFlagTexture(); + + /* Create a GFXMaskedFlagTexture using the specific GFX::MaskedFlag. + * Returns nullptr if setting gfx_masked_flag fails. */ + static godot::Ref make_gfx_masked_flag_texture(GFX::MaskedFlag const* gfx_masked_flag); + + /* Reset gfx_masked_flag, flag_country and flag_type to nullptr/an empty string, and unreference all images. + * This does not affect the godot::ImageTexture, which cannot be reset to a null or empty image. */ + void clear(); + + /* Set the GFX::MaskedFlag, load its overlay and mask textures, and regenerate the combined image. */ + godot::Error set_gfx_masked_flag(GFX::MaskedFlag const* new_gfx_masked_flag); + + /* Search for a GFX::MaskedFlag with the specfied name and, if successful, set it using set_gfx_masked_flag. */ + godot::Error set_gfx_masked_flag_name(godot::String const& gfx_masked_flag_name); + + /* Return the name of the GFX::MaskedFlag, or an empty String if it's null */ + godot::String get_gfx_masked_flag_name() const; + + /* Set flag_country and flag_type and update the combined image to use that flag, or no flag if it doesn't exist. */ + godot::Error set_flag_country_and_type(Country const* new_flag_country, godot::StringName const& new_flag_type); + + /* Look up the country with the specified identifier, then call set_flag_country_and_type with the country and + * specified flag_type as arguments. */ + godot::Error set_flag_country_name_and_type( + godot::String const& new_flag_country_name, godot::StringName const& new_flag_type + ); + + /* Return the name of the selected flag's country, or an empty String if it's null */ + godot::String get_flag_country_name() const; + }; +} diff --git a/extension/src/openvic-extension/classes/GFXPieChartTexture.cpp b/extension/src/openvic-extension/classes/GFXPieChartTexture.cpp new file mode 100644 index 0000000..6fe2fe0 --- /dev/null +++ b/extension/src/openvic-extension/classes/GFXPieChartTexture.cpp @@ -0,0 +1,167 @@ +#include "GFXPieChartTexture.hpp" + +#include + +#include "openvic-extension/singletons/AssetManager.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_view_to_godot_string; +using OpenVic::Utilities::std_view_to_godot_string_name; + +#define PI std::numbers::pi_v + +Error GFXPieChartTexture::_generate_pie_chart_image() { + ERR_FAIL_NULL_V(gfx_pie_chart, FAILED); + if (gfx_pie_chart->get_size() <= 0) { + UtilityFunctions::push_error("Invalid GFX::PieChart size for GFXPieChartTexture - ", gfx_pie_chart->get_size()); + return FAILED; + } + const int32_t pie_chart_size = 2 * gfx_pie_chart->get_size(); + bool can_update = true; + if ( + pie_chart_image.is_null() || pie_chart_image->get_width() != pie_chart_size || + pie_chart_image->get_height() != pie_chart_size + ) { + pie_chart_image = Image::create(pie_chart_size, pie_chart_size, false, Image::FORMAT_RGBA8); + ERR_FAIL_NULL_V(pie_chart_image, FAILED); + can_update = false; + } + + static const Color background_colour { 0.0f, 0.0f, 0.0f, 0.0f }; + if (!slices.empty()) { + const float pie_chart_radius = gfx_pie_chart->get_size(); + const Vector2 centre_translation = Vector2 { 0.5f, 0.5f } - static_cast(pie_chart_image->get_size()) * 0.5f; + for (Vector2i point { 0, 0 }; point.y < pie_chart_image->get_height(); ++point.y) { + for (point.x = 0; point.x < pie_chart_image->get_width(); ++point.x) { + const Vector2 offset = centre_translation + point; + if (offset.length() <= pie_chart_radius) { + float theta = 0.5f * PI + atan2(offset.y, offset.x); + if (theta < 0.0f) { + theta += 2.0f * PI; + } + /* Rescale angle so that total_weight is a full rotation. */ + theta *= total_weight / (2.0f * PI); + Color colour = slices.front().first; + /* Find the slice theta lies in. */ + for (slice_t const& slice : slices) { + if (theta <= slice.second) { + colour = slice.first; + break; + } else { + theta -= slice.second; + } + } + pie_chart_image->set_pixelv(point, colour); + } else { + pie_chart_image->set_pixelv(point, background_colour); + } + } + } + } else { + pie_chart_image->fill(background_colour); + } + + if (can_update) { + update(pie_chart_image); + } else { + set_image(pie_chart_image); + } + return OK; +} + +Error GFXPieChartTexture::set_slices(Array const& new_slices) { + static const StringName colour_key = "colour"; + static const StringName weight_key = "weight"; + + slices.clear(); + total_weight = 0.0f; + for (int32_t i = 0; i < new_slices.size(); ++i) { + Dictionary const& slice_dict = new_slices[i]; + if (!slice_dict.has(colour_key) || !slice_dict.has(weight_key)) { + UtilityFunctions::push_error("Invalid slice keys at index ", i, " - ", slice_dict); + continue; + } + const slice_t slice = std::make_pair(slice_dict[colour_key], slice_dict[weight_key]); + if (slice.second <= 0.0f) { + UtilityFunctions::push_error("Invalid slice weight at index ", i, " - ", slice.second); + continue; + } + total_weight += slice.second; + slices.emplace_back(std::move(slice)); + } + return _generate_pie_chart_image(); +} + +void GFXPieChartTexture::_bind_methods() { + OV_BIND_METHOD(GFXPieChartTexture::clear); + + OV_BIND_METHOD(GFXPieChartTexture::set_gfx_pie_chart_name, { "gfx_pie_chart_name" }); + OV_BIND_METHOD(GFXPieChartTexture::get_gfx_pie_chart_name); + + OV_BIND_METHOD(GFXPieChartTexture::set_slices, { "new_slices" }); +} + +GFXPieChartTexture::GFXPieChartTexture() : total_weight { 0.0f } {} + +Ref GFXPieChartTexture::make_gfx_pie_chart_texture(GFX::PieChart const* gfx_pie_chart) { + Ref pie_chart_texture; + pie_chart_texture.instantiate(); + ERR_FAIL_NULL_V(pie_chart_texture, nullptr); + if (pie_chart_texture->set_gfx_pie_chart(gfx_pie_chart) == OK) { + return pie_chart_texture; + } else { + return nullptr; + } +} + +void GFXPieChartTexture::clear() { + gfx_pie_chart = nullptr; + slices.clear(); + total_weight = 0.0f; + + pie_chart_image.unref(); +} + +Error GFXPieChartTexture::set_gfx_pie_chart(GFX::PieChart const* new_gfx_pie_chart) { + if (gfx_pie_chart == new_gfx_pie_chart) { + return OK; + } + if (new_gfx_pie_chart == nullptr) { + clear(); + return OK; + } + + gfx_pie_chart = new_gfx_pie_chart; + + return _generate_pie_chart_image(); +} + +Error GFXPieChartTexture::set_gfx_pie_chart_name(String const& gfx_pie_chart_name) { + if (gfx_pie_chart_name.is_empty()) { + return set_gfx_pie_chart(nullptr); + } + GameSingleton* game_singleton = GameSingleton::get_singleton(); + ERR_FAIL_NULL_V(game_singleton, FAILED); + GFX::Sprite const* sprite = game_singleton->get_game_manager().get_ui_manager().get_sprite_by_identifier( + godot_to_std_string(gfx_pie_chart_name) + ); + ERR_FAIL_NULL_V_MSG(sprite, FAILED, vformat("GFX sprite not found: %s", gfx_pie_chart_name)); + GFX::PieChart const* new_pie_chart = sprite->cast_to(); + ERR_FAIL_NULL_V_MSG( + new_pie_chart, FAILED, vformat( + "Invalid type for GFX sprite %s: %s (expected %s)", gfx_pie_chart_name, + std_view_to_godot_string(sprite->get_type()), std_view_to_godot_string(GFX::PieChart::get_type_static()) + ) + ); + return set_gfx_pie_chart(new_pie_chart); +} + +String GFXPieChartTexture::get_gfx_pie_chart_name() const { + return gfx_pie_chart != nullptr ? std_view_to_godot_string(gfx_pie_chart->get_name()) : String {}; +} diff --git a/extension/src/openvic-extension/classes/GFXPieChartTexture.hpp b/extension/src/openvic-extension/classes/GFXPieChartTexture.hpp new file mode 100644 index 0000000..315b00e --- /dev/null +++ b/extension/src/openvic-extension/classes/GFXPieChartTexture.hpp @@ -0,0 +1,49 @@ +#pragma once + +#include + +#include + +namespace OpenVic { + class GFXPieChartTexture : public godot::ImageTexture { + GDCLASS(GFXPieChartTexture, godot::ImageTexture) + + using slice_t = std::pair; + + GFX::PieChart const* PROPERTY(gfx_pie_chart); + std::vector slices; + float total_weight; + godot::Ref pie_chart_image; + + godot::Error _generate_pie_chart_image(); + + protected: + static void _bind_methods(); + + public: + GFXPieChartTexture(); + + /* Set slices given new_slices, an Array of Dictionaries, each with the following keys: + * - colour: Color + * - weight: float + */ + godot::Error set_slices(godot::Array const& new_slices); + + /* Create a GFXPieChartTexture using the specific GFX::PieChart. + * Returns nullptr if setting gfx_pie_chart fails. */ + static godot::Ref make_gfx_pie_chart_texture(GFX::PieChart const* gfx_pie_chart); + + /* Reset gfx_pie_chart, flag_country and flag_type to nullptr/an empty string, and unreference all images. + * This does not affect the godot::ImageTexture, which cannot be reset to a null or empty image. */ + void clear(); + + /* Set the GFX::PieChart and regenerate the pie chart image. */ + godot::Error set_gfx_pie_chart(GFX::PieChart const* new_gfx_pie_chart); + + /* Search for a GFX::PieChart with the specfied name and, if successful, set it using set_gfx_pie_chart. */ + godot::Error set_gfx_pie_chart_name(godot::String const& gfx_pie_chart_name); + + /* Return the name of the GFX::PieChart, or an empty String if it's null */ + godot::String get_gfx_pie_chart_name() const; + }; +} diff --git a/extension/src/openvic-extension/classes/GUINode.cpp b/extension/src/openvic-extension/classes/GUINode.cpp new file mode 100644 index 0000000..1d55c54 --- /dev/null +++ b/extension/src/openvic-extension/classes/GUINode.cpp @@ -0,0 +1,161 @@ +#include "GUINode.hpp" + +#include +#include + +#include "openvic-extension/UIAdapter.hpp" +#include "openvic-extension/singletons/AssetManager.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_view_to_godot_string; + +void GUINode::_bind_methods() { + OV_BIND_METHOD(GUINode::add_gui_element, { "gui_file", "gui_element", "name" }, DEFVAL(String {})); + + OV_BIND_METHOD(GUINode::get_button_node, { "path" }); + OV_BIND_METHOD(GUINode::get_check_box_node, { "path" }); + OV_BIND_METHOD(GUINode::get_label_node, { "path" }); + OV_BIND_METHOD(GUINode::get_panel_node, { "path" }); + OV_BIND_METHOD(GUINode::get_progress_bar_node, { "path" }); + OV_BIND_METHOD(GUINode::get_texture_rect_node, { "path" }); + + OV_BIND_METHOD(GUINode::get_texture_from_node, { "path" }); + OV_BIND_METHOD(GUINode::get_gfx_icon_texture_from_node, { "path" }); + OV_BIND_METHOD(GUINode::get_gfx_masked_flag_texture_from_node, { "path" }); + OV_BIND_METHOD(GUINode::get_gfx_pie_chart_texture_from_node, { "path" }); + + OV_BIND_METHOD(GUINode::hide_node, { "path" }); + OV_BIND_METHOD(GUINode::hide_nodes, { "paths" }); +} + +Error GUINode::_add_gui_element(GUI::Element const* element, String const& name) { + ERR_FAIL_NULL_V(element, FAILED); + AssetManager* asset_manager = AssetManager::get_singleton(); + ERR_FAIL_NULL_V(asset_manager, FAILED); + Error err = OK; + Control* result = nullptr; + if (!GodotGUIBuilder::generate_element(element, name, *asset_manager, result)) { + UtilityFunctions::push_error("Failed to generate GUI element ", std_view_to_godot_string(element->get_name())); + err = FAILED; + } + if (result != nullptr) { + add_child(result); + } + return err; +} + +Error GUINode::add_gui_element(String const& gui_file, String const& gui_element, String const& name) { + GameSingleton const* game_singleton = GameSingleton::get_singleton(); + ERR_FAIL_NULL_V(game_singleton, FAILED); + GUI::Scene const* scene = + game_singleton->get_game_manager().get_ui_manager().get_scene_by_identifier(godot_to_std_string(gui_file)); + ERR_FAIL_NULL_V_MSG(scene, FAILED, vformat("Failed to find GUI file %s", gui_file)); + GUI::Element const* element = scene->get_scene_element_by_identifier(godot_to_std_string(gui_element)); + ERR_FAIL_NULL_V_MSG(element, FAILED, vformat("Failed to find GUI element %s in GUI file %s", gui_element, gui_file)); + return _add_gui_element(element, name); +} + +template T> +T* GUINode::_get_cast_node(NodePath const& path) const { + Node* node = get_node_or_null(path); + ERR_FAIL_NULL_V_MSG(node, nullptr, vformat("Failed to find node %s", path)); + T* result = Object::cast_to(node); + ERR_FAIL_NULL_V_MSG(result, nullptr, vformat("Failed to cast node %s to type %s", path, T::get_class_static())); + return result; +} + +Button* GUINode::get_button_node(NodePath const& path) const { + return _get_cast_node